Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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",
},
}
214 changes: 189 additions & 25 deletions platform/Sqlite.roc
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ module [
query!,
query_many!,
execute!,
prepare!,
query_prepared!,
query_many_prepared!,
execute_prepared!,
prepare_query!,
prepare_query_many!,
prepare_execute!,
prepare_transaction!,
errcode_to_str,
decode_record,
map_value,
Expand Down Expand Up @@ -223,6 +223,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
Expand Down Expand Up @@ -264,13 +290,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
Expand All @@ -279,9 +333,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 +361,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
Expand All @@ -322,15 +407,93 @@ 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

## 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))

Expand Down Expand Up @@ -411,19 +574,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