Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions examples/sqlite.roc
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,26 @@ 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 } ->
Stdout.line! "\tid: $(id), task: $(task)"

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",
},
}
231 changes: 205 additions & 26 deletions platform/Sqlite.roc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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))

Expand Down Expand Up @@ -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)
Comment on lines +592 to +605
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why the formatter decided to change this.


## Decode a [Value] keeping it tagged. This is useful when data could be many possible types.
##
Expand Down
Loading