diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4ab8d5..dc465be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,10 +19,14 @@ 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 - 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 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.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.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 - ) - ); -} diff --git a/src/sqlight_ffi.mjs b/src/sqlight_ffi.mjs new file mode 100644 index 0000000..bc13243 --- /dev/null +++ b/src/sqlight_ffi.mjs @@ -0,0 +1,141 @@ +import { DatabaseSync } from "node:sqlite"; +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 { + // Empty path should open in-memory database + const dbPath = path === "" ? ":memory:" : path; + return Result$Ok(new DatabaseSync(dbPath)); + } catch (error) { + return convert_error(error); + } +} + +export function close(connection) { + try { + connection.close(); + 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 Result$Ok(undefined); + } + return convert_error(error); + } +} + +export function coerce_value(value) { + return value; +} + +export function coerce_blob(value) { + // Convert BitArray to Uint8Array/Buffer for Node.js/Deno sqlite + 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(uint8Array); + } else { + // Deno: return Uint8Array directly + return uint8Array; + } + } + return value; +} + +export function status(connection) { + throw new Error("status"); +} + +export function exec(sql, connection) { + try { + connection.exec(sql); + return Result$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); + + const stmt = connection.prepare(sql); + // Use all() to get query results, not run() + 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, ...} + 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] = BitArray$BitArray(val); + } + else { + indexed[idx] = val; + } + }); + return indexed; + }); + + return Result$Ok(toList(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 Result$Error( + Error$SqlightError( + errorCode, + error.message || String(error), + error.offset !== undefined ? error.offset : -1 + ) + ); +}