From bf0eaef97e8592bbb0c8316d46b9fbe23da60aa5 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Tue, 11 Aug 2026 20:31:46 -0700 Subject: [PATCH 01/11] Initial, experimental, implementation of SQLite functions --- gren.json | 3 ++- integration-tests/sqlite/src/Main.gren | 26 ++++++++++++++++++++ src/Gren/Kernel/Sqlite.js | 29 ++++++++++++++++++++++ src/Sqlite/Function.gren | 34 ++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/Sqlite/Function.gren diff --git a/gren.json b/gren.json index ea07cf7..78510a6 100644 --- a/gren.json +++ b/gren.json @@ -20,7 +20,8 @@ "WebSocketServer.Connection", "Sqlite", "Sqlite.Decode", - "Sqlite.Encode" + "Sqlite.Encode", + "Sqlite.Function" ], "gren-version": "0.6.0 <= v < 0.7.0", "dependencies": { diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index bb9588a..6763192 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -6,6 +6,7 @@ import Test.Runner.Effectful as Effectful exposing (test, await, awaitError, con import Sqlite import Sqlite.Encode as Encode import Sqlite.Decode as Decode exposing (Decoder) +import Sqlite.Function import Task exposing (Task) import FileSystem import FileSystem.Path as Path exposing (Path) @@ -232,6 +233,24 @@ tests fsPerm = await "Read test times back" (Sqlite.getAll db allTimesQ) <| \fromDb -> test "Time values survive float round-trip through db" <| \_ -> Expect.equalArrays testTimes fromDb + , await "Open db for custom SQLite function tests" createTestDb <| \db -> + concat + [ await "Create a custom function" + (Sqlite.Function.fn "always_joey" (\_first -> Sqlite.Function.String "Joey") db) <| \_ -> + await "Insert person into the database" (insertPerson joey db) <| \_ -> + await "Run the custom function in a query" + (let + query = + { query = "SELECT name, role FROM people WHERE name = always_joey(:name)" + , parameters = [ Encode.string "name" "Robin" ] + , rowDecoder = personDecoder + } + in + Sqlite.getOne db query) <| \person -> + test "The function overwrites the name in the query" + (\_ -> Expect.equal person.name "Joey") + + ] ] @@ -258,6 +277,13 @@ justin = } +joey : Person +joey = + { name = "Joey" + , role = "Contributor" + } + + people : Array Person people = [ robin, justin ] diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 5ee456c..ebb10b6 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -52,6 +52,35 @@ var _Sqlite_close = function (db) { }); }; +var _Sqlite_function = F4(function (db, name, func, args) { + return __Scheduler_binding(function (callback) { + try { + const options = { + deterministic: true, + directOnly: true, + useBigIntArguments: false, + varargs: false, + }; + var wrappedFunc; + if (args == 2) { + wrappedFunc = function (first, second) { + const value = A2(func, JSON.stringify(first), JSON.stringify(second)); + return value.a; + }; + } else { + wrappedFunc = function (first) { + const value = func(JSON.stringify(first)); + return value.a; + }; + } + db.function(name, options, wrappedFunc); + callback(__Scheduler_succeed({})); + } catch (e) { + callback(_Sqlite_constructError(e)); + } + }); +}); + var _Sqlite_foldl = F4(function (query, db, func, acc) { return __Scheduler_binding(function (callback) { try { diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren new file mode 100644 index 0000000..00815e0 --- /dev/null +++ b/src/Sqlite/Function.gren @@ -0,0 +1,34 @@ +module Sqlite.Function exposing ( .. ) + +{-|-} + +import Json.Decode +import Json.Encode +import Sqlite +import Task exposing (Task) + + +{-| The result that must be procuded + +When a function run + +- `Error` ... +- `String` when a result will save a `String` to the database +-} +type Result + = Error + | String String + + +{-| Add a custom SQLite function with one expected argument. +-} +fn : String -> (String -> Result) -> Sqlite.Database -> Task a {} +fn name func db = + Gren.Kernel.Sqlite.function db name func 1 + + +{-| Add a custom SQLite function with two expected arguments. +-} +fn2 : String -> (String -> String -> Result) -> Sqlite.Database -> Task a {} +fn2 name func db = + Gren.Kernel.Sqlite.function db name func 2 From b025e2f6bd798e6fd290ca3ee3de412bf5351768 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sat, 22 Aug 2026 10:46:19 -0700 Subject: [PATCH 02/11] Implement new SQLite decoder and encoder APIs In preparation for using the simple encoders/decoders for custom SQLite functions --- integration-tests/sqlite/src/Main.gren | 105 +++++++++-------- src/Sqlite.gren | 8 +- src/Sqlite/Decode.gren | 153 +++++++++++++------------ src/Sqlite/Encode.gren | 119 ++++++++++--------- 4 files changed, 195 insertions(+), 190 deletions(-) diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index 6763192..887b8f4 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -173,14 +173,14 @@ tests fsPerm = statements = [ { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" , parameters = - [ Encode.string "name" robin.name - , Encode.string "role" robin.role + [ Encode.column "name" <| Encode.string robin.name + , Encode.column "role" <| Encode.string robin.role ] } , { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" , parameters = - [ Encode.string "name" justin.name - , Encode.string "role" justin.role + [ Encode.column "name" <| Encode.string justin.name + , Encode.column "role" <| Encode.string justin.role ] } , { statement = @@ -191,8 +191,8 @@ tests fsPerm = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.string "personA" robin.name - , Encode.string "personB" justin.name + [ Encode.column "personA" <| Encode.string robin.name + , Encode.column "personB" <| Encode.string justin.name ] } ] @@ -242,14 +242,13 @@ tests fsPerm = (let query = { query = "SELECT name, role FROM people WHERE name = always_joey(:name)" - , parameters = [ Encode.string "name" "Robin" ] + , parameters = [ Encode.column "name" <| Encode.string "Robin" ] , rowDecoder = personDecoder } in Sqlite.getOne db query) <| \person -> test "The function overwrites the name in the query" (\_ -> Expect.equal person.name "Joey") - ] ] @@ -318,24 +317,24 @@ execDbSchema db = """ -personEncoder : Person -> Array Encode.Value +personEncoder : Person -> Array Encode.Column personEncoder p = - [ Encode.string "name" p.name - , Encode.string "role" p.role + [ Encode.column "name" <| Encode.string p.name + , Encode.column "role" <| Encode.string p.role ] -personDecoder : Decoder Person +personDecoder : Decode.Column Person personDecoder = - Decode.string "name" <| \name -> - Decode.string "role" <| \role -> + Decode.column "name" Decode.string <| \name -> + Decode.column "role" Decode.string <| \role -> Decode.succeed { name = name, role = role } -badPersonDecoder : Decoder Person +badPersonDecoder : Decode.Column Person badPersonDecoder = - Decode.string "namee" <| \name -> - Decode.string "role_" <| \role -> + Decode.column "namee" Decode.string <| \name -> + Decode.column "role_" Decode.string <| \role -> Decode.succeed { name = name, role = role } @@ -358,8 +357,8 @@ defineFriends nameA nameB db = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.string "personA" nameA - , Encode.string "personB" nameB + [ Encode.column "personA" <| Encode.string nameA + , Encode.column "personB" <| Encode.string nameB ] } @@ -372,8 +371,8 @@ defineFriendsById idA idB db = INSERT INTO friends (person_a, person_b) VALUES (:idA, :idB) """ , parameters = - [ Encode.int "idA" idA - , Encode.int "idB" idB + [ Encode.column "idA" <| Encode.int idA + , Encode.column "idB" <| Encode.int idB ] } @@ -381,7 +380,7 @@ defineFriendsById idA idB db = personByNameQ : String -> Sqlite.Query Person personByNameQ name = { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.string "name" name ] + , parameters = [ Encode.column "name" <| Encode.string name ] , rowDecoder = personDecoder } @@ -406,8 +405,8 @@ friendshipQ = """ , parameters = [] , rowDecoder = - Decode.string "person_a" <| \personA -> - Decode.string "person_b" <| \personB -> + Decode.column "person_a" Decode.string <| \personA -> + Decode.column "person_b" Decode.string <| \personB -> Decode.succeed { personA = personA, personB = personB } } @@ -430,7 +429,7 @@ execBadTemplate num db = , parameters = Array.initialize num 0 (\idx -> - Encode.int ("id" ++ String.fromInt idx) idx + Encode.column ("id" ++ String.fromInt idx) <| Encode.int idx ) } @@ -439,7 +438,7 @@ failDecoder : Sqlite.Database -> Task Sqlite.Error Int failDecoder db = Sqlite.getOne db { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.string "name" "Robin" ] + , parameters = [ Encode.column "name" <| Encode.string "Robin" ] , rowDecoder = Decode.fail "Oopsy!" } @@ -448,10 +447,10 @@ badDecoder : Sqlite.Database -> Task Sqlite.Error { name : Int, role : String } badDecoder db = Sqlite.getOne db { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.string "name" "Robin" ] + , parameters = [ Encode.column "name" <| Encode.string "Robin" ] , rowDecoder = - Decode.int "name" <| \name -> - Decode.string "role" <| \role -> + Decode.column "name" Decode.int <| \name -> + Decode.column "role" Decode.string <| \role -> Decode.succeed { name = name, role = role } } @@ -511,33 +510,33 @@ initializeFieldTypeDb fsPerm = ) -allFieldTypesEncoder : AllFieldTypes -> Array Encode.Value +allFieldTypesEncoder : AllFieldTypes -> Array Encode.Column allFieldTypesEncoder row = - [ Encode.string "string_field" row.stringField - , Encode.int "int_field" row.intField - , Encode.float "float_field" row.floatField - , Encode.bool "bool_true" row.boolTrue - , Encode.bool "bool_false" row.boolFalse - , Encode.json "json_field" (Json.Encode.array Json.Encode.string row.jsonField) - , Encode.time "time_field" row.timeField - , Encode.timeWithMillis "time_with_millis_field" row.timeWithMillisField - , Encode.maybe Encode.string "maybe_just_string" row.maybeJustString - , Encode.maybe Encode.string "maybe_nothing_string" row.maybeNothingString + [ Encode.column "string_field" <| Encode.string row.stringField + , Encode.column "int_field" <| Encode.int row.intField + , Encode.column "float_field" <| Encode.float row.floatField + , Encode.column "bool_true" <| Encode.bool row.boolTrue + , Encode.column "bool_false" <| Encode.bool row.boolFalse + , Encode.column "json_field" <| Encode.json (Json.Encode.array Json.Encode.string row.jsonField) + , Encode.column "time_field" <| Encode.time row.timeField + , Encode.column "time_with_millis_field" <| Encode.timeWithMillis row.timeWithMillisField + , Encode.column "maybe_just_string" <| Encode.maybe Encode.string row.maybeJustString + , Encode.column "maybe_nothing_string" <| Encode.maybe Encode.string row.maybeNothingString ] -allFieldTypesDecoder : Decoder AllFieldTypes +allFieldTypesDecoder : Decode.Column AllFieldTypes allFieldTypesDecoder = - Decode.string "string_field" <| \stringField -> - Decode.int "int_field" <| \intField -> - Decode.float "float_field" <| \floatField -> - Decode.bool "bool_true" <| \boolTrue -> - Decode.bool "bool_false" <| \boolFalse -> - Decode.json (Json.Decode.array Json.Decode.string) "json_field" <| \jsonField -> - Decode.time "time_field" <| \timeField -> - Decode.time "time_with_millis_field" <| \timeWithMillisField -> - Decode.maybe Decode.string "maybe_just_string" <| \maybeJustString -> - Decode.maybe Decode.string "maybe_nothing_string" <| \maybeNothingString -> + Decode.column "string_field" Decode.string <| \stringField -> + Decode.column "int_field" Decode.int <| \intField -> + Decode.column "float_field" Decode.float <| \floatField -> + Decode.column "bool_true" Decode.bool <| \boolTrue -> + Decode.column "bool_false" Decode.bool <| \boolFalse -> + Decode.column "json_field" (Decode.json (Json.Decode.array Json.Decode.string)) <| \jsonField -> + Decode.column "time_field" Decode.time <| \timeField -> + Decode.column "time_with_millis_field" Decode.time <| \timeWithMillisField -> + Decode.column "maybe_just_string" (Decode.maybe Decode.string) <| \maybeJustString -> + Decode.column "maybe_nothing_string" (Decode.maybe Decode.string) <| \maybeNothingString -> Decode.succeed { stringField = stringField , intField = intField @@ -628,7 +627,7 @@ insertTimes : Array Time.Posix -> Sqlite.Database -> Task Sqlite.Error Sqlite.Ex insertTimes times db = Sqlite.executeForEach db { statement = "INSERT INTO times (t) VALUES (:t)" - , parameters = \t -> [ Encode.timeWithMillis "t" t ] + , parameters = \t -> [ Encode.column "t" <| Encode.timeWithMillis t ] } times @@ -638,6 +637,6 @@ allTimesQ = { query = "SELECT t FROM times ORDER BY rowid" , parameters = [] , rowDecoder = - Decode.time "t" <| \t -> + Decode.column "t" Decode.time <| \t -> Decode.succeed t } diff --git a/src/Sqlite.gren b/src/Sqlite.gren index 66c6207..c4acac2 100644 --- a/src/Sqlite.gren +++ b/src/Sqlite.gren @@ -70,8 +70,8 @@ close = type alias Query value = { query : String - , parameters : Array Sqlite.Encode.Value - , rowDecoder : Sqlite.Decode.Decoder value + , parameters : Array Sqlite.Encode.Column + , rowDecoder : Sqlite.Decode.Column value } @@ -110,7 +110,7 @@ foldl func acc db query = type alias Statement = { statement : String - , parameters : Array Sqlite.Encode.Value + , parameters : Array Sqlite.Encode.Column } @@ -145,7 +145,7 @@ executeAll db statements = executeForEach : Database -> { statement : String - , parameters : value -> Array Sqlite.Encode.Value + , parameters : value -> Array Sqlite.Encode.Column } -> Array value -> Task Error ExecutionSummary diff --git a/src/Sqlite/Decode.gren b/src/Sqlite/Decode.gren index 2ff7eb4..2e18c77 100644 --- a/src/Sqlite/Decode.gren +++ b/src/Sqlite/Decode.gren @@ -1,5 +1,9 @@ module Sqlite.Decode exposing ( Decoder + , Column + + -- Column decoders + , column -- Field decoders , bool @@ -61,28 +65,51 @@ type Decoder a = Decoder (Json.Decode.Decoder a) +{-|-} +type Column a + = Column (Json.Decode.Decoder a) + + -- FIELD DECODERS +{-| Decode a SQLite column with a specific value type. +-} +column : String -> Decoder a -> (a -> Column b) -> Column b +column columnName (Decoder jsonDecoder) cont = + Json.Decode.field columnName jsonDecoder + |> Json.Decode.andThen (\val -> + let + (Column decoder) = + cont val + in + decoder + ) + |> Column + + +--- + + {-| Decode a string field. -} -string : String -> (String -> Decoder a) -> Decoder a -string fieldName cont = - fieldHelper fieldName Json.Decode.string cont +string :Decoder String +string = + Json.Decode.string |> Decoder {-| Decode an integer field. -} -int : String -> (Int -> Decoder a) -> Decoder a -int fieldName cont = - fieldHelper fieldName Json.Decode.int cont +int : Decoder Int +int = + Json.Decode.int |> Decoder {-| Decode a float field. -} -float : String -> (Float -> Decoder a) -> Decoder a -float fieldName cont = - fieldHelper fieldName Json.Decode.float cont +float : Decoder Float +float = + Json.Decode.float |> Decoder {-| Decode a boolean field. @@ -90,27 +117,24 @@ float fieldName cont = Booleans in sqlite are stored as integers with 1 and 0 as True and False. See -} -bool : String -> (Bool -> Decoder a) -> Decoder a -bool fieldName cont = - let - boolDecoder = - Json.Decode.int - |> Json.Decode.andThen - (\i -> - when i is - 0 -> - Json.Decode.succeed False - - 1 -> - Json.Decode.succeed True - - n -> - Json.Decode.fail <| - "Expected 0 or 1 in boolean field, got " ++ - String.fromInt n - ) - in - fieldHelper fieldName boolDecoder cont +bool : Decoder Bool +bool = + Json.Decode.int + |> Json.Decode.andThen + (\i -> + when i is + 0 -> + Json.Decode.succeed False + + 1 -> + Json.Decode.succeed True + + n -> + Json.Decode.fail <| + "Expected 0 or 1 in boolean field, got " ++ + String.fromInt n + ) + |> Decoder {-| Decode a JSON field. @@ -128,22 +152,19 @@ Use `json()` in your SELECT to ensure you get text regardless of how the JSON wa See -} -json : Json.Decode.Decoder a -> String -> (a -> Decoder b) -> Decoder b -json jsonDecoder fieldName cont = - let - decoder = - Json.Decode.string - |> Json.Decode.andThen - (\str -> - when Json.Decode.decodeString jsonDecoder str is - Ok val -> - Json.Decode.succeed val - - Err err -> - Json.Decode.fail (Json.Decode.errorToString err) - ) - in - fieldHelper fieldName decoder cont +json : Json.Decode.Decoder a -> Decoder a +json jsonDecoder = + Json.Decode.string + |> Json.Decode.andThen + (\str -> + when Json.Decode.decodeString jsonDecoder str is + Ok val -> + Json.Decode.succeed val + + Err err -> + Json.Decode.fail (Json.Decode.errorToString err) + ) + |> Decoder {-| Decode a Time.Posix value. @@ -154,14 +175,13 @@ how both [Sqlite.Encode.time](Sqlite.Encode#time) and which aligns with SQLite's `unixepoch` function. See -} -time : String -> (Time.Posix -> Decoder a) -> Decoder a -time fieldName cont = - fieldHelper fieldName - (Json.Decode.map +time : Decoder Time.Posix +time = + Decoder <| + Json.Decode.map (\seconds -> Time.millisToPosix (Math.round (seconds * 1000.0))) Json.Decode.float - ) - cont + {-| Decode a nullable field in the database. @@ -169,17 +189,15 @@ time fieldName cont = 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: - Sqlite.Decode.maybe Decode.string "nickname" <| \maybeNickname -> + Decode.field (Decode.maybe Decode.string) "nickname" <| \maybeNickname -> Sqlite.Decode.succeed maybeNickname -} -maybe : (String -> (a -> Decoder b) -> Decoder b) -> String -> (Maybe a -> Decoder b) -> Decoder b -maybe decoderFn fieldName cont = +maybe : Decoder a -> Decoder (Maybe a) +maybe decoder = Decoder <| Json.Decode.oneOf - [ unwrap (decoderFn fieldName (\val -> cont (Just val))) - , Json.Decode.andThen - (\_ -> unwrap (cont Nothing)) - (Json.Decode.field fieldName (Json.Decode.null {})) + [ unwrap decoder |> Json.Decode.map Just + , Json.Decode.succeed Nothing ] @@ -194,16 +212,16 @@ Often used as the final step when chaining field decoders: Sqlite.Decode.int "age" <| \age -> Sqlite.Decode.succeed { name = name, age = age } -} -succeed : a -> Decoder a +succeed : a -> Column a succeed val = - Decoder (Json.Decode.succeed val) + Column (Json.Decode.succeed val) {-| Force a decoder to fail with the given message. -} -fail : String -> Decoder a +fail : String -> Column a fail reason = - Decoder (Json.Decode.fail reason) + Column (Json.Decode.fail reason) -- HELPERS @@ -217,15 +235,6 @@ toJson : Decoder a -> Json.Decode.Decoder a toJson = unwrap - -fieldHelper : String -> Json.Decode.Decoder a -> (a -> Decoder b) -> Decoder b -fieldHelper fieldName jsonDecoder cont = - Decoder <| - Json.Decode.andThen - (\val -> unwrap (cont val)) - (Json.Decode.field fieldName jsonDecoder) - - unwrap : Decoder a -> Json.Decode.Decoder a unwrap (Decoder d) = d diff --git a/src/Sqlite/Encode.gren b/src/Sqlite/Encode.gren index 8c2f9f5..cd6be66 100644 --- a/src/Sqlite/Encode.gren +++ b/src/Sqlite/Encode.gren @@ -1,5 +1,9 @@ module Sqlite.Encode exposing ( Value + , Column + + -- columns + , column -- fields , bool @@ -34,50 +38,58 @@ import Json.Encode import Time -{-| An encoded parameter value. +{-| An encoded SQLite value. -} type Value - = Value + = Value Json.Encode.Value + + +{-| An encoded SQLite column. +-} +type Column + = Column { key : String , value : Json.Encode.Value } +{-| Encode a SQLite column value +-} +column : String -> Value -> Column +column field (Value value) = + Column + { key = field + , value = value + } + + {-| Encode a boolean value. This will be stored in the db as 1 for true and 0 for false. See -} -bool : String -> Bool -> Value -bool field b = +bool : Bool -> Value +bool b = Value - { key = field - , value = - if b then - Json.Encode.int 1 - else - Json.Encode.int 0 - } + (if b then + Json.Encode.int 1 + else + Json.Encode.int 0 + ) {-| Encode a float. -} -float : String -> Float -> Value -float field f = - Value - { key = field - , value = Json.Encode.float f - } +float : Float -> Value +float f = + Value (Json.Encode.float f) {-| Encode an integer. -} -int : String -> Int -> Value -int field i = - Value - { key = field - , value = Json.Encode.int i - } +int : Int -> Value +int i = + Value (Json.Encode.int i) {-| Encode a JSON value. @@ -108,12 +120,9 @@ regardless of how it was stored: See -} -json : String -> Json.Encode.Value -> Value -json field val = - Value - { key = field - , value = Json.Encode.string (Json.Encode.encode 0 val) - } +json : Json.Encode.Value -> Value +json val = + Value (Json.Encode.string (Json.Encode.encode 0 val)) {-| Encode a Time.Posix value. @@ -124,12 +133,9 @@ See If you need subsecond precision, use [timeWithMillis](#timeWithMillis). -} -time : String -> Time.Posix -> Value -time field t = - Value - { key = field - , value = Json.Encode.int (Time.posixToMillis t // 1000) - } +time : Time.Posix -> Value +time t = + Value (Json.Encode.int (Time.posixToMillis t // 1000)) {-| Encode a Time.Posix value with subsecond precision. @@ -142,12 +148,9 @@ See If you don't need subsecond precision, use [time](#time), which will save storage for large data sets. -} -timeWithMillis : String -> Time.Posix -> Value -timeWithMillis field t = - Value - { key = field - , value = Json.Encode.float (toFloat (Time.posixToMillis t) / 1000.0) - } +timeWithMillis : Time.Posix -> Value +timeWithMillis t = + Value (Json.Encode.float (toFloat (Time.posixToMillis t) / 1000.0)) {-| Encode a nullable value. @@ -157,34 +160,28 @@ For example, to encode a nullable TEXT field: Sqlite.Encode.maybe Sqlite.Encode.string "nickname" maybeName -} -maybe : (String -> a -> Value) -> String -> Maybe a -> Value -maybe encoder name maybeVal = +maybe : (a -> Value) -> Maybe a -> Value +maybe encoder maybeVal = when maybeVal is Just val -> - encoder name val + encoder val Nothing -> - null name + null {-| Encode null. -} -null : String -> Value -null field = - Value - { key = field - , value = Json.Encode.null - } +null : Value +null = + Value Json.Encode.null {-| Encode a String value. -} -string : String -> String -> Value -string field s = - Value - { key = field - , value = Json.Encode.string s - } +string : String -> Value +string s = + Value (Json.Encode.string s) -- HELPERS @@ -192,15 +189,15 @@ string field s = {-| Convert an array of encoded values to a Json object. -This is used internally to communicate with sqlite in the kernel. +This is used internally to communicate with SQlite in the kernel. -} -toJson : Array Value -> Json.Encode.Value +toJson : Array Column -> Json.Encode.Value toJson values = values |> Array.map unwrap |> Json.Encode.object -unwrap : Value -> { key : String, value : Json.Encode.Value } -unwrap (Value v) = +unwrap : Column -> { key : String, value : Json.Encode.Value } +unwrap (Column v) = v From ff7dcee9952dc78aaa79bf013243575c7d62d876 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sat, 22 Aug 2026 21:39:32 -0700 Subject: [PATCH 03/11] New SQLite function implementation --- integration-tests/sqlite/src/Main.gren | 32 +++++++++++- src/Gren/Kernel/Sqlite.js | 27 +++++----- src/Sqlite/Decode.gren | 2 + src/Sqlite/Function.gren | 70 ++++++++++++++++---------- 4 files changed, 89 insertions(+), 42 deletions(-) diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index 887b8f4..9959d52 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -235,8 +235,11 @@ tests fsPerm = Expect.equalArrays testTimes fromDb , await "Open db for custom SQLite function tests" createTestDb <| \db -> concat - [ await "Create a custom function" - (Sqlite.Function.fn "always_joey" (\_first -> Sqlite.Function.String "Joey") db) <| \_ -> + [ await "Register a custom function" + (Sqlite.Function.register "always_joey" db <| + Sqlite.Function.arg Decode.string <| \first -> + Sqlite.Function.succeed (Encode.string "Joey") + ) <| \_ -> await "Insert person into the database" (insertPerson joey db) <| \_ -> await "Run the custom function in a query" (let @@ -249,6 +252,31 @@ tests fsPerm = Sqlite.getOne db query) <| \person -> test "The function overwrites the name in the query" (\_ -> Expect.equal person.name "Joey") + , await "Register a custom function with two arguments" + (Sqlite.Function.register "combine_strings" db <| + Sqlite.Function.arg Decode.string <| \firstString -> + Sqlite.Function.arg Decode.string <| \secondString -> + Sqlite.Function.succeed (Encode.string (firstString ++ secondString)) + ) <| \_ -> + await "Insert person into the database" (insertPerson robin db) <| \_ -> + await "Run the custom function in a query" + (let + query = + { query = "SELECT name, role FROM people WHERE name = combine_strings(:first, :second)" + , parameters = + [ Encode.column "first" <| Encode.string "Rob" + , Encode.column "second" <| Encode.string "in" + ] + , rowDecoder = personDecoder + } + in + Sqlite.getOne db query) <| \person -> + concat + [ test "The function gets the right person from the query" + (\_ -> Expect.equal person.name "Robin") + , test "The function ets the right person with the right role" + (\_ -> Expect.equal person.role "Creator") + ] ] ] diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index ebb10b6..5911b2b 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -52,30 +52,29 @@ var _Sqlite_close = function (db) { }); }; -var _Sqlite_function = F4(function (db, name, func, args) { +var _Sqlite_function = F3(function (name, func, db) { return __Scheduler_binding(function (callback) { try { const options = { deterministic: true, directOnly: true, useBigIntArguments: false, - varargs: false, + varargs: true, }; - var wrappedFunc; - if (args == 2) { - wrappedFunc = function (first, second) { - const value = A2(func, JSON.stringify(first), JSON.stringify(second)); - return value.a; - }; - } else { - wrappedFunc = function (first) { - const value = func(JSON.stringify(first)); - return value.a; - }; + const wrappedFunc = function(...args) { + const jsonArgs = args.map((v) => __Json_wrap(v)); + const result = func(jsonArgs); + if (__Result_isOk(result)) { + return result.a.a.a; + } else { + return null; + } } db.function(name, options, wrappedFunc); callback(__Scheduler_succeed({})); - } catch (e) { + } + catch (e) { + console.log("e", e) callback(_Sqlite_constructError(e)); } }); diff --git a/src/Sqlite/Decode.gren b/src/Sqlite/Decode.gren index 2e18c77..d334de5 100644 --- a/src/Sqlite/Decode.gren +++ b/src/Sqlite/Decode.gren @@ -17,6 +17,8 @@ module Sqlite.Decode exposing -- Composing , succeed , fail + + , unwrap ) {-| Decode SQL results into Gren values. diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren index 00815e0..ec88c91 100644 --- a/src/Sqlite/Function.gren +++ b/src/Sqlite/Function.gren @@ -6,29 +6,47 @@ import Json.Decode import Json.Encode import Sqlite import Task exposing (Task) - - -{-| The result that must be procuded - -When a function run - -- `Error` ... -- `String` when a result will save a `String` to the database --} -type Result - = Error - | String String - - -{-| Add a custom SQLite function with one expected argument. --} -fn : String -> (String -> Result) -> Sqlite.Database -> Task a {} -fn name func db = - Gren.Kernel.Sqlite.function db name func 1 - - -{-| Add a custom SQLite function with two expected arguments. --} -fn2 : String -> (String -> String -> Result) -> Sqlite.Database -> Task a {} -fn2 name func db = - Gren.Kernel.Sqlite.function db name func 2 +import Sqlite.Encode as Encode +import Sqlite.Decode as Decode + + +type Function = + Function (Array Json.Encode.Value -> Result String Encode.Value) + + +arg : Decode.Decoder a -> (a -> Function) -> Function +arg decoder func = + Function <| + (\args -> + when Array.popFirst args is + Just { first = first, rest = rest } -> + when Json.Decode.decodeValue (Decode.unwrap decoder) first is + Ok val -> + when func val is + Function innerFunc -> + innerFunc rest + + Err err -> + Err "decoding error" + + Nothing -> + Err "not enough arguments passed" + ) + + +succeed : Encode.Value -> Function +succeed value = + Function <| + (\args -> + when args is + [] -> + Ok value + + many -> + Err "too many arguments passed" + ) + + +register : String -> Sqlite.Database -> Function -> Task a {} +register name db (Function func) = + Gren.Kernel.Sqlite.function name func db From 20420d2718d09a5a81b62b282818ecdc0cb0c290 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sat, 29 Aug 2026 09:39:23 -0700 Subject: [PATCH 04/11] Separate row encoders and decoders into their own modules --- gren.json | 2 + integration-tests/sqlite/src/Main.gren | 124 +++++++++++++------------ src/Gren/Kernel/Sqlite.js | 7 +- src/Sqlite.gren | 10 +- src/Sqlite/Decode.gren | 39 +------- src/Sqlite/Decode/Row.gren | 53 +++++++++++ src/Sqlite/Encode.gren | 33 ++----- src/Sqlite/Encode/Row.gren | 49 ++++++++++ 8 files changed, 187 insertions(+), 130 deletions(-) create mode 100644 src/Sqlite/Decode/Row.gren create mode 100644 src/Sqlite/Encode/Row.gren diff --git a/gren.json b/gren.json index 78510a6..128215f 100644 --- a/gren.json +++ b/gren.json @@ -20,7 +20,9 @@ "WebSocketServer.Connection", "Sqlite", "Sqlite.Decode", + "Sqlite.Decode.Row", "Sqlite.Encode", + "Sqlite.Encode.Row", "Sqlite.Function" ], "gren-version": "0.6.0 <= v < 0.7.0", diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index d5d7d38..2e79497 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -5,7 +5,9 @@ import Node import Test.Runner.Effectful as Effectful exposing (test, await, awaitError, concat) import Sqlite import Sqlite.Encode as Encode +import Sqlite.Encode.Row import Sqlite.Decode as Decode exposing (Decoder) +import Sqlite.Decode.Row exposing (Decoder) import Sqlite.Function import Task exposing (Task) import FileSystem @@ -211,14 +213,14 @@ tests fsPerm = statements = [ { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" , parameters = - [ Encode.column "name" <| Encode.string robin.name - , Encode.column "role" <| Encode.string robin.role + [ Sqlite.Encode.Row.column "name" <| Encode.string robin.name + , Sqlite.Encode.Row.column "role" <| Encode.string robin.role ] } , { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" , parameters = - [ Encode.column "name" <| Encode.string justin.name - , Encode.column "role" <| Encode.string justin.role + [ Sqlite.Encode.Row.column "name" <| Encode.string justin.name + , Sqlite.Encode.Row.column "role" <| Encode.string justin.role ] } , { statement = @@ -229,8 +231,8 @@ tests fsPerm = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.column "personA" <| Encode.string robin.name - , Encode.column "personB" <| Encode.string justin.name + [ Sqlite.Encode.Row.column "personA" <| Encode.string robin.name + , Sqlite.Encode.Row.column "personB" <| Encode.string justin.name ] } ] @@ -283,7 +285,7 @@ tests fsPerm = (let query = { query = "SELECT name, role FROM people WHERE name = always_joey(:name)" - , parameters = [ Encode.column "name" <| Encode.string "Robin" ] + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] , rowDecoder = personDecoder } in @@ -302,8 +304,8 @@ tests fsPerm = query = { query = "SELECT name, role FROM people WHERE name = combine_strings(:first, :second)" , parameters = - [ Encode.column "first" <| Encode.string "Rob" - , Encode.column "second" <| Encode.string "in" + [ Sqlite.Encode.Row.column "first" <| Encode.string "Rob" + , Sqlite.Encode.Row.column "second" <| Encode.string "in" ] , rowDecoder = personDecoder } @@ -383,25 +385,25 @@ execDbSchema db = """ -personEncoder : Person -> Array Encode.Column +personEncoder : Person -> Array Sqlite.Encode.Row.Value personEncoder p = - [ Encode.column "name" <| Encode.string p.name - , Encode.column "role" <| Encode.string p.role + [ Sqlite.Encode.Row.column "name" <| Encode.string p.name + , Sqlite.Encode.Row.column "role" <| Encode.string p.role ] -personDecoder : Decode.Column Person +personDecoder : Sqlite.Decode.Row.Decoder Person personDecoder = - Decode.column "name" Decode.string <| \name -> - Decode.column "role" Decode.string <| \role -> - Decode.succeed { name = name, role = role } + Sqlite.Decode.Row.column "name" Decode.string <| \name -> + Sqlite.Decode.Row.column "role" Decode.string <| \role -> + Sqlite.Decode.Row.succeed { name = name, role = role } -badPersonDecoder : Decode.Column Person +badPersonDecoder : Sqlite.Decode.Row.Decoder Person badPersonDecoder = - Decode.column "namee" Decode.string <| \name -> - Decode.column "role_" Decode.string <| \role -> - Decode.succeed { name = name, role = role } + Sqlite.Decode.Row.column "namee" Decode.string <| \name -> + Sqlite.Decode.Row.column "role_" Decode.string <| \role -> + Sqlite.Decode.Row.succeed { name = name, role = role } insertPerson : Person -> Sqlite.Database -> Task Sqlite.Error Sqlite.ExecutionSummary @@ -423,8 +425,8 @@ defineFriends nameA nameB db = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.column "personA" <| Encode.string nameA - , Encode.column "personB" <| Encode.string nameB + [ Sqlite.Encode.Row.column "personA" <| Encode.string nameA + , Sqlite.Encode.Row.column "personB" <| Encode.string nameB ] } @@ -437,8 +439,8 @@ defineFriendsById idA idB db = INSERT INTO friends (person_a, person_b) VALUES (:idA, :idB) """ , parameters = - [ Encode.column "idA" <| Encode.int idA - , Encode.column "idB" <| Encode.int idB + [ Sqlite.Encode.Row.column "idA" <| Encode.int idA + , Sqlite.Encode.Row.column "idB" <| Encode.int idB ] } @@ -446,7 +448,7 @@ defineFriendsById idA idB db = personByNameQ : String -> Sqlite.Query Person personByNameQ name = { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.column "name" <| Encode.string name ] + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string name ] , rowDecoder = personDecoder } @@ -471,9 +473,9 @@ friendshipQ = """ , parameters = [] , rowDecoder = - Decode.column "person_a" Decode.string <| \personA -> - Decode.column "person_b" Decode.string <| \personB -> - Decode.succeed { personA = personA, personB = personB } + Sqlite.Decode.Row.column "person_a" Decode.string <| \personA -> + Sqlite.Decode.Row.column "person_b" Decode.string <| \personB -> + Sqlite.Decode.Row.succeed { personA = personA, personB = personB } } @@ -495,7 +497,7 @@ execBadTemplate num db = , parameters = Array.initialize num 0 (\idx -> - Encode.column ("id" ++ String.fromInt idx) <| Encode.int idx + Sqlite.Encode.Row.column ("id" ++ String.fromInt idx) <| Encode.int idx ) } @@ -504,8 +506,8 @@ failDecoder : Sqlite.Database -> Task Sqlite.Error Int failDecoder db = Sqlite.getOne db { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.column "name" <| Encode.string "Robin" ] - , rowDecoder = Decode.fail "Oopsy!" + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] + , rowDecoder = Sqlite.Decode.Row.fail "Oopsy!" } @@ -513,11 +515,11 @@ badDecoder : Sqlite.Database -> Task Sqlite.Error { name : Int, role : String } badDecoder db = Sqlite.getOne db { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.column "name" <| Encode.string "Robin" ] + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] , rowDecoder = - Decode.column "name" Decode.int <| \name -> - Decode.column "role" Decode.string <| \role -> - Decode.succeed { name = name, role = role } + Sqlite.Decode.Row.column "name" Decode.int <| \name -> + Sqlite.Decode.Row.column "role" Decode.string <| \role -> + Sqlite.Decode.Row.succeed { name = name, role = role } } @@ -576,34 +578,34 @@ initializeFieldTypeDb fsPerm = ) -allFieldTypesEncoder : AllFieldTypes -> Array Encode.Column +allFieldTypesEncoder : AllFieldTypes -> Array Sqlite.Encode.Row.Value allFieldTypesEncoder row = - [ Encode.column "string_field" <| Encode.string row.stringField - , Encode.column "int_field" <| Encode.int row.intField - , Encode.column "float_field" <| Encode.float row.floatField - , Encode.column "bool_true" <| Encode.bool row.boolTrue - , Encode.column "bool_false" <| Encode.bool row.boolFalse - , Encode.column "json_field" <| Encode.json (Json.Encode.array Json.Encode.string row.jsonField) - , Encode.column "time_field" <| Encode.time row.timeField - , Encode.column "time_with_millis_field" <| Encode.timeWithMillis row.timeWithMillisField - , Encode.column "maybe_just_string" <| Encode.maybe Encode.string row.maybeJustString - , Encode.column "maybe_nothing_string" <| Encode.maybe Encode.string row.maybeNothingString + [ Sqlite.Encode.Row.column "string_field" <| Encode.string row.stringField + , Sqlite.Encode.Row.column "int_field" <| Encode.int row.intField + , Sqlite.Encode.Row.column "float_field" <| Encode.float row.floatField + , Sqlite.Encode.Row.column "bool_true" <| Encode.bool row.boolTrue + , Sqlite.Encode.Row.column "bool_false" <| Encode.bool row.boolFalse + , Sqlite.Encode.Row.column "json_field" <| Encode.json (Json.Encode.array Json.Encode.string row.jsonField) + , Sqlite.Encode.Row.column "time_field" <| Encode.time row.timeField + , Sqlite.Encode.Row.column "time_with_millis_field" <| Encode.timeWithMillis row.timeWithMillisField + , Sqlite.Encode.Row.column "maybe_just_string" <| Encode.maybe Encode.string row.maybeJustString + , Sqlite.Encode.Row.column "maybe_nothing_string" <| Encode.maybe Encode.string row.maybeNothingString ] -allFieldTypesDecoder : Decode.Column AllFieldTypes +allFieldTypesDecoder : Sqlite.Decode.Row.Decoder AllFieldTypes allFieldTypesDecoder = - Decode.column "string_field" Decode.string <| \stringField -> - Decode.column "int_field" Decode.int <| \intField -> - Decode.column "float_field" Decode.float <| \floatField -> - Decode.column "bool_true" Decode.bool <| \boolTrue -> - Decode.column "bool_false" Decode.bool <| \boolFalse -> - Decode.column "json_field" (Decode.json (Json.Decode.array Json.Decode.string)) <| \jsonField -> - Decode.column "time_field" Decode.time <| \timeField -> - Decode.column "time_with_millis_field" Decode.time <| \timeWithMillisField -> - Decode.column "maybe_just_string" (Decode.maybe Decode.string) <| \maybeJustString -> - Decode.column "maybe_nothing_string" (Decode.maybe Decode.string) <| \maybeNothingString -> - Decode.succeed + Sqlite.Decode.Row.column "string_field" Decode.string <| \stringField -> + Sqlite.Decode.Row.column "int_field" Decode.int <| \intField -> + Sqlite.Decode.Row.column "float_field" Decode.float <| \floatField -> + Sqlite.Decode.Row.column "bool_true" Decode.bool <| \boolTrue -> + Sqlite.Decode.Row.column "bool_false" Decode.bool <| \boolFalse -> + Sqlite.Decode.Row.column "json_field" (Decode.json (Json.Decode.array Json.Decode.string)) <| \jsonField -> + Sqlite.Decode.Row.column "time_field" Decode.time <| \timeField -> + Sqlite.Decode.Row.column "time_with_millis_field" Decode.time <| \timeWithMillisField -> + Sqlite.Decode.Row.column "maybe_just_string" (Decode.maybe Decode.string) <| \maybeJustString -> + Sqlite.Decode.Row.column "maybe_nothing_string" (Decode.maybe Decode.string) <| \maybeNothingString -> + Sqlite.Decode.Row.succeed { stringField = stringField , intField = intField , floatField = floatField @@ -693,7 +695,7 @@ insertTimes : Array Time.Posix -> Sqlite.Database -> Task Sqlite.Error Sqlite.Ex insertTimes times db = Sqlite.executeForEach db { statement = "INSERT INTO times (t) VALUES (:t)" - , parameters = \t -> [ Encode.column "t" <| Encode.timeWithMillis t ] + , parameters = \t -> [ Sqlite.Encode.Row.column "t" <| Encode.timeWithMillis t ] } times @@ -703,6 +705,6 @@ allTimesQ = { query = "SELECT t FROM times ORDER BY rowid" , parameters = [] , rowDecoder = - Decode.column "t" Decode.time <| \t -> - Decode.succeed t + Sqlite.Decode.Row.column "t" Decode.time <| \t -> + Sqlite.Decode.Row.succeed t } diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 1250ea7..6de3d66 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -7,6 +7,7 @@ import Gren.Kernel.Json exposing (wrap, unwrap) import Json.Decode as Decode exposing (decodeValue) import Result exposing (isOk) import Sqlite.Encode as SqliteEncode exposing (toJson) +import Sqlite.Encode.Row as SqliteEncodeRow exposing (toJson) import Sqlite.Decode as SqliteDecode exposing (toJson) import Maybe exposing (Just, Nothing) @@ -105,7 +106,7 @@ var _Sqlite_foldl = F4(function (query, db, func, acc) { try { var acc_ = acc; const prepped = db.prepare(query.__$query); - const params = __Json_unwrap(__SqliteEncode_toJson(query.__$parameters)); + const params = __Json_unwrap(__SqliteEncodeRow_toJson(query.__$parameters)); const rowDecoder = __SqliteDecode_toJson(query.__$rowDecoder); for (const value of prepped.iterate(params)) { @@ -134,7 +135,7 @@ var _Sqlite_getMaybeOne = F2(function (query, db) { return __Scheduler_binding(function (callback) { try { const prepped = db.prepare(query.__$query); - const params = __Json_unwrap(__SqliteEncode_toJson(query.__$parameters)); + const params = __Json_unwrap(__SqliteEncodeRow_toJson(query.__$parameters)); const rowDecoder = __SqliteDecode_toJson(query.__$rowDecoder); const iterator = prepped.iterate(params); @@ -176,7 +177,7 @@ var _Sqlite_executeMany = F3(function (statement, values, db) { } else { for (const val of values) { lastResult = prepped.run( - __Json_unwrap(__SqliteEncode_toJson(statement.__$parameters(val))), + __Json_unwrap(__SqliteEncodeRow_toJson(statement.__$parameters(val))), ); } } diff --git a/src/Sqlite.gren b/src/Sqlite.gren index 8e6c565..08f84b5 100644 --- a/src/Sqlite.gren +++ b/src/Sqlite.gren @@ -6,7 +6,9 @@ import FileSystem import FileSystem.Path exposing (Path) import Json.Decode as Decode import Sqlite.Decode +import Sqlite.Decode.Row import Sqlite.Encode +import Sqlite.Encode.Row import Task exposing (Task) import Gren.Kernel.Sqlite @@ -103,8 +105,8 @@ runBackup (Backup { destination, pages, db }) = type alias Query value = { query : String - , parameters : Array Sqlite.Encode.Column - , rowDecoder : Sqlite.Decode.Column value + , parameters : Array Sqlite.Encode.Row.Value + , rowDecoder : Sqlite.Decode.Row.Decoder value } @@ -143,7 +145,7 @@ foldl func acc db query = type alias Statement = { statement : String - , parameters : Array Sqlite.Encode.Column + , parameters : Array Sqlite.Encode.Row.Value } @@ -178,7 +180,7 @@ executeAll db statements = executeForEach : Database -> { statement : String - , parameters : value -> Array Sqlite.Encode.Column + , parameters : value -> Array Sqlite.Encode.Row.Value } -> Array value -> Task Error ExecutionSummary diff --git a/src/Sqlite/Decode.gren b/src/Sqlite/Decode.gren index d334de5..b746ff8 100644 --- a/src/Sqlite/Decode.gren +++ b/src/Sqlite/Decode.gren @@ -1,9 +1,5 @@ module Sqlite.Decode exposing ( Decoder - , Column - - -- Column decoders - , column -- Field decoders , bool @@ -67,32 +63,6 @@ type Decoder a = Decoder (Json.Decode.Decoder a) -{-|-} -type Column a - = Column (Json.Decode.Decoder a) - - --- FIELD DECODERS - - -{-| Decode a SQLite column with a specific value type. --} -column : String -> Decoder a -> (a -> Column b) -> Column b -column columnName (Decoder jsonDecoder) cont = - Json.Decode.field columnName jsonDecoder - |> Json.Decode.andThen (\val -> - let - (Column decoder) = - cont val - in - decoder - ) - |> Column - - ---- - - {-| Decode a string field. -} string :Decoder String @@ -185,7 +155,6 @@ time = Json.Decode.float - {-| Decode a nullable field in the database. The first parameter is the field decoder function for the type if the value is not null. @@ -214,16 +183,16 @@ Often used as the final step when chaining field decoders: Sqlite.Decode.int "age" <| \age -> Sqlite.Decode.succeed { name = name, age = age } -} -succeed : a -> Column a +succeed : a -> Decoder a succeed val = - Column (Json.Decode.succeed val) + Decoder (Json.Decode.succeed val) {-| Force a decoder to fail with the given message. -} -fail : String -> Column a +fail : String -> Decoder a fail reason = - Column (Json.Decode.fail reason) + Decoder (Json.Decode.fail reason) -- HELPERS diff --git a/src/Sqlite/Decode/Row.gren b/src/Sqlite/Decode/Row.gren new file mode 100644 index 0000000..1580011 --- /dev/null +++ b/src/Sqlite/Decode/Row.gren @@ -0,0 +1,53 @@ +module Sqlite.Decode.Row exposing ( Decoder, column, succeed, fail ) + +{-|-} + +import Json.Decode +import Sqlite.Decode + + +{-|-} +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 + } +-} +column : String -> Sqlite.Decode.Decoder a -> (a -> Decoder b) -> Decoder b +column columnName decoder cont = + Json.Decode.field columnName (Sqlite.Decode.unwrap decoder) + |> Json.Decode.andThen (\val -> + let + (Decoder decoder_) = + cont val + in + decoder_ + ) + |> Decoder + + +{-| 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) diff --git a/src/Sqlite/Encode.gren b/src/Sqlite/Encode.gren index cd6be66..75f17fa 100644 --- a/src/Sqlite/Encode.gren +++ b/src/Sqlite/Encode.gren @@ -1,9 +1,5 @@ module Sqlite.Encode exposing ( Value - , Column - - -- columns - , column -- fields , bool @@ -15,6 +11,8 @@ module Sqlite.Encode exposing , timeWithMillis , maybe , null + + , unwrap ) {-| Encode SQL parameters. @@ -43,25 +41,6 @@ import Time type Value = Value Json.Encode.Value - -{-| An encoded SQLite column. --} -type Column - = Column - { key : String - , value : Json.Encode.Value - } - - -{-| Encode a SQLite column value --} -column : String -> Value -> Column -column field (Value value) = - Column - { key = field - , value = value - } - {-| Encode a boolean value. @@ -191,13 +170,13 @@ string s = This is used internally to communicate with SQlite in the kernel. -} -toJson : Array Column -> Json.Encode.Value +toJson : Array Value -> Array Json.Encode.Value toJson values = values |> Array.map unwrap - |> Json.Encode.object + -- |> Json.Encode.object -unwrap : Column -> { key : String, value : Json.Encode.Value } -unwrap (Column v) = +unwrap : Value -> Json.Encode.Value +unwrap (Value v) = v diff --git a/src/Sqlite/Encode/Row.gren b/src/Sqlite/Encode/Row.gren new file mode 100644 index 0000000..bb75199 --- /dev/null +++ b/src/Sqlite/Encode/Row.gren @@ -0,0 +1,49 @@ +module Sqlite.Encode.Row exposing ( Value, column ) + + +{-|-} + +import Sqlite.Encode +import Json.Encode + + +{-|-} +type Value + = Value + { column : String + , value : Sqlite.Encode.Value + } + + +{-| + +Sqlite.Encode.Row.column "name" <| Encode.string "John" + +-} +column : String -> Sqlite.Encode.Value -> Value +column name value = + Value + { column = name + , value = value + } + + +-- HELPERS + + +unwrap : Value -> { key : String, value : Json.Encode.Value } +unwrap (Value v) = + { key = v.column + , value = Sqlite.Encode.unwrap v.value + } + + +{-| Convert an array of encoded values to a Json object. + +This is used internally to communicate with SQlite in the kernel. +-} +toJson : Array Value -> Json.Encode.Value +toJson values = + values + |> Array.map unwrap + |> Json.Encode.object From 18bf7c39c03b83a29de78a6efa6985b5b23537ed Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sun, 30 Aug 2026 10:26:03 -0700 Subject: [PATCH 05/11] Initial aggregate function implementation --- gren.json | 3 +- integration-tests/sqlite/src/Main.gren | 32 ++++++++++ src/Gren/Kernel/Sqlite.js | 41 +++++++++++- src/Sqlite/Aggregate.gren | 88 ++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/Sqlite/Aggregate.gren diff --git a/gren.json b/gren.json index 128215f..4a534d8 100644 --- a/gren.json +++ b/gren.json @@ -23,7 +23,8 @@ "Sqlite.Decode.Row", "Sqlite.Encode", "Sqlite.Encode.Row", - "Sqlite.Function" + "Sqlite.Function", + "Sqlite.Aggregate" ], "gren-version": "0.6.0 <= v < 0.7.0", "dependencies": { diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index 2e79497..dde6f05 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -16,6 +16,7 @@ import Init import Json.Decode import Json.Encode import Time +import Sqlite.Aggregate main : Effectful.Program a @@ -317,6 +318,37 @@ tests fsPerm = , test "The function ets the right person with the right role" (\_ -> Expect.equal person.role "Creator") ] + , await "Register an aggregate function" + (Sqlite.Aggregate.register "count_names" db <| + Sqlite.Aggregate.aggregate + { init = 0 + , function = + Sqlite.Aggregate.start <| \state -> + Sqlite.Aggregate.arg Decode.string <| \firstString -> + Sqlite.Aggregate.arg Decode.string <| \secondString -> + Sqlite.Aggregate.return <| \direction -> + if firstString == secondString then + state + 1 + else + state + , result = \state -> + Encode.int state + } + ) <| \_ -> + await "Run the custom function in a query" + (let + query = + { query = "SELECT count_names(name, :name) AS name_count FROM people;" + , parameters = + [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" + ] + , rowDecoder = + Sqlite.Decode.Row.column "name_count" Decode.int <| \count -> + Sqlite.Decode.Row.succeed { count = count } + } + in + Sqlite.getOne db query) <| \{ count } -> + test "Works" <| \_ -> Expect.equal count 1 ] ] diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 6de3d66..285a3ac 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -9,6 +9,7 @@ import Result exposing (isOk) import Sqlite.Encode as SqliteEncode exposing (toJson) import Sqlite.Encode.Row as SqliteEncodeRow exposing (toJson) import Sqlite.Decode as SqliteDecode exposing (toJson) +import Sqlite.Aggregate as SqliteAggregate exposing (Entering, Exiting) import Maybe exposing (Just, Nothing) */ @@ -75,7 +76,45 @@ var _Sqlite_function = F3(function (name, func, db) { callback(__Scheduler_succeed({})); } catch (e) { - console.log("e", e) + console.log("e", e); + } + }); +}); + +var _Sqlite_aggregate = F5(function (name, init, func, result, db) { + return __Scheduler_binding(function (callback) { + try { + const wrappedFunc = function(direction) { + var env = __SqliteAggregate_Entering; + if (direction == "inverse") { + env = __SqliteAggregate_Exiting; + } + return function(state, ...args) { + const jsonArgs = args.map((v) => __Json_wrap(v)); + const result = A3(func, env, state, jsonArgs); + if (__Result_isOk(result)) { + return result.a; + } else { + return null; + } + } + } + const options = { + deterministic: true, + directOnly: true, + useBigIntArguments: false, + varargs: true, + start: () => { return init }, + step: wrappedFunc("step"), + result: (state) => { + return result(state).a.a; + }, + inverse: wrappedFunc("inverse") + } + db.aggregate(name, options); + callback(__Scheduler_succeed({})); + } catch (e) { + callback(_Sqlite_constructError(e)); } }); }); diff --git a/src/Sqlite/Aggregate.gren b/src/Sqlite/Aggregate.gren new file mode 100644 index 0000000..900cfc6 --- /dev/null +++ b/src/Sqlite/Aggregate.gren @@ -0,0 +1,88 @@ +module Sqlite.Aggregate exposing ( .. ) + +{-|-} + +import Sqlite +import Sqlite.Decode as Decode +import Sqlite.Encode as Encode +import Json.Encode +import Json.Decode +import Task exposing ( Task ) + + +type Function state = + Function (Direction -> state -> Array Json.Encode.Value -> Result String state) + + +{-|-} +type Direction + = Entering + | Exiting + + +{-|-} +type Aggregate state + = Aggregate + { init : state + , function : Function state + , result : state -> Encode.Value + } + + +aggregate : { init : state, function: Function state, result : state -> Encode.Value } -> Aggregate state +aggregate { init, function, result } = + Aggregate + { init = init + , function = function + , result = result + } + + +{-|-} +start : (state -> Function state) -> Function state +start func = + Function <| \direction state args -> + when func state is + Function innerFunc -> + innerFunc direction state args + + +{-|-} +arg : Decode.Decoder a -> (a -> Function state) -> Function state +arg decoder func = + Function <| \direction state args -> + when Array.popFirst args is + Just { first = first, rest = rest } -> + when Json.Decode.decodeValue (Decode.unwrap decoder) first is + Ok val -> + when func val is + Function innerFunc -> + innerFunc direction state rest + + Err _err -> + Err "decoding error" + + Nothing -> + Err "not enough arguments passed" + + +{-|-} +return : (Direction -> state) -> Function state +return func = + Function <| \direction _state args -> + when args is + [] -> + Ok (func direction) + + many -> + Err "too many arguments passed" + + +{-|-} +register : String -> Sqlite.Database -> Aggregate state -> Task x {} +register name db (Aggregate { init, function, result }) = + let + (Function unwrappedFunc) = + function + in + Gren.Kernel.Sqlite.aggregate name init unwrappedFunc result db From e9cbfb1673b54f91c0eae85b726421ed2a4e2ca5 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sun, 30 Aug 2026 10:27:11 -0700 Subject: [PATCH 06/11] `succeed` -> `return` for SQLite functions --- integration-tests/sqlite/src/Main.gren | 4 ++-- src/Sqlite/Function.gren | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index dde6f05..cb966b0 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -279,7 +279,7 @@ tests fsPerm = [ await "Register a custom function" (Sqlite.Function.register "always_joey" db <| Sqlite.Function.arg Decode.string <| \first -> - Sqlite.Function.succeed (Encode.string "Joey") + Sqlite.Function.return (Encode.string "Joey") ) <| \_ -> await "Insert person into the database" (insertPerson joey db) <| \_ -> await "Run the custom function in a query" @@ -297,7 +297,7 @@ tests fsPerm = (Sqlite.Function.register "combine_strings" db <| Sqlite.Function.arg Decode.string <| \firstString -> Sqlite.Function.arg Decode.string <| \secondString -> - Sqlite.Function.succeed (Encode.string (firstString ++ secondString)) + Sqlite.Function.return (Encode.string (firstString ++ secondString)) ) <| \_ -> await "Insert person into the database" (insertPerson robin db) <| \_ -> await "Run the custom function in a query" diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren index ec88c91..a1a9733 100644 --- a/src/Sqlite/Function.gren +++ b/src/Sqlite/Function.gren @@ -34,8 +34,8 @@ arg decoder func = ) -succeed : Encode.Value -> Function -succeed value = +return : Encode.Value -> Function +return value = Function <| (\args -> when args is From 9239a12e5f5961cdf3dae4934fbb33f6c2f95f50 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Sun, 30 Aug 2026 10:27:44 -0700 Subject: [PATCH 07/11] Construct proper error in kernel when encountering one in SQLite function --- src/Gren/Kernel/Sqlite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 285a3ac..757ef64 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -76,7 +76,7 @@ var _Sqlite_function = F3(function (name, func, db) { callback(__Scheduler_succeed({})); } catch (e) { - console.log("e", e); + callback(_Sqlite_constructError(e)); } }); }); From 72b763bc80f10e7d520e518ab1b82c6aeca9eb14 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Tue, 1 Sep 2026 20:30:20 -0700 Subject: [PATCH 08/11] Formatting updates, remove `String` error (given it doesn't matter given executed outside of Gren code) --- src/Sqlite/Function.gren | 48 ++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren index a1a9733..990c550 100644 --- a/src/Sqlite/Function.gren +++ b/src/Sqlite/Function.gren @@ -11,40 +11,36 @@ import Sqlite.Decode as Decode type Function = - Function (Array Json.Encode.Value -> Result String Encode.Value) + Function (Array Json.Encode.Value -> Result {} Encode.Value) arg : Decode.Decoder a -> (a -> Function) -> Function arg decoder func = - Function <| - (\args -> - when Array.popFirst args is - Just { first = first, rest = rest } -> - when Json.Decode.decodeValue (Decode.unwrap decoder) first is - Ok val -> - when func val is - Function innerFunc -> - innerFunc rest - - Err err -> - Err "decoding error" - - Nothing -> - Err "not enough arguments passed" - ) + Function <| \args -> + when Array.popFirst args is + Just { first = first, rest = rest } -> + when Json.Decode.decodeValue (Decode.unwrap decoder) first is + Ok val -> + when func val is + Function innerFunc -> + innerFunc rest + + Err err -> + Err {} + + Nothing -> + Err {} return : Encode.Value -> Function return value = - Function <| - (\args -> - when args is - [] -> - Ok value - - many -> - Err "too many arguments passed" - ) + Function <| \args -> + when args is + [] -> + Ok value + + many -> + Err {} register : String -> Sqlite.Database -> Function -> Task a {} From 6e1384c51f242f5880988b730a1703cbb399d00c Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Tue, 1 Sep 2026 20:30:54 -0700 Subject: [PATCH 09/11] Comment guide for why `.a.a.a` is a selector for a returned function value in kernel code --- src/Gren/Kernel/Sqlite.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 757ef64..9384fad 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -67,6 +67,9 @@ var _Sqlite_function = F3(function (name, func, db) { const jsonArgs = args.map((v) => __Json_wrap(v)); const result = func(jsonArgs); if (__Result_isOk(result)) { + // The triple `.a` gets the OK result, the SQLite encode + // value, and then grabs the actual value that needs to + // be returned return result.a.a.a; } else { return null; From e264dcc544f24b34e38c67f915f05b84f570ffa3 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Tue, 1 Sep 2026 20:31:09 -0700 Subject: [PATCH 10/11] More tests for custom SQLite functions and aggregate functions --- integration-tests/sqlite/src/Main.gren | 393 ++++++++++++++++++++----- 1 file changed, 323 insertions(+), 70 deletions(-) diff --git a/integration-tests/sqlite/src/Main.gren b/integration-tests/sqlite/src/Main.gren index cb966b0..b93fe21 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -1,5 +1,6 @@ module Main exposing (main) +import Dict import Expect import Node import Test.Runner.Effectful as Effectful exposing (test, await, awaitError, concat) @@ -274,81 +275,254 @@ tests fsPerm = await "Read test times back" (Sqlite.getAll db allTimesQ) <| \fromDb -> test "Time values survive float round-trip through db" <| \_ -> Expect.equalArrays testTimes fromDb - , await "Open db for custom SQLite function tests" createTestDb <| \db -> + , await "Open db for custom SQLite function and aggregate tests" (initializeFunctionTestsDb fsPerm) <| \db -> concat - [ await "Register a custom function" - (Sqlite.Function.register "always_joey" db <| - Sqlite.Function.arg Decode.string <| \first -> - Sqlite.Function.return (Encode.string "Joey") - ) <| \_ -> - await "Insert person into the database" (insertPerson joey db) <| \_ -> - await "Run the custom function in a query" - (let - query = - { query = "SELECT name, role FROM people WHERE name = always_joey(:name)" - , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] - , rowDecoder = personDecoder - } - in - Sqlite.getOne db query) <| \person -> - test "The function overwrites the name in the query" - (\_ -> Expect.equal person.name "Joey") - , await "Register a custom function with two arguments" - (Sqlite.Function.register "combine_strings" db <| - Sqlite.Function.arg Decode.string <| \firstString -> - Sqlite.Function.arg Decode.string <| \secondString -> - Sqlite.Function.return (Encode.string (firstString ++ secondString)) + [ await "Add data to the database" + (Task.sequence + [ insertPerson joey db + , insertPerson robin db + , insertBasket basketOne db + , insertBasket basketTwo db + , insertBasket basketThree db + , insertBasket basketOne db + , insertBasket basketTwo db + , insertBasket basketThree db + ] ) <| \_ -> - await "Insert person into the database" (insertPerson robin db) <| \_ -> - await "Run the custom function in a query" - (let - query = - { query = "SELECT name, role FROM people WHERE name = combine_strings(:first, :second)" - , parameters = - [ Sqlite.Encode.Row.column "first" <| Encode.string "Rob" - , Sqlite.Encode.Row.column "second" <| Encode.string "in" - ] - , rowDecoder = personDecoder - } - in - Sqlite.getOne db query) <| \person -> + concat [ + await "Register a custom function" + (Sqlite.Function.register "always_joey" db <| + Sqlite.Function.arg Decode.string <| \first -> + Sqlite.Function.return (Encode.string "Joey") + ) <| \_ -> concat - [ test "The function gets the right person from the query" - (\_ -> Expect.equal person.name "Robin") - , test "The function ets the right person with the right role" - (\_ -> Expect.equal person.role "Creator") + [ await "Run the custom function in a query" + (Sqlite.getOne db + { query = "SELECT name, role FROM people WHERE name = always_joey(:name)" + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] + , rowDecoder = personDecoder + } + ) <| \person -> + concat + [ test "The function gets the right person from the query" <| \_ -> + Expect.equal person.name joey.name + , test "The function gets the right person with the right role from the query" <| \_ -> + Expect.equal person.role joey.role + ] + , awaitError "Run the custom function with a different argument type" + (Sqlite.getOne db + { query = "SELECT name, role FROM people WHERE name = always_joey(0)" + , parameters = [] + , rowDecoder = personDecoder + } + ) <| \err -> + test "Expected to return `NoResultsError` given failure of a custom SQLite function returns `null`" <| \_ -> + Expect.equal err Sqlite.NoResultsError + , awaitError "Run the custom function with too many arguments" + (Sqlite.getOne db + { query = "SELECT name, role FROM people WHERE name = always_joey('first', 'second')" + , parameters = [] + , rowDecoder = personDecoder + } + ) <| \err -> + test "Expected to return `NoResultsError` given failure of a custom SQLite function returns `null`" <| \_ -> + Expect.equal err Sqlite.NoResultsError ] - , await "Register an aggregate function" - (Sqlite.Aggregate.register "count_names" db <| - Sqlite.Aggregate.aggregate - { init = 0 - , function = - Sqlite.Aggregate.start <| \state -> - Sqlite.Aggregate.arg Decode.string <| \firstString -> - Sqlite.Aggregate.arg Decode.string <| \secondString -> - Sqlite.Aggregate.return <| \direction -> - if firstString == secondString then - state + 1 - else - state - , result = \state -> - Encode.int state - } - ) <| \_ -> - await "Run the custom function in a query" - (let - query = - { query = "SELECT count_names(name, :name) AS name_count FROM people;" - , parameters = - [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" - ] - , rowDecoder = - Sqlite.Decode.Row.column "name_count" Decode.int <| \count -> - Sqlite.Decode.Row.succeed { count = count } + , await "Register a custom function with two arguments" + (Sqlite.Function.register "combine_strings" db <| + Sqlite.Function.arg Decode.string <| \firstString -> + Sqlite.Function.arg Decode.string <| \secondString -> + Sqlite.Function.return (Encode.string (firstString ++ secondString)) + ) <| \_ -> + concat + [ await "Run the custom function in a query" + (Sqlite.getOne db + { query = "SELECT name, role FROM people WHERE name = combine_strings(:first, :second)" + , parameters = + [ Sqlite.Encode.Row.column "first" <| Encode.string "Rob" + , Sqlite.Encode.Row.column "second" <| Encode.string "in" + ] + , rowDecoder = personDecoder + } + ) <| \person -> + concat + [ test "The function gets the right person from the query" <| \_ -> + Expect.equal person.name robin.name + , test "The function gets the right person with the right role" <| \_ -> + Expect.equal person.role robin.role + ] + , awaitError "Run the custom function with not enough arguments" + (Sqlite.getOne db + { query = "SELECT name, role FROM people WHERE name = combine_strings(:first)" + , parameters = + [ Sqlite.Encode.Row.column "first" <| Encode.string "Robin" + ] + , rowDecoder = personDecoder + } + ) <| \err -> + test "Expected to return `NoResultsError` given failure of a custom SQLite function returns `null`" <| \_ -> + Expect.equal err Sqlite.NoResultsError + ] + , await "Register an aggregate function" + (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 } - in - Sqlite.getOne db query) <| \{ count } -> - test "Works" <| \_ -> Expect.equal count 1 + ) <| \_ -> + concat + [ await "Run the custom aggregate function in a query" + (Sqlite.getOne db + { query = + """ + 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; + """ + , parameters = [] + , rowDecoder = + Sqlite.Decode.Row.column "banana_portion_count" Decode.int <| \banana -> + Sqlite.Decode.Row.column "apple_portion_count" Decode.int <| \apple -> + Sqlite.Decode.Row.column "pear_portion_count" Decode.int <| \pear -> + Sqlite.Decode.Row.succeed + { banana = banana + , apple = apple + , pear = pear + } + } + ) <| \{ banana, apple, pear } -> + concat + [ test "Banana portions calculated correctly" <| \_ -> Expect.equal banana 12 + , test "Apple portions calculated correctly" <| \_ -> Expect.equal apple 40 + , test "Pear portions calculated correctly" <| \_ -> Expect.equal pear 8 + ] + , await "Run the custom aggreagate function in a window query" + (Sqlite.getAll db + { query = + """ + SELECT + owner, + id, + fruit_portions('quarter', banana) + OVER (PARTITION BY owner ROWS BETWEEN CURRENT ROW AND CURRENT ROW) + AS banana_portion_count, + fruit_portions('half', apple) + OVER (PARTITION BY owner ROWS BETWEEN CURRENT ROW AND CURRENT ROW) + AS apple_portion_count, + fruit_portions('third', pear) + OVER (PARTITION BY owner ROWS BETWEEN CURRENT ROW AND CURRENT ROW) + AS pear_portion_count + FROM baskets; + """ + , parameters = [] + , rowDecoder = + Sqlite.Decode.Row.column "banana_portion_count" Decode.int <| \banana -> + Sqlite.Decode.Row.column "apple_portion_count" Decode.int <| \apple -> + Sqlite.Decode.Row.column "pear_portion_count" Decode.int <| \pear -> + Sqlite.Decode.Row.column "owner" Decode.string <| \owner -> + Sqlite.Decode.Row.column "id" Decode.int <| \id -> + Sqlite.Decode.Row.succeed + { id = id + , banana = banana + , apple = apple + , pear = pear + , owner = owner + } + } + ) <| \results -> + let + basketsByOwnerAndId = + Array.foldl + (\{ id, banana, apple, pear, owner } -> + Dict.set (owner ++ " - " ++ (String.fromInt id)) + { banana = banana + , apple = apple + , pear = pear + } + ) + Dict.empty + results + in + concat + [ test "Both Joey's baskets are the same with expected ids" <| \_ -> + Expect.equal + (Dict.get "Joey - 3" basketsByOwnerAndId) + (Dict.get "Joey - 6" basketsByOwnerAndId) + , test "Both Robin's baskets are the same with expected ids" <| \_ -> + Expect.equal + (Dict.get "Robin - 1" basketsByOwnerAndId) + (Dict.get "Robin - 4" basketsByOwnerAndId) + , test "Both Justins's baskets are the same with expected ids" <| \_ -> + Expect.equal + (Dict.get "Justin - 2" basketsByOwnerAndId) + (Dict.get "Justin - 5" basketsByOwnerAndId) + ] + , awaitError "Run the custom aggregate function with too many arguments" + (Sqlite.getOne db + { query = + """ + SELECT + fruit_portions('half', banana, 2) AS banana_portion_count + FROM baskets; + """ + , parameters = [] + , rowDecoder = + Sqlite.Decode.Row.column "banana_portion_count" Decode.int <| \banana -> + Sqlite.Decode.Row.succeed banana + } + ) <| \err -> + test "Fails as expected" <| \_ -> + when err is + Sqlite.DecodingError _ -> + Expect.pass + + _ -> + Expect.fail "Unexpected error" + , awaitError "Run the custom aggregate function with too few arguments" + (Sqlite.getOne db + { query = + """ + SELECT + fruit_portions('half') AS banana_portion_count + FROM baskets; + """ + , parameters = [] + , rowDecoder = + Sqlite.Decode.Row.column "banana_portion_count" Decode.int <| \banana -> + Sqlite.Decode.Row.succeed banana + } + ) <| \err -> + test "Fails as expected" <| \_ -> + when err is + Sqlite.DecodingError _ -> + Expect.pass + + _ -> + Expect.fail "Unexpected error" + ] + ] ] ] @@ -356,6 +530,47 @@ tests fsPerm = -- Helpers +type Fruit + = Banana + | Apple + | Pear + + +type alias Basket = + { banana : Int + , apple : Int + , pear : Int + , owner : String + } + + +basketOne : Basket +basketOne = + { banana = 2 + , apple = 3 + , pear = 0 + , owner = "Robin" + } + + +basketTwo : Basket +basketTwo = + { banana = 0 + , apple = 1 + , pear = 1 + , owner = "Justin" + } + + +basketThree : Basket +basketThree = + { banana = 1 + , apple = 1 + , pear = 3 + , owner = "Joey" + } + + type alias Person = { name : String , role : String @@ -393,6 +608,13 @@ initializeDb fsPerm = |> Task.andThen execDbSchema +initializeFunctionTestsDb : FileSystem.Permission -> Task Sqlite.Error Sqlite.Database +initializeFunctionTestsDb fsPerm = + Sqlite.open fsPerm Sqlite.defaultOptions Sqlite.Memory + |> Task.andThen execDbSchema + |> Task.andThen execBasketDbSchema + + initializePhysicalDb : FileSystem.Permission -> Task Sqlite.Error Sqlite.Database initializePhysicalDb fsPerm = Sqlite.open fsPerm Sqlite.defaultOptions (Sqlite.File diskDbPath) @@ -417,6 +639,20 @@ execDbSchema db = """ +execBasketDbSchema : Sqlite.Database -> Task Sqlite.Error Sqlite.Database +execBasketDbSchema db = + Sqlite.executeScript db + """ + CREATE TABLE IF NOT EXISTS baskets ( + id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, + banana INT NOT NULL, + apple INT NOT NULL, + pear INT NOT NULL + ) STRICT; + """ + + personEncoder : Person -> Array Sqlite.Encode.Row.Value personEncoder p = [ Sqlite.Encode.Row.column "name" <| Encode.string p.name @@ -424,6 +660,15 @@ personEncoder p = ] +basketEncoder : Basket -> Array Sqlite.Encode.Row.Value +basketEncoder b = + [ Sqlite.Encode.Row.column "owner" <| Encode.string b.owner + , Sqlite.Encode.Row.column "banana" <| Encode.int b.banana + , Sqlite.Encode.Row.column "apple" <| Encode.int b.apple + , Sqlite.Encode.Row.column "pear" <| Encode.int b.pear + ] + + personDecoder : Sqlite.Decode.Row.Decoder Person personDecoder = Sqlite.Decode.Row.column "name" Decode.string <| \name -> @@ -446,6 +691,14 @@ insertPerson person db = } +insertBasket : Basket -> Sqlite.Database -> Task Sqlite.Error Sqlite.ExecutionSummary +insertBasket basket db = + Sqlite.execute db + { statement = "INSERT INTO baskets (owner, banana, apple, pear) VALUES (:owner, :banana, :apple, :pear)" + , parameters = basketEncoder basket + } + + defineFriends : String -> String -> Sqlite.Database -> Task Sqlite.Error Sqlite.ExecutionSummary defineFriends nameA nameB db = Sqlite.execute db From 51c9c998a671c984b762d8827fdfe329d6ff01f8 Mon Sep 17 00:00:00 2001 From: Joey Bright Date: Tue, 1 Sep 2026 20:43:55 -0700 Subject: [PATCH 11/11] Prettier formatting --- src/Gren/Kernel/Sqlite.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Gren/Kernel/Sqlite.js b/src/Gren/Kernel/Sqlite.js index 9384fad..b193a46 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -63,22 +63,21 @@ var _Sqlite_function = F3(function (name, func, db) { useBigIntArguments: false, varargs: true, }; - const wrappedFunc = function(...args) { + const wrappedFunc = function (...args) { const jsonArgs = args.map((v) => __Json_wrap(v)); const result = func(jsonArgs); if (__Result_isOk(result)) { - // The triple `.a` gets the OK result, the SQLite encode + // The triple `.a` gets the OK result, the SQLite encode // value, and then grabs the actual value that needs to // be returned return result.a.a.a; } else { return null; } - } + }; db.function(name, options, wrappedFunc); callback(__Scheduler_succeed({})); - } - catch (e) { + } catch (e) { callback(_Sqlite_constructError(e)); } }); @@ -87,12 +86,12 @@ var _Sqlite_function = F3(function (name, func, db) { var _Sqlite_aggregate = F5(function (name, init, func, result, db) { return __Scheduler_binding(function (callback) { try { - const wrappedFunc = function(direction) { + const wrappedFunc = function (direction) { var env = __SqliteAggregate_Entering; if (direction == "inverse") { env = __SqliteAggregate_Exiting; } - return function(state, ...args) { + return function (state, ...args) { const jsonArgs = args.map((v) => __Json_wrap(v)); const result = A3(func, env, state, jsonArgs); if (__Result_isOk(result)) { @@ -100,20 +99,22 @@ var _Sqlite_aggregate = F5(function (name, init, func, result, db) { } else { return null; } - } - } + }; + }; const options = { deterministic: true, directOnly: true, useBigIntArguments: false, varargs: true, - start: () => { return init }, + start: () => { + return init; + }, step: wrappedFunc("step"), result: (state) => { return result(state).a.a; }, - inverse: wrappedFunc("inverse") - } + inverse: wrappedFunc("inverse"), + }; db.aggregate(name, options); callback(__Scheduler_succeed({})); } catch (e) { @@ -148,7 +149,9 @@ var _Sqlite_foldl = F4(function (query, db, func, acc) { try { var acc_ = acc; const prepped = db.prepare(query.__$query); - const params = __Json_unwrap(__SqliteEncodeRow_toJson(query.__$parameters)); + const params = __Json_unwrap( + __SqliteEncodeRow_toJson(query.__$parameters), + ); const rowDecoder = __SqliteDecode_toJson(query.__$rowDecoder); for (const value of prepped.iterate(params)) { @@ -177,7 +180,9 @@ var _Sqlite_getMaybeOne = F2(function (query, db) { return __Scheduler_binding(function (callback) { try { const prepped = db.prepare(query.__$query); - const params = __Json_unwrap(__SqliteEncodeRow_toJson(query.__$parameters)); + const params = __Json_unwrap( + __SqliteEncodeRow_toJson(query.__$parameters), + ); const rowDecoder = __SqliteDecode_toJson(query.__$rowDecoder); const iterator = prepped.iterate(params); @@ -219,7 +224,9 @@ var _Sqlite_executeMany = F3(function (statement, values, db) { } else { for (const val of values) { lastResult = prepped.run( - __Json_unwrap(__SqliteEncodeRow_toJson(statement.__$parameters(val))), + __Json_unwrap( + __SqliteEncodeRow_toJson(statement.__$parameters(val)), + ), ); } }