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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion gren.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
449 changes: 391 additions & 58 deletions integration-tests/sqlite/src/Main.gren

Large diffs are not rendered by default.

83 changes: 80 additions & 3 deletions src/Gren/Kernel/Sqlite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

*/
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it possible to throw an exception here in order to return a detailed Error? Would be helpful, I think, to see why func(jsonArgs) !== Ok

}
};
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same thing here. When result === Err, we'd ideally want the user to get that Err

@joeybright joeybright Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@robinheghan I can do a little more investigation on this, but my understanding is that because this function (and the one above) is called by SQLite outside of the Gren runtime, it can't interact with the Gren runtime. If we throw in this function in the kernel code, I believe the program will just crash? We aren't calling this function within the confines of the try catch block in the register function - SQLite is calling the wrappedFunc which, when passed as a variable to .register, I assume evokes it outside of the block its in when its registered.

It's easy enough for me to try, though. I'll do so when I get the chance and report back.

Another option we can do here is allow the user to pass an optional onErr function when registering the SQLite function or aggregate function. It'd have the error as an argument and the function just return a Sqlite.Encode.Value, so SQLite doesn't throw when this function is called on an error state. The only thing it'd really allow someone to do is return a value other than null in the case of an error, which may be helpful and does give them a bit more access to possible errors.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This explination makes sense. See what happens if you throw an exception, and leave the code as is if it doesn't work out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@robinheghan Tried to get this working tonight, but running into odd behavior. Throwing properly bubbles up the error when done in _Sqlite_function, but not in _Sqlite_aggregate. It always returns Unexpected error, and it does not get caught by the try catch block when running the SQL query using the aggregate function. I'm not sure if it has something to do with the Node implementation or the kernel code as written. I've tried just about everything I can think of, though, and nothing seems to work.

Do we want for custom SQLite functions to properly return errors, or have both the custom functions and aggregates do the same thing (return null as is in the current implementation)?

}
};
};
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 {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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)),
),
);
}
}
Expand Down
10 changes: 6 additions & 4 deletions src/Sqlite.gren
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
}


Expand Down Expand Up @@ -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
}


Expand Down Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions src/Sqlite/Aggregate.gren
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading