Capacitor plugin for libSQL on Android and iOS. Designed to drop in under Drizzle ORM via the sqlite-proxy adapter, with NULL-safe parameter binding.
Adding this plugin to a project? Read INTEGRATION.md for the full step-by-step recipe.
npx npm@10 install github:7999999166/kanik-libsql-capacitor#v0.3.4
npx cap syncThe plugin is published as a Git dependency. Use npx npm@10 — npm 11.12.0 ships a git-dep preparation bug (--prefer-online cannot be provided when using --prefer-offline).
- Android — Kotlin wrapper around
tech.turso.libsql(minSdk 24). - iOS — Swift wrapper around
libsql-swift(iOS 15+, SPM). - Web — not implemented; use a separate browser-side SQLite (e.g. wa-sqlite or Dexie).
import { Libsql } from '@kanik/libsql-capacitor';
const { connectionId } = await Libsql.connect({ url: 'file:/full/path/to.db' });
await Libsql.execute({ connectionId, statement: 'CREATE TABLE t (id INTEGER, name TEXT)' });
await Libsql.execute({ connectionId, statement: 'INSERT INTO t VALUES (?, ?)', values: [1, 'Kanik'] });
const { rows } = await Libsql.query({ connectionId, statement: 'SELECT * FROM t WHERE id = ?', values: [1] });
await Libsql.beginTransaction({ connectionId });
await Libsql.executeBatch({ connectionId, statements: [{ statement: 'INSERT INTO t VALUES (?, ?)', values: [2, 'A'] }] });
await Libsql.commitTransaction({ connectionId });
await Libsql.disconnect({ connectionId });| Method | Purpose |
|---|---|
connect |
Open a libSQL connection. Returns a connectionId. |
disconnect |
Close a connection. |
execute |
Run a statement that does not return rows. |
executeBatch |
Run multiple statements in one call. |
query |
Run a SELECT and return rows (array of arrays of values). |
beginTransaction |
Begin a transaction on the connection. |
commitTransaction |
Commit. |
rollbackTransaction |
Rollback. |
sync |
Sync (no-op for local-only setups). |
rows is Array<Array<value>> — positional, not keyed by column. Each value is a JSON-safe scalar: number, string, null, or Array<number> (byte array for blobs).
iOS column iteration:
libsql-swiftdoes not expose a column-count API and returns a nullValue(without throwing) for out-of-bounds indices. The plugin iterates each row up to a hard cap of 1024 columns and stops early after seeing 16 consecutive nulls, trimming them from the result. Realistic schemas are well within these bounds.
The Capacitor JS↔native bridge marshals null JS values to a sentinel that tech.turso.libsql rejects as "null not supported". If your app passes null parameters, rewrite them to literal NULL in the SQL text before sending — example:
function inlineNulls(sql, params) {
if (!params || !params.length) return { sql, params };
let out = '', i = 0, inSingle = false, inDouble = false, kept = [];
for (let c = 0; c < sql.length; c++) {
const ch = sql[c];
if (ch === "'" && !inDouble) inSingle = !inSingle;
else if (ch === '"' && !inSingle) inDouble = !inDouble;
if (ch === '?' && !inSingle && !inDouble) {
const p = params[i++];
if (p === null || p === undefined) { out += 'NULL'; continue; }
kept.push(p);
}
out += ch;
}
return { sql: out, params: kept };
}import { drizzle } from 'drizzle-orm/sqlite-proxy';
import { Libsql } from '@kanik/libsql-capacitor';
const { connectionId } = await Libsql.connect({ url: 'file:/data/app.db' });
const db = drizzle(async (sql, params, method) => {
const safe = inlineNulls(sql, params);
if (method === 'run') {
await Libsql.execute({ connectionId, statement: safe.sql, values: safe.params });
return { rows: [] };
}
const { rows } = await Libsql.query({ connectionId, statement: safe.sql, values: safe.params });
return method === 'get' ? { rows: rows[0] ?? [] } : { rows };
});git clone https://github.com/7999999166/kanik-libsql-capacitor.git
cd kanik-libsql-capacitor
npm install
npm run builddist/ is committed so consumers installing from a Git tag don't need a prepare step.
- iOS: Coerce bridged
NSNumber/NSNullparameters toValueRepresentablebefore binding. Previously any non-string parameter (numbers, booleans) failed withinvalidValues("All values must be arrays.").
- iOS: Convert
Libsql.Valueenum cases to JSON-serializable scalars inQueryResult. Previously everyqueryresponse failed Capacitor's bridge withUnable to serialize plugin response as JSON.and returned{}.
- iOS: Fix infinite loop in
QueryResult.convertRowsToAnyArraythat allocated multi-GB arrays and crashed the app.libsql-swift'sRow.get(index)returns.null(without throwing) for out-of-bounds indices and exposes no column count, so the previous catch-based loop never terminated. Now capped at 1024 columns with a 16-consecutive-null early stop.
MIT