import gleam/dynamic/decode.{type Decoder, type Dynamic}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
pub type Connection
pub type Statement
pub type Value
pub type Error {
LibsqlError(
code: ErrorCode,
message: String,
offset: Int,
)
}
pub type ErrorCode {
Abort
Auth
Busy
Cantopen
Constraint
Corrupt
Done
Empty
GenericError
Format
Full
Internal
Interrupt
Ioerr
Locked
Mismatch
Misuse
Nolfs
Nomem
Notadb
Notfound
Notice
GenericOk
Perm
Protocol
Range
Readonly
Row
Schema
Toobig
Warning
AbortRollback
AuthUser
BusyRecovery
BusySnapshot
BusyTimeout
CantopenConvpath
CantopenDirtywal
CantopenFullpath
CantopenIsdir
CantopenNotempdir
CantopenSymlink
ConstraintCheck
ConstraintCommithook
ConstraintDatatype
ConstraintForeignkey
ConstraintFunction
ConstraintNotnull
ConstraintPinned
ConstraintPrimarykey
ConstraintRowid
ConstraintTrigger
ConstraintUnique
ConstraintVtab
CorruptIndex
CorruptSequence
CorruptVtab
ErrorMissingCollseq
ErrorRetry
ErrorSnapshot
IoerrAccess
IoerrAuth
IoerrBeginAtomic
IoerrBlocked
IoerrCheckreservedlock
IoerrClose
IoerrCommitAtomic
IoerrConvpath
IoerrCorruptfs
IoerrData
IoerrDelete
IoerrDeleteNoent
IoerrDirClose
IoerrDirFsync
IoerrFstat
IoerrFsync
IoerrGettemppath
IoerrLock
IoerrMmap
IoerrNomem
IoerrRdlock
}
pub fn error_code_to_int(error: ErrorCode) -> Int {
case error {
GenericError -> 1
Abort -> 4
Auth -> 23
Busy -> 5
Cantopen -> 14
Constraint -> 19
Corrupt -> 11
Done -> 101
Empty -> 16
Format -> 24
Full -> 13
Internal -> 2
Interrupt -> 9
Ioerr -> 10
Locked -> 6
Mismatch -> 20
Misuse -> 21
Nolfs -> 22
Nomem -> 7
Notadb -> 26
Notfound -> 12
Notice -> 27
GenericOk -> 0
Perm -> 3
Protocol -> 15
Range -> 25
Readonly -> 8
Row -> 100
Schema -> 17
Toobig -> 18
Warning -> 28
AbortRollback -> 516
AuthUser -> 279
BusyRecovery -> 261
BusySnapshot -> 517
BusyTimeout -> 773
CantopenConvpath -> 1038
CantopenDirtywal -> 1294
CantopenFullpath -> 782
CantopenIsdir -> 526
CantopenNotempdir -> 270
CantopenSymlink -> 1550
ConstraintCheck -> 275
ConstraintCommithook -> 531
ConstraintDatatype -> 3091
ConstraintForeignkey -> 787
ConstraintFunction -> 1043
ConstraintNotnull -> 1299
ConstraintPinned -> 2835
ConstraintPrimarykey -> 1555
ConstraintRowid -> 2579
ConstraintTrigger -> 1811
ConstraintUnique -> 2067
ConstraintVtab -> 2323
CorruptIndex -> 779
CorruptSequence -> 523
CorruptVtab -> 267
ErrorMissingCollseq -> 257
ErrorRetry -> 513
ErrorSnapshot -> 769
IoerrAccess -> 3338
IoerrAuth -> 7178
IoerrBeginAtomic -> 7434
IoerrBlocked -> 2826
IoerrCheckreservedlock -> 3594
IoerrClose -> 4106
IoerrCommitAtomic -> 7690
IoerrConvpath -> 6666
IoerrCorruptfs -> 8458
IoerrData -> 8202
IoerrDelete -> 2570
IoerrDeleteNoent -> 5898
IoerrDirClose -> 4362
IoerrDirFsync -> 1290
IoerrFstat -> 1802
IoerrFsync -> 1034
IoerrGettemppath -> 6410
IoerrLock -> 3850
IoerrMmap -> 6154
IoerrNomem -> 3082
IoerrRdlock -> 2314
}
}
pub fn error_code_from_int(code: Int) -> ErrorCode {
case code {
4 -> Abort
23 -> Auth
5 -> Busy
14 -> Cantopen
19 -> Constraint
11 -> Corrupt
101 -> Done
16 -> Empty
1 -> GenericError
24 -> Format
13 -> Full
2 -> Internal
9 -> Interrupt
10 -> Ioerr
6 -> Locked
20 -> Mismatch
21 -> Misuse
22 -> Nolfs
7 -> Nomem
26 -> Notadb
12 -> Notfound
27 -> Notice
0 -> GenericOk
3 -> Perm
15 -> Protocol
25 -> Range
8 -> Readonly
100 -> Row
17 -> Schema
18 -> Toobig
28 -> Warning
516 -> AbortRollback
279 -> AuthUser
261 -> BusyRecovery
517 -> BusySnapshot
773 -> BusyTimeout
1038 -> CantopenConvpath
1294 -> CantopenDirtywal
782 -> CantopenFullpath
526 -> CantopenIsdir
270 -> CantopenNotempdir
1550 -> CantopenSymlink
275 -> ConstraintCheck
531 -> ConstraintCommithook
3091 -> ConstraintDatatype
787 -> ConstraintForeignkey
1043 -> ConstraintFunction
1299 -> ConstraintNotnull
2835 -> ConstraintPinned
1555 -> ConstraintPrimarykey
2579 -> ConstraintRowid
1811 -> ConstraintTrigger
2067 -> ConstraintUnique
2323 -> ConstraintVtab
779 -> CorruptIndex
523 -> CorruptSequence
267 -> CorruptVtab
257 -> ErrorMissingCollseq
513 -> ErrorRetry
769 -> ErrorSnapshot
3338 -> IoerrAccess
7178 -> IoerrAuth
7434 -> IoerrBeginAtomic
2826 -> IoerrBlocked
3594 -> IoerrCheckreservedlock
4106 -> IoerrClose
7690 -> IoerrCommitAtomic
6666 -> IoerrConvpath
8458 -> IoerrCorruptfs
8202 -> IoerrData
2570 -> IoerrDelete
5898 -> IoerrDeleteNoent
4362 -> IoerrDirClose
1290 -> IoerrDirFsync
1802 -> IoerrFstat
1034 -> IoerrFsync
6410 -> IoerrGettemppath
3850 -> IoerrLock
6154 -> IoerrMmap
3082 -> IoerrNomem
2314 -> IoerrRdlock
_ -> GenericError
}
}
@external(erlang, "libsql_ffi", "open")
fn open_(a: String) -> Result(Connection, Error)
@external(erlang, "libsql_ffi", "open_remote")
fn open_remote_(a: String, b: String) -> Result(Connection, Error)
@external(erlang, "libsql_ffi", "close")
fn close_(a: Connection) -> Result(Nil, Error)
pub fn open(path: String) -> Result(Connection, Error) {
open_(path)
}
pub fn open_remote(url: String, token: String) -> Result(Connection, Error) {
open_remote_(url, token)
}
pub fn close(connection: Connection) -> Result(Nil, Error) {
close_(connection)
}
pub fn with_connection(path: String, f: fn(Connection) -> a) -> a {
let assert Ok(connection) = open(path)
let value = f(connection)
let assert Ok(_) = close(connection)
value
}
pub fn with_remote_connection(url: String, token: String, f: fn(Connection) -> a) -> a {
let assert Ok(connection) = open_remote(url, token)
let value = f(connection)
let assert Ok(_) = close(connection)
value
}
@external(erlang, "libsql_ffi", "open_replica")
fn open_replica_(a: String, b: String, c: String) -> Result(Connection, Error)
pub fn open_replica(
path: String,
url: String,
token: String,
) -> Result(Connection, Error) {
open_replica_(path, url, token)
}
pub fn with_replica_connection(
path: String,
url: String,
token: String,
f: fn(Connection) -> a,
) -> a {
let assert Ok(connection) = open_replica(path, url, token)
let value = f(connection)
let assert Ok(_) = close(connection)
value
}
@external(erlang, "libsql_ffi", "open_synced_database")
fn open_synced_database_(a: String, b: String, c: String) -> Result(Connection, Error)
pub fn open_synced_database(path: String, url: String, token: String) -> Result(Connection, Error) {
open_synced_database_(path, url, token)
}
pub fn with_synced_database(
path: String,
url: String,
token: String,
f: fn(Connection) -> a,
) -> a {
let assert Ok(connection) = open_synced_database(path, url, token)
let value = f(connection)
let assert Ok(_) = close(connection)
value
}
pub type Replicated {
Replicated(frame_no: Option(Int), frames_synced: Int)
}
@external(erlang, "libsql_ffi", "sync")
fn sync_(a: Connection) -> Result(#(Int, Int), Error)
@external(erlang, "libsql_ffi", "replication_index")
fn replication_index_(a: Connection) -> Result(Dynamic, Error)
pub fn sync(connection: Connection) -> Result(Replicated, Error) {
use result <- result.try(sync_(connection))
let #(frame_no_raw, frames_synced) = result
let frame_no = case frame_no_raw {
-1 -> option.None
n -> option.Some(n)
}
Ok(Replicated(frame_no: frame_no, frames_synced: frames_synced))
}
pub fn replication_index(
connection: Connection,
) -> Result(Option(Int), Error) {
use raw <- result.try(replication_index_(connection))
case decode.run(raw, decode.optional(decode.int)) {
Ok(val) -> Ok(val)
Error(_) -> Ok(option.None)
}
}
pub fn exec(sql: String, on connection: Connection) -> Result(Nil, Error) {
exec_(sql, connection)
}
@external(erlang, "libsql_ffi", "exec_batch")
fn exec_batch_(a: String, b: Connection, c: List(List(Value))) -> Result(Nil, Error)
pub fn exec_batch(
sql: String,
on connection: Connection,
with batches: List(List(Value)),
) -> Result(Nil, Error) {
exec_batch_(sql, connection, batches)
}
pub fn begin(on connection: Connection) -> Result(Nil, Error) {
exec("BEGIN", connection)
}
pub fn commit(on connection: Connection) -> Result(Nil, Error) {
exec("COMMIT", connection)
}
pub fn rollback(on connection: Connection) -> Result(Nil, Error) {
exec("ROLLBACK", connection)
}
pub fn transaction(
on connection: Connection,
run f: fn() -> Result(a, Error),
) -> Result(a, Error) {
use _ <- result.try(begin(connection))
case f() {
Ok(value) -> {
use _ <- result.try(commit(connection))
Ok(value)
}
Error(e) -> {
let _ = rollback(connection)
Error(e)
}
}
}
pub fn query(
sql: String,
on connection: Connection,
with arguments: List(Value),
expecting decoder: Decoder(t),
) -> Result(List(t), Error) {
use rows <- result.try(run_query(sql, connection, arguments))
use rows <- result.try(
list.try_map(over: rows, with: fn(row) { decode.run(row, decoder) })
|> result.map_error(decode_error),
)
Ok(rows)
}
pub fn query_named(
sql: String,
on connection: Connection,
with arguments: List(#(String, Value)),
expecting decoder: Decoder(t),
) -> Result(List(t), Error) {
use rows <- result.try(run_query_named(sql, connection, arguments))
use rows <- result.try(
list.try_map(over: rows, with: fn(row) { decode.run(row, decoder) })
|> result.map_error(decode_error),
)
Ok(rows)
}
pub fn query_first(
sql: String,
on connection: Connection,
with arguments: List(Value),
expecting decoder: Decoder(t),
) -> Result(Option(t), Error) {
use rows <- result.try(query(sql, connection, arguments, decoder))
case rows {
[] -> Ok(None)
[first, ..] -> Ok(Some(first))
}
}
pub fn query_first_named(
sql: String,
on connection: Connection,
with arguments: List(#(String, Value)),
expecting decoder: Decoder(t),
) -> Result(Option(t), Error) {
use rows <- result.try(query_named(sql, connection, arguments, decoder))
case rows {
[] -> Ok(None)
[first, ..] -> Ok(Some(first))
}
}
pub fn query_one(
sql: String,
on connection: Connection,
with arguments: List(Value),
expecting decoder: Decoder(t),
) -> Result(t, Error) {
use rows <- result.try(query(sql, connection, arguments, decoder))
case rows {
[one] -> Ok(one)
[] ->
Error(LibsqlError(
GenericError,
"Query returned 0 rows, expected exactly 1",
-1,
))
_ ->
Error(LibsqlError(
GenericError,
"Query returned multiple rows, expected exactly 1",
-1,
))
}
}
pub fn query_one_named(
sql: String,
on connection: Connection,
with arguments: List(#(String, Value)),
expecting decoder: Decoder(t),
) -> Result(t, Error) {
use rows <- result.try(query_named(sql, connection, arguments, decoder))
case rows {
[one] -> Ok(one)
[] ->
Error(LibsqlError(
GenericError,
"Query returned 0 rows, expected exactly 1",
-1,
))
_ ->
Error(LibsqlError(
GenericError,
"Query returned multiple rows, expected exactly 1",
-1,
))
}
}
@external(erlang, "libsql_ffi", "changes")
fn changes_(a: Connection) -> Result(Int, Error)
@external(erlang, "libsql_ffi", "total_changes")
fn total_changes_(a: Connection) -> Result(Int, Error)
@external(erlang, "libsql_ffi", "last_insert_rowid")
fn last_insert_rowid_(a: Connection) -> Result(Int, Error)
@external(erlang, "libsql_ffi", "interrupt")
fn interrupt_(a: Connection) -> Result(Nil, Error)
pub fn changes(connection: Connection) -> Result(Int, Error) {
changes_(connection)
}
pub fn total_changes(connection: Connection) -> Result(Int, Error) {
total_changes_(connection)
}
pub fn last_insert_rowid(connection: Connection) -> Result(Int, Error) {
last_insert_rowid_(connection)
}
pub fn interrupt(connection: Connection) -> Result(Nil, Error) {
interrupt_(connection)
}
@external(erlang, "libsql_ffi", "prepare")
fn prepare_(a: String, b: Connection) -> Result(Statement, Error)
@external(erlang, "libsql_ffi", "exec_prepared")
fn exec_prepared_(a: List(Value), b: Statement) -> Result(Nil, Error)
@external(erlang, "libsql_ffi", "query_prepared")
fn run_query_prepared(
a: List(Value),
b: Statement,
) -> Result(List(Dynamic), Error)
@external(erlang, "libsql_ffi", "finalize")
fn finalize_(a: Statement) -> Result(Nil, Error)
pub fn prepare(sql: String, on connection: Connection) -> Result(Statement, Error) {
prepare_(sql, connection)
}
pub fn exec_prepared(
on statement: Statement,
with params: List(Value),
) -> Result(Nil, Error) {
exec_prepared_(params, statement)
}
pub fn query_prepared(
on statement: Statement,
with params: List(Value),
expecting decoder: Decoder(t),
) -> Result(List(t), Error) {
use rows <- result.try(run_query_prepared(params, statement))
use rows <- result.try(
list.try_map(over: rows, with: fn(row) { decode.run(row, decoder) })
|> result.map_error(decode_error),
)
Ok(rows)
}
pub fn finalize(statement: Statement) -> Result(Nil, Error) {
finalize_(statement)
}
pub fn with_statement(
sql: String,
on connection: Connection,
run f: fn(Statement) -> a,
) -> a {
let assert Ok(statement) = prepare(sql, connection)
let value = f(statement)
let assert Ok(_) = finalize(statement)
value
}
@external(erlang, "libsql_ffi", "query")
fn run_query(
a: String,
b: Connection,
c: List(Value),
) -> Result(List(Dynamic), Error)
@external(erlang, "libsql_ffi", "query_named")
fn run_query_named(
a: String,
b: Connection,
c: List(#(String, Value)),
) -> Result(List(Dynamic), Error)
@external(erlang, "libsql_ffi", "coerce_value")
fn coerce_value(a: a) -> Value
@external(erlang, "libsql_ffi", "exec")
fn exec_(a: String, b: Connection) -> Result(Nil, Error)
pub fn nullable(inner_type: fn(t) -> Value, value: Option(t)) -> Value {
case value {
Some(value) -> inner_type(value)
None -> null()
}
}
pub fn int(value: Int) -> Value {
coerce_value(value)
}
pub fn float(value: Float) -> Value {
coerce_value(value)
}
pub fn text(value: String) -> Value {
coerce_value(value)
}
@external(erlang, "libsql_ffi", "coerce_blob")
fn coerce_blob(a: BitArray) -> Value
pub fn blob(value: BitArray) -> Value {
coerce_blob(value)
}
pub fn bool(value: Bool) -> Value {
int(case value {
True -> 1
False -> 0
})
}
@external(erlang, "libsql_ffi", "null")
fn null_() -> Value
pub fn null() -> Value {
null_()
}
pub fn decode_bool() -> Decoder(Bool) {
use b <- decode.then(decode.int)
case b {
0 -> decode.success(False)
_ -> decode.success(True)
}
}
fn decode_error(errors: List(decode.DecodeError)) -> Error {
let assert [decode.DecodeError(expected, actual, path), ..] = errors
let path = string.join(path, ".")
let message =
"Decoder failed, expected "
<> expected
<> ", got "
<> actual
<> " in "
<> path
LibsqlError(code: GenericError, message: message, offset: -1)
}