From 546fba2ef9cb05a360effa6a0e2849b14bd9ffd5 Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sun, 5 Oct 2025 13:14:32 +0900 Subject: [PATCH 1/8] Make the JavaScript FFI compatible with node:sqlite Because this is a breaking change, it can only run on Node.js v22.5 or Deno v2.2 and later. --- src/sqlight.gleam | 14 ++--- src/sqlight_ffi.mjs | 148 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 src/sqlight_ffi.mjs diff --git a/src/sqlight.gleam b/src/sqlight.gleam index 6dbbeef..0adcfca 100644 --- a/src/sqlight.gleam +++ b/src/sqlight.gleam @@ -300,11 +300,11 @@ pub fn error_code_from_int(code: Int) -> ErrorCode { } @external(erlang, "sqlight_ffi", "open") -@external(javascript, "./sqlight_ffi.js", "open") +@external(javascript, "./sqlight_ffi.mjs", "open") fn open_(a: String) -> Result(Connection, Error) @external(erlang, "sqlight_ffi", "close") -@external(javascript, "./sqlight_ffi.js", "close") +@external(javascript, "./sqlight_ffi.mjs", "close") fn close_(a: Connection) -> Result(Nil, Error) /// Open a connection to a SQLite database. @@ -401,7 +401,7 @@ pub fn query( } @external(erlang, "sqlight_ffi", "query") -@external(javascript, "./sqlight_ffi.js", "query") +@external(javascript, "./sqlight_ffi.mjs", "query") fn run_query( a: String, b: Connection, @@ -409,11 +409,11 @@ fn run_query( ) -> Result(List(Dynamic), Error) @external(erlang, "sqlight_ffi", "coerce_value") -@external(javascript, "./sqlight_ffi.js", "coerce_value") +@external(javascript, "./sqlight_ffi.mjs", "coerce_value") fn coerce_value(a: a) -> Value @external(erlang, "sqlight_ffi", "exec") -@external(javascript, "./sqlight_ffi.js", "exec") +@external(javascript, "./sqlight_ffi.mjs", "exec") fn exec_(a: String, b: Connection) -> Result(Nil, Error) /// Convert a Gleam `Option` to an SQLite nullable value, to be used an argument @@ -451,7 +451,7 @@ pub fn text(value: String) -> Value { /// query. /// @external(erlang, "sqlight_ffi", "coerce_blob") -@external(javascript, "./sqlight_ffi.js", "coerce_blob") +@external(javascript, "./sqlight_ffi.mjs", "coerce_blob") pub fn blob(value: BitArray) -> Value /// Convert a Gleam `Bool` to an SQLite int, to be used an argument to a @@ -472,7 +472,7 @@ pub fn bool(value: Bool) -> Value { /// Construct an SQLite null, to be used an argument to a query. /// @external(erlang, "sqlight_ffi", "null") -@external(javascript, "./sqlight_ffi.js", "null_") +@external(javascript, "./sqlight_ffi.mjs", "null_") pub fn null() -> Value /// Decode an SQLite boolean value. diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs new file mode 100644 index 0000000..abcb015 --- /dev/null +++ b/src/sqlight_ffi.mjs @@ -0,0 +1,148 @@ +import { DatabaseSync } from "node:sqlite"; +import { List, Ok, Error as GlError, BitArray } from "./gleam.mjs"; +import { SqlightError, error_code_from_int } from "./sqlight.mjs"; + +export function open(path) { + try { + // Empty path should open in-memory database + const dbPath = path === "" ? ":memory:" : path; + return new Ok(new DatabaseSync(dbPath)); + } catch (error) { + return convert_error(error); + } +} + +export function close(connection) { + try { + connection.close(); + return new Ok(undefined); + } catch (error) { + // Ignore "database is not open" error on multiple close calls + if (error.message && error.message.includes("database is not open")) { + return new Ok(undefined); + } + return convert_error(error); + } +} + +export function coerce_value(value) { + return value; +} + +export function coerce_blob(value) { + // Convert BitArray to Buffer for Node.js sqlite + if (value instanceof BitArray) { + return Buffer.from(value.rawBuffer); + } + return value; +} + +export function status(connection) { + throw new Error("status"); +} + +export function exec(sql, connection) { + try { + connection.exec(sql); + return new Ok(undefined); + } catch (error) { + return convert_error(error); + } +} + +export function query(sql, connection, parameters) { + let rows; + try { + const paramsArray = parameters.toArray().map(p => p === undefined ? null : p); + + // Node.js sqlite doesn't support numbered parameters (?1, ?2, etc.) + // We need to expand the parameters according to the numbered placeholders + // For example: "select ?1, typeof(?1)" with [value] should become "select ?, ?" with [value, value] + const numberedParams = []; + const convertedSql = sql.replace(/\?(\d+)?/g, (match, num) => { + if (num) { + // Numbered parameter like ?1, ?2 + const index = parseInt(num) - 1; // Convert to 0-based index + numberedParams.push(paramsArray[index]); + } else { + // Anonymous parameter ? + numberedParams.push(paramsArray[numberedParams.length]); + } + return '?'; + }); + + const stmt = connection.prepare(convertedSql); + // Use all() to get query results, not run() + rows = stmt.all(...numberedParams); + + // Convert rows from object format to indexed format for Gleam decoder + // Gleam expects {0: val1, 1: val2, ...} but Node.js gives {col1: val1, col2: val2, ...} + const indexedRows = rows.map(row => { + const values = Object.values(row); + const indexed = {}; + values.forEach((val, idx) => { + // Convert null to undefined for Gleam's optional decoder + if (val === null) { + indexed[idx] = undefined; + } + // Convert Uint8Array (BLOB) to BitArray + else if (val instanceof Uint8Array) { + indexed[idx] = new BitArray(val); + } + else { + indexed[idx] = val; + } + }); + return indexed; + }); + + return new Ok(List.fromArray(indexedRows)); + } catch (error) { + return convert_error(error); + } +} + +export function null_() { + // Node.js SQLite expects null, but Gleam uses undefined internally + // The query function will convert undefined to null when binding + return undefined; +} + +function convert_error(error) { + // Node.js sqlite errors use errcode (numeric) instead of code (string) + // Deno's node:sqlite compatibility layer doesn't provide errcode + let code; + + if (error.errcode !== undefined && typeof error.errcode === 'number') { + // Node.js: Use the extended error code + code = error.errcode; + } else { + // Deno: Infer basic error code from message + const message = error.message || ''; + + if (message.includes('FOREIGN KEY')) { + code = 787; // ConstraintForeignkey + } else if (message.includes('NOT NULL')) { + code = 1299; // ConstraintNotnull + } else if (message.includes('UNIQUE')) { + code = 2067; // ConstraintUnique + } else if (message.includes('CHECK')) { + code = 275; // ConstraintCheck + } else if (message.includes('constraint')) { + code = 19; // Constraint (generic) + } else if (message.includes('incomplete input') || message.includes('syntax error')) { + code = 1; // GenericError + } else { + code = 1; // GenericError (fallback) + } + } + + const errorCode = error_code_from_int(code); + return new GlError( + new SqlightError( + errorCode, + error.message || String(error), + error.offset !== undefined ? error.offset : -1 + ) + ); +} From 0bef471379f5d1acf99126e7c2406fe07aa60ad2 Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sun, 5 Oct 2025 22:01:37 +0900 Subject: [PATCH 2/8] Delete unused file. --- src/sqlight_ffi.js | 57 ---------------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 src/sqlight_ffi.js diff --git a/src/sqlight_ffi.js b/src/sqlight_ffi.js deleted file mode 100644 index 734022c..0000000 --- a/src/sqlight_ffi.js +++ /dev/null @@ -1,57 +0,0 @@ -import { List, Ok, Error as GlError } from "./gleam.mjs"; -import { SqlightError, error_code_from_int } from "./sqlight.mjs"; -import { DB } from "https://deno.land/x/sqlite/mod.ts"; - -export function open(path) { - return new Ok(new DB(path)); -} - -export function close(connection) { - connection.close(); - return new Ok(undefined); -} - -export function coerce_value(value) { - return value; -} - -export function coerce_blob(value) { - return value.buffer; -} - -export function status(connection) { - throw new Error("status"); -} - -export function exec(sql, connection) { - try { - connection.execute(sql); - return new Ok(undefined); - } catch (error) { - return convert_error(error); - } -} - -export function query(sql, connection, parameters) { - let rows; - try { - rows = connection.query(sql, parameters.toArray()); - } catch (error) { - return convert_error(error); - } - return new Ok(List.fromArray(rows)); -} - -export function null_() { - return undefined; -} - -function convert_error(error) { - return new GlError( - new SqlightError( - error_code_from_int(error.code), - error.message, - error.offset || -1 - ) - ); -} From d6fffa3beb6abb0f4cafdb3f07c4d84d2cc467d7 Mon Sep 17 00:00:00 2001 From: Comamoca Date: Mon, 13 Oct 2025 08:28:26 +0900 Subject: [PATCH 3/8] Delete unused statements --- src/sqlight_ffi.mjs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs index abcb015..ff92a2c 100644 --- a/src/sqlight_ffi.mjs +++ b/src/sqlight_ffi.mjs @@ -55,25 +55,9 @@ export function query(sql, connection, parameters) { try { const paramsArray = parameters.toArray().map(p => p === undefined ? null : p); - // Node.js sqlite doesn't support numbered parameters (?1, ?2, etc.) - // We need to expand the parameters according to the numbered placeholders - // For example: "select ?1, typeof(?1)" with [value] should become "select ?, ?" with [value, value] - const numberedParams = []; - const convertedSql = sql.replace(/\?(\d+)?/g, (match, num) => { - if (num) { - // Numbered parameter like ?1, ?2 - const index = parseInt(num) - 1; // Convert to 0-based index - numberedParams.push(paramsArray[index]); - } else { - // Anonymous parameter ? - numberedParams.push(paramsArray[numberedParams.length]); - } - return '?'; - }); - - const stmt = connection.prepare(convertedSql); + const stmt = connection.prepare(sql); // Use all() to get query results, not run() - rows = stmt.all(...numberedParams); + rows = stmt.all(...paramsArray); // Convert rows from object format to indexed format for Gleam decoder // Gleam expects {0: val1, 1: val2, ...} but Node.js gives {col1: val1, col2: val2, ...} From 3a42e0dcdba1f550fb70657e5e7a636bd5fe8f4d Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sun, 11 Jan 2026 13:31:19 +0900 Subject: [PATCH 4/8] Support Deno by avoiding unconditional Buffer usage in coerce_blob --- src/sqlight_ffi.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs index ff92a2c..304e2a4 100644 --- a/src/sqlight_ffi.mjs +++ b/src/sqlight_ffi.mjs @@ -30,9 +30,15 @@ export function coerce_value(value) { } export function coerce_blob(value) { - // Convert BitArray to Buffer for Node.js sqlite + // Convert BitArray to Uint8Array/Buffer for Node.js/Deno sqlite if (value instanceof BitArray) { - return Buffer.from(value.rawBuffer); + // For Deno, Buffer may not be available, use Uint8Array + if (typeof Buffer !== 'undefined') { + return Buffer.from(value.rawBuffer); + } else { + // Deno: return Uint8Array directly + return value.rawBuffer; + } } return value; } From 20aec1b78b3cfbe30af487856476a6be9b450f50 Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sun, 11 Jan 2026 13:31:53 +0900 Subject: [PATCH 5/8] CI: Test JavaScript target on both Node.js and Deno --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4ab8d5..cba9cc6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,9 +20,13 @@ jobs: - uses: denoland/setup-deno@v1 with: deno-version: v1.x + - uses: actions/setup-node@v6 + with: + node-version: 25 - run: gleam format --check src test - run: gleam deps download - run: gleam test --target erlang - - run: gleam test --target javascript + - run: gleam test --target javascript --runtime node + - run: gleam test --target javascript --runtime deno From ab22d2ef707c8a0e6df1c19c20b830981dbbcf0a Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sun, 11 Jan 2026 13:48:40 +0900 Subject: [PATCH 6/8] Fix the Deno version used for running tests --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cba9cc6..dc465be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: rebar3-version: "3" - uses: denoland/setup-deno@v1 with: - deno-version: v1.x + deno-version: v2.x - uses: actions/setup-node@v6 with: node-version: 25 From 5eb87b441ba3a646430473eba766c68cd7d0df7b Mon Sep 17 00:00:00 2001 From: Comamoca Date: Sat, 28 Feb 2026 13:44:53 +0900 Subject: [PATCH 7/8] Update the generation method for Gleam built-in types to the latest approach --- src/sqlight_ffi.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs index 304e2a4..8a0010c 100644 --- a/src/sqlight_ffi.mjs +++ b/src/sqlight_ffi.mjs @@ -1,12 +1,12 @@ import { DatabaseSync } from "node:sqlite"; -import { List, Ok, Error as GlError, BitArray } from "./gleam.mjs"; +import { List, Result$Ok, Result$Error, BitArray } from "./gleam.mjs"; import { SqlightError, error_code_from_int } from "./sqlight.mjs"; export function open(path) { try { // Empty path should open in-memory database const dbPath = path === "" ? ":memory:" : path; - return new Ok(new DatabaseSync(dbPath)); + return Result$Ok(new DatabaseSync(dbPath)); } catch (error) { return convert_error(error); } @@ -15,11 +15,11 @@ export function open(path) { export function close(connection) { try { connection.close(); - return new Ok(undefined); + return Result$Ok(undefined); } catch (error) { // Ignore "database is not open" error on multiple close calls if (error.message && error.message.includes("database is not open")) { - return new Ok(undefined); + return Result$Ok(undefined); } return convert_error(error); } @@ -50,7 +50,7 @@ export function status(connection) { export function exec(sql, connection) { try { connection.exec(sql); - return new Ok(undefined); + return Result$Ok(undefined); } catch (error) { return convert_error(error); } @@ -86,7 +86,7 @@ export function query(sql, connection, parameters) { return indexed; }); - return new Ok(List.fromArray(indexedRows)); + return Result$Ok(List.fromArray(indexedRows)); } catch (error) { return convert_error(error); } @@ -128,7 +128,7 @@ function convert_error(error) { } const errorCode = error_code_from_int(code); - return new GlError( + return Result$Error( new SqlightError( errorCode, error.message || String(error), From 3943be91dc1d296245bbaec096a00d5dc2c008d4 Mon Sep 17 00:00:00 2001 From: Comamoca Date: Mon, 6 Apr 2026 02:04:03 +0900 Subject: [PATCH 8/8] Refactor the parts that directly use the BitArray and SlightError classes to use the JS FFI format --- gleam.toml | 2 +- src/sqlight_ffi.mjs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/gleam.toml b/gleam.toml index 3cb7cf2..8e56127 100644 --- a/gleam.toml +++ b/gleam.toml @@ -3,7 +3,7 @@ version = "1.0.3" licences = ["Apache-2.0"] description = "Use SQLite from Gleam!" -gleam = ">= 0.32.0" +gleam = ">= 1.15.0" repository = { type = "github", user = "lpil", repo = "sqlight" } links = [ diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs index 8a0010c..bc13243 100644 --- a/src/sqlight_ffi.mjs +++ b/src/sqlight_ffi.mjs @@ -1,6 +1,6 @@ import { DatabaseSync } from "node:sqlite"; -import { List, Result$Ok, Result$Error, BitArray } from "./gleam.mjs"; -import { SqlightError, error_code_from_int } from "./sqlight.mjs"; +import { toList, Result$Ok, Result$Error, BitArray$BitArray, BitArray$isBitArray, BitArray$BitArray$data } from "./gleam.mjs"; +import { Error$SqlightError, error_code_from_int } from "./sqlight.mjs"; export function open(path) { try { @@ -31,13 +31,16 @@ export function coerce_value(value) { export function coerce_blob(value) { // Convert BitArray to Uint8Array/Buffer for Node.js/Deno sqlite - if (value instanceof BitArray) { + if (BitArray$isBitArray(value)) { + const data = BitArray$BitArray$data(value); + // Convert DataView to Uint8Array + const uint8Array = new Uint8Array(data.buffer, data.byteOffset, data.byteLength); // For Deno, Buffer may not be available, use Uint8Array if (typeof Buffer !== 'undefined') { - return Buffer.from(value.rawBuffer); + return Buffer.from(uint8Array); } else { // Deno: return Uint8Array directly - return value.rawBuffer; + return uint8Array; } } return value; @@ -77,7 +80,7 @@ export function query(sql, connection, parameters) { } // Convert Uint8Array (BLOB) to BitArray else if (val instanceof Uint8Array) { - indexed[idx] = new BitArray(val); + indexed[idx] = BitArray$BitArray(val); } else { indexed[idx] = val; @@ -86,7 +89,7 @@ export function query(sql, connection, parameters) { return indexed; }); - return Result$Ok(List.fromArray(indexedRows)); + return Result$Ok(toList(indexedRows)); } catch (error) { return convert_error(error); } @@ -129,7 +132,7 @@ function convert_error(error) { const errorCode = error_code_from_int(code); return Result$Error( - new SqlightError( + Error$SqlightError( errorCode, error.message || String(error), error.offset !== undefined ? error.offset : -1