diff --git a/gren.json b/gren.json index ea07cf7..4a534d8 100644 --- a/gren.json +++ b/gren.json @@ -20,7 +20,11 @@ "WebSocketServer.Connection", "Sqlite", "Sqlite.Decode", - "Sqlite.Encode" + "Sqlite.Decode.Row", + "Sqlite.Encode", + "Sqlite.Encode.Row", + "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 5a6f4e0..b93fe21 100644 --- a/integration-tests/sqlite/src/Main.gren +++ b/integration-tests/sqlite/src/Main.gren @@ -1,11 +1,15 @@ module Main exposing (main) +import Dict import Expect 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 import FileSystem.Path as Path exposing (Path) @@ -13,6 +17,7 @@ import Init import Json.Decode import Json.Encode import Time +import Sqlite.Aggregate main : Effectful.Program a @@ -210,14 +215,14 @@ tests fsPerm = statements = [ { statement = "INSERT INTO people (name, role) VALUES (:name, :role)" , parameters = - [ Encode.string "name" robin.name - , Encode.string "role" 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.string "name" justin.name - , Encode.string "role" justin.role + [ Sqlite.Encode.Row.column "name" <| Encode.string justin.name + , Sqlite.Encode.Row.column "role" <| Encode.string justin.role ] } , { statement = @@ -228,8 +233,8 @@ tests fsPerm = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.string "personA" robin.name - , Encode.string "personB" justin.name + [ Sqlite.Encode.Row.column "personA" <| Encode.string robin.name + , Sqlite.Encode.Row.column "personB" <| Encode.string justin.name ] } ] @@ -270,12 +275,302 @@ 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 and aggregate tests" (initializeFunctionTestsDb fsPerm) <| \db -> + concat + [ 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 + ] + ) <| \_ -> + 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 + [ 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 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 + } + ) <| \_ -> + 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" + ] + ] + ] ] -- 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 @@ -313,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) @@ -337,25 +639,48 @@ execDbSchema db = """ -personEncoder : Person -> Array Encode.Value +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 = - [ Encode.string "name" p.name - , Encode.string "role" p.role + [ Sqlite.Encode.Row.column "name" <| Encode.string p.name + , Sqlite.Encode.Row.column "role" <| Encode.string p.role + ] + + +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 : Decoder Person +personDecoder : Sqlite.Decode.Row.Decoder Person personDecoder = - Decode.string "name" <| \name -> - Decode.string "role" <| \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 : Decoder Person +badPersonDecoder : Sqlite.Decode.Row.Decoder Person badPersonDecoder = - Decode.string "namee" <| \name -> - Decode.string "role_" <| \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 @@ -366,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 @@ -377,8 +710,8 @@ defineFriends nameA nameB db = WHERE a.name = :personA AND b.name = :personB """ , parameters = - [ Encode.string "personA" nameA - , Encode.string "personB" nameB + [ Sqlite.Encode.Row.column "personA" <| Encode.string nameA + , Sqlite.Encode.Row.column "personB" <| Encode.string nameB ] } @@ -391,8 +724,8 @@ defineFriendsById idA idB db = INSERT INTO friends (person_a, person_b) VALUES (:idA, :idB) """ , parameters = - [ Encode.int "idA" idA - , Encode.int "idB" idB + [ Sqlite.Encode.Row.column "idA" <| Encode.int idA + , Sqlite.Encode.Row.column "idB" <| Encode.int idB ] } @@ -400,7 +733,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 = [ Sqlite.Encode.Row.column "name" <| Encode.string name ] , rowDecoder = personDecoder } @@ -425,9 +758,9 @@ friendshipQ = """ , parameters = [] , rowDecoder = - Decode.string "person_a" <| \personA -> - Decode.string "person_b" <| \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 } } @@ -449,7 +782,7 @@ execBadTemplate num db = , parameters = Array.initialize num 0 (\idx -> - Encode.int ("id" ++ String.fromInt idx) idx + Sqlite.Encode.Row.column ("id" ++ String.fromInt idx) <| Encode.int idx ) } @@ -458,8 +791,8 @@ failDecoder : Sqlite.Database -> Task Sqlite.Error Int failDecoder db = Sqlite.getOne db { query = "SELECT * FROM people WHERE name = :name" - , parameters = [ Encode.string "name" "Robin" ] - , rowDecoder = Decode.fail "Oopsy!" + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] + , rowDecoder = Sqlite.Decode.Row.fail "Oopsy!" } @@ -467,11 +800,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.string "name" "Robin" ] + , parameters = [ Sqlite.Encode.Row.column "name" <| Encode.string "Robin" ] , rowDecoder = - Decode.int "name" <| \name -> - Decode.string "role" <| \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 } } @@ -530,34 +863,34 @@ initializeFieldTypeDb fsPerm = ) -allFieldTypesEncoder : AllFieldTypes -> Array Encode.Value +allFieldTypesEncoder : AllFieldTypes -> Array Sqlite.Encode.Row.Value 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 + [ 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 : Decoder AllFieldTypes +allFieldTypesDecoder : Sqlite.Decode.Row.Decoder 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.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 @@ -647,7 +980,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 -> [ Sqlite.Encode.Row.column "t" <| Encode.timeWithMillis t ] } times @@ -657,6 +990,6 @@ allTimesQ = { query = "SELECT t FROM times ORDER BY rowid" , parameters = [] , rowDecoder = - Decode.time "t" <| \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 c516be1..b193a46 100644 --- a/src/Gren/Kernel/Sqlite.js +++ b/src/Gren/Kernel/Sqlite.js @@ -7,7 +7,9 @@ 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 Sqlite.Aggregate as SqliteAggregate exposing (Entering, Exiting) import Maybe exposing (Just, Nothing) */ @@ -52,6 +54,75 @@ var _Sqlite_close = function (db) { }); }; +var _Sqlite_function = F3(function (name, func, db) { + return __Scheduler_binding(function (callback) { + try { + const options = { + deterministic: true, + directOnly: true, + useBigIntArguments: false, + varargs: true, + }; + 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 + // 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) { + callback(_Sqlite_constructError(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)); + } + }); +}); + var _Sqlite_backup = F3(function (destination, pages, db) { return __Scheduler_binding(function (callback) { try { @@ -78,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(__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)) { @@ -107,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(__SqliteEncode_toJson(query.__$parameters)); + const params = __Json_unwrap( + __SqliteEncodeRow_toJson(query.__$parameters), + ); const rowDecoder = __SqliteDecode_toJson(query.__$rowDecoder); const iterator = prepped.iterate(params); @@ -149,7 +224,9 @@ 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 7438e74..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.Value - , rowDecoder : Sqlite.Decode.Decoder 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.Value + , parameters : Array Sqlite.Encode.Row.Value } @@ -178,7 +180,7 @@ executeAll db statements = executeForEach : Database -> { statement : String - , parameters : value -> Array Sqlite.Encode.Value + , parameters : value -> Array Sqlite.Encode.Row.Value } -> Array value -> Task Error ExecutionSummary 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 diff --git a/src/Sqlite/Decode.gren b/src/Sqlite/Decode.gren index 2ff7eb4..b746ff8 100644 --- a/src/Sqlite/Decode.gren +++ b/src/Sqlite/Decode.gren @@ -13,6 +13,8 @@ module Sqlite.Decode exposing -- Composing , succeed , fail + + , unwrap ) {-| Decode SQL results into Gren values. @@ -61,28 +63,25 @@ type Decoder a = Decoder (Json.Decode.Decoder a) --- FIELD DECODERS - - {-| 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 +89,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 +124,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 +147,12 @@ 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 +160,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 ] @@ -217,15 +206,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/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 8c2f9f5..75f17fa 100644 --- a/src/Sqlite/Encode.gren +++ b/src/Sqlite/Encode.gren @@ -11,6 +11,8 @@ module Sqlite.Encode exposing , timeWithMillis , maybe , null + + , unwrap ) {-| Encode SQL parameters. @@ -34,13 +36,10 @@ import Json.Encode import Time -{-| An encoded parameter value. +{-| An encoded SQLite value. -} type Value - = Value - { key : String - , value : Json.Encode.Value - } + = Value Json.Encode.Value {-| Encode a boolean value. @@ -48,36 +47,28 @@ type 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 +99,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 +112,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 +127,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 +139,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 +168,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 Value -> Array Json.Encode.Value toJson values = values |> Array.map unwrap - |> Json.Encode.object + -- |> Json.Encode.object -unwrap : Value -> { key : String, value : Json.Encode.Value } +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 diff --git a/src/Sqlite/Function.gren b/src/Sqlite/Function.gren new file mode 100644 index 0000000..990c550 --- /dev/null +++ b/src/Sqlite/Function.gren @@ -0,0 +1,48 @@ +module Sqlite.Function exposing ( .. ) + +{-|-} + +import Json.Decode +import Json.Encode +import Sqlite +import Task exposing (Task) +import Sqlite.Encode as Encode +import Sqlite.Decode as Decode + + +type Function = + 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 {} + + Nothing -> + Err {} + + +return : Encode.Value -> Function +return value = + Function <| \args -> + when args is + [] -> + Ok value + + many -> + Err {} + + +register : String -> Sqlite.Database -> Function -> Task a {} +register name db (Function func) = + Gren.Kernel.Sqlite.function name func db