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..632636ef 100644 --- a/platform/Sqlite.roc +++ b/platform/Sqlite.roc @@ -2,14 +2,17 @@ module [ Value, ErrCode, Binding, - Stmt, query!, query_many!, execute!, - prepare!, - query_prepared!, - query_many_prepared!, - execute_prepared!, + prepare_query!, + prepare_query_many!, + prepare_execute!, + prepare_transaction!, + ExecuteFn, + QueryFn, + QueryManyFn, + TransactionFn, errcode_to_str, decode_record, map_value, @@ -223,6 +226,35 @@ 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. +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). +## +## 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 (ExecuteFn in) [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 +296,44 @@ 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 } +## A function that executes a perpared query and decodes exactly one row into a value. +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. +## +## 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 (QueryFn in out 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 +342,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 +370,47 @@ 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 } +## A function that executes a perpared query and decodes mutliple rows into a list of values. +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. +## +## 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 (QueryManyFn in out 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,15 +419,96 @@ 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 try reset! stmt res +## A function to execute a transaction lambda and automatically rollback on failure. +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. +## +## 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 (TransactionFn ok 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)) @@ -411,19 +589,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. ##