From 866b3cf2bbd7d9dd754bd3040e8c8971d36b8966 Mon Sep 17 00:00:00 2001 From: Felix Mayer Date: Thu, 21 Nov 2024 10:41:11 +0100 Subject: [PATCH 1/5] Add support for prepared statements --- CHANGELOG.md | 5 ++++ README.md | 5 ++++ gleam.toml | 2 +- src/sqlight.gleam | 49 ++++++++++++++++++++++++++++++++ src/sqlight_ffi.erl | 32 +++++++++++++++++++-- src/sqlight_ffi.js | 40 +++++++++++++++++++++++--- test/sqlight_test.gleam | 62 +++++++++++++++++++++++++++++++---------- 7 files changed, 174 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e733c62..35819b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.10.0 - 2024-11-21 + +- Added support for prepared statements. They can be created with `prepare` and + queried with `query_prepared`. + ## v0.9.1 - 2024-08-19 - Fixed a bug where bit arrays could bind to the incorrect SQLite type. diff --git a/README.md b/README.md index d329d6a..0b80d9d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ pub fn main() { " let assert Ok([#("Nubi", 4), #("Ginny", 6)]) = sqlight.query(sql, on: conn, with: [sqlight.int(7)], expecting: cat_decoder) + + let assert Ok(prepared) = + sqlight.prepare(sql, on: conn, expecting: cat_decoder) + let assert Ok([#("Nubi", 4), #("Ginny", 6)]) = + sqlight.query_prepared(prepared, with: [sqlight.int(7)]) } ``` diff --git a/gleam.toml b/gleam.toml index 462b640..d5a43d7 100644 --- a/gleam.toml +++ b/gleam.toml @@ -1,5 +1,5 @@ name = "sqlight" -version = "0.9.1" +version = "0.10.0" licences = ["Apache-2.0"] description = "Use SQLite from Gleam!" diff --git a/src/sqlight.gleam b/src/sqlight.gleam index 2321c9c..29a1cb7 100644 --- a/src/sqlight.gleam +++ b/src/sqlight.gleam @@ -6,6 +6,16 @@ import gleam/string pub type Connection +type PreparedStatement + +pub opaque type Statement(t) { + Statement( + connection: Connection, + prepared_statement: PreparedStatement, + decoder: Decoder(t), + ) +} + /// A value that can be sent to SQLite as one of the arguments to a /// parameterised SQL query. pub type Value @@ -389,6 +399,33 @@ pub fn query( Ok(rows) } +pub fn prepare( + sql: String, + on connection: Connection, + expecting decoder: Decoder(t), +) -> Result(Statement(t), Error) { + do_prepare(sql, connection) + |> result.then(fn(prepared_statement) { + Ok(Statement(connection, prepared_statement, decoder)) + }) +} + +pub fn query_prepared( + statement: Statement(t), + with arguments: List(Value), +) -> Result(List(t), Error) { + use rows <- result.then(run_prepared_query( + statement.prepared_statement, + statement.connection, + arguments, + )) + use rows <- result.then( + list.try_map(over: rows, with: statement.decoder) + |> result.map_error(decode_error), + ) + Ok(rows) +} + @external(erlang, "sqlight_ffi", "query") @external(javascript, "./sqlight_ffi.js", "query") fn run_query( @@ -397,6 +434,18 @@ fn run_query( c: List(Value), ) -> Result(List(Dynamic), Error) +@external(erlang, "sqlight_ffi", "prepare") +@external(javascript, "./sqlight_ffi.js", "prepare") +fn do_prepare(a: String, b: Connection) -> Result(PreparedStatement, Error) + +@external(erlang, "sqlight_ffi", "query_prepared") +@external(javascript, "./sqlight_ffi.js", "query_prepared") +fn run_prepared_query( + a: PreparedStatement, + b: Connection, + c: List(Value), +) -> Result(List(Dynamic), Error) + @external(erlang, "sqlight_ffi", "coerce_value") @external(javascript, "./sqlight_ffi.js", "coerce_value") fn coerce_value(a: a) -> Value diff --git a/src/sqlight_ffi.erl b/src/sqlight_ffi.erl index e86247e..83a3ff1 100644 --- a/src/sqlight_ffi.erl +++ b/src/sqlight_ffi.erl @@ -1,13 +1,22 @@ -module(sqlight_ffi). -export([ - status/0, query/3, exec/2, coerce_value/1, coerce_blob/1, null/0, open/1, close/1 + status/0, + query/3, + prepare/2, + query_prepared/3, + exec/2, + coerce_value/1, + coerce_blob/1, + null/0, + open/1, + close/1 ]). open(Name) -> case esqlite3:open(unicode:characters_to_list(Name)) of {ok, Connection} -> {ok, Connection}; - {error, Code} -> + {error, Code} -> Code1 = sqlight:error_code_from_int(Code), {error, {sqlight_error, Code1, <<>>, -1}} end. @@ -24,6 +33,25 @@ query(Sql, Connection, Arguments) when is_binary(Sql) -> Rows -> {ok, lists:map(fun erlang:list_to_tuple/1, Rows)} end. +prepare(Sql, Connection) when is_binary(Sql) -> + case esqlite3:prepare(Connection, Sql) of + {error, Code} -> to_error(Connection, Code); + {ok, Statement} -> {ok, Statement} + end. + +query_prepared(Statement, Connection, Arguments) -> + case + (case esqlite3:bind(Statement, Arguments) of + ok -> + esqlite3:fetchall(Statement); + {error, _} = Error -> + Error + end) + of + {error, Code} -> to_error(Connection, Code); + Rows -> {ok, lists:map(fun erlang:list_to_tuple/1, Rows)} + end. + exec(Sql, Connection) -> case esqlite3:exec(Connection, Sql) of {error, Code} -> to_error(Connection, Code); diff --git a/src/sqlight_ffi.js b/src/sqlight_ffi.js index 14f5b74..33c174e 100644 --- a/src/sqlight_ffi.js +++ b/src/sqlight_ffi.js @@ -2,12 +2,23 @@ import { List, Ok, Error as GlError } from "./gleam.mjs"; import { SqlightError, error_code_from_int } from "./sqlight.mjs"; import { DB } from "https://deno.land/x/sqlite@v3.7.0/mod.ts"; +function wrapDB(db) { + return { + db: db, + statements: [], + }; +} + export function open(path) { - return new Ok(new DB(path)); + return new Ok(wrapDB(new DB(path))); } export function close(connection) { - connection.close(); + for(let statement of connection.statements) { + statement.finalize(); + } + connection.statements = []; + connection.db.close(); return new Ok(undefined); } @@ -26,7 +37,7 @@ export function status(connection) { export function exec(sql, connection) { try { - connection.execute(sql); + connection.db.execute(sql); return new Ok(undefined); } catch (error) { return convert_error(error); @@ -36,7 +47,28 @@ export function exec(sql, connection) { export function query(sql, connection, parameters) { let rows; try { - rows = connection.query(sql, parameters.toArray()); + rows = connection.db.query(sql, parameters.toArray()); + } catch (error) { + return convert_error(error); + } + return new Ok(List.fromArray(rows)); +} + +export function prepare(sql, connection) { + let statement; + try { + statement = connection.db.prepareQuery(sql); + connection.statements.push(statement); + } catch (error) { + return convert_error(error); + } + return new Ok(statement); +} + +export function query_prepared(statement, _connection, parameters) { + let rows; + try { + rows = statement.all(parameters.toArray()); } catch (error) { return convert_error(error); } diff --git a/test/sqlight_test.gleam b/test/sqlight_test.gleam index ced5a62..e08ac34 100644 --- a/test/sqlight_test.gleam +++ b/test/sqlight_test.gleam @@ -51,10 +51,38 @@ pub fn with_connection_test() { } } +fn prepare_and_query( + sql: String, + on connection: sqlight.Connection, + with arguments: List(sqlight.Value), + expecting decoder: dynamic.Decoder(t), +) -> Result(List(t), sqlight.Error) { + case sqlight.prepare(sql, connection, decoder) { + Ok(prepared) -> { + let result = sqlight.query_prepared(prepared, arguments) + + sqlight.query(sql, connection, arguments, decoder) + |> should.equal(result) + + sqlight.query_prepared(prepared, arguments) + |> should.equal(result) + + result + } + Error(error) -> { + let result = Error(error) + sqlight.query(sql, connection, arguments, decoder) + |> should.equal(result) + + result + } + } +} + pub fn query_1_test() { use conn <- connect() let assert Ok([#(1, 2, 3), #(4, 5, 6)]) = - sqlight.query( + prepare_and_query( "select 1, 2, 3 union all select 4, 5, 6", conn, [], @@ -65,13 +93,13 @@ pub fn query_1_test() { pub fn query_2_test() { use conn <- connect() let assert Ok([1337]) = - sqlight.query("select 1337", conn, [], dynamic.element(0, dynamic.int)) + prepare_and_query("select 1337", conn, [], dynamic.element(0, dynamic.int)) } pub fn bind_int_test() { use conn <- connect() let assert Ok([12_345]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.int(12_345)], @@ -82,7 +110,7 @@ pub fn bind_int_test() { pub fn bind_float_test() { use conn <- connect() let assert Ok([12_345.6789]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.float(12_345.6789)], @@ -93,7 +121,7 @@ pub fn bind_float_test() { pub fn bind_text_test() { use conn <- connect() let assert Ok(["hello"]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.text("hello")], @@ -104,7 +132,7 @@ pub fn bind_text_test() { pub fn bind_blob_test() { use conn <- connect() let assert Ok([#(<<123, 0>>, "blob")]) = - sqlight.query( + prepare_and_query( "select ?1, typeof(?1)", conn, [sqlight.blob(<<123, 0>>)], @@ -115,7 +143,7 @@ pub fn bind_blob_test() { pub fn bind_null_test() { use conn <- connect() let assert Ok([option.None]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.null()], @@ -126,7 +154,7 @@ pub fn bind_null_test() { pub fn bind_bool_test() { use conn <- connect() let assert Ok([True]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.bool(True)], @@ -140,7 +168,7 @@ pub fn exec_test() { let assert Ok(Nil) = sqlight.exec("insert into cats (name) values ('Tim')", conn) let assert Ok(["Tim"]) = - sqlight.query( + prepare_and_query( "select name from cats", conn, [], @@ -176,7 +204,12 @@ pub fn readme_example_test() { where age < ? " let assert Ok([#("Nubi", 4), #("Ginny", 6)]) = - sqlight.query(sql, on: conn, with: [sqlight.int(7)], expecting: cat_decoder) + prepare_and_query( + sql, + on: conn, + with: [sqlight.int(7)], + expecting: cat_decoder, + ) } pub fn error_syntax_error_test() { @@ -242,14 +275,15 @@ pub fn decode_error_test() { sqlight.GenericError, "Decoder failed, expected String, got Int in 0", -1, - )) = sqlight.query("select 1", conn, [], dynamic.element(0, dynamic.string)) + )) = + prepare_and_query("select 1", conn, [], dynamic.element(0, dynamic.string)) } pub fn query_error_test() { use conn <- sqlight.with_connection(":memory:") let assert Error(SqlightError(sqlight.GenericError, _, _)) = - sqlight.query( + prepare_and_query( "this isn't a valid query", conn, [], @@ -261,7 +295,7 @@ pub fn bind_nullable_test() { use conn <- connect() let assert Ok([option.Some(12_345)]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.nullable(sqlight.int, option.Some(12_345))], @@ -269,7 +303,7 @@ pub fn bind_nullable_test() { ) let assert Ok([option.None]) = - sqlight.query( + prepare_and_query( "select ?", conn, [sqlight.nullable(sqlight.int, option.None)], From 0d100429ab09bcc432384e3e11919755efea21a5 Mon Sep 17 00:00:00 2001 From: Felix Mayer Date: Thu, 21 Nov 2024 14:06:35 +0100 Subject: [PATCH 2/5] Refactoring --- src/sqlight.gleam | 24 ++++++++++++------------ src/sqlight_ffi.erl | 11 +---------- test/sqlight_test.gleam | 40 +++++++++++++++++++++++++--------------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/sqlight.gleam b/src/sqlight.gleam index 29a1cb7..fd17199 100644 --- a/src/sqlight.gleam +++ b/src/sqlight.gleam @@ -6,12 +6,12 @@ import gleam/string pub type Connection -type PreparedStatement +type Statement -pub opaque type Statement(t) { - Statement( +pub opaque type PreparedStatement(t) { + PreparedStatement( connection: Connection, - prepared_statement: PreparedStatement, + prepared_statement: Statement, decoder: Decoder(t), ) } @@ -403,24 +403,24 @@ pub fn prepare( sql: String, on connection: Connection, expecting decoder: Decoder(t), -) -> Result(Statement(t), Error) { +) -> Result(PreparedStatement(t), Error) { do_prepare(sql, connection) |> result.then(fn(prepared_statement) { - Ok(Statement(connection, prepared_statement, decoder)) + Ok(PreparedStatement(connection, prepared_statement, decoder)) }) } pub fn query_prepared( - statement: Statement(t), + prepared_statement: PreparedStatement(t), with arguments: List(Value), ) -> Result(List(t), Error) { use rows <- result.then(run_prepared_query( - statement.prepared_statement, - statement.connection, + prepared_statement.prepared_statement, + prepared_statement.connection, arguments, )) use rows <- result.then( - list.try_map(over: rows, with: statement.decoder) + list.try_map(over: rows, with: prepared_statement.decoder) |> result.map_error(decode_error), ) Ok(rows) @@ -436,12 +436,12 @@ fn run_query( @external(erlang, "sqlight_ffi", "prepare") @external(javascript, "./sqlight_ffi.js", "prepare") -fn do_prepare(a: String, b: Connection) -> Result(PreparedStatement, Error) +fn do_prepare(a: String, b: Connection) -> Result(Statement, Error) @external(erlang, "sqlight_ffi", "query_prepared") @external(javascript, "./sqlight_ffi.js", "query_prepared") fn run_prepared_query( - a: PreparedStatement, + a: Statement, b: Connection, c: List(Value), ) -> Result(List(Dynamic), Error) diff --git a/src/sqlight_ffi.erl b/src/sqlight_ffi.erl index 83a3ff1..eac8f83 100644 --- a/src/sqlight_ffi.erl +++ b/src/sqlight_ffi.erl @@ -1,16 +1,7 @@ -module(sqlight_ffi). -export([ - status/0, - query/3, - prepare/2, - query_prepared/3, - exec/2, - coerce_value/1, - coerce_blob/1, - null/0, - open/1, - close/1 + status/0, query/3, prepare/2, query_prepared/3, exec/2, coerce_value/1, coerce_blob/1, null/0, open/1, close/1 ]). open(Name) -> diff --git a/test/sqlight_test.gleam b/test/sqlight_test.gleam index e08ac34..7ad9550 100644 --- a/test/sqlight_test.gleam +++ b/test/sqlight_test.gleam @@ -51,7 +51,7 @@ pub fn with_connection_test() { } } -fn prepare_and_query( +fn prepare_and_query_multiple_times( sql: String, on connection: sqlight.Connection, with arguments: List(sqlight.Value), @@ -82,7 +82,7 @@ fn prepare_and_query( pub fn query_1_test() { use conn <- connect() let assert Ok([#(1, 2, 3), #(4, 5, 6)]) = - prepare_and_query( + prepare_and_query_multiple_times( "select 1, 2, 3 union all select 4, 5, 6", conn, [], @@ -93,13 +93,18 @@ pub fn query_1_test() { pub fn query_2_test() { use conn <- connect() let assert Ok([1337]) = - prepare_and_query("select 1337", conn, [], dynamic.element(0, dynamic.int)) + prepare_and_query_multiple_times( + "select 1337", + conn, + [], + dynamic.element(0, dynamic.int), + ) } pub fn bind_int_test() { use conn <- connect() let assert Ok([12_345]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.int(12_345)], @@ -110,7 +115,7 @@ pub fn bind_int_test() { pub fn bind_float_test() { use conn <- connect() let assert Ok([12_345.6789]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.float(12_345.6789)], @@ -121,7 +126,7 @@ pub fn bind_float_test() { pub fn bind_text_test() { use conn <- connect() let assert Ok(["hello"]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.text("hello")], @@ -132,7 +137,7 @@ pub fn bind_text_test() { pub fn bind_blob_test() { use conn <- connect() let assert Ok([#(<<123, 0>>, "blob")]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?1, typeof(?1)", conn, [sqlight.blob(<<123, 0>>)], @@ -143,7 +148,7 @@ pub fn bind_blob_test() { pub fn bind_null_test() { use conn <- connect() let assert Ok([option.None]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.null()], @@ -154,7 +159,7 @@ pub fn bind_null_test() { pub fn bind_bool_test() { use conn <- connect() let assert Ok([True]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.bool(True)], @@ -168,7 +173,7 @@ pub fn exec_test() { let assert Ok(Nil) = sqlight.exec("insert into cats (name) values ('Tim')", conn) let assert Ok(["Tim"]) = - prepare_and_query( + prepare_and_query_multiple_times( "select name from cats", conn, [], @@ -204,7 +209,7 @@ pub fn readme_example_test() { where age < ? " let assert Ok([#("Nubi", 4), #("Ginny", 6)]) = - prepare_and_query( + prepare_and_query_multiple_times( sql, on: conn, with: [sqlight.int(7)], @@ -276,14 +281,19 @@ pub fn decode_error_test() { "Decoder failed, expected String, got Int in 0", -1, )) = - prepare_and_query("select 1", conn, [], dynamic.element(0, dynamic.string)) + prepare_and_query_multiple_times( + "select 1", + conn, + [], + dynamic.element(0, dynamic.string), + ) } pub fn query_error_test() { use conn <- sqlight.with_connection(":memory:") let assert Error(SqlightError(sqlight.GenericError, _, _)) = - prepare_and_query( + prepare_and_query_multiple_times( "this isn't a valid query", conn, [], @@ -295,7 +305,7 @@ pub fn bind_nullable_test() { use conn <- connect() let assert Ok([option.Some(12_345)]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.nullable(sqlight.int, option.Some(12_345))], @@ -303,7 +313,7 @@ pub fn bind_nullable_test() { ) let assert Ok([option.None]) = - prepare_and_query( + prepare_and_query_multiple_times( "select ?", conn, [sqlight.nullable(sqlight.int, option.None)], From 152b7fc60d89aaa0db3c41a7c9f655779525c573 Mon Sep 17 00:00:00 2001 From: Felix Mayer Date: Thu, 21 Nov 2024 22:24:37 +0100 Subject: [PATCH 3/5] Update CHANGELOG.md Update Changelog date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35819b8..4a5d7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v0.10.0 - 2024-11-21 +## v0.10.0 - 2024-11-22 - Added support for prepared statements. They can be created with `prepare` and queried with `query_prepared`. From e9a4fc4f6f5b25459c99f710e2462d5ff75b0820 Mon Sep 17 00:00:00 2001 From: Felix Mayer Date: Wed, 27 Nov 2024 00:39:29 +0100 Subject: [PATCH 4/5] Reset prepared statement after use and flag them as persistent --- src/sqlight_ffi.erl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/sqlight_ffi.erl b/src/sqlight_ffi.erl index eac8f83..8aefdd0 100644 --- a/src/sqlight_ffi.erl +++ b/src/sqlight_ffi.erl @@ -25,7 +25,7 @@ query(Sql, Connection, Arguments) when is_binary(Sql) -> end. prepare(Sql, Connection) when is_binary(Sql) -> - case esqlite3:prepare(Connection, Sql) of + case esqlite3:prepare(Connection, Sql, [{persistent, true}]) of {error, Code} -> to_error(Connection, Code); {ok, Statement} -> {ok, Statement} end. @@ -39,8 +39,12 @@ query_prepared(Statement, Connection, Arguments) -> Error end) of - {error, Code} -> to_error(Connection, Code); - Rows -> {ok, lists:map(fun erlang:list_to_tuple/1, Rows)} + {error, Code} -> + esqlite3:reset(Statement), + to_error(Connection, Code); + Rows -> + esqlite3:reset(Statement), + {ok, lists:map(fun erlang:list_to_tuple/1, Rows)} end. exec(Sql, Connection) -> From 0ce797a8727b95583d76e056698757c172000a86 Mon Sep 17 00:00:00 2001 From: Felix Mayer Date: Wed, 27 Nov 2024 00:43:02 +0100 Subject: [PATCH 5/5] Add test for a configuration use case --- test/sqlight_test.gleam | 142 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/test/sqlight_test.gleam b/test/sqlight_test.gleam index 7ad9550..0f32974 100644 --- a/test/sqlight_test.gleam +++ b/test/sqlight_test.gleam @@ -1,6 +1,7 @@ import gleam/dynamic import gleam/list import gleam/option +import gleam/result import gleeunit import gleeunit/should import sqlight.{SqlightError} @@ -320,3 +321,144 @@ pub fn bind_nullable_test() { dynamic.element(0, dynamic.optional(dynamic.int)), ) } + +pub fn configuration_use_case_test() { + use conn <- connect() + + sqlight.exec( + " +CREATE TABLE \"configuration\" ( + \"key\" TEXT NOT NULL, + \"value\" TEXT NOT NULL, + \"created_at\" INTEGER NOT NULL, + PRIMARY KEY(\"key\", \"created_at\" DESC) +); + ", + conn, + ) + |> should.be_ok + + let decoder = dynamic.tuple3(dynamic.string, dynamic.string, dynamic.int) + + let get_sql = + " +SELECT + \"key\", + \"value\", + \"created_at\" +FROM + \"configuration\" +WHERE + \"key\" = ? +ORDER BY + \"created_at\" DESC +LIMIT 1; + " + + let set_sql = + " +INSERT INTO + \"configuration\" + (\"key\", \"value\", \"created_at\") +VALUES + (?, ?, ?); + " + + let get_stmt = sqlight.prepare(get_sql, conn, decoder) |> should.be_ok + let set_stmt = sqlight.prepare(set_sql, conn, dynamic.dynamic) |> should.be_ok + + let get_queried = fn(key: String) -> Result( + option.Option(String), + sqlight.Error, + ) { + sqlight.query(get_sql, conn, [sqlight.text(key)], decoder) + |> result.map(fn(values) { + case values { + [] -> option.None + [value, ..] -> option.Some(value.1) + } + }) + } + let set_queried = fn(key: String, value: String, now: Int) -> Result( + Nil, + sqlight.Error, + ) { + sqlight.query( + set_sql, + conn, + [sqlight.text(key), sqlight.text(value), sqlight.int(now)], + dynamic.dynamic, + ) + |> result.map(fn(_) { Nil }) + } + + let get_prepared = fn(key: String) -> Result( + option.Option(String), + sqlight.Error, + ) { + sqlight.query_prepared(get_stmt, [sqlight.text(key)]) + |> result.map(fn(values) { + case values { + [] -> option.None + [value, ..] -> option.Some(value.1) + } + }) + } + let set_prepared = fn(key: String, value: String, now: Int) -> Result( + Nil, + sqlight.Error, + ) { + sqlight.query_prepared(set_stmt, [ + sqlight.text(key), + sqlight.text(value), + sqlight.int(now), + ]) + |> result.map(fn(_) { Nil }) + } + + list.map( + [#(get_queried, set_queried), #(get_prepared, set_prepared)], + fn(sql) { + let #(get, set) = sql + sqlight.exec("DELETE FROM \"configuration\";", conn) |> should.be_ok + + list.map(["test", "a.test", "b.test", "a.test.a"], fn(key) { + get(key) + |> should.be_ok + |> should.be_none + + set(key, key <> "1", 1) |> should.be_ok + get(key) + |> should.be_ok + |> option.map(should.equal(_, key <> "1")) + |> should.be_some + + set(key, key <> "3", 3) |> should.be_ok + get(key) + |> should.be_ok + |> option.map(should.equal(_, key <> "3")) + |> should.be_some + + set(key, key <> "2", 2) |> should.be_ok + get(key) + |> should.be_ok + |> option.map(should.equal(_, key <> "3")) + |> should.be_some + + set(key, key <> "5", 5) |> should.be_ok + get(key) + |> should.be_ok + |> option.map(should.equal(_, key <> "5")) + |> should.be_some + + set(key, key <> "0", 0) |> should.be_ok + get(key) + |> should.be_ok + |> option.map(should.equal(_, key <> "5")) + |> should.be_some + + set(key, key <> "0", 0) |> should.be_error + }) + }, + ) +}