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