Initial commit

This commit is contained in:
Ary Borenszweig 2015-03-12 18:21:33 -03:00
commit d96255d766
11 changed files with 534 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.crystal/

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# crystal-sqlite3
SQLite3 bindings for [Crystal](http://crystal-lang.org/).
### Projectfile
```crystal
deps do
github "manastech/crystal-sqlite3"
end
```

100
spec/database_spec.cr Normal file
View file

@ -0,0 +1,100 @@
require "./spec_helper"
DB_FILENAME = "./test.db"
private def with_db
yield Database.new DB_FILENAME
ensure
File.delete(DB_FILENAME)
end
describe Database do
it "opens a database" do
with_db do |db|
File.exists?(DB_FILENAME).should be_true
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 bind blob" do
ary = UInt8[0x53, 0x51, 0x4C, 0x69, 0x74, 0x65]
rows = with_db(&.execute(%(select cast(? as BLOB)), Slice.new(ary.buffer, ary.length)))
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 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 "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
end

4
spec/spec_helper.cr Normal file
View file

@ -0,0 +1,4 @@
require "spec"
require "../src/sqlite3"
include SQLite3

1
src/sqlite3.cr Normal file
View file

@ -0,0 +1 @@
require "./sqlite3/**"

View file

@ -0,0 +1,3 @@
module SQLite3
alias ColumnType = Nil | Int64 | Float64 | String | Slice(UInt8)
end

124
src/sqlite3/database.cr Normal file
View file

@ -0,0 +1,124 @@
class SQLite3::Database
def initialize(filename)
code = LibSQLite3.open_v2(filename, out @db, (LibSQLite3::Flag::READWRITE | LibSQLite3::Flag::CREATE), nil)
if code != 0
raise Exception.new(@db)
end
@closed = false
end
def self.new(filename)
db = new filename
begin
yield db
ensure
db.close
end
end
def execute(sql, *binds)
execute(sql, binds)
end
def execute(sql, *binds, &block)
execute(sql, binds) do |row|
yield row
end
end
def execute(sql, binds : Enumerable)
rows = [] of Array(SQLite3::ColumnType)
execute(sql, binds) do |row|
rows << row
end
rows
end
def execute(sql, binds : Enumerable, &block)
query(sql, binds) do |result_set|
while result_set.next
yield result_set.to_a
end
end
end
def get_first_row(sql, *binds)
get_first_row(sql, binds)
end
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
def get_first_value(sql, *binds)
get_first_value(sql, binds)
end
def get_first_value(sql, binds : Enumerable)
query(sql, binds) do |result_set|
if result_set.next
return result_set[0]
else
raise "no results"
end
end
end
def query(sql, *binds)
query(sql, binds)
end
def query(sql, *binds, &block)
query(sql, binds) do |result_set|
yield result_set
end
end
def query(sql, binds : Enumerable)
prepare(sql).execute(binds)
end
def query(sql, binds : Enumerable, &block)
prepare(sql).execute(binds) do |result_set|
yield result_set
end
end
def prepare(sql)
Statement.new(self, sql)
end
def last_insert_row_id
LibSQLite3.last_insert_rowid(self)
end
def quote(string)
string.gsub('\'', "''")
end
def closed?
@closed
end
def close
return if @closed
@closed = true
LibSQLite3.close_v2(@db)
end
def finalize
close
end
def to_unsafe
@db
end
end

8
src/sqlite3/exception.cr Normal file
View file

@ -0,0 +1,8 @@
class SQLite3::Exception < ::Exception
getter code
def initialize(db)
super(String.new(LibSQLite3.errmsg(db)))
@code = LibSQLite3.errcode(db)
end
end

View file

@ -0,0 +1,74 @@
@[Link("sqlite3")]
lib LibSQLite3
type SQLite3 = Void*
type Statement = Void*
enum 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
enum Code
ROW = 100
DONE = 101
end
enum Type
INTEGER = 1
FLOAT = 2
BLOB = 4
NULL = 5
TEXT = 3
end
alias Callback = (Void*, Int32, UInt8**, UInt8**) -> Int32
fun open = sqlite3_open_v2(filename : UInt8*, db : SQLite3*) : Int32
fun open_v2 = sqlite3_open_v2(filename : UInt8*, db : SQLite3*, flags: Flag, zVfs : UInt8*) : Int32
fun errcode = sqlite3_errcode(SQLite3) : Int32
fun errmsg = sqlite3_errmsg(SQLite3) : UInt8*
fun prepare_v2 = sqlite3_prepare_v2(db : SQLite3, zSql : UInt8*, nByte : Int32, ppStmt : Statement*, pzTail : 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) : Int32
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_text(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 finalize = sqlite3_finalize(stmt : Statement) : Int32
fun close_v2 = sqlite3_close_v2(SQLite3) : Int32
end

31
src/sqlite3/result_set.cr Normal file
View file

@ -0,0 +1,31 @@
class SQLite3::ResultSet
def initialize(@statement)
end
def column_count
@statement.column_count
end
def [](index)
@statement[index]
end
def next
case @statement.step
when LibSQLite3::Code::ROW
true
when LibSQLite3::Code::DONE
false
else
raise Exception.new(@db)
end
end
def close
@statement.close
end
def to_a
Array(ColumnType).new(column_count) { |i| self[i] }
end
end

177
src/sqlite3/statement.cr Normal file
View file

@ -0,0 +1,177 @@
class SQLite3::Statement
def initialize(@db, sql)
check LibSQLite3.prepare_v2(@db, sql, sql.bytesize + 1, out @stmt, nil)
@closed = false
end
def self.new(db, sql)
statement = new db, sql
begin
yield statement
ensure
statement.close
end
end
def step
LibSQLite3::Code.new LibSQLite3.step(self)
end
def column_count
LibSQLite3.column_count(self)
end
def column_type(index)
LibSQLite3::Type.new LibSQLite3.column_type(self, index.to_i32)
end
def column_name(index)
String.new LibSQLite3.column_name(self, index.to_i32)
end
def column_int64(index)
LibSQLite3.column_int64(self, index.to_i32)
end
def column_double(index)
LibSQLite3.column_double(self, index.to_i32)
end
def column_text(index)
LibSQLite3.column_text(self, index.to_i32)
end
def column_blob(index)
LibSQLite3.column_blob(self, index.to_i32)
end
def column_bytes(index)
LibSQLite3.column_bytes(self, index.to_i32)
end
def execute(*binds)
execute binds
end
def execute(*binds)
execute(binds) do |row|
yield row
end
end
def execute(binds : Slice(UInt8))
reset
self[1] = binds
ResultSet.new self
end
def execute(binds : Enumerable)
reset
binds.each_with_index(1) do |bind_value, index|
self[index] = bind_value
end
ResultSet.new self
end
def execute(binds : Enumerable | Slice(UInt8), &block)
result_set = execute(binds)
yield result_set
close
end
def [](index : Int)
case type = column_type(index)
when LibSQLite3::Type::INTEGER
column_int64(index)
when LibSQLite3::Type::FLOAT
column_double(index)
when LibSQLite3::Type::TEXT
String.new(column_text(index))
when LibSQLite3::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 LibSQLite3::Type::NULL
nil
else
raise "Unknown column type: #{type}"
end
end
def [](name : String)
column_count.times do |i|
if column_name(i) == name
return self[i]
end
end
raise "Unknown column: #{name}"
end
def []=(index : Int, value : Nil)
check LibSQLite3.bind_null(self, index.to_i32)
end
def []=(index : Int, value : Int32)
check LibSQLite3.bind_int(self, index.to_i32, value)
end
def []=(index : Int, value : Int64)
check LibSQLite3.bind_int64(self, index.to_i32, value)
end
def []=(index : Int, value : Float)
check LibSQLite3.bind_double(self, index.to_i32, value.to_f64)
end
def []=(index : Int, value : String)
check LibSQLite3.bind_text(self, index.to_i32, value, value.bytesize, nil)
end
def []=(index : Int, value : Slice(UInt8))
check LibSQLite3.bind_blob(self, index.to_i32, value, value.length, nil)
end
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
def []=(index : Int, hash : Hash)
hash.each do |key, value|
self[key] = value
end
end
def columns
Array.new(column_count) { |i| column_name(i) }
end
def reset
LibSQLite3.reset(self)
end
def close
raise "Statement already closed" if @closed
@closed = true
check LibSQLite3.finalize(self)
end
def closed?
@closed
end
def to_unsafe
@stmt
end
private def check(code)
raise Exception.new(@db) unless code == 0
end
end