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
================================================
[](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 <aborenszweig@manas.tech>
- Brian J. Cardiff <bcardiff@gmail.com>
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
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
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (50K chars).
[
{
"path": ".circleci/config.yml",
"chars": 1119,
"preview": "version: 2.1\n\norbs:\n crystal: manastech/crystal@1.0\n\ncommands:\n install-sqlite:\n steps:\n - run:\n name"
},
{
"path": ".github/workflows/ci.yml",
"chars": 758,
"preview": "name: CI\n\non:\n push:\n pull_request:\n branches: [master]\n schedule:\n - cron: '0 6 * * 1' # Every monday 6 AM\n\nj"
},
{
"path": ".gitignore",
"chars": 147,
"preview": "/doc/\n/lib/\n/.crystal/\n/.shards/\n\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in application t"
},
{
"path": "CHANGELOG.md",
"chars": 4338,
"preview": "## v0.23.0 (2026-04-12)\n\n* Use `URI#hostname` to avoid interpreting `:memory:` as `[:memory:]` ([#105](https://github.co"
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Brian J. Cardiff\n\nPermission is hereby granted, free of charge, to any person "
},
{
"path": "README.md",
"chars": 3155,
"preview": "[](https://github.com/crystal-l"
},
{
"path": "compile_and_link_sqlite.md",
"chars": 2631,
"preview": "# How to Compile And Link SQLite\n\nThere are two main reasons to compile SQLite from source and they are both about getti"
},
{
"path": "samples/memory.cr",
"chars": 689,
"preview": "require \"db\"\nrequire \"../src/sqlite3\"\n\nDB.open \"sqlite3://%3Amemory%3A\" do |db|\n db.exec \"create table contacts (name t"
},
{
"path": "shard.yml",
"chars": 252,
"preview": "name: sqlite3\nversion: 0.23.0\n\ndependencies:\n db:\n github: crystal-lang/crystal-db\n version: ~> 0.14.0\n\nauthors:\n"
},
{
"path": "spec/connection_spec.cr",
"chars": 3941,
"preview": "require \"./spec_helper\"\n\nprivate def dump(source, target)\n source.using_connection do |conn|\n conn = conn.as(SQLite3"
},
{
"path": "spec/db_spec.cr",
"chars": 5698,
"preview": "require \"./spec_helper\"\nrequire \"db/spec\"\n\nprivate class NotSupportedType\nend\n\nprivate def cast_if_blob(expr, sql_type)\n"
},
{
"path": "spec/driver_spec.cr",
"chars": 1167,
"preview": "require \"./spec_helper\"\n\ndef assert_filename(uri, filename)\n SQLite3::Connection.filename(URI.parse(uri)).should eq(fil"
},
{
"path": "spec/pool_spec.cr",
"chars": 903,
"preview": "require \"./spec_helper\"\n\ndescribe DB::Pool do\n it \"should write from multiple connections\" do\n channel = Channel(Nil"
},
{
"path": "spec/result_set_spec.cr",
"chars": 1866,
"preview": "require \"./spec_helper\"\n\ndescribe SQLite3::ResultSet do\n it \"reads integer data types\" do\n with_db do |db|\n db."
},
{
"path": "spec/spec_helper.cr",
"chars": 733,
"preview": "require \"spec\"\nrequire \"../src/sqlite3\"\n\ninclude SQLite3\n\nDB_FILENAME = \"./test.db\"\n\ndef with_db(&block : DB::Database -"
},
{
"path": "src/sqlite3/connection.cr",
"chars": 4061,
"preview": "class SQLite3::Connection < DB::Connection\n record Options,\n filename : String = \":memory:\",\n # pragmas\n busy_"
},
{
"path": "src/sqlite3/driver.cr",
"chars": 574,
"preview": "class SQLite3::Driver < DB::Driver\n class ConnectionBuilder < ::DB::ConnectionBuilder\n def initialize(@options : ::D"
},
{
"path": "src/sqlite3/exception.cr",
"chars": 266,
"preview": "# Exception thrown on invalid SQLite3 operations.\nclass SQLite3::Exception < ::Exception\n # The internal code associate"
},
{
"path": "src/sqlite3/flags.cr",
"chars": 1130,
"preview": "@[Flags]\nenum SQLite3::Flag\n READONLY = 0x00000001 # Ok for sqlite3_open_v2()\n READWRITE = 0x00000002 # Ok "
},
{
"path": "src/sqlite3/lib_sqlite3.cr",
"chars": 4787,
"preview": "require \"./type\"\n\n@[Link(\"sqlite3\")]\n{% if flag?(:msvc) %}\n @[Link(dll: \"sqlite3.dll\")]\n{% end %}\nlib LibSQLite3\n type"
},
{
"path": "src/sqlite3/result_set.cr",
"chars": 3467,
"preview": "class SQLite3::ResultSet < DB::ResultSet\n @column_index = 0\n\n protected def do_close\n LibSQLite3.reset(self)\n su"
},
{
"path": "src/sqlite3/statement.cr",
"chars": 3602,
"preview": "class SQLite3::Statement < DB::Statement\n # Keep references to bound strings and bytes to prevent them from being\n # g"
},
{
"path": "src/sqlite3/type.cr",
"chars": 144,
"preview": "# Each of the possible types of an SQLite3 column.\nenum SQLite3::Type\n INTEGER = 1\n FLOAT = 2\n BLOB = 4\n NULL "
},
{
"path": "src/sqlite3/version.cr",
"chars": 40,
"preview": "module SQLite3\n VERSION = \"0.23.0\"\nend\n"
},
{
"path": "src/sqlite3.cr",
"chars": 616,
"preview": "require \"db\"\nrequire \"./sqlite3/**\"\n\nmodule SQLite3\n DATE_FORMAT_SUBSECOND = \"%F %H:%M:%S.%L\"\n DATE_FORMAT_SECOND ="
}
]
About this extraction
This page contains the full source code of the crystal-lang/crystal-sqlite3 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (46.1 KB), approximately 14.1k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.