mirror of
https://gitea.invidious.io/iv-org/shard-crystal-sqlite3.git
synced 2024-08-15 00:53:26 +00:00
Initial commit
This commit is contained in:
commit
d96255d766
11 changed files with 534 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.crystal/
|
11
README.md
Normal file
11
README.md
Normal 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
100
spec/database_spec.cr
Normal 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
4
spec/spec_helper.cr
Normal file
|
@ -0,0 +1,4 @@
|
|||
require "spec"
|
||||
require "../src/sqlite3"
|
||||
|
||||
include SQLite3
|
1
src/sqlite3.cr
Normal file
1
src/sqlite3.cr
Normal file
|
@ -0,0 +1 @@
|
|||
require "./sqlite3/**"
|
3
src/sqlite3/column_type.cr
Normal file
3
src/sqlite3/column_type.cr
Normal file
|
@ -0,0 +1,3 @@
|
|||
module SQLite3
|
||||
alias ColumnType = Nil | Int64 | Float64 | String | Slice(UInt8)
|
||||
end
|
124
src/sqlite3/database.cr
Normal file
124
src/sqlite3/database.cr
Normal 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
8
src/sqlite3/exception.cr
Normal 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
|
74
src/sqlite3/lib_sqlite3.cr
Normal file
74
src/sqlite3/lib_sqlite3.cr
Normal 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
31
src/sqlite3/result_set.cr
Normal 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
177
src/sqlite3/statement.cr
Normal 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
|
Loading…
Reference in a new issue