From 8cea1afe262db3c6985c1e9e6302462ae0b12e31 Mon Sep 17 00:00:00 2001 From: Steve Whittaker Date: Sun, 22 Mar 2026 17:32:54 -0500 Subject: [PATCH] Fix Capacitor iOS binary params in SQLite run() Normalize typed-array payloads before calling Capacitor SQLite's iOS run() path. This avoids the "Error in reading buffer" failure when PowerSync sends line_binary payloads through powersync_control(...). --- .changeset/wobbly-octopi-juggle.md | 5 +++ .../src/adapter/CapacitorSQLiteAdapter.ts | 13 ++++---- .../capacitor/src/adapter/sqliteParams.ts | 33 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 .changeset/wobbly-octopi-juggle.md create mode 100644 packages/capacitor/src/adapter/sqliteParams.ts diff --git a/.changeset/wobbly-octopi-juggle.md b/.changeset/wobbly-octopi-juggle.md new file mode 100644 index 000000000..53ca2c988 --- /dev/null +++ b/.changeset/wobbly-octopi-juggle.md @@ -0,0 +1,5 @@ +--- +'@powersync/capacitor': patch +--- + +Normalize binary SQLite parameters for Capacitor iOS so `Uint8Array` sync payloads can be passed through `powersync_control(...)` without hitting `Error in reading buffer`. diff --git a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts index fe40fe915..fced364f9 100644 --- a/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts +++ b/packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts @@ -18,6 +18,7 @@ import { import { PowerSyncCore } from '../plugin/PowerSyncCore.js'; import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js'; import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.js'; +import { normalizeIOSSqliteParams } from './sqliteParams.js'; /** * Monitors the execution time of a query and logs it to the performance timeline. */ @@ -39,6 +40,7 @@ class CapacitorConnectionPool extends BaseObserver implements protected initializedPromise: Promise; protected writeMutex: Mutex; protected readMutex: Mutex; + protected readonly platform: string; constructor(protected options: CapacitorSQLiteOpenFactoryOptions) { super(); @@ -46,6 +48,7 @@ class CapacitorConnectionPool extends BaseObserver implements this._readConnection = null; this.writeMutex = new Mutex(); this.readMutex = new Mutex(); + this.platform = Capacitor.getPlatform(); this.initializedPromise = this.init(); } @@ -98,8 +101,7 @@ class CapacitorConnectionPool extends BaseObserver implements await this._readConnection.open(); - const platform = Capacitor.getPlatform(); - if (platform == 'android') { + if (this.platform == 'android') { /** * SQLCipher for Android enables dynamic loading of extensions. * On iOS we use a static auto extension registration. @@ -132,13 +134,11 @@ class CapacitorConnectionPool extends BaseObserver implements }; const _execute = async (query: string, params: any[] = []): Promise => { - const platform = Capacitor.getPlatform(); - if (db.getConnectionReadOnly()) { return _query(query, params); } - if (platform == 'android') { + if (this.platform == 'android') { // Android: use query for SELECT and executeSet for mutations // We cannot use `run` here for both cases. if (query.toLowerCase().trim().startsWith('select')) { @@ -158,7 +158,8 @@ class CapacitorConnectionPool extends BaseObserver implements } // iOS (and other platforms): use run("all") - const result = await db.run(query, params, false, 'all'); + const sqliteParams = this.platform == 'ios' ? normalizeIOSSqliteParams(params) : params; + const result = await db.run(query, sqliteParams, false, 'all'); const resultSet = result.changes?.values ?? []; return { insertId: result.changes?.lastId, diff --git a/packages/capacitor/src/adapter/sqliteParams.ts b/packages/capacitor/src/adapter/sqliteParams.ts new file mode 100644 index 000000000..79c7f5e1d --- /dev/null +++ b/packages/capacitor/src/adapter/sqliteParams.ts @@ -0,0 +1,33 @@ +export type IOSSqliteBufferParam = Record; + +export type NormalizedIOSSqliteParam = unknown | IOSSqliteBufferParam; + +export function normalizeIOSSqliteParams(params: unknown[]): NormalizedIOSSqliteParam[] { + return params.map((param) => normalizeIOSSqliteParam(param)); +} + +function normalizeIOSSqliteParam(value: unknown): NormalizedIOSSqliteParam { + if (value instanceof Uint8Array) { + return uint8ArrayToIOSBuffer(value); + } + + if (value instanceof ArrayBuffer) { + return uint8ArrayToIOSBuffer(new Uint8Array(value)); + } + + if (ArrayBuffer.isView(value)) { + return uint8ArrayToIOSBuffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)); + } + + return value; +} + +function uint8ArrayToIOSBuffer(array: Uint8Array): IOSSqliteBufferParam { + // The Capacitor SQLite iOS bridge expects BLOB params as an index-keyed object + // with integer values. It does not accept typed arrays directly. + const result: IOSSqliteBufferParam = {}; + for (let i = 0; i < array.length; i++) { + result[String(i)] = array[i]; + } + return result; +}