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
8 changes: 6 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
14 changes: 7 additions & 7 deletions src/sqlight.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -401,19 +401,19 @@ 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,
c: List(Value),
) -> 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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
57 changes: 0 additions & 57 deletions src/sqlight_ffi.js

This file was deleted.

141 changes: 141 additions & 0 deletions src/sqlight_ffi.mjs
Original file line number Diff line number Diff line change
@@ -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
)
);
}