Docs

Usage

Day-to-day CRUD with the plugin: executing DDL, running parameterized writes, querying rows, and binding values — including BLOBs.

DDL and unparameterized bulk SQL: execute()

Use execute() for CREATE TABLE, other DDL, or bulk DML that has no bound parameters. statements must be a non-empty array, and each array element must be exactly one SQL statement — packing "INSERT ...; INSERT ..." into one string fails on every platform.

await CapacitorSqlite.execute({
  database: 'myapp',
  statements: [
    'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)',
    'CREATE INDEX idx_users_name ON users (name)',
  ],
});

Statements run in a single transaction by default. Pass transaction: false to keep prior successful statements committed if a later one fails, or when calling execute() from inside an already-active beginTransaction() (nested transactions return TRANSACTION_FAILED).

Parameterized writes: run()

run() executes one parameterized statement and reports changes and lastInsertId.

const result = await CapacitorSqlite.run({
  database: 'myapp',
  statement: 'INSERT INTO users (name) VALUES (?)',
  values: ['Alice'],
});
if (!result.success) throw new Error(result.error.message);
console.log(result.data.lastInsertId);

lastInsertId is 0 for UPDATE/DELETE, for statements that insert no row, and for any INSERT/REPLACE containing an ON CONFLICT clause — SQLite does not update its rowid counter when the DO UPDATE arm of an upsert runs. It is also conservatively 0 whenever SQLite's connection-level rowid counter is otherwise unchanged (WITHOUT ROWID tables, replacing the same explicit rowid, or reusing a deleted rowid). When the exact affected row's id matters, use query() with RETURNING instead:

const upserted = await CapacitorSqlite.query<{ id: number }>({
  database: 'myapp',
  statement: `INSERT INTO users (id, name) VALUES (?, ?)
              ON CONFLICT(id) DO UPDATE SET name = excluded.name
              RETURNING id`,
  values: [1, 'Alice'],
});

Reading rows: query()

query() accepts only result-producing SQL: SELECT, PRAGMA, EXPLAIN, and INSERT/UPDATE/DELETE/REPLACE ... RETURNING. Plain write DML without RETURNING returns INVALID_PARAMS — use run() for that.

const queried = await CapacitorSqlite.query<{ id: number; name: string }>({
  database: 'myapp',
  statement: 'SELECT * FROM users WHERE id = ?',
  values: [1],
});
if (!queried.success) throw new Error(queried.error.message);
console.log(queried.data.rows);

Column names become object keys. INTEGER results outside Number.MAX_SAFE_INTEGER come back as strings, not imprecise numbers.

Android caveat: query() runs through SQLiteDatabase.rawQuery(sql, String[]), which only accepts string bind arguments. The plugin scans the SQL first and inlines numeric, boolean, and BLOB ? values as SQL literals to preserve their type. The scanner skips ? inside string literals, quoted identifiers, and comments.

Placeholders

Only anonymous ? placeholders with a positional values array are supported — the value count must exactly match the placeholder count. ? inside strings, quoted identifiers, and SQL comments is not counted.

await CapacitorSqlite.query({
  database: 'myapp',
  statement: 'SELECT * FROM users WHERE id = ? AND active = ?',
  values: [123, true],
});

Numbered (?1) and named (:name, @name, $name) placeholders are valid SQLite syntax but are rejected by this plugin's cross-platform contract — the wrapper exposes positional arrays only, and a statement using them fails validation before it reaches the native layer.

Value types and BLOBs

JS type SQLite affinity
string TEXT
number INTEGER / REAL
boolean INTEGER (0 / 1)
null NULL
Uint8Array BLOB

Prefer Uint8Array for BLOB parameters. A plain number[] (each item 0-255) is also accepted on iOS, Android, and Electron, but the Web implementation accepts Uint8Array only.

Because the Capacitor bridge does not transport typed arrays natively, the JS wrapper sends Android/iOS BLOB inputs through a private tagged base64 envelope, and native code decodes directly to ByteArray/Data. Ordinary strings — including strings that happen to look like the envelope marker — remain TEXT. Electron and Web keep Uint8Array through structured clone and never touch that envelope. Keep individual bridge payloads reasonably bounded; for multi-megabyte media, store files outside SQLite and persist a reference instead.

Next steps

Last updated on July 17, 2026