From 02a61c3369969220b0b1e7ed81beafe768f6f901 Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Thu, 2 Jan 2025 16:54:34 -0800 Subject: [PATCH 1/4] add improved prepared queries --- examples/sqlite.roc | 23 ++++---- platform/Sqlite.roc | 135 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 121 insertions(+), 37 deletions(-) diff --git a/examples/sqlite.roc b/examples/sqlite.roc index f014c9f4..e62b55a6 100644 --- a/examples/sqlite.roc +++ b/examples/sqlite.roc @@ -9,13 +9,22 @@ import pf.Sqlite main! = \_args -> db_path = try Env.var! "DB_PATH" - todo = try query_todos_by_status! db_path "todo" + query_todos_by_status! = try Sqlite.prepare_query_many! { + path: db_path, + query: "SELECT id, task FROM todos WHERE status = :status;", + bindings: \status -> [{ name: ":status", value: String status }], + rows: { Sqlite.decode_record <- + id: Sqlite.i64 "id" |> Sqlite.map_value Num.toStr, + task: Sqlite.str "task", + }, + } + todo = try query_todos_by_status! "todo" try Stdout.line! "Todo Tasks:" try List.forEachTry! todo \{ id, task } -> Stdout.line! "\tid: $(id), task: $(task)" - completed = try query_todos_by_status! db_path "completed" + completed = try query_todos_by_status! "completed" try Stdout.line! "\nCompleted Tasks:" try List.forEachTry! completed \{ id, task } -> @@ -23,13 +32,3 @@ main! = \_args -> Ok {} -query_todos_by_status! = \db_path, status -> - Sqlite.query_many! { - path: db_path, - query: "SELECT id, task FROM todos WHERE status = :status;", - bindings: [{ name: ":status", value: String status }], - rows: { Sqlite.decode_record <- - id: Sqlite.i64 "id" |> Sqlite.map_value Num.toStr, - task: Sqlite.str "task", - }, - } diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index dfe044f0..0fa87102 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -6,10 +6,9 @@ module [ query!, query_many!, execute!, - prepare!, - query_prepared!, - query_many_prepared!, - execute_prepared!, + prepare_query!, + prepare_query_many!, + prepare_execute!, errcode_to_str, decode_record, map_value, @@ -223,6 +222,32 @@ execute! = \{ path, query: q, bindings } -> stmt = try prepare! { path, query: q } execute_prepared! { stmt, bindings } +## Prepare a lambda to execute a SQL statement that doesn't return any rows (like INSERT, UPDATE, DELETE). +## +## This is useful when you have a query that will be called many times, as it is more efficient than +## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. +## +## ``` +## prepared_query! = try Sqlite.prepare_execute! { +## path: "path/to/database.db", +## query: "INSERT INTO todos (task, status) VALUES (:task, :status)", +## bindings: \{task, status} -> [{name: ":task", value: String task}, {name: ":status", value: String task}] +## } +## +## try prepared_query! { task: "create a todo", status: "completed" } +## ``` +prepare_execute! : + { + path : Str, + query : Str, + bindings : in -> List Binding, + } + => Result (in => Result {} [SqliteErr ErrCode Str, UnhandledRows]) [SqliteErr ErrCode Str] +prepare_execute! = \{ path, query: q, bindings: tranform } -> + stmt = try prepare! { path, query: q } + Ok \input -> + execute_prepared! { stmt, bindings: tranform input } + ## Execute a prepared SQL statement that doesn't return any rows. ## ## This is more efficient than [execute!] when running the same query multiple times @@ -264,13 +289,41 @@ query! : path : Str, query : Str, bindings : List Binding, - row : SqlDecode a (RowCountErr err), + row : SqlDecode out (RowCountErr err), } - => Result a (SqlDecodeErr (RowCountErr err)) + => Result out (SqlDecodeErr (RowCountErr err)) query! = \{ path, query: q, bindings, row } -> stmt = try prepare! { path, query: q } query_prepared! { stmt, bindings, row } +## Prepare a lambda to execute a SQL query and decode exactly one row into a value. +## +## This is useful when you have a query that will be called many times, as it is more efficient than +## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. +## +## ``` +## prepared_query! = try Sqlite.prepare_query! { +## path: "path/to/database.db", +## query: "SELECT COUNT(*) as \"count\" FROM users;", +## bindings: \{} -> [] +## row: Sqlite.u64 "count", +## } +## +## count = try prepared_query! {} +## ``` +prepare_query! : + { + path : Str, + query : Str, + bindings : in -> List Binding, + row : SqlDecode out (RowCountErr err), + } + => Result (in => Result out (SqlDecodeErr (RowCountErr err))) [SqliteErr ErrCode Str] +prepare_query! = \{ path, query: q, bindings: tranform, row } -> + stmt = try prepare! { path, query: q } + Ok \input -> + query_prepared! { stmt, bindings: tranform input, row } + ## Execute a prepared SQL query and decode exactly one row into a value. ## ## This is more efficient than [query!] when running the same query multiple times @@ -279,9 +332,9 @@ query_prepared! : { stmt : Stmt, bindings : List Binding, - row : SqlDecode a (RowCountErr err), + row : SqlDecode out (RowCountErr err), } - => Result a (SqlDecodeErr (RowCountErr err)) + => Result out (SqlDecodeErr (RowCountErr err)) query_prepared! = \{ stmt, bindings, row: decode } -> try bind! stmt bindings res = decode_exactly_one_row! stmt decode @@ -307,13 +360,44 @@ query_many! : path : Str, query : Str, bindings : List Binding, - rows : SqlDecode a err, + rows : SqlDecode out err, } - => Result (List a) (SqlDecodeErr err) + => Result (List out) (SqlDecodeErr err) query_many! = \{ path, query: q, bindings, rows } -> stmt = try prepare! { path, query: q } query_many_prepared! { stmt, bindings, rows } +## Prepare a lambda to execute a SQL query and decode multiple rows into a list of values. +## +## This is useful when you have a query that will be called many times, as it is more efficient than +## preparing the query each time it is called. This is usually done in `init!` with the prepared `Stmt` stored in the model. +## +## ``` +## prepared_query! = try Sqlite.prepare_query_many! { +## path: "path/to/database.db", +## query: "SELECT * FROM todos;", +## bindings: \{} -> [] +## rows: { Sqlite.decode_record <- +## id: Sqlite.i64 "id", +## task: Sqlite.str "task", +## }, +## } +## +## rows = try prepared_query! {} +## ``` +prepare_query_many! : + { + path : Str, + query : Str, + bindings : in -> List Binding, + rows : SqlDecode out err, + } + => Result (in => Result (List out) (SqlDecodeErr err)) [SqliteErr ErrCode Str] +prepare_query_many! = \{ path, query: q, bindings: tranform, rows } -> + stmt = try prepare! { path, query: q } + Ok \input -> + query_many_prepared! { stmt, bindings: tranform input, rows } + ## Execute a prepared SQL query and decode multiple rows into a list of values. ## ## This is more efficient than [query_many!] when running the same query multiple times @@ -322,9 +406,9 @@ query_many_prepared! : { stmt : Stmt, bindings : List Binding, - rows : SqlDecode a err, + rows : SqlDecode out err, } - => Result (List a) (SqlDecodeErr err) + => Result (List out) (SqlDecodeErr err) query_many_prepared! = \{ stmt, bindings, rows: decode } -> try bind! stmt bindings res = decode_rows! stmt decode @@ -411,19 +495,20 @@ decode_rows! = \stmt, @SqlDecode gen_decode -> # internal use only decoder : (Value -> Result a (SqlDecodeErr err)) -> (Str -> SqlDecode a err) -decoder = \fn -> \name -> - @SqlDecode \cols -> - - found = List.findFirstIndex cols \x -> x == name - when found is - Ok index -> - \stmt -> - try column_value! stmt index - |> fn - - Err NotFound -> - \_ -> - Err (FieldNotFound name) +decoder = \fn -> + \name -> + @SqlDecode \cols -> + + found = List.findFirstIndex cols \x -> x == name + when found is + Ok index -> + \stmt -> + try column_value! stmt index + |> fn + + Err NotFound -> + \_ -> + Err (FieldNotFound name) ## Decode a [Value] keeping it tagged. This is useful when data could be many possible types. ## From da7e97cef13cf6d089b6fcfc2aa817b116e8d15c Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Thu, 2 Jan 2025 17:26:09 -0800 Subject: [PATCH 2/4] add transaction prep and running --- platform/Sqlite.roc | 79 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 0fa87102..bb6dc186 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -9,6 +9,7 @@ module [ prepare_query!, prepare_query_many!, prepare_execute!, + prepare_transaction!, errcode_to_str, decode_record, map_value, @@ -415,6 +416,84 @@ query_many_prepared! = \{ stmt, bindings, rows: decode } -> try reset! stmt res +## Generates a higher order function for running a transaction. +## The transaction will automatically rollback on any error. +## +## Deferred means that the transaction does not actually start until the database is first accessed. +## Immediate causes the database connection to start a new write immediately, without waiting for a write statement. +## Exclusive is similar to Immediate in that a write transaction is started immediately. Exclusive and Immediate are the same in WAL mode, but in other journaling modes, Exclusive prevents other database connections from reading the database while the transaction is underway. +## +## ``` +## exec_transaction! = try prepare_transaction! { path: "path/to/database.db" } +## +## try exec_transaction! \{} -> +## try Sqlite.execute! { +## path: "path/to/database.db", +## query: "INSERT INTO users (first, last) VALUES (:first, :last);", +## bindings: [ +## { name: ":first", value: String "John" }, +## { name: ":last", value: String "Smith" }, +## ], +## } +## +## # Oh no, hit an error. Need to rollback. +## # Note: Error could be anything. +## Err NeedToRollback +## ``` +prepare_transaction! : + { + path : Str, + mode ? [Deferred, Immediate, Exclusive], + } + => + Result (({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err]) [SqliteErr ErrCode Str] +prepare_transaction! = \{ path, mode ? Deferred } -> + mode_str = + when mode is + Deferred -> "DEFERRED" + Immediate -> "IMMEDIATE" + Exclusive -> "EXCLUSIVE" + + begin_stmt = try prepare! { path, query: "BEGIN $(mode_str)" } + end_stmt = try prepare! { path, query: "END" } + rollback_stmt = try prepare! { path, query: "ROLLBACK" } + + Ok \transaction! -> + Sqlite.execute_prepared! { + stmt: begin_stmt, + bindings: [], + } + |> Result.mapErr \_ -> FailedToBeginTransaction + |> try + + end_transaction! = \res -> + when res is + Ok v -> + Sqlite.execute_prepared! { + stmt: end_stmt, + bindings: [], + } + |> Result.mapErr \_ -> FailedToEndTransaction + |> try + Ok v + + Err e -> + Err (TransactionFailed e) + + when transaction! {} |> end_transaction! is + Ok v -> + Ok v + + Err e -> + Sqlite.execute_prepared! { + stmt: rollback_stmt, + bindings: [], + } + |> Result.mapErr \_ -> FailedToRollbackTransaction + |> try + + Err e + SqlDecodeErr err : [FieldNotFound Str, SqliteErr ErrCode Str]err SqlDecode a err := List Str -> (Stmt => Result a (SqlDecodeErr err)) From 278a57ec804a2e30f250e641d621a4638f5b0e9a Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Thu, 2 Jan 2025 17:50:06 -0800 Subject: [PATCH 3/4] make types a bit nicer --- platform/Sqlite.roc | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index bb6dc186..65cb4acb 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -2,7 +2,6 @@ module [ Value, ErrCode, Binding, - Stmt, query!, query_many!, execute!, @@ -10,6 +9,10 @@ module [ prepare_query_many!, prepare_execute!, prepare_transaction!, + PreparedExecuteStmt, + PreparedQueryStmt, + PreparedQueryManyStmt, + ExecuteTransaction, errcode_to_str, decode_record, map_value, @@ -223,6 +226,9 @@ execute! = \{ path, query: q, bindings } -> stmt = try prepare! { path, query: q } execute_prepared! { stmt, bindings } +## A function that executes a prepared execute stmt that doesn't return any data. +PreparedExecuteStmt in : in => Result {} [SqliteErr ErrCode Str, UnhandledRows] + ## Prepare a lambda to execute a SQL statement that doesn't return any rows (like INSERT, UPDATE, DELETE). ## ## This is useful when you have a query that will be called many times, as it is more efficient than @@ -243,7 +249,7 @@ prepare_execute! : query : Str, bindings : in -> List Binding, } - => Result (in => Result {} [SqliteErr ErrCode Str, UnhandledRows]) [SqliteErr ErrCode Str] + => Result (PreparedExecuteStmt in) [SqliteErr ErrCode Str] prepare_execute! = \{ path, query: q, bindings: tranform } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -297,6 +303,9 @@ query! = \{ path, query: q, bindings, row } -> stmt = try prepare! { path, query: q } query_prepared! { stmt, bindings, row } +## A function that executes a perpared query and decodes exactly one row into a value. +PreparedQueryStmt in out err : in => Result out (SqlDecodeErr (RowCountErr err)) + ## Prepare a lambda to execute a SQL query and decode exactly one row into a value. ## ## This is useful when you have a query that will be called many times, as it is more efficient than @@ -319,7 +328,7 @@ prepare_query! : bindings : in -> List Binding, row : SqlDecode out (RowCountErr err), } - => Result (in => Result out (SqlDecodeErr (RowCountErr err))) [SqliteErr ErrCode Str] + => Result (PreparedQueryStmt in out err) [SqliteErr ErrCode Str] prepare_query! = \{ path, query: q, bindings: tranform, row } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -368,6 +377,9 @@ query_many! = \{ path, query: q, bindings, rows } -> stmt = try prepare! { path, query: q } query_many_prepared! { stmt, bindings, rows } +## A function that executes a perpared query and decodes mutliple rows into a list of values. +PreparedQueryManyStmt in out err : in => Result (List out) (SqlDecodeErr err) + ## Prepare a lambda to execute a SQL query and decode multiple rows into a list of values. ## ## This is useful when you have a query that will be called many times, as it is more efficient than @@ -393,7 +405,7 @@ prepare_query_many! : bindings : in -> List Binding, rows : SqlDecode out err, } - => Result (in => Result (List out) (SqlDecodeErr err)) [SqliteErr ErrCode Str] + => Result (PreparedQueryManyStmt in out err) [SqliteErr ErrCode Str] prepare_query_many! = \{ path, query: q, bindings: tranform, rows } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -416,6 +428,9 @@ query_many_prepared! = \{ stmt, bindings, rows: decode } -> try reset! stmt res +## A function to execute a transaction lambda and automatically rollback on failure. +ExecuteTransaction ok err : ({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err] + ## Generates a higher order function for running a transaction. ## The transaction will automatically rollback on any error. ## @@ -446,7 +461,7 @@ prepare_transaction! : mode ? [Deferred, Immediate, Exclusive], } => - Result (({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err]) [SqliteErr ErrCode Str] + Result (ExecuteTransaction ok err) [SqliteErr ErrCode Str] prepare_transaction! = \{ path, mode ? Deferred } -> mode_str = when mode is From df278fe23568765ba40acd552057f49a69c3e84d Mon Sep 17 00:00:00 2001 From: Brendan Hansknecht Date: Thu, 2 Jan 2025 18:31:51 -0800 Subject: [PATCH 4/4] simplify naming a bit more --- platform/Sqlite.roc | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/platform/Sqlite.roc b/platform/Sqlite.roc index 65cb4acb..632636ef 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -9,10 +9,10 @@ module [ prepare_query_many!, prepare_execute!, prepare_transaction!, - PreparedExecuteStmt, - PreparedQueryStmt, - PreparedQueryManyStmt, - ExecuteTransaction, + ExecuteFn, + QueryFn, + QueryManyFn, + TransactionFn, errcode_to_str, decode_record, map_value, @@ -227,7 +227,7 @@ execute! = \{ path, query: q, bindings } -> execute_prepared! { stmt, bindings } ## A function that executes a prepared execute stmt that doesn't return any data. -PreparedExecuteStmt in : in => Result {} [SqliteErr ErrCode Str, UnhandledRows] +ExecuteFn in : in => Result {} [SqliteErr ErrCode Str, UnhandledRows] ## Prepare a lambda to execute a SQL statement that doesn't return any rows (like INSERT, UPDATE, DELETE). ## @@ -249,7 +249,7 @@ prepare_execute! : query : Str, bindings : in -> List Binding, } - => Result (PreparedExecuteStmt in) [SqliteErr ErrCode Str] + => Result (ExecuteFn in) [SqliteErr ErrCode Str] prepare_execute! = \{ path, query: q, bindings: tranform } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -304,7 +304,7 @@ query! = \{ path, query: q, bindings, row } -> query_prepared! { stmt, bindings, row } ## A function that executes a perpared query and decodes exactly one row into a value. -PreparedQueryStmt in out err : in => Result out (SqlDecodeErr (RowCountErr err)) +QueryFn in out err : in => Result out (SqlDecodeErr (RowCountErr err)) ## Prepare a lambda to execute a SQL query and decode exactly one row into a value. ## @@ -328,7 +328,7 @@ prepare_query! : bindings : in -> List Binding, row : SqlDecode out (RowCountErr err), } - => Result (PreparedQueryStmt in out err) [SqliteErr ErrCode Str] + => Result (QueryFn in out err) [SqliteErr ErrCode Str] prepare_query! = \{ path, query: q, bindings: tranform, row } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -378,7 +378,7 @@ query_many! = \{ path, query: q, bindings, rows } -> query_many_prepared! { stmt, bindings, rows } ## A function that executes a perpared query and decodes mutliple rows into a list of values. -PreparedQueryManyStmt in out err : in => Result (List out) (SqlDecodeErr err) +QueryManyFn in out err : in => Result (List out) (SqlDecodeErr err) ## Prepare a lambda to execute a SQL query and decode multiple rows into a list of values. ## @@ -405,7 +405,7 @@ prepare_query_many! : bindings : in -> List Binding, rows : SqlDecode out err, } - => Result (PreparedQueryManyStmt in out err) [SqliteErr ErrCode Str] + => Result (QueryManyFn in out err) [SqliteErr ErrCode Str] prepare_query_many! = \{ path, query: q, bindings: tranform, rows } -> stmt = try prepare! { path, query: q } Ok \input -> @@ -429,7 +429,7 @@ query_many_prepared! = \{ stmt, bindings, rows: decode } -> res ## A function to execute a transaction lambda and automatically rollback on failure. -ExecuteTransaction ok err : ({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err] +TransactionFn ok err : ({} => Result ok err) => Result ok [FailedToBeginTransaction, FailedToEndTransaction, FailedToRollbackTransaction, TransactionFailed err] ## Generates a higher order function for running a transaction. ## The transaction will automatically rollback on any error. @@ -461,7 +461,7 @@ prepare_transaction! : mode ? [Deferred, Immediate, Exclusive], } => - Result (ExecuteTransaction ok err) [SqliteErr ErrCode Str] + Result (TransactionFn ok err) [SqliteErr ErrCode Str] prepare_transaction! = \{ path, mode ? Deferred } -> mode_str = when mode is