compliant with crystal-db.

Merge branch 'db' into master
This commit is contained in:
Brian J. Cardiff 2016-06-27 12:39:33 -03:00
commit fb0f5ea6d7
17 changed files with 637 additions and 736 deletions

11
.gitignore vendored
View File

@ -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
View 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.

View File

@ -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
View 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

View File

@ -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
View 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

View File

@ -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
View 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

View File

@ -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

View File

@ -1 +1,6 @@
require "db"
require "./sqlite3/**"
module SQLite3
DATE_FORMAT = "%F %H:%M:%S.%L"
end

52
src/sqlite3/connection.cr Normal file
View 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

View File

@ -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
View 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

View File

@ -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

View File

@ -1,71 +1,117 @@
# The ResultSet object encapsulates the enumerability of a querys 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

View File

@ -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

View File

@ -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