Skip to content

Commit 8cea1af

Browse files
committed
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(...).
1 parent 7c4da41 commit 8cea1af

File tree

3 files changed

+45
-6
lines changed

3 files changed

+45
-6
lines changed

.changeset/wobbly-octopi-juggle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/capacitor': patch
3+
---
4+
5+
Normalize binary SQLite parameters for Capacitor iOS so `Uint8Array` sync payloads can be passed through `powersync_control(...)` without hitting `Error in reading buffer`.

packages/capacitor/src/adapter/CapacitorSQLiteAdapter.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { PowerSyncCore } from '../plugin/PowerSyncCore.js';
1919
import { messageForErrorCode } from '../plugin/PowerSyncPlugin.js';
2020
import { CapacitorSQLiteOpenFactoryOptions, DEFAULT_SQLITE_OPTIONS } from './CapacitorSQLiteOpenFactory.js';
21+
import { normalizeIOSSqliteParams } from './sqliteParams.js';
2122
/**
2223
* Monitors the execution time of a query and logs it to the performance timeline.
2324
*/
@@ -39,13 +40,15 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
3940
protected initializedPromise: Promise<void>;
4041
protected writeMutex: Mutex;
4142
protected readMutex: Mutex;
43+
protected readonly platform: string;
4244

4345
constructor(protected options: CapacitorSQLiteOpenFactoryOptions) {
4446
super();
4547
this._writeConnection = null;
4648
this._readConnection = null;
4749
this.writeMutex = new Mutex();
4850
this.readMutex = new Mutex();
51+
this.platform = Capacitor.getPlatform();
4952
this.initializedPromise = this.init();
5053
}
5154

@@ -98,8 +101,7 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
98101

99102
await this._readConnection.open();
100103

101-
const platform = Capacitor.getPlatform();
102-
if (platform == 'android') {
104+
if (this.platform == 'android') {
103105
/**
104106
* SQLCipher for Android enables dynamic loading of extensions.
105107
* On iOS we use a static auto extension registration.
@@ -132,13 +134,11 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
132134
};
133135

134136
const _execute = async (query: string, params: any[] = []): Promise<QueryResult> => {
135-
const platform = Capacitor.getPlatform();
136-
137137
if (db.getConnectionReadOnly()) {
138138
return _query(query, params);
139139
}
140140

141-
if (platform == 'android') {
141+
if (this.platform == 'android') {
142142
// Android: use query for SELECT and executeSet for mutations
143143
// We cannot use `run` here for both cases.
144144
if (query.toLowerCase().trim().startsWith('select')) {
@@ -158,7 +158,8 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
158158
}
159159

160160
// iOS (and other platforms): use run("all")
161-
const result = await db.run(query, params, false, 'all');
161+
const sqliteParams = this.platform == 'ios' ? normalizeIOSSqliteParams(params) : params;
162+
const result = await db.run(query, sqliteParams, false, 'all');
162163
const resultSet = result.changes?.values ?? [];
163164
return {
164165
insertId: result.changes?.lastId,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export type IOSSqliteBufferParam = Record<string, number>;
2+
3+
export type NormalizedIOSSqliteParam = unknown | IOSSqliteBufferParam;
4+
5+
export function normalizeIOSSqliteParams(params: unknown[]): NormalizedIOSSqliteParam[] {
6+
return params.map((param) => normalizeIOSSqliteParam(param));
7+
}
8+
9+
function normalizeIOSSqliteParam(value: unknown): NormalizedIOSSqliteParam {
10+
if (value instanceof Uint8Array) {
11+
return uint8ArrayToIOSBuffer(value);
12+
}
13+
14+
if (value instanceof ArrayBuffer) {
15+
return uint8ArrayToIOSBuffer(new Uint8Array(value));
16+
}
17+
18+
if (ArrayBuffer.isView(value)) {
19+
return uint8ArrayToIOSBuffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
20+
}
21+
22+
return value;
23+
}
24+
25+
function uint8ArrayToIOSBuffer(array: Uint8Array): IOSSqliteBufferParam {
26+
// The Capacitor SQLite iOS bridge expects BLOB params as an index-keyed object
27+
// with integer values. It does not accept typed arrays directly.
28+
const result: IOSSqliteBufferParam = {};
29+
for (let i = 0; i < array.length; i++) {
30+
result[String(i)] = array[i];
31+
}
32+
return result;
33+
}

0 commit comments

Comments
 (0)