diff --git a/gren.json b/gren.json index 72a0bfc..06b3fb6 100644 --- a/gren.json +++ b/gren.json @@ -1,34 +1,42 @@ { - "type": "package", - "platform": "node", - "name": "gren-lang/node", - "summary": "Run Gren on Node.js", - "license": "BSD-3-Clause", - "version": "6.1.3", - "exposed-modules": [ - "Node", - "Init", - "Terminal", - "ChildProcess", - "FileSystem", - "FileSystem.FileHandle", - "FileSystem.Path", - "HttpClient", - "HttpServer", - "HttpServer.Response", - "WebSocketServer", - "WebSocketServer.Connection", - "Sqlite", - "Sqlite.Decode", - "Sqlite.Decode.Row", - "Sqlite.Encode", - "Sqlite.Encode.Row", - "Sqlite.Function", - "Sqlite.Aggregate" - ], - "gren-version": "0.6.0 <= v < 0.7.0", - "dependencies": { - "gren-lang/core": "7.0.0 <= v < 8.0.0", - "gren-lang/url": "6.0.0 <= v < 7.0.0" - } + "type": "package", + "platform": "node", + "name": "gren-lang/node", + "summary": "Run Gren on Node.js", + "license": "BSD-3-Clause", + "version": "6.1.3", + "exposed-modules": { + "Core": [ + "Node", + "Init", + "Terminal", + "ChildProcess" + ], + "File System": [ + "FileSystem", + "FileSystem.FileHandle", + "FileSystem.Path" + ], + "HTTP": [ + "HttpClient", + "HttpServer", + "HttpServer.Response", + "WebSocketServer", + "WebSocketServer.Connection" + ], + "SQLite": [ + "Sqlite", + "Sqlite.Decode", + "Sqlite.Encode", + "Sqlite.Decode.Row", + "Sqlite.Encode.Row", + "Sqlite.Function", + "Sqlite.Aggregate" + ] + }, + "gren-version": "0.6.0 <= v < 0.7.0", + "dependencies": { + "gren-lang/core": "7.0.0 <= v < 8.0.0", + "gren-lang/url": "6.0.0 <= v < 7.0.0" + } } diff --git a/src/Sqlite.gren b/src/Sqlite.gren index 902bdd9..59cd208 100644 --- a/src/Sqlite.gren +++ b/src/Sqlite.gren @@ -1,5 +1,90 @@ -module Sqlite exposing (..) +module Sqlite exposing ( + Database, + Location(..), + Options, + defaultOptions, open, close, + Query, + getOne, getMaybeOne, getAll, foldl, + Statement, + ExecutionSummary, + execute, executeAll, executeForEach, executeScript, + Backup, + backup, withBackupPageRate, runBackup, + Error(..), + AbortError(..), + AuthError(..), + BusyError(..), + CantOpenError(..), + ConstraintError(..), + CorruptError(..), + IoError(..), + LockedError(..), + NoticeError(..), + ReadOnlyError(..), + errorToString, + inTransaction ) +{-| Use SQLite natively in Gren. + +@docs Database + +@docs Location + +@docs Options + +@docs defaultOptions, open, close + +## Query + +@docs Query + +@docs getOne, getMaybeOne, getAll, foldl + +## Statement + +@docs Statement + +@docs ExecutionSummary + +@docs execute, executeAll, executeForEach, executeScript + +## Backup + +@docs Backup + +@docs backup, withBackupPageRate, runBackup + +## Errors + +@docs Error + +@docs AbortError + +@docs AuthError + +@docs BusyError + +@docs CantOpenError + +@docs ConstraintError + +@docs CorruptError + +@docs IoError + +@docs LockedError + +@docs NoticeError + +@docs ReadOnlyError + +@docs errorToString + +## Utilities + +@docs inTransaction + +-} import Array.Builder import FileSystem @@ -17,11 +102,20 @@ import Gren.Kernel.Sqlite -- From: https://nodejs.org/docs/latest-v22.x/api/sqlite.html +{-| An opened SQLite database. This value is required to complete operations on that +database. +-} type Database -- NOTE: implemented in kernel code = Database +{-| Where a database is located when [opened](#open). + +- `Memory` opens the database in memory. When your program stops, any data +written is lost. +- `File` opens the database in a given `Path` in the file system. +-} type Location = Memory | File Path @@ -30,10 +124,17 @@ type Location {-| When opening the database, you can define a set of restrictions that apply for all queries and statements executed. - * `readOnly` - If true, the database is opened in read-only mode. All attempted modifications will fail. - * `allowExtension` - If true, the loadExtension SQL function and the loadExtension() method are enabled. - * `enableForeignKeyConstraints` - If true, foreign key constraints are enabled. This is recommended but can be disabled for compatibility with legacy database schemas. The enforcement of foreign key constraints can be enabled and disabled after opening the database using PRAGMA foreign_keys. - * `timeout` - The busy timeout in milliseconds. This is the maximum amount of time that SQLite will wait for a database lock to be released before returning an error. 0 or negative number means no timeout. +- `readOnly`, when `True`, opens the database in read-only mode. All attempted +modifications will fail. +- `allowExtension`, when `True`, enables the loadExtension SQL function and the +loadExtension() method. +- `enableForeignKeyConstraints`, when `True`, foreign key constraints are enabled. This +is recommended but can be disabled for compatibility with legacy database schemas. The +enforcement of foreign key constraints can be enabled and disabled after opening the +database using `PRAGMA foreign_keys`. +- `timeout` is the busy timeout in milliseconds. This is the maximum amount of time that +SQLite will wait for a database lock to be released before returning an error. 0 or +negative number means no timeout. -} type alias Options = { readOnly : Bool @@ -43,6 +144,14 @@ type alias Options = } +{-| A sensible set of default [`Options`](#Options) for a SQLite database. + + { readOnly = False + , enableForeignKeyConstraints = True + , allowExtension = False + , timeout = 5000 + } +-} defaultOptions : Options defaultOptions = { readOnly = False @@ -52,6 +161,13 @@ defaultOptions = } +{-| Open a database at the given [`Location`](#Location) if it already exists. If a +database does not exist at the given location, this function creates a database at +that location. + +This is the only way to obtain the [`Database`](#Database) type that is required for +nearly all functionality in the `Sqlite` family of modules. +-} open : FileSystem.Permission -> Options -> Location -> Task Error Database open _ opts location = when location is @@ -62,6 +178,8 @@ open _ opts location = Gren.Kernel.Sqlite.openInMemory opts +{-| Close the given [`Database`](#Database). +-} close : Database -> Task Error {} close = Gren.Kernel.Sqlite.close @@ -71,6 +189,9 @@ close = -- Backup +{-| A backup configuration for a database. This value can be created by running the +[`backup`](#backup) function. +-} type Backup = Backup { destination : Path @@ -79,7 +200,8 @@ type Backup } - +{-| Produce a backup configuration for the given [`Database`](#Database). +-} backup : FileSystem.Permission -> Path -> Database -> Backup backup _ path db = Backup @@ -89,11 +211,18 @@ backup _ path db = } +{-| Configure the number of pages that are saved in each batch of the backup process. By +default, the backup page rate is 100. +-} withBackupPageRate : Int -> Backup -> Backup withBackupPageRate pages (Backup record) = Backup { record | pages = pages } +{-| Run a database [`Backup`](#Backup) after it's been configured. The database can still +be used safely during the backup process. However, if there are any mutations performed +on the database outside of your Gren program, the backup process will restart. +-} runBackup : Backup -> Task Error {} runBackup (Backup { destination, pages, db }) = Gren.Kernel.Sqlite.backup destination pages db @@ -103,6 +232,23 @@ runBackup (Backup { destination, pages, db }) = -- Query +{-| A SQL query to get some data from a database. An example of how this can +be used is below: + + { query = "SELECT name, email, age FROM users WHERE id = :user_id + , parameters = + [ Sqlite.Encode.Row.column "user_id" <| Encode.string "123" + ] + , rowDecoder = + Sqlite.Decode.Row.column "name" Decode.string <| \name -> + Sqlite.Decode.Row.column "email" Decode.string <| \email -> + Sqlite.Decode.Row.succeed { name = name, email = email } + } + +For more information on encoding parameters, visit [`Sqlite.Encode.Row`](Sqlite.Encode.Row) +module documentation. For more information on decoding the returned rows, visit +[`Sqlite.Decode.Row`](Sqlite.Decode.Row) module documentation. +-} type alias Query value = { query : String , parameters : Array Sqlite.Encode.Row.Value @@ -110,6 +256,9 @@ type alias Query value = } +{-| Get a single value returned by a [`Query`](#Query). This will fail if +either no results or more than one result is returned. +-} getOne : Database -> Query value -> Task Error value getOne db query = Gren.Kernel.Sqlite.getMaybeOne query db @@ -124,17 +273,24 @@ getOne db query = ) +{-| Get either `Nothing` (no result) or a single value returned by a [`Query`](#Query). +-} getMaybeOne : Database -> Query value -> Task Error (Maybe value) getMaybeOne db query = Gren.Kernel.Sqlite.getMaybeOne query db - + +{-| Get all values returned by a [`Query`](#Query). +-} getAll : Database -> Query value -> Task Error (Array value) getAll db query = foldl Array.Builder.pushLast (Array.Builder.empty 0) db query |> Task.map Array.Builder.toArray +{-| Accumulate a new value by iterating over all results from a given +[`Query`](#Query), one row at a time. +-} foldl : (a -> b -> b) -> b -> Database -> Query a -> Task Error b foldl func acc db query = Gren.Kernel.Sqlite.foldl query db func acc @@ -143,6 +299,19 @@ foldl func acc db query = -- EXECUTIONS +{-| SQL that does not return any data from the database. An example of how this can +be used is below: + + { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" + , parameters = + [ Sqlite.Encode.Row.column "name" <| Encode.string p.name + , Sqlite.Encode.Row.column "role" <| Encode.string p.role + ] + } + +For more information on encoding parameters, visit [`Sqlite.Encode.Row`](Sqlite.Encode.Row) +module documentation. +-} type alias Statement = { statement : String , parameters : Array Sqlite.Encode.Row.Value @@ -151,8 +320,9 @@ type alias Statement = {-| A summary of how an execution changed the database. - * `changes` - how many rows were inserted/updated/removed - * `lastInsertRowId` - the ID of the last row inserted. Might be negative if no row was inserted +- `changes` is how many rows were inserted, updated, or removed. +- `lastInsertRowId` is the ID of the last row inserted. This could be be negative if no row +was inserted. -} type alias ExecutionSummary = { changes : Int @@ -160,6 +330,8 @@ type alias ExecutionSummary = } +{-| Execute some given SQL. +-} execute : Database -> Statement -> Task Error ExecutionSummary execute db { statement, parameters } = executeForEach @@ -170,6 +342,8 @@ execute db { statement, parameters } = [{}] +{-| Execute many [statements](#Statement) sequentially. +-} executeAll : Database -> Array Statement -> Task Error (Array ExecutionSummary) executeAll db statements = statements @@ -177,6 +351,18 @@ executeAll db statements = |> Task.sequence +{-| Execute a single [`Statement`](#Statement) for many sets of encoded parameters. This is +very useful for inserting lots of rows into the database. + + Sqlite.executeForEach db + { statement = "INSERT INTO people (name) VALUES (:name)" + , parameters = encoder + } + [ { name = "Joey" } + , { name = "Robin" } + , { name = "Justin" } + ] +-} executeForEach : Database -> { statement : String @@ -188,6 +374,10 @@ executeForEach db stmt vals = Gren.Kernel.Sqlite.executeMany stmt vals db +{-| Execute some arbitrary SQL. This is most helpful when you need to run more than one +SQL statement, as [`execute`](#execute), [`executeForAll`](#executeForAll), and +[`executeForEach`](#executeForEach) only allow a single SQL statement. +-} executeScript : Database -> String -> Task Error Database executeScript db script = Gren.Kernel.Sqlite.executeScript script db @@ -196,6 +386,16 @@ executeScript db script = -- TRANSACTIONS +{-| Run a `Task` (like [statements](#Statement) or [queries](#Query)) in a +transaction. This is equivalant to running some SQL like this: + + BEGIN TRANSACTION + -- The given `Task` runs here + COMMIT + +If the given `Task` has an error, `COMMIT` will instead be `ROLLBACK`, reverting +any changes within the transaction. +-} inTransaction : Database -> Task Error a -> Task Error a inTransaction db f = executeScript db "BEGIN TRANSACTION" @@ -218,7 +418,18 @@ inTransaction db f = -- ERRORS -{-|-} +{-| All possible errors that can happen when running functions in this module. Most of these +come from the [SQLite error documentation](https://sqlite.org/rescode.html). + +While any of these errors can happen when interacting with a SQLite database, some more +commmon types of errors are below: + +- `ConstraintError` and its associated errors. These set of errors deal with any failed +constraints, like primary keys, foreign keys, or column types. +- `NoResultsError` and `MultipleResultsError` are isolated to the [`getOne`](#getOne) +function. +- `DecodingError` when decoding data returned from a [`Query`](#Query). +-} type Error = AbortError AbortError | AuthError AuthError @@ -730,8 +941,9 @@ errorCodeToError errorType errorCode message = {-| Transform a given `Error` into a descriptive `String`. -Each error string for known SQLite errors contain the error code found at https://sqlite.org/rescode.html. -More details on the errors can be found by their code on that page. +Each error string for known SQLite errors contain the error code found at +[https://sqlite.org/rescode.html](https://sqlite.org/rescode.html).More details on the +errors can be found by their code on that page. -} errorToString : Error -> String errorToString passedError = diff --git a/src/Sqlite/Aggregate.gren b/src/Sqlite/Aggregate.gren index 900cfc6..347c974 100644 --- a/src/Sqlite/Aggregate.gren +++ b/src/Sqlite/Aggregate.gren @@ -1,6 +1,67 @@ -module Sqlite.Aggregate exposing ( .. ) - -{-|-} +module Sqlite.Aggregate exposing (Direction(..), Function, Aggregate, aggregate, start, arg, return, register ) + +{-| Create and add custom aggregate functions to a SQLite database. + + Sqlite.Aggregate.register "fruit_portions" db <| + Sqlite.Aggregate.aggregate + { init = 0 + , function = + Sqlite.Aggregate.start <| \state -> + Sqlite.Aggregate.arg Decode.string <| \split -> + Sqlite.Aggregate.arg Decode.int <| \count -> + Sqlite.Aggregate.return <| \direction -> ( + let + portions = + when split is + "half" -> 2 + "third" -> 3 + "quarter" -> 4 + _ -> 1 + in + when direction is + Sqlite.Aggregate.Entering -> + state + (portions * count) + + Sqlite.Aggregate.Exiting -> + state - (portions * count) + ) + , result = \state -> Encode.int state + } + +SQLite aggregate functions are much like `foldl` in Gren - You take some state and some amount of data +(in this case, rows returned from some SQL) and run a function over that state and each row, updating +the state until there's no more data and the final state can be returned. + +As an example, the custom aggregate function above could be used with the following SQL: + + SELECT + fruit_portions('half', banana) AS banana_portion_count, + fruit_portions('quarter', apple) AS apple_portion_count, + fruit_portions('full', pear) AS pear_portion_count + FROM baskets; + +In this case, you'd get the appropriate amount of portions for each type of fruit returned without +any additional work or code in Gren or SQL. The custom aggregate function handles it for you! + +Like [custom functions](Sqlite.Function), custom aggregate functions can have any number of expected +arguments by chaining together multiple `arg` functions for each expected argument. All arguments are +expected to be provided when using the function in SQL. + +Custom aggregate functions in this module work as normal aggregate functions and as window +functions. It does this by providing the [`Direction`](#Direction) type, which allows both +aggregate and window functions to work properly. If you want to write a custom aggregate function +that will not be used as a window function, `Direction` can be ignored or is expected to always +be `Entering`. + +@docs Function + +@docs Aggregate + +@docs Direction + +@docs aggregate, start, arg, return, register + +-} import Sqlite import Sqlite.Decode as Decode @@ -10,17 +71,21 @@ import Json.Decode import Task exposing ( Task ) +{-| A custom SQLite aggregate function. +-} type Function state = Function (Direction -> state -> Array Json.Encode.Value -> Result String state) -{-|-} +{-| A type representing if a value is entering or exiting the aggregate window. +-} type Direction = Entering | Exiting -{-|-} +{-| An custom SQLite aggregate. +-} type Aggregate state = Aggregate { init : state @@ -29,6 +94,17 @@ type Aggregate state } +{-| Define an aggregate function. + +- `init` is the initial state the aggregate function will start with. +- `function` is the actual function that runs on SQLite data . You can construct this function +with the [`start`](#start), [`arg`](#arg), and [`return`](#return) functions in this module. +- `result` transforms the final state value into a [`Sqlite.Encode.Value`](Sqlite.Encode#Value) +so SQLite can properly understand and use the result. + +When the aggregate function is constructed, it can be registered with the SQLite database using +the [`register`](#register) function. +-} aggregate : { init : state, function: Function state, result : state -> Encode.Value } -> Aggregate state aggregate { init, function, result } = Aggregate @@ -38,7 +114,9 @@ aggregate { init, function, result } = } -{-|-} +{-| Start the process of creating a new aggregate function. This is required in order to get +access to the aggregate state. +-} start : (state -> Function state) -> Function state start func = Function <| \direction state args -> @@ -47,7 +125,9 @@ start func = innerFunc direction state args -{-|-} +{-| An argument in a custom SQLite function. The argument must be decoded into a Gren +value by using a [`Sqlite.Decode.Decoder`](Sqlite.Decode#Decoder). +-} arg : Decode.Decoder a -> (a -> Function state) -> Function state arg decoder func = Function <| \direction state args -> @@ -66,7 +146,9 @@ arg decoder func = Err "not enough arguments passed" -{-|-} +{-| The final step in an aggregate function, giving you the [`Direction`](#Direction) +of the data and allowing you to return a final state value. +-} return : (Direction -> state) -> Function state return func = Function <| \direction _state args -> @@ -78,7 +160,12 @@ return func = Err "too many arguments passed" -{-|-} +{-| Register an aggregate function with the given SQLite `Database`, allowing it to be used +in SQL queries. + +Aggregate functions must be registered with a database each time it's opened and used. It's +recommended you register all necessary functions right after [opening](Sqlite#open) the database. +-} register : String -> Sqlite.Database -> Aggregate state -> Task x {} register name db (Aggregate { init, function, result }) = let diff --git a/src/Sqlite/Decode.gren b/src/Sqlite/Decode.gren index b746ff8..7e51078 100644 --- a/src/Sqlite/Decode.gren +++ b/src/Sqlite/Decode.gren @@ -10,45 +10,28 @@ module Sqlite.Decode exposing , time , maybe - -- Composing - , succeed - , fail - , unwrap ) -{-| Decode SQL results into Gren values. - -Decoders can be chained to decode all fields in the result set, -with the string parameter matching the name of the field in the query. +{-| Decode SQL values into Gren values. -For example: +You will typically see these while decoding results from queries: - Sqlite.getOne - { query = "SELECT id, name FROM users WHERE id = :id" - , parameters = [ Sqlite.Encode.int "id" 1 ] - , rowDecoder = - Sqlite.Decode.int "id" <| \id -> - Sqlite.Decode.string "name" <| \name -> - Sqlite.Decode.succeed - { id = id - , name = name - } + Sqlite.getAll + { statement = "SELECT name, level FROM hero" + , parameters = [] + , rowDecoder = + Sqlite.Decode.Row "name" Sqlite.Decode.string <| \name -> + Sqlite.Decode.Row "level" Sqlite.Decode.int <| \level -> + Sqlite.Decode.Row.suceed { name = name, level = level } } + db @docs Decoder -## Field Decoders - @docs string, int, float, bool, json, time, maybe -## Composing Decoders - -@docs succeed, fail - -## Helpers - -@docs toJson +@docs unwrap -} @@ -57,7 +40,7 @@ import Math import Time -{-| A decoder for a database row. +{-| A decoder for a SQLite value. -} type Decoder a = Decoder (Json.Decode.Decoder a) @@ -86,8 +69,8 @@ float = {-| Decode a boolean field. -Booleans in sqlite are stored as integers with 1 and 0 as True and False. -See +Booleans in SQLite are stored as integers with 1 and 0 as True and False. +See for more information. -} bool : Decoder Bool bool = @@ -118,11 +101,11 @@ Use `json()` in your SELECT to ensure you get text regardless of how the JSON wa { query = "SELECT json(tags) as tags FROM items WHERE id = 1" , parameters = [] , rowDecoder = - Sqlite.Decode.json (Json.Decode.array Json.Decode.string) "tags" <| \tags -> - Sqlite.Decode.succeed { tags = tags } + Sqlite.Decode.Row.column "tags" Sqlite.Decode.json (Json.Decode.array Json.Decode.string) <| \tags -> + Sqlite.Decode.Row.succeed { tags = tags } } -See +See for more information. -} json : Json.Decode.Decoder a -> Decoder a json jsonDecoder = @@ -141,11 +124,11 @@ json jsonDecoder = {-| Decode a Time.Posix value. -Expects the db field to hold the number of seconds since unix epoch, which is +Expects the database field to hold the number of seconds since unix epoch, which is how both [Sqlite.Encode.time](Sqlite.Encode#time) and [Sqlite.Encode.timeWithMillis](Sqlite.Encode#timeWithMillis) store it, and which aligns with SQLite's `unixepoch` function. -See +See for more information. -} time : Decoder Time.Posix time = @@ -160,8 +143,8 @@ time = The first parameter is the field decoder function for the type if the value is not null. For example, to decode a nullable TEXT field: - Decode.field (Decode.maybe Decode.string) "nickname" <| \maybeNickname -> - Sqlite.Decode.succeed maybeNickname + Sqlite.Decode.Row.column "nickname" (Decode.maybe Decode.string) <| \maybeNickname -> + Sqlite.Decode.Row.succeed maybeNickname -} maybe : Decoder a -> Decoder (Maybe a) maybe decoder = @@ -172,29 +155,6 @@ maybe decoder = ] --- COMPOSING - - -{-| Create a decoder that always succeeds with the given value. - -Often used as the final step when chaining field decoders: - - Sqlite.Decode.string "name" <| \name -> - Sqlite.Decode.int "age" <| \age -> - Sqlite.Decode.succeed { name = name, age = age } --} -succeed : a -> Decoder a -succeed val = - Decoder (Json.Decode.succeed val) - - -{-| Force a decoder to fail with the given message. --} -fail : String -> Decoder a -fail reason = - Decoder (Json.Decode.fail reason) - - -- HELPERS @@ -206,6 +166,10 @@ toJson : Decoder a -> Json.Decode.Decoder a toJson = unwrap + +{-| Transform a `Decoder` into a `Json.Decode.Decoder`. This is used by internal modules +and is not needed for normal use. +-} unwrap : Decoder a -> Json.Decode.Decoder a unwrap (Decoder d) = d diff --git a/src/Sqlite/Decode/Row.gren b/src/Sqlite/Decode/Row.gren index 1580011..497933e 100644 --- a/src/Sqlite/Decode/Row.gren +++ b/src/Sqlite/Decode/Row.gren @@ -1,24 +1,26 @@ module Sqlite.Decode.Row exposing ( Decoder, column, succeed, fail ) -{-|-} +{-| Decode SQL values by their column name. + +This decoder is primarily used to decode results from a SQLite [query](Sqlite#Query). + +@docs Decoder, column + +@docs succeed, fail + +-} import Json.Decode import Sqlite.Decode -{-|-} +{-| A decoder for a row. +-} type Decoder a = Decoder (Json.Decode.Decoder a) -{-| - -Sqlite.Decode.Row.column "name" Sqlite.Decode.string <| \name -> -Sqlite.Decode.Row.column "something" Sqlite.Decode.string <| \something -> -Sqlite.Decode.Row.succeed - { name = name - , something = something - } +{-| Decode a [`Sqlite.Encode.Value`](Sqlite.Encode#Value) for the given column name. -} column : String -> Sqlite.Decode.Decoder a -> (a -> Decoder b) -> Decoder b column columnName decoder cont = @@ -37,9 +39,12 @@ column columnName decoder cont = Often used as the final step when chaining field decoders: - Sqlite.Decode.string "name" <| \name -> - Sqlite.Decode.int "age" <| \age -> - Sqlite.Decode.succeed { name = name, age = age } + Sqlite.Decode.Row.column "name" Sqlite.Decode.string <| \name -> + Sqlite.Decode.Row.column "age" Sqlite.Decode.int <| \age -> + Sqlite.Decode.Row.succeed + { name = name + , age = age + } -} succeed : a -> Decoder a succeed val = diff --git a/src/Sqlite/Encode.gren b/src/Sqlite/Encode.gren index 75f17fa..1baf09d 100644 --- a/src/Sqlite/Encode.gren +++ b/src/Sqlite/Encode.gren @@ -22,13 +22,16 @@ You will typically use these in the `parameters` field. For example: Sqlite.execute { statement = "INSERT INTO hero (name, level) VALUES (:name, :level)" , parameters = - [ Encode.string "name" "Peach" - , Encode.int "level" 99 + [ Sqlite.Row.Encode "name" <| Encode.string "name" + , Sqlite.Row.Encode "level" <| Encode.int 99 ] } db -@docs Value, string, int, float, bool, json, time, timeWithMillis, null, maybe, toJson +They are also used to encode values in custom SQLite [functions](Sqlite.Function) and +[aggregates](Sqlite.Aggregate). + +@docs Value, string, int, float, bool, json, time, timeWithMillis, null, maybe, unwrap -} @@ -45,7 +48,7 @@ type Value {-| Encode a boolean value. This will be stored in the db as 1 for true and 0 for false. -See +See for more information. -} bool : Bool -> Value bool b = @@ -97,7 +100,7 @@ regardless of how it was stored: "SELECT json(data) as data FROM items" -See +See for more information. -} json : Json.Encode.Value -> Value json val = @@ -108,7 +111,7 @@ json val = The value will be encoded as an int representing the number of seconds since unix epoch, which aligns with SQLite's `unixepoch()` function. -See +See for more information. If you need subsecond precision, use [timeWithMillis](#timeWithMillis). -} @@ -122,7 +125,7 @@ time t = The value will be encoded as a float representing the number of seconds since unix epoch, with subsecond precision to the millisecond, which aligns with SQLite's `unixepoch` function using the `"subsec"` modifier. -See +See for more information. If you don't need subsecond precision, use [time](#time), which will save storage for large data sets. @@ -137,7 +140,7 @@ timeWithMillis t = The first parameter is the encoder for the type if the value is not null. For example, to encode a nullable TEXT field: - Sqlite.Encode.maybe Sqlite.Encode.string "nickname" maybeName + Sqlite.Encode.Row "nickname" (Sqlite.Encode.maybe Sqlite.Encode.string maybeName) -} maybe : (a -> Value) -> Maybe a -> Value maybe encoder maybeVal = @@ -174,9 +177,11 @@ toJson : Array Value -> Array Json.Encode.Value toJson values = values |> Array.map unwrap - -- |> Json.Encode.object +{-| Transform a `Decoder` into a `Json.Decode.Decoder`. This is used by internal modules +and is not needed for normal use. +-} unwrap : Value -> Json.Encode.Value unwrap (Value v) = v diff --git a/src/Sqlite/Encode/Row.gren b/src/Sqlite/Encode/Row.gren index bb75199..3602fd8 100644 --- a/src/Sqlite/Encode/Row.gren +++ b/src/Sqlite/Encode/Row.gren @@ -1,13 +1,21 @@ module Sqlite.Encode.Row exposing ( Value, column ) -{-|-} +{-| Encode SQL values by their column name. + +These encoders are used to encode parameters when running a SQLite [query](Sqlite#Query) +or [statement](Sqlite#Statement). + +@docs Value, column + +-} import Sqlite.Encode import Json.Encode -{-|-} +{-| An encoded row. +-} type Value = Value { column : String @@ -15,10 +23,7 @@ type Value } -{-| - -Sqlite.Encode.Row.column "name" <| Encode.string "John" - +{-| Encode a given [`Sqlite.Encode.Value`](Sqlite.Encode#Value) for the given column name. -} column : String -> Sqlite.Encode.Value -> Value column name value = diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren index 990c550..8ff9d4f 100644 --- a/src/Sqlite/Function.gren +++ b/src/Sqlite/Function.gren @@ -1,6 +1,34 @@ -module Sqlite.Function exposing ( .. ) +module Sqlite.Function exposing ( Function, arg, return, register ) -{-|-} +{-| Create and add custom functions to a SQLite database. + + Sqlite.Function.register "add_one" db <| + Sqlite.Function.arg Decode.int <| \num -> + Sqlite.Function.return (Encode.int (num + 1)) + +Function arguments must be decoded with [`Sqlite.Decode`](Sqlite.Decode) and the +return value of the function must be an encoded with [`Sqlite.Encode`](Sqlite.Encode). + +The above function can then be using in SQL. + + SELECT answer FROM answers WHERE answer_number = add_one(1) + +For this example, `answer_number` in the SQL above you be equivalant to writing +`answer_number = 2` and will be evaluated by SQLite as such. + +Functions can have any number of arguments by stringing together multiple `arg` +functions for each expected argument. All arguments are expected to be provided +when using the function in SQL. + +Due to limitations in the underlying Node SQLite implementation, custom functions +do not error. Instead, any failures change the return value of the function to +`null`. + +@docs Function + +@docs arg, return, register + +-} import Json.Decode import Json.Encode @@ -10,10 +38,16 @@ import Sqlite.Encode as Encode import Sqlite.Decode as Decode +{-| A custom SQLite function. This function can be used in a database by giving it as +an argument to the [`register`](#register) function. +-} type Function = Function (Array Json.Encode.Value -> Result {} Encode.Value) +{-| An argument in a custom SQLite function. The argument must be decoded into a Gren +value by using a [`Sqlite.Decode.Decoder`](Sqlite.Decode#Decoder). +-} arg : Decode.Decoder a -> (a -> Function) -> Function arg decoder func = Function <| \args -> @@ -32,6 +66,9 @@ arg decoder func = Err {} +{-| The encoded SQLite value that's returned by a custom SQLite function. This return value +is what will be handed to SQLite and must be encoded as [`Sqlite.Encode.Value`](Sqlite.Encode#Value). +-} return : Encode.Value -> Function return value = Function <| \args -> @@ -43,6 +80,12 @@ return value = Err {} +{-| Register a given `Function` with the given SQLite `Database`, allowing it to be used +in SQL queries. + +Functions must be registered with a database each time it's opened and used. It's recommended +you register all necessary functions right after [opening](Sqlite#open) the database. +-} register : String -> Sqlite.Database -> Function -> Task a {} register name db (Function func) = Gren.Kernel.Sqlite.function name func db