├── src ├── sqlite3 │ ├── version.cr │ ├── type.cr │ ├── exception.cr │ ├── driver.cr │ ├── flags.cr │ ├── statement.cr │ ├── result_set.cr │ ├── connection.cr │ └── lib_sqlite3.cr └── sqlite3.cr ├── .gitignore ├── shard.yml ├── samples └── memory.cr ├── spec ├── spec_helper.cr ├── pool_spec.cr ├── driver_spec.cr ├── result_set_spec.cr ├── connection_spec.cr └── db_spec.cr ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── .circleci └── config.yml ├── compile_and_link_sqlite.md ├── README.md └── CHANGELOG.md /src/sqlite3/version.cr: -------------------------------------------------------------------------------- 1 | module SQLite3 2 | VERSION = "0.22.0" 3 | end 4 | -------------------------------------------------------------------------------- /src/sqlite3/type.cr: -------------------------------------------------------------------------------- 1 | # Each of the possible types of an SQLite3 column. 2 | enum SQLite3::Type 3 | INTEGER = 1 4 | FLOAT = 2 5 | BLOB = 4 6 | NULL = 5 7 | TEXT = 3 8 | end 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /.crystal/ 4 | /.shards/ 5 | 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in application that uses them 9 | /shard.lock 10 | 11 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: sqlite3 2 | version: 0.22.0 3 | 4 | dependencies: 5 | db: 6 | github: crystal-lang/crystal-db 7 | version: ~> 0.14.0 8 | 9 | authors: 10 | - Ary Borenszweig 11 | - Brian J. Cardiff 12 | 13 | crystal: ">= 1.0.0, < 2.0.0" 14 | 15 | license: MIT 16 | -------------------------------------------------------------------------------- /src/sqlite3/exception.cr: -------------------------------------------------------------------------------- 1 | # Exception thrown on invalid SQLite3 operations. 2 | class SQLite3::Exception < ::Exception 3 | # The internal code associated with the failure. 4 | getter code 5 | 6 | def initialize(db) 7 | super(String.new(LibSQLite3.errmsg(db))) 8 | @code = LibSQLite3.errcode(db) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/sqlite3/driver.cr: -------------------------------------------------------------------------------- 1 | class SQLite3::Driver < DB::Driver 2 | class ConnectionBuilder < ::DB::ConnectionBuilder 3 | def initialize(@options : ::DB::Connection::Options, @sqlite3_options : SQLite3::Connection::Options) 4 | end 5 | 6 | def build : ::DB::Connection 7 | SQLite3::Connection.new(@options, @sqlite3_options) 8 | end 9 | end 10 | 11 | def connection_builder(uri : URI) : ::DB::ConnectionBuilder 12 | params = HTTP::Params.parse(uri.query || "") 13 | ConnectionBuilder.new(connection_options(params), SQLite3::Connection::Options.from_uri(uri)) 14 | end 15 | end 16 | 17 | DB.register_driver "sqlite3", SQLite3::Driver 18 | -------------------------------------------------------------------------------- /src/sqlite3.cr: -------------------------------------------------------------------------------- 1 | require "db" 2 | require "./sqlite3/**" 3 | 4 | module SQLite3 5 | DATE_FORMAT_SUBSECOND = "%F %H:%M:%S.%L" 6 | DATE_FORMAT_SECOND = "%F %H:%M:%S" 7 | 8 | alias Any = DB::Any | Int16 | Int8 | UInt32 | UInt16 | UInt8 9 | 10 | # :nodoc: 11 | TIME_ZONE = Time::Location::UTC 12 | 13 | # :nodoc: 14 | REGEXP_FN = ->(context : LibSQLite3::SQLite3Context, argc : Int32, argv : LibSQLite3::SQLite3Value*) do 15 | argv = Slice.new(argv, sizeof(Void*)) 16 | pattern = LibSQLite3.value_text(argv[0]) 17 | text = LibSQLite3.value_text(argv[1]) 18 | LibSQLite3.result_int(context, Regex.new(String.new(pattern)).matches?(String.new(text)).to_unsafe) 19 | nil 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /samples/memory.cr: -------------------------------------------------------------------------------- 1 | require "db" 2 | require "../src/sqlite3" 3 | 4 | DB.open "sqlite3://%3Amemory%3A" do |db| 5 | db.exec "create table contacts (name text, age integer)" 6 | db.exec "insert into contacts values (?, ?)", "John Doe", 30 7 | 8 | args = [] of DB::Any 9 | args << "Sarah" 10 | args << 33 11 | db.exec "insert into contacts values (?, ?)", args: args 12 | 13 | puts "max age:" 14 | puts db.scalar "select max(age) from contacts" # => 33 15 | 16 | puts "contacts:" 17 | db.query "select name, age from contacts order by age desc" do |rs| 18 | puts "#{rs.column_name(0)} (#{rs.column_name(1)})" 19 | # => name (age) 20 | rs.each do 21 | puts "#{rs.read(String)} (#{rs.read(Int32)})" 22 | # => Sarah (33) 23 | # => John Doe (30) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/sqlite3" 3 | 4 | include SQLite3 5 | 6 | DB_FILENAME = "./test.db" 7 | 8 | def with_db(&block : DB::Database ->) 9 | File.delete(DB_FILENAME) rescue nil 10 | DB.open "sqlite3:#{DB_FILENAME}", &block 11 | ensure 12 | File.delete(DB_FILENAME) 13 | end 14 | 15 | def with_cnn(&block : DB::Connection ->) 16 | File.delete(DB_FILENAME) rescue nil 17 | DB.connect "sqlite3:#{DB_FILENAME}", &block 18 | ensure 19 | File.delete(DB_FILENAME) 20 | end 21 | 22 | def with_db(config, &block : DB::Database ->) 23 | uri = "sqlite3:#{config}" 24 | filename = SQLite3::Connection.filename(URI.parse(uri)) 25 | File.delete(filename) rescue nil 26 | DB.open uri, &block 27 | ensure 28 | File.delete(filename) if filename 29 | end 30 | 31 | def with_mem_db(&block : DB::Database ->) 32 | DB.open "sqlite3://%3Amemory%3A", &block 33 | end 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [master] 7 | schedule: 8 | - cron: '0 6 * * 1' # Every monday 6 AM 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest] 16 | crystal: [1.0.0, latest, nightly] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Install Crystal 20 | uses: crystal-lang/install-crystal@v1 21 | with: 22 | crystal: ${{ matrix.crystal }} 23 | 24 | - name: Download source 25 | uses: actions/checkout@v2 26 | 27 | - name: Install shards 28 | run: shards install 29 | 30 | - name: Run specs 31 | run: crystal spec 32 | 33 | - name: Check formatting 34 | run: crystal tool format; git diff --exit-code 35 | if: matrix.crystal == 'latest' && matrix.os == 'ubuntu-latest' 36 | -------------------------------------------------------------------------------- /spec/pool_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe DB::Pool do 4 | it "should write from multiple connections" do 5 | channel = Channel(Nil).new 6 | fibers = 5 7 | max_n = 50 8 | with_db "#{DB_FILENAME}?max_pool_size=#{fibers}" do |db| 9 | db.exec "create table numbers (n integer, fiber integer)" 10 | 11 | fibers.times do |f| 12 | spawn do 13 | (1..max_n).each do |n| 14 | db.exec "insert into numbers (n, fiber) values (?, ?)", n, f 15 | sleep 0.01 16 | end 17 | channel.send nil 18 | end 19 | end 20 | 21 | fibers.times { channel.receive } 22 | 23 | # all numbers were inserted 24 | s = fibers * max_n * (max_n + 1) // 2 25 | db.scalar("select sum(n) from numbers").should eq(s) 26 | 27 | # numbers were not inserted one fiber at a time 28 | rows = db.query_all "select n, fiber from numbers", as: {Int32, Int32} 29 | rows.map(&.[1]).should_not eq(rows.map(&.[1]).sort) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Brian J. Cardiff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/sqlite3/flags.cr: -------------------------------------------------------------------------------- 1 | @[Flags] 2 | enum SQLite3::Flag 3 | READONLY = 0x00000001 # Ok for sqlite3_open_v2() 4 | READWRITE = 0x00000002 # Ok for sqlite3_open_v2() 5 | CREATE = 0x00000004 # Ok for sqlite3_open_v2() 6 | DELETEONCLOSE = 0x00000008 # VFS only 7 | EXCLUSIVE = 0x00000010 # VFS only 8 | AUTOPROXY = 0x00000020 # VFS only 9 | URI = 0x00000040 # Ok for sqlite3_open_v2() 10 | MEMORY = 0x00000080 # Ok for sqlite3_open_v2() 11 | MAIN_DB = 0x00000100 # VFS only 12 | TEMP_DB = 0x00000200 # VFS only 13 | TRANSIENT_DB = 0x00000400 # VFS only 14 | MAIN_JOURNAL = 0x00000800 # VFS only 15 | TEMP_JOURNAL = 0x00001000 # VFS only 16 | SUBJOURNAL = 0x00002000 # VFS only 17 | MASTER_JOURNAL = 0x00004000 # VFS only 18 | NOMUTEX = 0x00008000 # Ok for sqlite3_open_v2() 19 | FULLMUTEX = 0x00010000 # Ok for sqlite3_open_v2() 20 | SHAREDCACHE = 0x00020000 # Ok for sqlite3_open_v2() 21 | PRIVATECACHE = 0x00040000 # Ok for sqlite3_open_v2() 22 | WAL = 0x00080000 # VFS only 23 | end 24 | 25 | module SQLite3 26 | # Same as doing SQLite3::Flag.flag(*values) 27 | macro flags(*values) 28 | ::SQLite3::Flag.flags({{*values}}) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/driver_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | def assert_filename(uri, filename) 4 | SQLite3::Connection.filename(URI.parse(uri)).should eq(filename) 5 | end 6 | 7 | describe Driver do 8 | it "should register sqlite3 name" do 9 | DB.driver_class("sqlite3").should eq(SQLite3::Driver) 10 | end 11 | 12 | it "should get filename from uri" do 13 | assert_filename("sqlite3:%3Amemory%3A", ":memory:") 14 | assert_filename("sqlite3://%3Amemory%3A", ":memory:") 15 | 16 | assert_filename("sqlite3:./file.db", "./file.db") 17 | assert_filename("sqlite3://./file.db", "./file.db") 18 | 19 | assert_filename("sqlite3:/path/to/file.db", "/path/to/file.db") 20 | assert_filename("sqlite3:///path/to/file.db", "/path/to/file.db") 21 | 22 | assert_filename("sqlite3:./file.db?max_pool_size=5", "./file.db") 23 | assert_filename("sqlite3:/path/to/file.db?max_pool_size=5", "/path/to/file.db") 24 | assert_filename("sqlite3://./file.db?max_pool_size=5", "./file.db") 25 | assert_filename("sqlite3:///path/to/file.db?max_pool_size=5", "/path/to/file.db") 26 | end 27 | 28 | it "should use database option as file to open" do 29 | with_db do |db| 30 | db.checkout.should be_a(SQLite3::Connection) 31 | File.exists?(DB_FILENAME).should be_true 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | crystal: manastech/crystal@1.0 5 | 6 | commands: 7 | install-sqlite: 8 | steps: 9 | - run: 10 | name: Install `sqlite` 11 | command: apt-get update && apt-get install -y libsqlite3-dev 12 | 13 | jobs: 14 | test: 15 | parameters: 16 | executor: 17 | type: executor 18 | default: crystal/default 19 | executor: << parameters.executor >> 20 | steps: 21 | - install-sqlite 22 | - crystal/version 23 | - checkout 24 | - crystal/with-shards-cache: 25 | steps: 26 | - crystal/shards-install 27 | - crystal/spec 28 | - crystal/format-check 29 | 30 | executors: 31 | nightly: 32 | docker: 33 | - image: 'crystallang/crystal:nightly' 34 | environment: 35 | SHARDS_OPTS: --ignore-crystal-version 36 | 37 | workflows: 38 | version: 2 39 | 40 | build: 41 | jobs: 42 | - test 43 | - test: 44 | name: test-on-nightly 45 | executor: 46 | name: nightly 47 | 48 | nightly: 49 | triggers: 50 | - schedule: 51 | cron: "0 3 * * *" 52 | filters: 53 | branches: 54 | only: 55 | - master 56 | jobs: 57 | - test: 58 | name: test-on-nightly 59 | executor: 60 | name: nightly 61 | 62 | -------------------------------------------------------------------------------- /spec/result_set_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe SQLite3::ResultSet do 4 | it "reads integer data types" do 5 | with_db do |db| 6 | db.exec "CREATE TABLE test_table (test_int integer)" 7 | db.exec "INSERT INTO test_table (test_int) values (?)", 42 8 | db.query("SELECT test_int FROM test_table") do |rs| 9 | rs.each do 10 | rs.read.should eq(42) 11 | end 12 | end 13 | end 14 | end 15 | 16 | it "reads string data types" do 17 | with_db do |db| 18 | db.exec "CREATE TABLE test_table (test_text text)" 19 | db.exec "INSERT INTO test_table (test_text) values (?), (?)", "abc", "123" 20 | db.query("SELECT test_text FROM test_table") do |rs| 21 | rs.each do 22 | rs.read.should match(/abc|123/) 23 | end 24 | end 25 | end 26 | end 27 | 28 | it "reads time data types" do 29 | with_db do |db| 30 | db.exec "CREATE TABLE test_table (test_date datetime)" 31 | timestamp = Time.utc 32 | db.exec "INSERT INTO test_table (test_date) values (current_timestamp)" 33 | db.query("SELECT test_date FROM test_table") do |rs| 34 | rs.each do 35 | rs.read(Time).should be_close(timestamp, 1.second) 36 | end 37 | end 38 | end 39 | end 40 | 41 | it "reads time stored in text fields, too" do 42 | with_db do |db| 43 | db.exec "CREATE TABLE test_table (test_date text)" 44 | timestamp = Time.utc 45 | # Try 3 different ways: our own two formats and using SQLite's current_timestamp. 46 | # They should all work. 47 | db.exec "INSERT INTO test_table (test_date) values (?)", timestamp.to_s SQLite3::DATE_FORMAT_SUBSECOND 48 | db.exec "INSERT INTO test_table (test_date) values (?)", timestamp.to_s SQLite3::DATE_FORMAT_SECOND 49 | db.exec "INSERT INTO test_table (test_date) values (current_timestamp)" 50 | db.query("SELECT test_date FROM test_table") do |rs| 51 | rs.each do 52 | rs.read(Time).should be_close(timestamp, 1.second) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /compile_and_link_sqlite.md: -------------------------------------------------------------------------------- 1 | # How to Compile And Link SQLite 2 | 3 | There are two main reasons to compile SQLite from source and they are both about getting features that are otherwise unavailable. 4 | 5 | - You may need a feature from a release that haven't made it to your distro yet or you want to use the latest code from development. 6 | - Perhaps you want some compile time features enabled that are not commonly enabled by default. 7 | 8 | This guide assumes the first reason and goes through how to compile the latest release. 9 | 10 | 11 | ## Install Prerequisites (Ubuntu) 12 | 13 | On Ubuntu you will need build-essential installed at a minimum. 14 | 15 | ```sh 16 | sudo apt update 17 | sudo apt install build-essential 18 | ``` 19 | 20 | 21 | ## Download And Extract The Source Code 22 | 23 | Source code for the latest release can be downloaded from the [SQLite Download Page](https://sqlite.org/download.html). 24 | Look for "C source code as an amalgamation", It should be the first one on the page. 25 | 26 | ```sh 27 | wget https://sqlite.org/2021/sqlite-amalgamation-3370000.zip 28 | unzip sqlite-amalgamation-3370000.zip 29 | cd sqlite-amalgamation-3370000 30 | ``` 31 | 32 | 33 | ## Compile SQLite 34 | 35 | Compile the sqlite command. 36 | 37 | ```sh 38 | gcc shell.c sqlite3.c -lpthread -ldl -o sqlite3 39 | ./sqlite3 --version 40 | ``` 41 | 42 | Compile libsqlite. 43 | 44 | ```sh 45 | gcc -lpthread -ldl -shared -o libsqlite3.so.0 -fPIC sqlite3.c 46 | ``` 47 | 48 | ## Using The New Version of SQLite 49 | 50 | The path to libsqlite can be specified at runtime with "LD_LIBRARY_PATH". 51 | 52 | ```sh 53 | # directory of your crystal app 54 | cd ../app 55 | 56 | # Crystal run 57 | LD_LIBRARY_PATH=../sqlite-amalgamation-3370000 crystal run src/app.cr 58 | 59 | # This way will allow specifying the library location at runtime if it is different from the system default. 60 | crystal build --release --link-flags -L"$(realpath ../sqlite-amalgamation-3370000/libsqlite3.so.0)" src/app.cr 61 | LD_LIBRARY_PATH=../sqlite-amalgamation-3370000 ./app 62 | 63 | # ldd can be used to see which libsqlite is being linked 64 | LD_LIBRARY_PATH=../sqlite-amalgamation-3370000 ldd ./app 65 | ``` 66 | 67 | Or the absolute path to libsqlite can be specified at compile time. 68 | 69 | ```sh 70 | crystal run --link-flags "$(realpath ../sqlite-amalgamation-3370000/libsqlite3.so.0)" src/app.cr 71 | 72 | # This will create a version that only works if libsqlite in the excact same location as when it was compiled. 73 | crystal build --release --link-flags "$(realpath ../sqlite-amalgamation-3370000/libsqlite3.so.0)" src/app.cr 74 | ./app 75 | 76 | # Use ldd to see which libsqlite is being linked 77 | ldd ./app 78 | ``` 79 | 80 | 81 | ## Check SQLite Version From Crystal 82 | 83 | To check which version of SQLite is being used from Crystal. 84 | 85 | ```crystal 86 | # src/app.cr 87 | 88 | DB_URI = "sqlite3://:memory:" 89 | 90 | DB.open DB_URI do |db| 91 | db_version = db.scalar "select sqlite_version();" 92 | puts "SQLite #{db_version}" 93 | end 94 | ``` 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/crystal-lang/crystal-sqlite3/actions/workflows/ci.yml/badge.svg)](https://github.com/crystal-lang/crystal-sqlite3/actions/workflows/ci.yml) 2 | 3 | # crystal-sqlite3 4 | 5 | SQLite3 bindings for [Crystal](http://crystal-lang.org/). 6 | 7 | Check [crystal-db](https://github.com/crystal-lang/crystal-db) for general db driver documentation. crystal-sqlite3 driver is registered under `sqlite3://` uri. 8 | 9 | ## Installation 10 | 11 | Add this to your application's `shard.yml`: 12 | 13 | ```yml 14 | dependencies: 15 | sqlite3: 16 | github: crystal-lang/crystal-sqlite3 17 | ``` 18 | 19 | ### Usage 20 | 21 | ```crystal 22 | require "sqlite3" 23 | 24 | DB.open "sqlite3://./data.db" do |db| 25 | db.exec "create table contacts (name text, age integer)" 26 | db.exec "insert into contacts values (?, ?)", "John Doe", 30 27 | 28 | args = [] of DB::Any 29 | args << "Sarah" 30 | args << 33 31 | db.exec "insert into contacts values (?, ?)", args: args 32 | 33 | puts "max age:" 34 | puts db.scalar "select max(age) from contacts" # => 33 35 | 36 | puts "contacts:" 37 | db.query "select name, age from contacts order by age desc" do |rs| 38 | puts "#{rs.column_name(0)} (#{rs.column_name(1)})" 39 | # => name (age) 40 | rs.each do 41 | puts "#{rs.read(String)} (#{rs.read(Int32)})" 42 | # => Sarah (33) 43 | # => John Doe (30) 44 | end 45 | end 46 | end 47 | ``` 48 | 49 | ### DB::Any 50 | 51 | * `Time` is implemented as `TEXT` column using `SQLite3::DATE_FORMAT_SUBSECOND` format (or `SQLite3::DATE_FORMAT_SECOND` if the text does not contain a dot). 52 | * `Bool` is implemented as `INT` column mapping `0`/`1` values. 53 | 54 | ### Setting PRAGMAs 55 | 56 | You can adjust certain [SQLite3 PRAGMAs](https://www.sqlite.org/pragma.html) 57 | automatically when the connection is created by using the query parameters: 58 | 59 | ```crystal 60 | require "sqlite3" 61 | 62 | DB.open "sqlite3://./data.db?journal_mode=wal&synchronous=normal" do |db| 63 | # this database now uses WAL journal and normal synchronous mode 64 | # (defaults were `delete` and `full`, respectively) 65 | end 66 | ``` 67 | 68 | The following is the list of supported options: 69 | 70 | | Name | Connection key | 71 | |---------------------------|-----------------| 72 | | [Busy Timeout][pragma-to] | `busy_timeout` | 73 | | [Cache Size][pragma-cs] | `cache_size` | 74 | | [Foreign Keys][pragma-fk] | `foreign_keys` | 75 | | [Journal Mode][pragma-jm] | `journal_mode` | 76 | | [Synchronous][pragma-sync] | `synchronous` | 77 | | [WAL autocheckoint][pragma-walck] | `wal_autocheckpoint` | 78 | 79 | Please note there values passed using these connection keys are passed 80 | directly to SQLite3 without check or evaluation. Using incorrect values result 81 | in no error by the library. 82 | 83 | [pragma-to]: https://www.sqlite.org/pragma.html#pragma_busy_timeout 84 | [pragma-cs]: https://www.sqlite.org/pragma.html#pragma_cache_size 85 | [pragma-fk]: https://www.sqlite.org/pragma.html#pragma_foreign_keys 86 | [pragma-jm]: https://www.sqlite.org/pragma.html#pragma_journal_mode 87 | [pragma-sync]: https://www.sqlite.org/pragma.html#pragma_synchronous 88 | [pragma-walck]: https://www.sqlite.org/pragma.html#pragma_wal_autocheckpoint 89 | 90 | ## Guides 91 | 92 | - [Compile and link SQLite](compile_and_link_sqlite.md) 93 | -------------------------------------------------------------------------------- /src/sqlite3/statement.cr: -------------------------------------------------------------------------------- 1 | class SQLite3::Statement < DB::Statement 2 | def initialize(connection, command) 3 | super(connection, command) 4 | check LibSQLite3.prepare_v2(sqlite3_connection, command, command.bytesize + 1, out @stmt, nil) 5 | end 6 | 7 | protected def perform_query(args : Enumerable) : DB::ResultSet 8 | LibSQLite3.reset(self) 9 | args.each_with_index(1) do |arg, index| 10 | bind_arg(index, arg) 11 | end 12 | ResultSet.new(self) 13 | end 14 | 15 | protected def perform_exec(args : Enumerable) : DB::ExecResult 16 | LibSQLite3.reset(self.to_unsafe) 17 | args.each_with_index(1) do |arg, index| 18 | bind_arg(index, arg) 19 | end 20 | 21 | # exec 22 | step = uninitialized LibSQLite3::Code 23 | loop do 24 | step = LibSQLite3::Code.new LibSQLite3.step(self) 25 | break unless step == LibSQLite3::Code::ROW 26 | end 27 | raise Exception.new(sqlite3_connection) unless step == LibSQLite3::Code::DONE 28 | 29 | rows_affected = LibSQLite3.changes(sqlite3_connection).to_i64 30 | last_id = LibSQLite3.last_insert_rowid(sqlite3_connection) 31 | 32 | DB::ExecResult.new rows_affected, last_id 33 | end 34 | 35 | protected def do_close 36 | super 37 | check LibSQLite3.finalize(self) 38 | end 39 | 40 | private def bind_arg(index, value : Nil) 41 | check LibSQLite3.bind_null(self, index) 42 | end 43 | 44 | private def bind_arg(index, value : Bool) 45 | check LibSQLite3.bind_int(self, index, value ? 1 : 0) 46 | end 47 | 48 | private def bind_arg(index, value : UInt8) 49 | check LibSQLite3.bind_int(self, index, value.to_i) 50 | end 51 | 52 | private def bind_arg(index, value : UInt16) 53 | check LibSQLite3.bind_int(self, index, value.to_i) 54 | end 55 | 56 | private def bind_arg(index, value : UInt32) 57 | check LibSQLite3.bind_int64(self, index, value.to_i64) 58 | end 59 | 60 | private def bind_arg(index, value : Int8) 61 | check LibSQLite3.bind_int(self, index, value.to_i) 62 | end 63 | 64 | private def bind_arg(index, value : Int16) 65 | check LibSQLite3.bind_int(self, index, value.to_i) 66 | end 67 | 68 | private def bind_arg(index, value : Int32) 69 | check LibSQLite3.bind_int(self, index, value) 70 | end 71 | 72 | private def bind_arg(index, value : Int64) 73 | check LibSQLite3.bind_int64(self, index, value) 74 | end 75 | 76 | private def bind_arg(index, value : Float32) 77 | check LibSQLite3.bind_double(self, index, value.to_f64) 78 | end 79 | 80 | private def bind_arg(index, value : Float64) 81 | check LibSQLite3.bind_double(self, index, value) 82 | end 83 | 84 | private def bind_arg(index, value : String) 85 | check LibSQLite3.bind_text(self, index, value, value.bytesize, nil) 86 | end 87 | 88 | private def bind_arg(index, value : Bytes) 89 | check LibSQLite3.bind_blob(self, index, value, value.size, nil) 90 | end 91 | 92 | private def bind_arg(index, value : Time) 93 | bind_arg(index, value.in(SQLite3::TIME_ZONE).to_s(SQLite3::DATE_FORMAT_SUBSECOND)) 94 | end 95 | 96 | private def bind_arg(index, value) 97 | raise "#{self.class} does not support #{value.class} params" 98 | end 99 | 100 | private def check(code) 101 | raise Exception.new(sqlite3_connection) unless code == 0 102 | end 103 | 104 | protected def sqlite3_connection 105 | @connection.as(Connection) 106 | end 107 | 108 | def to_unsafe 109 | @stmt 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.22.0 (2025-09-05) 2 | 3 | * Add arg support for `Int16`, `Int8`, `UInt16`, `UInt8`. ([#98](https://github.com/crystal-lang/crystal-sqlite3/pull/98), [#99](https://github.com/crystal-lang/crystal-sqlite3/pull/99), thanks @baseballlover723, @bcardiff) 4 | * Add docs on how to compile and link SQLite ([#74](https://github.com/crystal-lang/crystal-sqlite3/pull/74), thanks @chillfox) 5 | * Update to crystal-db ~> 0.14.0. ([#102](https://github.com/crystal-lang/crystal-sqlite3/pull/102), thanks @bcardiff) 6 | * Update formatting ([#100](https://github.com/crystal-lang/crystal-sqlite3/pull/100), thanks @straight-shoota) 7 | 8 | ## v0.21.0 (2023-12-12) 9 | 10 | * Update to crystal-db ~> 0.13.0. ([#94](https://github.com/crystal-lang/crystal-sqlite3/pull/94)) 11 | 12 | ## v0.20.0 (2023-06-23) 13 | 14 | * Update to crystal-db ~> 0.12.0. ([#91](https://github.com/crystal-lang/crystal-sqlite3/pull/91)) 15 | * Fix result set & connection release lifecycle. ([#90](https://github.com/crystal-lang/crystal-sqlite3/pull/90)) 16 | * Automatically set PRAGMAs using connection query params. ([#85](https://github.com/crystal-lang/crystal-sqlite3/pull/85), thanks @luislavena) 17 | 18 | ## v0.19.0 (2022-01-28) 19 | 20 | * Update to crystal-db ~> 0.11.0. ([#77](https://github.com/crystal-lang/crystal-sqlite3/pull/77)) 21 | * Fix timestamps support to allow dealing with exact seconds values ([#68](https://github.com/crystal-lang/crystal-sqlite3/pull/68), thanks @yujiri8, @tenebrousedge) 22 | * Migrate CI to GitHub Actions. ([#78](https://github.com/crystal-lang/crystal-sqlite3/pull/78)) 23 | 24 | This release requires Crystal 1.0.0 or later. 25 | 26 | ## v0.18.0 (2021-01-26) 27 | 28 | * Add `REGEXP` support powered by Crystal's std-lib Regex. ([#62](https://github.com/crystal-lang/crystal-sqlite3/pull/62), thanks @yujiri8) 29 | 30 | ## v0.17.0 (2020-09-30) 31 | 32 | * Update to crystal-db ~> 0.10.0. ([#58](https://github.com/crystal-lang/crystal-sqlite3/pull/58)) 33 | 34 | This release requires Crystal 0.35.0 or later. 35 | 36 | ## v0.16.0 (2020-04-06) 37 | 38 | * Update to crystal-db ~> 0.9.0. ([#55](https://github.com/crystal-lang/crystal-sqlite3/pull/55)) 39 | 40 | ## v0.15.0 (2019-12-11) 41 | 42 | * Update to crystal-db ~> 0.8.0. ([#50](https://github.com/crystal-lang/crystal-sqlite3/pull/50)) 43 | 44 | ## v0.14.0 (2019-09-23) 45 | 46 | * Update to crystal-db ~> 0.7.0. ([#44](https://github.com/crystal-lang/crystal-sqlite3/pull/44)) 47 | 48 | ## v0.13.0 (2019-08-02) 49 | 50 | * Fix compatibility issues for Crystal 0.30.0. ([#43](https://github.com/crystal-lang/crystal-sqlite3/pull/43)) 51 | 52 | ## v0.12.0 (2019-06-07) 53 | 54 | This release requires crystal >= 0.28.0 55 | 56 | * Fix compatibility issues for crystal 0.29.0 ([#40](https://github.com/crystal-lang/crystal-sqlite3/pull/40)) 57 | 58 | ## v0.11.0 (2019-04-18) 59 | 60 | * Fix compatibility issues for crystal 0.28.0 ([#38](https://github.com/crystal-lang/crystal-sqlite3/pull/38)) 61 | * Add complete list of `LibSQLite3::Code` values. ([#36](https://github.com/crystal-lang/crystal-sqlite3/pull/36), thanks @t-richards) 62 | 63 | ## v0.10.0 (2018-06-18) 64 | 65 | * Fix compatibility issues for crystal 0.25.0 ([#34](https://github.com/crystal-lang/crystal-sqlite3/pull/34)) 66 | * All the time instances are translated to UTC before saving them in the db 67 | 68 | ## v0.9.0 (2017-12-31) 69 | 70 | * Update to crystal-db ~> 0.5.0 71 | 72 | ## v0.8.3 (2017-11-07) 73 | 74 | * Update to crystal-db ~> 0.4.1 75 | * Add `SQLite3::VERSION` constant with shard version. 76 | * Add support for multi-steps statements execution. (see [#27](https://github.com/crystal-lang/crystal-sqlite3/pull/27), thanks @t-richards) 77 | * Fix how resources are released. (see [#23](https://github.com/crystal-lang/crystal-sqlite3/pull/23), thanks @benoist) 78 | * Fix blob c bindings. (see [#28](https://github.com/crystal-lang/crystal-sqlite3/pull/28), thanks @rufusroflpunch) 79 | 80 | ## v0.8.2 (2017-03-21) 81 | -------------------------------------------------------------------------------- /src/sqlite3/result_set.cr: -------------------------------------------------------------------------------- 1 | class SQLite3::ResultSet < DB::ResultSet 2 | @column_index = 0 3 | 4 | protected def do_close 5 | LibSQLite3.reset(self) 6 | super 7 | end 8 | 9 | # Advances to the next row. Returns `true` if there's a next row, 10 | # `false` otherwise. Must be called at least once to advance to the first 11 | # row. 12 | def move_next : Bool 13 | @column_index = 0 14 | 15 | case step 16 | when LibSQLite3::Code::ROW 17 | true 18 | when LibSQLite3::Code::DONE 19 | false 20 | else 21 | raise Exception.new(sqlite3_statement.sqlite3_connection) 22 | end 23 | end 24 | 25 | def read 26 | col = @column_index 27 | value = 28 | case LibSQLite3.column_type(self, col) 29 | when Type::INTEGER 30 | LibSQLite3.column_int64(self, col) 31 | when Type::FLOAT 32 | LibSQLite3.column_double(self, col) 33 | when Type::BLOB 34 | blob = LibSQLite3.column_blob(self, col) 35 | bytes = LibSQLite3.column_bytes(self, col) 36 | ptr = Pointer(UInt8).malloc(bytes) 37 | ptr.copy_from(blob, bytes) 38 | Bytes.new(ptr, bytes) 39 | when Type::TEXT 40 | String.new(LibSQLite3.column_text(self, col)) 41 | when Type::NULL 42 | nil 43 | else 44 | raise Exception.new(sqlite3_statement.sqlite3_connection) 45 | end 46 | @column_index += 1 47 | value 48 | end 49 | 50 | def next_column_index : Int32 51 | @column_index 52 | end 53 | 54 | def read(t : UInt8.class) : UInt8 55 | read(Int64).to_u8 56 | end 57 | 58 | def read(type : UInt8?.class) : UInt8? 59 | read(Int64?).try &.to_u8 60 | end 61 | 62 | def read(t : UInt16.class) : UInt16 63 | read(Int64).to_u16 64 | end 65 | 66 | def read(type : UInt16?.class) : UInt16? 67 | read(Int64?).try &.to_u16 68 | end 69 | 70 | def read(t : UInt32.class) : UInt32 71 | read(Int64).to_u32 72 | end 73 | 74 | def read(type : UInt32?.class) : UInt32? 75 | read(Int64?).try &.to_u32 76 | end 77 | 78 | def read(t : Int8.class) : Int8 79 | read(Int64).to_i8 80 | end 81 | 82 | def read(type : Int8?.class) : Int8? 83 | read(Int64?).try &.to_i8 84 | end 85 | 86 | def read(t : Int16.class) : Int16 87 | read(Int64).to_i16 88 | end 89 | 90 | def read(type : Int16?.class) : Int16? 91 | read(Int64?).try &.to_i16 92 | end 93 | 94 | def read(t : Int32.class) : Int32 95 | read(Int64).to_i32 96 | end 97 | 98 | def read(type : Int32?.class) : Int32? 99 | read(Int64?).try &.to_i32 100 | end 101 | 102 | def read(t : Float32.class) : Float32 103 | read(Float64).to_f32 104 | end 105 | 106 | def read(type : Float32?.class) : Float32? 107 | read(Float64?).try &.to_f32 108 | end 109 | 110 | def read(t : Time.class) : Time 111 | text = read(String) 112 | if text.includes? "." 113 | Time.parse text, SQLite3::DATE_FORMAT_SUBSECOND, location: SQLite3::TIME_ZONE 114 | else 115 | Time.parse text, SQLite3::DATE_FORMAT_SECOND, location: SQLite3::TIME_ZONE 116 | end 117 | end 118 | 119 | def read(t : Time?.class) : Time? 120 | read(String?).try { |v| 121 | if v.includes? "." 122 | Time.parse v, SQLite3::DATE_FORMAT_SUBSECOND, location: SQLite3::TIME_ZONE 123 | else 124 | Time.parse v, SQLite3::DATE_FORMAT_SECOND, location: SQLite3::TIME_ZONE 125 | end 126 | } 127 | end 128 | 129 | def read(t : Bool.class) : Bool 130 | read(Int64) != 0 131 | end 132 | 133 | def read(t : Bool?.class) : Bool? 134 | read(Int64?).try &.!=(0) 135 | end 136 | 137 | def column_count : Int32 138 | LibSQLite3.column_count(self) 139 | end 140 | 141 | def column_name(index) : String 142 | String.new LibSQLite3.column_name(self, index) 143 | end 144 | 145 | def to_unsafe 146 | sqlite3_statement.to_unsafe 147 | end 148 | 149 | # :nodoc: 150 | private def step 151 | LibSQLite3::Code.new LibSQLite3.step(sqlite3_statement) 152 | end 153 | 154 | protected def sqlite3_statement 155 | @statement.as(Statement) 156 | end 157 | 158 | private def moving_column(&) 159 | res = yield @column_index 160 | @column_index += 1 161 | res 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /spec/connection_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | private def dump(source, target) 4 | source.using_connection do |conn| 5 | conn = conn.as(SQLite3::Connection) 6 | target.using_connection do |backup_conn| 7 | backup_conn = backup_conn.as(SQLite3::Connection) 8 | conn.dump(backup_conn) 9 | end 10 | end 11 | end 12 | 13 | private def it_sets_pragma_on_connection(pragma : String, value : String, expected, file = __FILE__, line = __LINE__) 14 | it "sets pragma '#{pragma}' to #{expected}", file, line do 15 | with_db("#{DB_FILENAME}?#{pragma}=#{value}") do |db| 16 | db.scalar("PRAGMA #{pragma}").should eq(expected) 17 | end 18 | end 19 | end 20 | 21 | describe Connection do 22 | it "opens a database and then backs it up to another db" do 23 | with_db do |db| 24 | with_db("./test2.db") do |backup_db| 25 | db.exec "create table person (name text, age integer)" 26 | db.exec "insert into person values (\"foo\", 10)" 27 | 28 | dump db, backup_db 29 | 30 | backup_name = backup_db.scalar "select name from person" 31 | backup_age = backup_db.scalar "select age from person" 32 | source_name = db.scalar "select name from person" 33 | source_age = db.scalar "select age from person" 34 | 35 | {backup_name, backup_age}.should eq({source_name, source_age}) 36 | end 37 | end 38 | end 39 | 40 | it "opens a database, inserts records, dumps to an in-memory db, insers some more, then dumps to the source" do 41 | with_db do |db| 42 | with_mem_db do |in_memory_db| 43 | db.exec "create table person (name text, age integer)" 44 | db.exec "insert into person values (\"foo\", 10)" 45 | dump db, in_memory_db 46 | 47 | in_memory_db.scalar("select count(*) from person").should eq(1) 48 | in_memory_db.exec "insert into person values (\"bar\", 22)" 49 | dump in_memory_db, db 50 | 51 | db.scalar("select count(*) from person").should eq(2) 52 | end 53 | end 54 | end 55 | 56 | it "opens a database, inserts records (>1024K), and dumps to an in-memory db" do 57 | with_db do |db| 58 | with_mem_db do |in_memory_db| 59 | db.exec "create table person (name text, age integer)" 60 | db.transaction do |tx| 61 | 100_000.times { tx.connection.exec "insert into person values (\"foo\", 10)" } 62 | end 63 | dump db, in_memory_db 64 | in_memory_db.scalar("select count(*) from person").should eq(100_000) 65 | end 66 | end 67 | end 68 | 69 | it "opens a connection without the pool" do 70 | with_cnn do |cnn| 71 | cnn.should be_a(SQLite3::Connection) 72 | 73 | cnn.exec "create table person (name text, age integer)" 74 | cnn.exec "insert into person values (\"foo\", 10)" 75 | 76 | cnn.scalar("select count(*) from person").should eq(1) 77 | end 78 | end 79 | 80 | # adjust busy_timeout pragma (default is 0) 81 | it_sets_pragma_on_connection "busy_timeout", "1000", 1000 82 | 83 | # adjust cache_size pragma (default is -2000, 2MB) 84 | it_sets_pragma_on_connection "cache_size", "-4000", -4000 85 | 86 | # enable foreign_keys, no need to test off (is the default) 87 | it_sets_pragma_on_connection "foreign_keys", "1", 1 88 | it_sets_pragma_on_connection "foreign_keys", "yes", 1 89 | it_sets_pragma_on_connection "foreign_keys", "true", 1 90 | it_sets_pragma_on_connection "foreign_keys", "on", 1 91 | 92 | # change journal_mode (default is delete) 93 | it_sets_pragma_on_connection "journal_mode", "delete", "delete" 94 | it_sets_pragma_on_connection "journal_mode", "truncate", "truncate" 95 | it_sets_pragma_on_connection "journal_mode", "persist", "persist" 96 | 97 | # change synchronous mode (default is 2, FULL) 98 | it_sets_pragma_on_connection "synchronous", "0", 0 99 | it_sets_pragma_on_connection "synchronous", "off", 0 100 | it_sets_pragma_on_connection "synchronous", "1", 1 101 | it_sets_pragma_on_connection "synchronous", "normal", 1 102 | it_sets_pragma_on_connection "synchronous", "2", 2 103 | it_sets_pragma_on_connection "synchronous", "full", 2 104 | it_sets_pragma_on_connection "synchronous", "3", 3 105 | it_sets_pragma_on_connection "synchronous", "extra", 3 106 | 107 | # change wal_autocheckpoint (default is 1000) 108 | it_sets_pragma_on_connection "wal_autocheckpoint", "0", 0 109 | end 110 | -------------------------------------------------------------------------------- /src/sqlite3/connection.cr: -------------------------------------------------------------------------------- 1 | class SQLite3::Connection < DB::Connection 2 | record Options, 3 | filename : String = ":memory:", 4 | # pragmas 5 | busy_timeout : String? = nil, 6 | cache_size : String? = nil, 7 | foreign_keys : String? = nil, 8 | journal_mode : String? = nil, 9 | synchronous : String? = nil, 10 | wal_autocheckpoint : String? = nil do 11 | def self.from_uri(uri : URI, default = Options.new) 12 | params = HTTP::Params.parse(uri.query || "") 13 | 14 | Options.new( 15 | filename: URI.decode_www_form((uri.hostname || "") + uri.path), 16 | # pragmas 17 | busy_timeout: params.fetch("busy_timeout", default.busy_timeout), 18 | cache_size: params.fetch("cache_size", default.cache_size), 19 | foreign_keys: params.fetch("foreign_keys", default.foreign_keys), 20 | journal_mode: params.fetch("journal_mode", default.journal_mode), 21 | synchronous: params.fetch("synchronous", default.synchronous), 22 | wal_autocheckpoint: params.fetch("wal_autocheckpoint", default.wal_autocheckpoint), 23 | ) 24 | end 25 | 26 | def pragma_statement 27 | res = String.build do |str| 28 | pragma_append(str, "busy_timeout", busy_timeout) 29 | pragma_append(str, "cache_size", cache_size) 30 | pragma_append(str, "foreign_keys", foreign_keys) 31 | pragma_append(str, "journal_mode", journal_mode) 32 | pragma_append(str, "synchronous", synchronous) 33 | pragma_append(str, "wal_autocheckpoint", wal_autocheckpoint) 34 | end 35 | 36 | res.empty? ? nil : res 37 | end 38 | 39 | private def pragma_append(io, key, value) 40 | return unless value 41 | io << "PRAGMA #{key}=#{value};" 42 | end 43 | end 44 | 45 | def initialize(options : ::DB::Connection::Options, sqlite3_options : Options) 46 | super(options) 47 | check LibSQLite3.open_v2(sqlite3_options.filename, out @db, (Flag::READWRITE | Flag::CREATE), nil) 48 | # 2 means 2 arguments; 1 is the code for UTF-8 49 | check LibSQLite3.create_function(@db, "regexp", 2, 1, nil, SQLite3::REGEXP_FN, nil, nil) 50 | 51 | if pragma_statement = sqlite3_options.pragma_statement 52 | check LibSQLite3.exec(@db, pragma_statement, nil, nil, nil) 53 | end 54 | rescue 55 | raise DB::ConnectionRefused.new 56 | end 57 | 58 | def self.filename(uri : URI) 59 | URI.decode_www_form((uri.hostname || "") + uri.path) 60 | end 61 | 62 | def build_prepared_statement(query) : Statement 63 | Statement.new(self, query) 64 | end 65 | 66 | def build_unprepared_statement(query) : Statement 67 | # sqlite3 does not support unprepared statement. 68 | # All statements once prepared should be released 69 | # when unneeded. Unprepared statement are not aim 70 | # to leave state in the connection. Mimicking them 71 | # with prepared statement would be wrong with 72 | # respect connection resources. 73 | raise DB::Error.new("SQLite3 driver does not support unprepared statements") 74 | end 75 | 76 | def do_close 77 | super 78 | check LibSQLite3.close(self) 79 | end 80 | 81 | # :nodoc: 82 | def perform_begin_transaction 83 | self.prepared.exec "BEGIN" 84 | end 85 | 86 | # :nodoc: 87 | def perform_commit_transaction 88 | self.prepared.exec "COMMIT" 89 | end 90 | 91 | # :nodoc: 92 | def perform_rollback_transaction 93 | self.prepared.exec "ROLLBACK" 94 | end 95 | 96 | # :nodoc: 97 | def perform_create_savepoint(name) 98 | self.prepared.exec "SAVEPOINT #{name}" 99 | end 100 | 101 | # :nodoc: 102 | def perform_release_savepoint(name) 103 | self.prepared.exec "RELEASE SAVEPOINT #{name}" 104 | end 105 | 106 | # :nodoc: 107 | def perform_rollback_savepoint(name) 108 | self.prepared.exec "ROLLBACK TO #{name}" 109 | end 110 | 111 | # Dump the database to another SQLite3 database. This can be used for backing up a SQLite3 Database 112 | # to disk or the opposite 113 | def dump(to : SQLite3::Connection) 114 | backup_item = LibSQLite3.backup_init(to.@db, "main", @db, "main") 115 | if backup_item.null? 116 | raise Exception.new(to.@db) 117 | end 118 | code = LibSQLite3.backup_step(backup_item, -1) 119 | 120 | if code != LibSQLite3::Code::DONE 121 | raise Exception.new(to.@db) 122 | end 123 | code = LibSQLite3.backup_finish(backup_item) 124 | if code != LibSQLite3::Code::OKAY 125 | raise Exception.new(to.@db) 126 | end 127 | end 128 | 129 | def to_unsafe 130 | @db 131 | end 132 | 133 | private def check(code) 134 | raise Exception.new(self) unless code == 0 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /src/sqlite3/lib_sqlite3.cr: -------------------------------------------------------------------------------- 1 | require "./type" 2 | 3 | @[Link("sqlite3")] 4 | lib LibSQLite3 5 | type SQLite3 = Void* 6 | type Statement = Void* 7 | type SQLite3Backup = Void* 8 | type SQLite3Context = Void* 9 | type SQLite3Value = Void* 10 | 11 | enum Code 12 | # Successful result 13 | OKAY = 0 14 | # Generic error 15 | ERROR = 1 16 | # Internal logic error in SQLite 17 | INTERNAL = 2 18 | # Access permission denied 19 | PERM = 3 20 | # Callback routine requested an abort 21 | ABORT = 4 22 | # The database file is locked 23 | BUSY = 5 24 | # A table in the database is locked 25 | LOCKED = 6 26 | # A malloc() failed 27 | NOMEM = 7 28 | # Attempt to write a readonly database 29 | READONLY = 8 30 | # Operation terminated by sqlite3_interrupt() 31 | INTERRUPT = 9 32 | # Some kind of disk I/O error occurred 33 | IOERR = 10 34 | # The database disk image is malformed 35 | CORRUPT = 11 36 | # Unknown opcode in sqlite3_file_control() 37 | NOTFOUND = 12 38 | # Insertion failed because database is full 39 | FULL = 13 40 | # Unable to open the database file 41 | CANTOPEN = 14 42 | # Database lock protocol error 43 | PROTOCOL = 15 44 | # Internal use only 45 | EMPTY = 16 46 | # The database schema changed 47 | SCHEMA = 17 48 | # String or BLOB exceeds size limit 49 | TOOBIG = 18 50 | # Abort due to constraint violation 51 | CONSTRAINT = 19 52 | # Data type mismatch 53 | MISMATCH = 20 54 | # Library used incorrectly 55 | MISUSE = 21 56 | # Uses OS features not supported on host 57 | NOLFS = 22 58 | # Authorization denied 59 | AUTH = 23 60 | # Not used 61 | FORMAT = 24 62 | # 2nd parameter to sqlite3_bind out of range 63 | RANGE = 25 64 | # File opened that is not a database file 65 | NOTADB = 26 66 | # Notifications from sqlite3_log() 67 | NOTICE = 27 68 | # Warnings from sqlite3_log() 69 | WARNING = 28 70 | # sqlite3_step() has another row ready 71 | ROW = 100 72 | # sqlite3_step() has finished executing 73 | DONE = 101 74 | end 75 | 76 | alias Callback = (Void*, Int32, UInt8**, UInt8**) -> Int32 77 | alias FuncCallback = (SQLite3Context, Int32, SQLite3Value*) -> Void 78 | 79 | fun open_v2 = sqlite3_open_v2(filename : UInt8*, db : SQLite3*, flags : ::SQLite3::Flag, zVfs : UInt8*) : Int32 80 | 81 | fun errcode = sqlite3_errcode(SQLite3) : Int32 82 | fun errmsg = sqlite3_errmsg(SQLite3) : UInt8* 83 | 84 | fun backup_init = sqlite3_backup_init(SQLite3, UInt8*, SQLite3, UInt8*) : SQLite3Backup 85 | fun backup_step = sqlite3_backup_step(SQLite3Backup, Int32) : Code 86 | fun backup_finish = sqlite3_backup_finish(SQLite3Backup) : Code 87 | 88 | fun prepare_v2 = sqlite3_prepare_v2(db : SQLite3, zSql : UInt8*, nByte : Int32, ppStmt : Statement*, pzTail : UInt8**) : Int32 89 | fun exec = sqlite3_exec(db : SQLite3, zSql : UInt8*, pCallback : Callback, pCallbackArgs : Void*, pzErrMsg : UInt8**) : Int32 90 | fun step = sqlite3_step(stmt : Statement) : Int32 91 | fun column_count = sqlite3_column_count(stmt : Statement) : Int32 92 | fun column_type = sqlite3_column_type(stmt : Statement, iCol : Int32) : ::SQLite3::Type 93 | fun column_int64 = sqlite3_column_int64(stmt : Statement, iCol : Int32) : Int64 94 | fun column_double = sqlite3_column_double(stmt : Statement, iCol : Int32) : Float64 95 | fun column_text = sqlite3_column_text(stmt : Statement, iCol : Int32) : UInt8* 96 | fun column_bytes = sqlite3_column_bytes(stmt : Statement, iCol : Int32) : Int32 97 | fun column_blob = sqlite3_column_blob(stmt : Statement, iCol : Int32) : UInt8* 98 | 99 | fun bind_int = sqlite3_bind_int(stmt : Statement, idx : Int32, value : Int32) : Int32 100 | fun bind_int64 = sqlite3_bind_int64(stmt : Statement, idx : Int32, value : Int64) : Int32 101 | fun bind_text = sqlite3_bind_text(stmt : Statement, idx : Int32, value : UInt8*, bytes : Int32, destructor : Void* ->) : Int32 102 | fun bind_blob = sqlite3_bind_blob(stmt : Statement, idx : Int32, value : UInt8*, bytes : Int32, destructor : Void* ->) : Int32 103 | fun bind_null = sqlite3_bind_null(stmt : Statement, idx : Int32) : Int32 104 | fun bind_double = sqlite3_bind_double(stmt : Statement, idx : Int32, value : Float64) : Int32 105 | 106 | fun bind_parameter_index = sqlite3_bind_parameter_index(stmt : Statement, name : UInt8*) : Int32 107 | fun reset = sqlite3_reset(stmt : Statement) : Int32 108 | fun column_name = sqlite3_column_name(stmt : Statement, idx : Int32) : UInt8* 109 | fun last_insert_rowid = sqlite3_last_insert_rowid(db : SQLite3) : Int64 110 | fun changes = sqlite3_changes(db : SQLite3) : Int32 111 | 112 | fun finalize = sqlite3_finalize(stmt : Statement) : Int32 113 | fun close_v2 = sqlite3_close_v2(SQLite3) : Int32 114 | fun close = sqlite3_close(SQLite3) : Int32 115 | 116 | fun create_function = sqlite3_create_function(SQLite3, funcName : UInt8*, nArg : Int32, eTextRep : Int32, pApp : Void*, xFunc : FuncCallback, xStep : Void*, xFinal : Void*) : Int32 117 | fun value_text = sqlite3_value_text(SQLite3Value) : UInt8* 118 | fun result_int = sqlite3_result_int(SQLite3Context, Int32) : Nil 119 | end 120 | -------------------------------------------------------------------------------- /spec/db_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | require "db/spec" 3 | 4 | private class NotSupportedType 5 | end 6 | 7 | private def cast_if_blob(expr, sql_type) 8 | case sql_type 9 | when "blob" 10 | "cast(#{expr} as blob)" 11 | else 12 | expr 13 | end 14 | end 15 | 16 | DB::DriverSpecs(SQLite3::Any).run do |ctx| 17 | support_unprepared false 18 | 19 | before do 20 | File.delete(DB_FILENAME) if File.exists?(DB_FILENAME) 21 | end 22 | after do 23 | File.delete(DB_FILENAME) if File.exists?(DB_FILENAME) 24 | end 25 | 26 | connection_string "sqlite3:#{DB_FILENAME}" 27 | # ? can use many ... (:memory:) 28 | 29 | sample_value true, "int", "1", type_safe_value: false 30 | sample_value false, "int", "0", type_safe_value: false 31 | sample_value 2, "int", "2", type_safe_value: false 32 | sample_value 1_i64, "int", "1" 33 | sample_value "hello", "text", "'hello'" 34 | sample_value 1.5_f32, "float", "1.5", type_safe_value: false 35 | sample_value 1.5, "float", "1.5" 36 | sample_value Time.utc(2016, 2, 15), "text", "'2016-02-15 00:00:00.000'", type_safe_value: false 37 | sample_value Time.utc(2016, 2, 15, 10, 15, 30), "text", "'2016-02-15 10:15:30'", type_safe_value: false 38 | sample_value Time.utc(2016, 2, 15, 10, 15, 30), "text", "'2016-02-15 10:15:30.000'", type_safe_value: false 39 | sample_value Time.utc(2016, 2, 15, 10, 15, 30, nanosecond: 123000000), "text", "'2016-02-15 10:15:30.123'", type_safe_value: false 40 | sample_value Time.local(2016, 2, 15, 7, 15, 30, location: Time::Location.fixed("fixed", -3*3600)), "text", "'2016-02-15 10:15:30.000'", type_safe_value: false 41 | sample_value Int8::MIN, "int", Int8::MIN.to_s, type_safe_value: false 42 | sample_value Int8::MAX, "int", Int8::MAX.to_s, type_safe_value: false 43 | sample_value UInt8::MIN, "int", UInt8::MIN.to_s, type_safe_value: false 44 | sample_value UInt8::MAX, "int", UInt8::MAX.to_s, type_safe_value: false 45 | sample_value Int16::MIN, "int", Int16::MIN.to_s, type_safe_value: false 46 | sample_value Int16::MAX, "int", Int16::MAX.to_s, type_safe_value: false 47 | sample_value UInt16::MIN, "int", UInt16::MIN.to_s, type_safe_value: false 48 | sample_value UInt16::MAX, "int", UInt16::MAX.to_s, type_safe_value: false 49 | sample_value Int32::MIN, "int", Int32::MIN.to_s, type_safe_value: false 50 | sample_value Int32::MAX, "int", Int32::MAX.to_s, type_safe_value: false 51 | sample_value UInt32::MIN, "int", UInt32::MIN.to_s, type_safe_value: false 52 | sample_value UInt32::MAX, "int", UInt32::MAX.to_s, type_safe_value: false 53 | sample_value Int64::MIN, "int", Int64::MIN.to_s, type_safe_value: false 54 | sample_value Int64::MAX, "int", Int64::MAX.to_s, type_safe_value: false 55 | 56 | ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65] 57 | sample_value Bytes.new(ary.to_unsafe, ary.size), "blob", "X'53514C697465'" # , type_safe_value: false 58 | 59 | binding_syntax do |index| 60 | "?" 61 | end 62 | 63 | create_table_1column_syntax do |table_name, col1| 64 | "create table #{table_name} (#{col1.name} #{col1.sql_type} #{col1.null ? "NULL" : "NOT NULL"})" 65 | end 66 | 67 | create_table_2columns_syntax do |table_name, col1, col2| 68 | "create table #{table_name} (#{col1.name} #{col1.sql_type} #{col1.null ? "NULL" : "NOT NULL"}, #{col2.name} #{col2.sql_type} #{col2.null ? "NULL" : "NOT NULL"})" 69 | end 70 | 71 | select_1column_syntax do |table_name, col1| 72 | "select #{cast_if_blob(col1.name, col1.sql_type)} from #{table_name}" 73 | end 74 | 75 | select_2columns_syntax do |table_name, col1, col2| 76 | "select #{cast_if_blob(col1.name, col1.sql_type)}, #{cast_if_blob(col2.name, col2.sql_type)} from #{table_name}" 77 | end 78 | 79 | select_count_syntax do |table_name| 80 | "select count(*) from #{table_name}" 81 | end 82 | 83 | select_scalar_syntax do |expression, sql_type| 84 | "select #{cast_if_blob(expression, sql_type)}" 85 | end 86 | 87 | insert_1column_syntax do |table_name, col, expression| 88 | "insert into #{table_name} (#{col.name}) values (#{expression})" 89 | end 90 | 91 | insert_2columns_syntax do |table_name, col1, expr1, col2, expr2| 92 | "insert into #{table_name} (#{col1.name}, #{col2.name}) values (#{expr1}, #{expr2})" 93 | end 94 | 95 | drop_table_if_exists_syntax do |table_name| 96 | "drop table if exists #{table_name}" 97 | end 98 | 99 | it "gets last insert row id", prepared: :both do |db| 100 | db.exec "create table person (name text, age integer)" 101 | db.exec %(insert into person values ("foo", 10)) 102 | res = db.exec %(insert into person values ("foo", 10)) 103 | res.last_insert_id.should eq(2) 104 | res.rows_affected.should eq(1) 105 | end 106 | 107 | # TODO timestamp support 108 | 109 | it "raises on unsupported param types" do |db| 110 | expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do 111 | db.query "select ?", NotSupportedType.new 112 | end 113 | # TODO raising exception does not close the connection and pool is exhausted 114 | end 115 | 116 | it "ensures statements are closed" do |db| 117 | db.exec %(create table if not exists a (i int not null, str text not null);) 118 | db.exec %(insert into a (i, str) values (23, "bai bai");) 119 | 120 | 2.times do |i| 121 | DB.open ctx.connection_string do |db| 122 | begin 123 | db.query("SELECT i, str FROM a WHERE i = ?", 23) do |rs| 124 | rs.move_next 125 | break 126 | end 127 | rescue e : SQLite3::Exception 128 | fail("Expected no exception, but got \"#{e.message}\"") 129 | end 130 | 131 | begin 132 | db.exec("UPDATE a SET i = ? WHERE i = ?", 23, 23) 133 | rescue e : SQLite3::Exception 134 | fail("Expected no exception, but got \"#{e.message}\"") 135 | end 136 | end 137 | end 138 | end 139 | 140 | it "handles single-step pragma statements" do |db| 141 | db.exec %(PRAGMA synchronous = OFF) 142 | end 143 | 144 | it "handles multi-step pragma statements" do |db| 145 | db.exec %(PRAGMA journal_mode = memory) 146 | end 147 | 148 | it "handles REGEXP operator" do |db| 149 | (db.scalar "select 'unmatching text' REGEXP '^m'").should eq 0 150 | (db.scalar "select 'matching text' REGEXP '^m'").should eq 1 151 | end 152 | end 153 | --------------------------------------------------------------------------------