Skip to content

Commit 7e5ded0

Browse files
committed
Fix Capacitor iOS binary parameter handling in SQLite adapter
1 parent 7c4da41 commit 7e5ded0

File tree

3 files changed

+48
-8
lines changed

3 files changed

+48
-8
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: 10 additions & 8 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.
@@ -118,8 +120,10 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
118120
}
119121

120122
protected generateLockContext(db: SQLiteDBConnection): LockContext {
123+
const normalizeParams = (params: any[]) => (this.platform == 'ios' ? normalizeIOSSqliteParams(params) : params);
124+
121125
const _query = async (query: string, params: any[] = []) => {
122-
const result = await db.query(query, params);
126+
const result = await db.query(query, normalizeParams(params));
123127
const arrayResult = result.values ?? [];
124128
return {
125129
rowsAffected: 0,
@@ -132,13 +136,11 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
132136
};
133137

134138
const _execute = async (query: string, params: any[] = []): Promise<QueryResult> => {
135-
const platform = Capacitor.getPlatform();
136-
137139
if (db.getConnectionReadOnly()) {
138140
return _query(query, params);
139141
}
140142

141-
if (platform == 'android') {
143+
if (this.platform == 'android') {
142144
// Android: use query for SELECT and executeSet for mutations
143145
// We cannot use `run` here for both cases.
144146
if (query.toLowerCase().trim().startsWith('select')) {
@@ -158,7 +160,7 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
158160
}
159161

160162
// iOS (and other platforms): use run("all")
161-
const result = await db.run(query, params, false, 'all');
163+
const result = await db.run(query, normalizeParams(params), false, 'all');
162164
const resultSet = result.changes?.values ?? [];
163165
return {
164166
insertId: result.changes?.lastId,
@@ -207,7 +209,7 @@ class CapacitorConnectionPool extends BaseObserver<DBAdapterListener> implements
207209
let result = await db.executeSet(
208210
params.map((param) => ({
209211
statement: query,
210-
values: param
212+
values: normalizeParams(param)
211213
}))
212214
);
213215

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)