package sqlite3
import "core:c"
import "core:mem"
import "core:slice"
import "core:strings"
import "bindings"
DB :: struct {
_conn: bindings.Connection,
allocator: mem.Allocator,
}
Rows :: struct {
_stmt: bindings.Statement,
allocator: mem.Allocator,
}
Error :: union {
DB_Error,
Stmt_Error,
}
DB_Error :: enum {
None = 0,
Error,
Busy,
Cant_Open,
Disk_Full,
No_Memory,
Not_A_Database,
Permission_Denied,
Read_Only,
}
Stmt_Error :: enum {
None = 0,
Error,
Constraint_Violation,
Data_Too_Big,
Argument_Mismatch,
Invalid_Datatype,
Index_Out_Of_Range,
}
Open_Flags :: distinct bit_set[Open_Flag;i32]
Open_Flag :: enum i32 {
Create = i32(bindings.OpenFlag.Create),
Read_Only = i32(bindings.OpenFlag.Readonly),
Read_Write = i32(bindings.OpenFlag.Readwrite),
}
@(private)
db_result_to_error :: proc(res: bindings.Result) -> Error {
err: Error
#partial switch res {
case .Ok:
err = nil
case .Perm:
err = DB_Error.Permission_Denied
case .Busy:
err = DB_Error.Busy
case .Cantopen:
err = DB_Error.Cant_Open
case .Notadb:
err = DB_Error.Not_A_Database
case .Nomem:
err = DB_Error.No_Memory
case .Readonly:
err = DB_Error.Read_Only
case .Constraint:
err = Stmt_Error.Constraint_Violation
case .Toobig:
err = Stmt_Error.Data_Too_Big
case .Mismatch:
err = Stmt_Error.Argument_Mismatch
case .Range:
err = Stmt_Error.Index_Out_Of_Range
case:
err = DB_Error.Error
}
return err
}
// TODO: Refactor
// - [ ] Make sure to separate concerns, query to get data, exec to insert/update/delete data
// - [x] Implement errors
// - [x] Wrap bindings.connection in DB struct instead of type alias
// - [x] Look over allocator, store allocator in DB struct
// - [x] Look over memory ownership
// - [x] Open flags
open :: proc(
filename: string,
flags := Open_Flags{.Read_Write, .Create},
allocater := context.allocator,
) -> (
^DB,
Error,
) {
db, err := new(DB, allocater)
if err != nil {
return nil, DB_Error.No_Memory
}
db.allocator = allocater
c_filename := strings.clone_to_cstring(filename, context.temp_allocator)
if res := bindings.Result(bindings.open_v2(c_filename, &db._conn, transmute(bindings.OpenFlags)flags, nil));
res != .Ok {
free(db, allocater)
return nil, db_result_to_error(res)
}
return db, nil
}
close :: proc(db: ^DB) -> Error {
if res := bindings.Result(bindings.close(db._conn)); res != .Ok {
return db_result_to_error(res)
}
free(db, db.allocator)
return nil
}
query :: proc(db: ^DB, sql: string, args: ..any) -> (^Rows, Error) {
rows, err := new(Rows, db.allocator)
if err != nil {
return nil, DB_Error.No_Memory
}
rows.allocator = db.allocator
c_sql := strings.clone_to_cstring(sql, context.temp_allocator)
if res := bindings.Result(bindings.prepare_v2(db._conn, c_sql, -1, &rows._stmt, nil)); res != .Ok {
return nil, db_result_to_error(res)
}
for arg, i in args {
switch v in arg {
case int:
if res := bindings.Result(bindings.bind_int(rows._stmt, c.int(i + 1), c.int(v))); res != .Ok {
return nil, db_result_to_error(res)
}
case f64:
if res := bindings.Result(bindings.bind_double(rows._stmt, c.int(i + 1), c.double(v))); res != .Ok {
return nil, db_result_to_error(res)
}
case string:
if res := bindings.Result(
bindings.bind_text(
rows._stmt,
c.int(i + 1),
strings.clone_to_cstring(v, context.temp_allocator),
c.int(len(v)),
nil,
),
); res != .Ok {
return nil, db_result_to_error(res)
}
case []u8:
if res := bindings.Result(bindings.bind_blob(rows._stmt, c.int(i + 1), raw_data(v), c.int(len(v)), nil));
res != .Ok {
return nil, db_result_to_error(res)
}
case:
return nil, Stmt_Error.Invalid_Datatype
}
}
return rows, nil
}
exec :: proc(db: ^DB, sql: string) -> Error {
c_sql := strings.clone_to_cstring(sql, context.temp_allocator)
if res := bindings.Result(bindings.exec(db._conn, c_sql, nil, nil, nil)); res != .Ok {
return db_result_to_error(res)
}
return nil
}
rows_next :: proc(rows: ^Rows) -> (bool, Error) {
if res := bindings.Result(bindings.step(rows._stmt)); res != .Ok && res != .Row {
if res != .Done {
return false, db_result_to_error(res)
}
return false, nil
}
return true, nil
}
rows_scan :: proc(rows: ^Rows, columns: ..any) -> Error {
for &col, i in columns {
datatype := bindings.column_type(rows._stmt, c.int(i))
switch datatype {
case .Integer:
col_int, ok := col.(^int)
if !ok {
return Stmt_Error.Invalid_Datatype
}
col_int^ = int(bindings.column_int(rows._stmt, c.int(i)))
case .Float:
col_float, ok := col.(^f64)
if !ok {
return Stmt_Error.Invalid_Datatype
}
col_float^ = f64(bindings.column_double(rows._stmt, c.int(i)))
case .Text:
col_str, ok := col.(^string)
if !ok {
return Stmt_Error.Invalid_Datatype
}
raw_str := bindings.column_text(rows._stmt, c.int(i))
err: mem.Allocator_Error
col_str^, err = strings.clone_from_cstring(raw_str, context.temp_allocator)
if err != nil {
return DB_Error.No_Memory
}
case .Blob:
col_bytes, ok := col.(^[]u8)
if !ok {
return Stmt_Error.Invalid_Datatype
}
n_bytes := bindings.column_bytes(rows._stmt, c.int(i))
raw_bytes := bindings.column_blob(rows._stmt, c.int(i))
col_bytes^ = slice.bytes_from_ptr(raw_bytes, int(n_bytes))
case .Null:
continue
}
}
return nil
}
rows_close :: proc(rows: ^Rows) -> Error {
if rows == nil {
return DB_Error.Error
}
res := bindings.Result(bindings.finalize(rows._stmt))
free(rows, rows.allocator)
if res != .Ok {
return db_result_to_error(res)
}
return nil
}
/*
if true {
conn: bindings.Connection
fmt.printf("bindings open: %s\n", bindings.open(cstring("test.db"), &conn))
stmt: bindings.Statement
fmt.printf(
"bindings prepare: %s\n",
bindings.prepare(
conn,
cstring("create table t1(x integer primary key asc,y);"),
-1,
&stmt,
nil,
),
)
fmt.printf("bindings step: %s\n", bindings.step(stmt))
fmt.printf("bindings finalize: %s\n", bindings.finalize(stmt))
fmt.printf(
"bindings exec: %s\n",
bindings.exec(
conn,
cstring(
"insert into t1 (y) values (1); insert into t1 (y) values (2); select * from t1;",
),
exec_proc,
conn,
nil,
),
)
errmsg: cstring
if bindings.exec(conn, cstring("insert into t2 (y) values (1)"), nil, nil, &errmsg) != .Ok {
fmt.printf("bindings exec err: %#v\n", errmsg)
bindings.free(rawptr(errmsg))
}
fmt.printf("bindings close: %s\n", bindings.close(conn))
os.exit(1)
}
*/