mirror of
https://gitea.invidious.io/iv-org/shard-crystal-sqlite3.git
synced 2024-08-15 00:53:26 +00:00
compliant with crystal-db.
Merge branch 'db' into master
This commit is contained in:
commit
fb0f5ea6d7
17 changed files with 637 additions and 736 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1 +1,10 @@
|
|||
.crystal/
|
||||
/doc/
|
||||
/libs/
|
||||
/.crystal/
|
||||
/.shards/
|
||||
|
||||
|
||||
# Libraries don't need dependency lock
|
||||
# Dependencies will be locked in application that uses them
|
||||
/shard.lock
|
||||
|
||||
|
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
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.
|
27
README.md
27
README.md
|
@ -17,11 +17,30 @@ dependencies:
|
|||
### Usage
|
||||
|
||||
```crystal
|
||||
require "db"
|
||||
require "sqlite3"
|
||||
|
||||
db = SQLite3::Database.new( "data.db" )
|
||||
db.execute("select * from table") do |row|
|
||||
p row
|
||||
DB.open "sqlite3://./data.db" do |db|
|
||||
db.exec "create table contacts (name string, 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
|
||||
|
||||
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
|
||||
db.close
|
||||
```
|
||||
|
|
26
samples/memory.cr
Normal file
26
samples/memory.cr
Normal file
|
@ -0,0 +1,26 @@
|
|||
require "db"
|
||||
require "../src/sqlite3"
|
||||
|
||||
DB.open "sqlite3://%3Amemory%3A" do |db|
|
||||
db.exec "create table contacts (name string, 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
|
||||
|
||||
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
|
|
@ -1,6 +1,12 @@
|
|||
name: sqlite3
|
||||
version: 0.4.0
|
||||
|
||||
dependencies:
|
||||
db:
|
||||
github: bcardiff/crystal-db
|
||||
|
||||
authors:
|
||||
- Ary Borenszweig <aborenszweig@manas.com.ar>
|
||||
- Brian J. Cardiff <bcardiff@manas.com.ar>
|
||||
|
||||
license: MIT
|
||||
|
|
47
spec/connection_spec.cr
Normal file
47
spec/connection_spec.cr
Normal file
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
|
||||
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 string, 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 string, 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
|
||||
end
|
|
@ -1,217 +0,0 @@
|
|||
require "./spec_helper"
|
||||
|
||||
DB_FILENAME = "./test.db"
|
||||
|
||||
private def with_db
|
||||
yield Database.new DB_FILENAME
|
||||
ensure
|
||||
File.delete(DB_FILENAME)
|
||||
end
|
||||
|
||||
private def with_db(name)
|
||||
yield Database.new name
|
||||
ensure
|
||||
File.delete(name)
|
||||
end
|
||||
|
||||
describe Database do
|
||||
it "opens a database" do
|
||||
with_db do |db|
|
||||
File.exists?(DB_FILENAME).should be_true
|
||||
end
|
||||
end
|
||||
|
||||
it "opens a database and then backs it up to another db" do
|
||||
with_db do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute "insert into person values (\"foo\", 10)"
|
||||
with_db("./test2.db") do |backup_database|
|
||||
db.dump(backup_database)
|
||||
|
||||
backup_rows = backup_database.execute "select * from person"
|
||||
source_rows = db.execute "select * from person"
|
||||
backup_rows.should eq(source_rows)
|
||||
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|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute "insert into person values (\"foo\", 10)"
|
||||
in_memory_db = Database.new("file:memdb1?mode=memory&cache=shared",
|
||||
SQLite3.flags(URI, CREATE, READWRITE, FULLMUTEX))
|
||||
source_rows = db.execute "select * from person"
|
||||
|
||||
db.dump(in_memory_db)
|
||||
in_memory_db_rows = in_memory_db.execute "select * from person"
|
||||
in_memory_db_rows.should eq(source_rows)
|
||||
in_memory_db.execute "insert into person values (\"bar\", 22)"
|
||||
in_memory_db.dump(db)
|
||||
|
||||
in_memory_db_rows = in_memory_db.execute "select * from person"
|
||||
source_rows = db.execute "select * from person"
|
||||
in_memory_db_rows.should eq(source_rows)
|
||||
end
|
||||
end
|
||||
|
||||
[nil, 1, 1_i64, "hello", 1.5, 1.5_f32].each do |value|
|
||||
it "executes and select #{value}" do
|
||||
with_db(&.execute("select #{value ? value.inspect : "null"}")).should eq([[value]])
|
||||
end
|
||||
|
||||
it "executes with bind #{value}" do
|
||||
with_db(&.execute(%(select ?), value)).should eq([[value]])
|
||||
end
|
||||
|
||||
it "executes with bind #{value} as array" do
|
||||
with_db(&.execute(%(select ?), [value])).should eq([[value]])
|
||||
end
|
||||
end
|
||||
|
||||
it "executes and selects blob" do
|
||||
rows = with_db(&.execute(%(select X'53514C697465')))
|
||||
row = rows[0]
|
||||
cell = row[0].as(Slice(UInt8))
|
||||
cell.to_a.should eq([0x53, 0x51, 0x4C, 0x69, 0x74, 0x65])
|
||||
end
|
||||
|
||||
it "executes with named bind using symbol" do
|
||||
with_db(&.execute(%(select :value), {:value => "hello"})).should eq([["hello"]])
|
||||
end
|
||||
|
||||
it "executes with named bind using string" do
|
||||
with_db(&.execute(%(select :value), {"value" => "hello"})).should eq([["hello"]])
|
||||
end
|
||||
|
||||
it "executes with named bind using named argument" do
|
||||
with_db(&.execute(%(select :value), {value: "hello"})).should eq([["hello"]])
|
||||
end
|
||||
|
||||
it "executes with named bind using named arguments" do
|
||||
with_db(&.execute(%(select :value), value: "hello")).should eq([["hello"]])
|
||||
end
|
||||
|
||||
it "executes with bind blob" do
|
||||
ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]
|
||||
rows = with_db(&.execute(%(select cast(? as BLOB)), Slice.new(ary.to_unsafe, ary.size)))
|
||||
row = rows[0]
|
||||
cell = row[0].as(Slice(UInt8))
|
||||
cell.to_a.should eq(ary)
|
||||
end
|
||||
|
||||
it "gets column names" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
stmt = db.prepare("select * from person")
|
||||
stmt.columns.should eq(%w(name age))
|
||||
stmt.close
|
||||
end
|
||||
end
|
||||
|
||||
it "gets column types" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute %(insert into person values ("foo", 10))
|
||||
stmt = db.prepare("select * from person")
|
||||
stmt.execute
|
||||
stmt.step
|
||||
stmt.types.should eq([Type::TEXT, Type::INTEGER])
|
||||
stmt.close
|
||||
end
|
||||
end
|
||||
|
||||
it "uses named arguments in statement execute" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute %(insert into person values ("foo", 10))
|
||||
db.execute %(insert into person values ("bar", 2))
|
||||
stmt = db.prepare("select * from person where age > :age")
|
||||
stmt.execute age: 5
|
||||
stmt.step
|
||||
stmt["age"].should eq(10)
|
||||
stmt.types.should eq([Type::TEXT, Type::INTEGER])
|
||||
stmt.close
|
||||
end
|
||||
end
|
||||
|
||||
it "gets column by name" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute %(insert into person values ("foo", 10))
|
||||
db.query("select * from person") do |result_set|
|
||||
result_set.next.should be_true
|
||||
result_set["name"].should eq("foo")
|
||||
result_set["age"].should eq(10)
|
||||
expect_raises { result_set["lala"] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "uses named argument in query" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
db.execute %(insert into person values ("foo", 10))
|
||||
db.execute %(insert into person values ("bar", 2))
|
||||
db.query("select * from person where age > :age", age: 5) do |result_set|
|
||||
result_set.next.should be_true
|
||||
result_set["name"].should eq("foo")
|
||||
result_set["age"].should eq(10)
|
||||
expect_raises { result_set["lala"] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gets last insert row id" do
|
||||
Database.new(":memory:") do |db|
|
||||
db.execute "create table person (name string, age integer)"
|
||||
|
||||
db.last_insert_row_id.should eq(0)
|
||||
db.execute %(insert into person values ("foo", 10))
|
||||
db.last_insert_row_id.should eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "quotes" do
|
||||
db = Database.new(":memory:")
|
||||
db.quote("'hello'").should eq("''hello''")
|
||||
end
|
||||
|
||||
it "gets first row" do
|
||||
with_db(&.get_first_row(%(select 1))).should eq([1])
|
||||
end
|
||||
|
||||
it "gets first value" do
|
||||
with_db(&.get_first_value(%(select 1))).should eq(1)
|
||||
end
|
||||
|
||||
it "ensures statements are closed" do
|
||||
begin
|
||||
Database.new(DB_FILENAME) do |db|
|
||||
db.execute(%(create table if not exists a (i int not null, str text not null);))
|
||||
db.execute(%(insert into a (i, str) values (23, "bai bai");))
|
||||
end
|
||||
|
||||
2.times do |i|
|
||||
Database.new(DB_FILENAME) do |db|
|
||||
begin
|
||||
db.query("SELECT i, str FROM a WHERE i = ?", 23) do |rs|
|
||||
rs.next
|
||||
break
|
||||
end
|
||||
rescue e : SQLite3::Exception
|
||||
fail("Expected no exception, but got \"#{e.message}\"")
|
||||
end
|
||||
|
||||
begin
|
||||
db.execute("UPDATE a SET i = ? WHERE i = ?", 23, 23)
|
||||
rescue e : SQLite3::Exception
|
||||
fail("Expected no exception, but got \"#{e.message}\"")
|
||||
end
|
||||
end
|
||||
end
|
||||
ensure
|
||||
File.delete(DB_FILENAME)
|
||||
end
|
||||
end
|
||||
end
|
274
spec/driver_spec.cr
Normal file
274
spec/driver_spec.cr
Normal file
|
@ -0,0 +1,274 @@
|
|||
require "./spec_helper"
|
||||
|
||||
def sql(s : String)
|
||||
"#{s.inspect}"
|
||||
end
|
||||
|
||||
def sql(s)
|
||||
"#{s}"
|
||||
end
|
||||
|
||||
def sqlite_type_for(v)
|
||||
case v
|
||||
when String ; "text"
|
||||
when Int32, Int64 ; "int"
|
||||
when Float32, Float64; "float"
|
||||
when Time ; "text"
|
||||
else
|
||||
raise "not implemented for #{typeof(v)}"
|
||||
end
|
||||
end
|
||||
|
||||
def assert_single_read(rs, value_type, value)
|
||||
rs.move_next.should be_true
|
||||
rs.read(value_type).should eq(value)
|
||||
rs.move_next.should be_false
|
||||
end
|
||||
|
||||
def assert_single_read?(rs, value_type, value)
|
||||
rs.move_next.should be_true
|
||||
rs.read?(value_type).should eq(value)
|
||||
rs.move_next.should be_false
|
||||
end
|
||||
|
||||
def assert_filename(uri, filename)
|
||||
SQLite3::Connection.filename(URI.parse(uri)).should eq(filename)
|
||||
end
|
||||
|
||||
class NotSupportedType
|
||||
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")
|
||||
end
|
||||
|
||||
it "should use database option as file to open" do
|
||||
with_db do |db|
|
||||
db.driver.should be_a(SQLite3::Driver)
|
||||
File.exists?(DB_FILENAME).should be_true
|
||||
end
|
||||
end
|
||||
|
||||
{% for value in [1, 1_i64, "hello", 1.5, 1.5_f32] %}
|
||||
it "executes and select {{value.id}}" do
|
||||
with_db do |db|
|
||||
db.scalar("select #{sql({{value}})}").should eq({{value}})
|
||||
|
||||
db.query "select #{sql({{value}})}" do |rs|
|
||||
assert_single_read rs, typeof({{value}}), {{value}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "executes and select nil as type of {{value.id}}" do
|
||||
with_db do |db|
|
||||
db.scalar("select null").should be_nil
|
||||
|
||||
db.query "select null" do |rs|
|
||||
assert_single_read? rs, typeof({{value}}), nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "executes with bind {{value.id}}" do
|
||||
with_db do |db|
|
||||
db.scalar(%(select ?), {{value}}).should eq({{value}})
|
||||
end
|
||||
end
|
||||
|
||||
it "executes with bind nil as typeof {{value.id}}" do
|
||||
with_db do |db|
|
||||
db.scalar("select ?", nil).should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it "executes with bind {{value.id}} as array" do
|
||||
with_db do |db|
|
||||
db.scalar(%(select ?), [{{value}}]).should eq({{value}})
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
||||
it "executes and selects blob" do
|
||||
with_db do |db|
|
||||
slice = db.scalar(%(select X'53514C697465')).as(Bytes)
|
||||
slice.to_a.should eq([0x53, 0x51, 0x4C, 0x69, 0x74, 0x65])
|
||||
end
|
||||
end
|
||||
|
||||
it "executes with bind blob" do
|
||||
with_db do |db|
|
||||
ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]
|
||||
slice = db.scalar(%(select cast(? as BLOB)), Bytes.new(ary.to_unsafe, ary.size)).as(Bytes)
|
||||
slice.to_a.should eq(ary)
|
||||
end
|
||||
end
|
||||
|
||||
it "gets column count" do
|
||||
with_mem_db do |db|
|
||||
db.exec "create table person (name string, age integer)"
|
||||
|
||||
db.query "select * from person" do |rs|
|
||||
rs.column_count.should eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gets column name" do
|
||||
with_mem_db do |db|
|
||||
db.exec "create table person (name string, age integer)"
|
||||
|
||||
db.query "select * from person" do |rs|
|
||||
rs.column_name(0).should eq("name")
|
||||
rs.column_name(1).should eq("age")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gets column types" do
|
||||
with_mem_db do |db|
|
||||
db.exec "create table table1 (aText text, anInteger integer, aReal real, aBlob blob)"
|
||||
db.exec "insert into table1 (aText, anInteger, aReal, aBlob) values ('a', 1, 1.5, X'53')"
|
||||
|
||||
# sqlite is unable to get column_type information
|
||||
# from the query itself without executing and getting
|
||||
# actual data.
|
||||
db.query "select * from table1" do |rs|
|
||||
rs.move_next
|
||||
rs.column_type(0).should eq(String)
|
||||
rs.column_type(1).should eq(Int64)
|
||||
rs.column_type(2).should eq(Float64)
|
||||
rs.column_type(3).should eq(Bytes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gets last insert row id" do
|
||||
with_mem_db do |db|
|
||||
db.exec "create table person (name string, 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
|
||||
end
|
||||
|
||||
{% for value in [1, 1_i64, "hello", 1.5, 1.5_f32] %}
|
||||
it "insert/get value {{value.id}} from table" do
|
||||
with_db do |db|
|
||||
db.exec "create table table1 (col1 #{sqlite_type_for({{value}})})"
|
||||
db.exec %(insert into table1 values (#{sql({{value}})}))
|
||||
db.scalar("select col1 from table1").should eq({{value}})
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
||||
it "insert/get blob value from table" do
|
||||
with_db do |db|
|
||||
ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]
|
||||
|
||||
db.exec "create table table1 (col1 blob)"
|
||||
db.exec %(insert into table1 values (?)), Bytes.new(ary.to_unsafe, ary.size)
|
||||
|
||||
slice = db.scalar("select cast(col1 as blob) from table1").as(Bytes)
|
||||
slice.to_a.should eq(ary)
|
||||
end
|
||||
end
|
||||
|
||||
it "insert/get value date from table" do
|
||||
with_db do |db|
|
||||
value = Time.new(2016, 7, 22, 15, 0, 0, 0)
|
||||
db.exec "create table table1 (col1 #{sqlite_type_for(value)})"
|
||||
db.exec %(insert into table1 values (?)), value
|
||||
|
||||
db.query "select col1 from table1" do |rs|
|
||||
rs.move_next
|
||||
rs.read(Time).should eq(value)
|
||||
end
|
||||
|
||||
db.query "select col1 from table1" do |rs|
|
||||
rs.move_next
|
||||
rs.read?(Time).should eq(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "raises on unsupported param types" do
|
||||
with_db do |db|
|
||||
expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do
|
||||
db.query "select 1", NotSupportedType.new
|
||||
end
|
||||
# TODO raising exception does not close the connection and pool is exhausted
|
||||
end
|
||||
|
||||
with_db do |db|
|
||||
expect_raises Exception, "SQLite3::Statement does not support NotSupportedType params" do
|
||||
db.exec "select 1", NotSupportedType.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "gets many rows from table" do
|
||||
with_mem_db do |db|
|
||||
db.exec "create table person (name string, age integer)"
|
||||
db.exec %(insert into person values ("foo", 10))
|
||||
db.exec %(insert into person values ("bar", 20))
|
||||
db.exec %(insert into person values ("baz", 30))
|
||||
|
||||
names = [] of String
|
||||
ages = [] of Int32
|
||||
db.query "select * from person" do |rs|
|
||||
rs.each do
|
||||
names << rs.read(String)
|
||||
ages << rs.read(Int32)
|
||||
end
|
||||
end
|
||||
names.should eq(["foo", "bar", "baz"])
|
||||
ages.should eq([10, 20, 30])
|
||||
end
|
||||
end
|
||||
|
||||
it "ensures statements are closed" do
|
||||
begin
|
||||
DB.open "sqlite3:#{DB_FILENAME}" 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");)
|
||||
end
|
||||
|
||||
2.times do |i|
|
||||
DB.open "sqlite3:#{DB_FILENAME}" 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
|
||||
ensure
|
||||
File.delete(DB_FILENAME)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,3 +2,23 @@ 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_db(name, &block : DB::Database ->)
|
||||
File.delete(name) rescue nil
|
||||
DB.open "sqlite3:#{name}", &block
|
||||
ensure
|
||||
File.delete(name)
|
||||
end
|
||||
|
||||
def with_mem_db(&block : DB::Database ->)
|
||||
DB.open "sqlite3://%3Amemory%3A", &block
|
||||
end
|
||||
|
|
|
@ -1 +1,6 @@
|
|||
require "db"
|
||||
require "./sqlite3/**"
|
||||
|
||||
module SQLite3
|
||||
DATE_FORMAT = "%F %H:%M:%S.%L"
|
||||
end
|
||||
|
|
52
src/sqlite3/connection.cr
Normal file
52
src/sqlite3/connection.cr
Normal file
|
@ -0,0 +1,52 @@
|
|||
class SQLite3::Connection < DB::Connection
|
||||
def initialize(database)
|
||||
super
|
||||
filename = self.class.filename(database.uri)
|
||||
# TODO maybe enable Flag::URI to parse query string in the uri as additional flags
|
||||
check LibSQLite3.open_v2(filename, out @db, (Flag::READWRITE | Flag::CREATE), nil)
|
||||
end
|
||||
|
||||
def self.filename(uri : URI)
|
||||
URI.unescape (if path = uri.path
|
||||
(uri.host || "") + path
|
||||
else
|
||||
uri.opaque.not_nil!
|
||||
end)
|
||||
end
|
||||
|
||||
def build_statement(query)
|
||||
Statement.new(self, query)
|
||||
end
|
||||
|
||||
def do_close
|
||||
@statements_cache.values.each &.close
|
||||
super
|
||||
LibSQLite3.close_v2(self)
|
||||
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
|
|
@ -1,246 +0,0 @@
|
|||
# The Database class encapsulates single connection to an SQLite3 database. Its usage is very straightforward:
|
||||
#
|
||||
# ```
|
||||
# require "sqlite3"
|
||||
#
|
||||
# db = SQLite3::Database.new("data.db")
|
||||
# db.execute("select * from table") do |row|
|
||||
# p row
|
||||
# end
|
||||
# db.close
|
||||
# ```
|
||||
#
|
||||
# Lower level methods are also provided.
|
||||
class SQLite3::Database
|
||||
# Creates a new Database object that opens the given file.
|
||||
def initialize(filename)
|
||||
code = LibSQLite3.open_v2(filename, out @db, SQLite3.flags(READWRITE, CREATE), nil)
|
||||
if code != 0
|
||||
raise Exception.new(@db)
|
||||
end
|
||||
@closed = false
|
||||
end
|
||||
|
||||
# Allows for initialization with specific flags. Primary use case is to allow
|
||||
# for sqlite3 URI opening and in memory DB operations.
|
||||
def initialize(filename, flags : SQLite3::Flag)
|
||||
code = LibSQLite3.open_v2(filename, out @db, flags, nil)
|
||||
if code != 0
|
||||
raise Exception.new(@db)
|
||||
end
|
||||
@closed = false
|
||||
end
|
||||
|
||||
# Creates a new Database object that opens the given file, yields it, and closes it at the end.
|
||||
def self.new(filename)
|
||||
db = new filename
|
||||
begin
|
||||
yield db
|
||||
ensure
|
||||
db.close
|
||||
end
|
||||
end
|
||||
|
||||
# Dump the database to another SQLite3 instance. This can be used for backing up a SQLite3::Database
|
||||
# to disk or the opposite
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# ```
|
||||
# source_database = SQLite3::Database.new("mydatabase.db")
|
||||
# in_memory_db = SQLite3::Database.new(
|
||||
# "file:memdb1?mode=memory&cache=shared",
|
||||
# SQLite3.flags(URI, CREATE, READWRITE, FULLMUTEX))
|
||||
# source_database.dump(in_memory_db)
|
||||
# source_database.close
|
||||
# in_memory_db.exectute do |row|
|
||||
# # ...
|
||||
# end
|
||||
# ```
|
||||
def dump(to : SQLite3::Database)
|
||||
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
|
||||
|
||||
# Executes the given SQL statement. If additional parameters are given, they are treated as bind variables,
|
||||
# and are bound to the placeholders in the query.
|
||||
#
|
||||
# Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately,
|
||||
# with the key being used as the name of the placeholder to bind the value to.
|
||||
#
|
||||
# Returns an `Array(Array(Value))`.
|
||||
def execute(sql, *binds)
|
||||
execute(sql, binds)
|
||||
end
|
||||
|
||||
def execute(sql, **binds)
|
||||
execute(sql, binds)
|
||||
end
|
||||
|
||||
# Executes the given SQL statement. If additional parameters are given, they are treated as bind variables,
|
||||
# and are bound to the placeholders in the query.
|
||||
#
|
||||
# Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately,
|
||||
# with the key being used as the name of the placeholder to bind the value to.
|
||||
#
|
||||
# Yields one `Array(Value)` for each result.
|
||||
def execute(sql, *binds, &block)
|
||||
execute(sql, binds) do |row|
|
||||
yield row
|
||||
end
|
||||
end
|
||||
|
||||
def execute(sql, **binds, &block)
|
||||
execute(sql, binds) do |row|
|
||||
yield row
|
||||
end
|
||||
end
|
||||
|
||||
# Executes the given SQL statement. If additional parameters are given, they are treated as bind variables,
|
||||
# and are bound to the placeholders in the query.
|
||||
#
|
||||
# Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately,
|
||||
# with the key being used as the name of the placeholder to bind the value to.
|
||||
#
|
||||
# Returns an `Array(Array(Value))`.
|
||||
def execute(sql, binds : Enumerable | NamedTuple)
|
||||
rows = [] of Array(Value)
|
||||
execute(sql, binds) do |row|
|
||||
rows << row
|
||||
end
|
||||
rows
|
||||
end
|
||||
|
||||
# Executes the given SQL statement. If additional parameters are given, they are treated as bind variables,
|
||||
# and are bound to the placeholders in the query.
|
||||
#
|
||||
# Note that if any of the values passed to this are hashes, then the key/value pairs are each bound separately,
|
||||
# with the key being used as the name of the placeholder to bind the value to.
|
||||
#
|
||||
# Yields one `Array(Value)` for each result.
|
||||
def execute(sql, binds : Enumerable | NamedTuple, &block)
|
||||
query(sql, binds) do |result_set|
|
||||
while result_set.next
|
||||
yield result_set.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience method that returns the first row of a query result.
|
||||
def get_first_row(sql, *binds)
|
||||
get_first_row(sql, binds)
|
||||
end
|
||||
|
||||
# A convenience method that returns the first row of a query result.
|
||||
def get_first_row(sql, binds : Enumerable)
|
||||
query(sql, binds) do |result_set|
|
||||
if result_set.next
|
||||
return result_set.to_a
|
||||
else
|
||||
raise "no results"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A convenience method that returns the first value of the first row of a query result.
|
||||
def get_first_value(sql, *binds)
|
||||
get_first_value(sql, binds)
|
||||
end
|
||||
|
||||
# A convenience method that returns the first value of the first row of a query result.
|
||||
def get_first_value(sql, binds : Enumerable | NamedTuple)
|
||||
query(sql, binds) do |result_set|
|
||||
if result_set.next
|
||||
return result_set[0]
|
||||
else
|
||||
raise "no results"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Executes a query and gives back a `ResultSet`.
|
||||
def query(sql, *binds)
|
||||
query(sql, binds)
|
||||
end
|
||||
|
||||
# Executes a query and yields a `ResultSet` that will be closed at the end of the given block.
|
||||
def query(sql, *binds, &block)
|
||||
query(sql, binds) do |result_set|
|
||||
yield result_set
|
||||
end
|
||||
end
|
||||
|
||||
# Executes a query and gives back a `ResultSet`.
|
||||
def query(sql, binds : Enumerable | NamedTuple)
|
||||
prepare(sql).execute(binds)
|
||||
end
|
||||
|
||||
# Executes a query and yields a `ResultSet` that will be closed at the end of the given block.
|
||||
def query(sql, binds : Enumerable | NamedTuple, &block)
|
||||
prepare(sql).execute(binds) do |result_set|
|
||||
yield result_set
|
||||
end
|
||||
end
|
||||
|
||||
def query(sql, **binds)
|
||||
query(sql, binds)
|
||||
end
|
||||
|
||||
def query(sql, **binds, &block)
|
||||
query(sql, binds) do |rs|
|
||||
yield rs
|
||||
end
|
||||
end
|
||||
|
||||
# Prepares an sql statement. Returns a `Statement`.
|
||||
def prepare(sql)
|
||||
Statement.new(self, sql)
|
||||
end
|
||||
|
||||
# Obtains the unique row ID of the last row to be inserted by this Database instance.
|
||||
# This is an `Int64`.
|
||||
def last_insert_row_id
|
||||
LibSQLite3.last_insert_rowid(self)
|
||||
end
|
||||
|
||||
# Quotes the given string, making it safe to use in an SQL statement.
|
||||
# It replaces all instances of the single-quote character with two single-quote characters.
|
||||
def quote(string)
|
||||
string.gsub('\'', "''")
|
||||
end
|
||||
|
||||
# Returns `true` if this database instance has been closed (see `#close`).
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
# Closes this database.
|
||||
def close
|
||||
return if @closed
|
||||
|
||||
@closed = true
|
||||
|
||||
LibSQLite3.close_v2(@db)
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
def finalize
|
||||
close
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
def to_unsafe
|
||||
@db
|
||||
end
|
||||
end
|
7
src/sqlite3/driver.cr
Normal file
7
src/sqlite3/driver.cr
Normal file
|
@ -0,0 +1,7 @@
|
|||
class SQLite3::Driver < DB::Driver
|
||||
def build_connection(db)
|
||||
SQLite3::Connection.new(db)
|
||||
end
|
||||
end
|
||||
|
||||
DB.register_driver "sqlite3", SQLite3::Driver
|
|
@ -45,6 +45,7 @@ lib LibSQLite3
|
|||
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
|
||||
|
|
|
@ -1,71 +1,117 @@
|
|||
# The ResultSet object encapsulates the enumerability of a query’s output.
|
||||
# It is a simple cursor over the data that the query returns.
|
||||
#
|
||||
# Typical usage is:
|
||||
#
|
||||
# ```
|
||||
# require "sqlite3"
|
||||
#
|
||||
# db = SQLite3::Database.new("foo.db")
|
||||
# stmt = db.prepare("select * from person")
|
||||
# result_set = stmt.execute
|
||||
# while result_set.next
|
||||
# p result_set.to_a
|
||||
# end
|
||||
# stmt.close
|
||||
# db.close
|
||||
# ```
|
||||
class SQLite3::ResultSet
|
||||
# :nodoc:
|
||||
def initialize(@statement : Statement)
|
||||
end
|
||||
class SQLite3::ResultSet < DB::ResultSet
|
||||
@column_index = 0
|
||||
|
||||
# Returns the number of columns.
|
||||
def column_count
|
||||
@statement.column_count
|
||||
end
|
||||
|
||||
# Returns the value of a column by index or name.
|
||||
def [](index_or_name)
|
||||
@statement[index_or_name]
|
||||
end
|
||||
|
||||
# Returns the types of the columns, an `Array(Type)`.
|
||||
def types
|
||||
@statement.types
|
||||
end
|
||||
|
||||
# Returns the names of the columns, an `Array(String)`.
|
||||
def columns
|
||||
@statement.columns
|
||||
protected def do_close
|
||||
super
|
||||
LibSQLite3.reset(self)
|
||||
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 next
|
||||
case @statement.step
|
||||
def move_next
|
||||
@column_index = 0
|
||||
|
||||
case step
|
||||
when LibSQLite3::Code::ROW
|
||||
true
|
||||
when LibSQLite3::Code::DONE
|
||||
false
|
||||
else
|
||||
raise Exception.new(@statement.db)
|
||||
raise Exception.new(@statement.connection)
|
||||
end
|
||||
end
|
||||
|
||||
# Closes this result set, closing the associated statement.
|
||||
def close
|
||||
@statement.close
|
||||
macro nilable_read_for(t)
|
||||
def read?(t : {{t}}.class) : {{t}}?
|
||||
if read_nil?
|
||||
moving_column { nil }
|
||||
else
|
||||
read(t)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns `true` if the associated statement is closed.
|
||||
def closed?
|
||||
@statement.closed?
|
||||
{% for t in DB::TYPES %}
|
||||
nilable_read_for({{t}})
|
||||
{% end %}
|
||||
|
||||
def read(t : String.class) : String
|
||||
moving_column { |col| String.new(LibSQLite3.column_text(self, col)) }
|
||||
end
|
||||
|
||||
# Return the current row's value as an `Array(Value)`.
|
||||
def to_a
|
||||
Array(Value).new(column_count) { |i| self[i] }
|
||||
def read(t : Int32.class) : Int32
|
||||
read(Int64).to_i32
|
||||
end
|
||||
|
||||
def read(t : Int64.class) : Int64
|
||||
moving_column { |col| LibSQLite3.column_int64(self, col) }
|
||||
end
|
||||
|
||||
def read(t : Float32.class) : Float32
|
||||
read(Float64).to_f32
|
||||
end
|
||||
|
||||
def read(t : Float64.class) : Float64
|
||||
moving_column { |col| LibSQLite3.column_double(self, col) }
|
||||
end
|
||||
|
||||
def read(t : Bytes.class) : Bytes
|
||||
moving_column do |col|
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
def read(t : Time.class) : Time
|
||||
Time.parse read(String), SQLite3::DATE_FORMAT
|
||||
end
|
||||
|
||||
nilable_read_for Time
|
||||
|
||||
def column_count
|
||||
LibSQLite3.column_count(self)
|
||||
end
|
||||
|
||||
def column_name(index)
|
||||
String.new LibSQLite3.column_name(self, index)
|
||||
end
|
||||
|
||||
def column_type(index : Int32)
|
||||
case LibSQLite3.column_type(self, index)
|
||||
when Type::INTEGER; Int64
|
||||
when Type::FLOAT ; Float64
|
||||
when Type::BLOB ; Bytes
|
||||
when Type::TEXT ; String
|
||||
when Type::NULL ; Nil
|
||||
else
|
||||
raise Exception.new(@statement.connection)
|
||||
end
|
||||
end
|
||||
|
||||
def to_unsafe
|
||||
@statement.to_unsafe
|
||||
end
|
||||
|
||||
private def read_nil?
|
||||
column_sqlite_type == Type::NULL
|
||||
end
|
||||
|
||||
private def column_sqlite_type
|
||||
LibSQLite3.column_type(self, @column_index)
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
private def step
|
||||
LibSQLite3::Code.new LibSQLite3.step(@statement)
|
||||
end
|
||||
|
||||
private def moving_column
|
||||
res = yield @column_index
|
||||
@column_index += 1
|
||||
res
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,239 +1,74 @@
|
|||
# A statement represents a prepared-but-unexecuted SQL query.
|
||||
class SQLite3::Statement
|
||||
getter db
|
||||
|
||||
# :nodoc:
|
||||
def initialize(@db : Database, sql : String)
|
||||
check LibSQLite3.prepare_v2(@db, sql, sql.bytesize + 1, out @stmt, nil)
|
||||
@closed = false
|
||||
class SQLite3::Statement < DB::Statement
|
||||
def initialize(connection, sql)
|
||||
super(connection)
|
||||
check LibSQLite3.prepare_v2(@connection, sql, sql.bytesize + 1, out @stmt, nil)
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
def self.new(db, sql)
|
||||
statement = new db, sql
|
||||
begin
|
||||
yield statement
|
||||
ensure
|
||||
statement.close
|
||||
end
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
def step
|
||||
LibSQLite3::Code.new LibSQLite3.step(self)
|
||||
end
|
||||
|
||||
# Returns the number of columns in this statement.
|
||||
def column_count
|
||||
LibSQLite3.column_count(self)
|
||||
end
|
||||
|
||||
# Returns the `Type` of the column at the given index.
|
||||
def column_type(index : Int)
|
||||
LibSQLite3.column_type(self, index)
|
||||
end
|
||||
|
||||
# Returns the name of the column at the given index.
|
||||
def column_name(index)
|
||||
String.new LibSQLite3.column_name(self, index)
|
||||
end
|
||||
|
||||
# Executes this statement with the given binds and returns a `ResultSet`.
|
||||
def execute(*binds)
|
||||
execute binds
|
||||
end
|
||||
|
||||
# Executes this statement with the given binds and yields a `ResultSet` that
|
||||
# will be closed at the end of the block.
|
||||
def execute(*binds)
|
||||
execute(binds) do |row|
|
||||
yield row
|
||||
end
|
||||
end
|
||||
|
||||
# Executes this statement with a single BLOB bind and returns a `ResultSet`.
|
||||
def execute(binds : Slice(UInt8))
|
||||
reset
|
||||
self[1] = binds
|
||||
ResultSet.new self
|
||||
end
|
||||
|
||||
# Executes this statement with the given binds and returns a `ResultSet`.
|
||||
def execute(binds : Enumerable)
|
||||
reset
|
||||
binds.each_with_index(1) do |bind_value, index|
|
||||
self[index] = bind_value
|
||||
end
|
||||
ResultSet.new self
|
||||
end
|
||||
|
||||
# Executes this statement with the given binds and returns a `ResultSet`.
|
||||
def execute(binds : NamedTuple)
|
||||
reset
|
||||
binds.each do |name, bind_value|
|
||||
self[name] = bind_value
|
||||
end
|
||||
ResultSet.new self
|
||||
end
|
||||
|
||||
# Executes this statement with the given binds and yields a `ResultSet` that
|
||||
# will be closed at the end of the block.
|
||||
def execute(binds : Enumerable | NamedTuple | Slice(UInt8), &block)
|
||||
result_set = execute(binds)
|
||||
yield result_set
|
||||
ensure
|
||||
close
|
||||
end
|
||||
|
||||
def execute(**binds)
|
||||
execute(binds)
|
||||
end
|
||||
|
||||
def execute(**binds, &block)
|
||||
execute(binds) do |rs|
|
||||
yield rs
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the value of the given column by index (1-based).
|
||||
def [](index : Int)
|
||||
case type = column_type(index)
|
||||
when Type::INTEGER
|
||||
column_int64(index)
|
||||
when Type::FLOAT
|
||||
column_double(index)
|
||||
when Type::TEXT
|
||||
String.new(column_text(index))
|
||||
when Type::BLOB
|
||||
blob = column_blob(index)
|
||||
bytes = column_bytes(index)
|
||||
ptr = Pointer(UInt8).malloc(bytes)
|
||||
ptr.copy_from(blob, bytes)
|
||||
Slice.new(ptr, bytes)
|
||||
when Type::NULL
|
||||
nil
|
||||
else
|
||||
raise "Unknown column type: #{type}"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the value of the given column by name.
|
||||
def [](name : String)
|
||||
column_count.times do |i|
|
||||
if column_name(i) == name
|
||||
return self[i]
|
||||
end
|
||||
end
|
||||
raise "Unknown column: #{name}"
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to an Int.
|
||||
def []=(index : Int, value : Nil)
|
||||
check LibSQLite3.bind_null(self, index)
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to an Int32.
|
||||
def []=(index : Int, value : Int32)
|
||||
check LibSQLite3.bind_int(self, index, value)
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to an Int64.
|
||||
def []=(index : Int, value : Int64)
|
||||
check LibSQLite3.bind_int64(self, index, value)
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to a Float.
|
||||
def []=(index : Int, value : Float)
|
||||
check LibSQLite3.bind_double(self, index, value.to_f64)
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to a String.
|
||||
def []=(index : Int, value : String)
|
||||
check LibSQLite3.bind_text(self, index, value, value.bytesize, nil)
|
||||
end
|
||||
|
||||
# Binds the parameter at the given index to a BLOB.
|
||||
def []=(index : Int, value : Slice(UInt8))
|
||||
check LibSQLite3.bind_blob(self, index, value, value.size, nil)
|
||||
end
|
||||
|
||||
# Binds a named parameter, using the `:AAAA` naming scheme for parameters.
|
||||
def []=(name : String | Symbol, value)
|
||||
converted_name = ":#{name}"
|
||||
index = LibSQLite3.bind_parameter_index(self, converted_name)
|
||||
if index == 0
|
||||
raise "Unknown parameter: #{name}"
|
||||
end
|
||||
self[index] = value
|
||||
end
|
||||
|
||||
# Binds a hash to this statement (the `index` is ignored).
|
||||
def []=(index : Int, hash : Hash)
|
||||
hash.each do |key, value|
|
||||
self[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
def []=(index : Int, tuple : Tuple(String | Symbol, U))
|
||||
self[tuple[0]] = tuple[1]
|
||||
end
|
||||
|
||||
# Returns the column names of this statement.
|
||||
def columns
|
||||
Array.new(column_count) { |i| column_name(i) }
|
||||
end
|
||||
|
||||
# Returns an `Array(Type)` of this statement's columns. Note that the statement
|
||||
# must be executed in order for this to return sensible values, otherwise all types
|
||||
# will be NULL.
|
||||
def types
|
||||
Array.new(column_count) { |i| column_type(i) }
|
||||
end
|
||||
|
||||
# Reset this statment, allowing to re-execute it with new binds.
|
||||
def reset
|
||||
protected def perform_query(args : Enumerable) : DB::ResultSet
|
||||
LibSQLite3.reset(self)
|
||||
args.each_with_index(1) do |arg, index|
|
||||
bind_arg(index, arg)
|
||||
end
|
||||
ResultSet.new(self)
|
||||
end
|
||||
|
||||
# Closes this statement.
|
||||
def close
|
||||
raise "Statement already closed" if @closed
|
||||
@closed = true
|
||||
protected def perform_exec(args : Enumerable) : DB::ExecResult
|
||||
rs = perform_query(args)
|
||||
rs.move_next
|
||||
rs.close
|
||||
|
||||
rows_affected = LibSQLite3.changes(connection).to_i64
|
||||
last_id = LibSQLite3.last_insert_rowid(connection)
|
||||
|
||||
DB::ExecResult.new rows_affected, last_id
|
||||
end
|
||||
|
||||
protected def on_close
|
||||
super
|
||||
check LibSQLite3.finalize(self)
|
||||
end
|
||||
|
||||
# Returns `true` if this statement is closed. See `#close`.
|
||||
def closed?
|
||||
@closed
|
||||
private def bind_arg(index, value : Nil)
|
||||
check LibSQLite3.bind_null(self, index)
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
def to_unsafe
|
||||
@stmt
|
||||
private def bind_arg(index, value : Int32)
|
||||
check LibSQLite3.bind_int(self, index, value)
|
||||
end
|
||||
|
||||
private def column_int64(index)
|
||||
LibSQLite3.column_int64(self, index)
|
||||
private def bind_arg(index, value : Int64)
|
||||
check LibSQLite3.bind_int64(self, index, value)
|
||||
end
|
||||
|
||||
private def column_double(index)
|
||||
LibSQLite3.column_double(self, index)
|
||||
private def bind_arg(index, value : Float32)
|
||||
check LibSQLite3.bind_double(self, index, value.to_f64)
|
||||
end
|
||||
|
||||
private def column_text(index)
|
||||
LibSQLite3.column_text(self, index)
|
||||
private def bind_arg(index, value : Float64)
|
||||
check LibSQLite3.bind_double(self, index, value)
|
||||
end
|
||||
|
||||
private def column_blob(index)
|
||||
LibSQLite3.column_blob(self, index)
|
||||
private def bind_arg(index, value : String)
|
||||
check LibSQLite3.bind_text(self, index, value, value.bytesize, nil)
|
||||
end
|
||||
|
||||
private def column_bytes(index)
|
||||
LibSQLite3.column_bytes(self, index)
|
||||
private def bind_arg(index, value : Bytes)
|
||||
check LibSQLite3.bind_blob(self, index, value, value.size, nil)
|
||||
end
|
||||
|
||||
private def bind_arg(index, value : Time)
|
||||
bind_arg(index, value.to_s(SQLite3::DATE_FORMAT))
|
||||
end
|
||||
|
||||
private def bind_arg(index, value)
|
||||
raise "#{self.class} does not support #{value.class} params"
|
||||
end
|
||||
|
||||
private def check(code)
|
||||
raise Exception.new(@db) unless code == 0
|
||||
raise Exception.new(@connection) unless code == 0
|
||||
end
|
||||
|
||||
def to_unsafe
|
||||
@stmt
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
module SQLite3
|
||||
# All possible values of each column of a row returned by `Database#execute`.
|
||||
alias Value = Nil | Int64 | Float64 | String | Slice(UInt8)
|
||||
end
|
Loading…
Reference in a new issue