Skip to content

Commit 914ee48

Browse files
committed
Working towards sqlcipher upgrade options.
1 parent 6c3ea55 commit 914ee48

File tree

5 files changed

+87
-18
lines changed

5 files changed

+87
-18
lines changed

.changeset/rotten-pugs-beam.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
---
2-
'@powersync/op-sqlite': major
2+
'@powersync/op-sqlite': minor
33
---
44

55
Initial stable version release.
66

7-
Updated op-sqlite upstream dependency from 11.x.x to 14.0.2, the @powersync/op-sqlite package will now reflect the supported major version in its version (for example 0.14.x indicates support for version of 14.x.x of op-sqlite).
7+
Updated op-sqlite upstream peer dependency from 11.x.x to support ^13.x.x and ^14.x.x,
88

9-
Noteworthy changes for 11 > 14 bump include:
9+
Noteworthy changes from version 11 to version 14 include:
1010

1111
1. SQLite updated to 3.49.1
1212
2. Major/breaking update to SQLCipher 4.8. Be careful when upgrading to this version, your sqlcipher database will need to be updated as well (upgrading instructions [here](https://discuss.zetetic.net/t/upgrading-to-sqlcipher-4/3283)), read more on the [sqlcipher repo](https://github.com/sqlcipher/sqlcipher). Be sure to test your changes before upgrading.

packages/powersync-op-sqlite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"access": "public"
6666
},
6767
"peerDependencies": {
68-
"@op-engineering/op-sqlite": "^14.0.2",
68+
"@op-engineering/op-sqlite": "^13.0.0 || ^14.0.0",
6969
"@powersync/common": "workspace:^1.31.1",
7070
"react": "*",
7171
"react-native": "*"

packages/powersync-op-sqlite/src/db/OPSqliteAdapter.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, QueryResult,
33
import Lock from 'async-lock';
44
import { Platform } from 'react-native';
55
import { OPSQLiteConnection } from './OPSQLiteConnection';
6-
import { SqliteOptions } from './SqliteOptions';
6+
import { CipherVersion, SqliteOptions } from './SqliteOptions';
77

88
/**
99
* Adapter for React Native Quick SQLite
@@ -50,7 +50,7 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
5050
this.options.sqliteOptions!;
5151
const dbFilename = this.options.name;
5252

53-
this.writeConnection = await this.openConnection(dbFilename);
53+
this.writeConnection = await this.openConnection(dbFilename, true);
5454

5555
const baseStatements = [
5656
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
@@ -97,9 +97,13 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
9797
}
9898
}
9999

100-
protected async openConnection(filenameOverride?: string): Promise<OPSQLiteConnection> {
100+
protected async openConnection(filenameOverride?: string, writeConnection?: boolean): Promise<OPSQLiteConnection> {
101101
const dbFilename = filenameOverride ?? this.options.name;
102-
const DB: DB = this.openDatabase(dbFilename, this.options.sqliteOptions?.encryptionKey ?? undefined);
102+
const DB: DB = await this.openDatabase(
103+
dbFilename,
104+
this.options.sqliteOptions?.encryptionKey ?? undefined,
105+
writeConnection
106+
);
103107

104108
//Load extensions for all connections
105109
this.loadAdditionalExtensions(DB);
@@ -120,16 +124,12 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
120124
}
121125
}
122126

123-
private openDatabase(dbFilename: string, encryptionKey?: string): DB {
127+
private async openDatabase(dbFilename: string, encryptionKey?: string, writeConnection?: boolean): Promise<DB> {
124128
//This is needed because an undefined/null dbLocation will cause the open function to fail
125129
const location = this.getDbLocation(this.options.dbLocation);
126130
//Simarlily if the encryption key is undefined/null when using SQLCipher it will cause the open function to fail
127131
if (encryptionKey) {
128-
return open({
129-
name: dbFilename,
130-
location: location,
131-
encryptionKey: encryptionKey
132-
});
132+
return await this.openSQLCipher(dbFilename, location, encryptionKey, writeConnection);
133133
} else {
134134
return open({
135135
name: dbFilename,
@@ -138,6 +138,57 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
138138
}
139139
}
140140

141+
/**
142+
* We set up SQLCipher compatibility here.
143+
*
144+
* - For compability with SQLCipher 3 databases, we set the cipher_compatibility pragma to 3 for all connections.
145+
* - For upgrading SQLCipher 3 databases to version 4, we do the migration on the write connection.
146+
* - For SQLCipher 4 databases, we do not need to set the cipher_compatibility pragma, as it is the default.
147+
*/
148+
private async openSQLCipher(
149+
dbFilename: string,
150+
location: string,
151+
encryptionKey?: string,
152+
writeConnection?: boolean
153+
): Promise<DB> {
154+
const openDb = () =>
155+
open({
156+
name: dbFilename,
157+
location: location,
158+
encryptionKey: encryptionKey
159+
});
160+
const db = openDb();
161+
162+
const cipherVersion = this.options.sqliteOptions?.cipherVersion;
163+
164+
if (cipherVersion == CipherVersion.VERSION_3) {
165+
await db.execute('PRAGMA cipher_compatibility = 3;');
166+
return db;
167+
} else if (cipherVersion == CipherVersion.UPGRADE_3_TO_4 && writeConnection) {
168+
// Based on steps described at https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_migrate
169+
try {
170+
// Valid version agnostic query to confirm SQLCipher version
171+
await db.execute('SELECT count(*) FROM sqlite_master;');
172+
// Query succeeded, so we are on SQLCipher 4 and don't have to do anything
173+
console.log('SQLCipher 4 database detected, no migration needed.');
174+
return db;
175+
} catch (e: any) {
176+
// Query failed, assuming we are on SQLCipher 3 and need to upgrade
177+
// Catch any error
178+
}
179+
180+
db.close();
181+
const reopenedDB = openDb();
182+
183+
const migrationResult = await reopenedDB.execute('PRAGMA cipher_migrate;');
184+
console.log('SQLCipher migration result:', migrationResult);
185+
186+
return reopenedDB;
187+
}
188+
189+
return db;
190+
}
191+
141192
private loadAdditionalExtensions(DB: DB) {
142193
if (this.options.sqliteOptions?.extensions && this.options.sqliteOptions.extensions.length > 0) {
143194
for (const extension of this.options.sqliteOptions.extensions) {

packages/powersync-op-sqlite/src/db/SqliteOptions.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ export interface SqliteOptions {
3030
*/
3131
encryptionKey?: string | null;
3232

33+
/**
34+
* SQLCipher version compatibility option. Only applicable when encryptionKey is set.
35+
*
36+
* - '4': Use SQLCipher 4 format (recommended for new databases)
37+
* - 'upgrade3to4': Automatically attempt to upgrade SQLCipher 3 databases to version 4
38+
* - '3': Use SQLCipher 3 compatibility mode for existing databases (used by default)
39+
*
40+
* Note: The 'upgrade3to4' option will attempt migration only when needed and may
41+
* be expensive on first run. Migration requires the correct encryption key.
42+
*/
43+
cipherVersion?: CipherVersion | null;
44+
3345
/**
3446
* Where to store SQLite temporary files. Defaults to 'MEMORY'.
3547
* Setting this to `FILESYSTEM` can cause issues with larger queries or datasets.
@@ -55,6 +67,12 @@ export interface SqliteOptions {
5567
}>;
5668
}
5769

70+
export enum CipherVersion {
71+
VERSION_4 = '4',
72+
UPGRADE_3_TO_4 = 'upgrade3to4',
73+
VERSION_3 = '3'
74+
}
75+
5876
export enum TemporaryStorageOption {
5977
MEMORY = 'memory',
6078
FILESYSTEM = 'file'
@@ -89,5 +107,6 @@ export const DEFAULT_SQLITE_OPTIONS: Required<SqliteOptions> = {
89107
temporaryStorage: TemporaryStorageOption.MEMORY,
90108
lockTimeoutMs: 30000,
91109
encryptionKey: null,
110+
cipherVersion: CipherVersion.VERSION_3,
92111
extensions: []
93112
};
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
export {
2-
OPSqliteOpenFactory,
3-
OPSQLiteOpenFactoryOptions
4-
} from './db/OPSqliteDBOpenFactory';
1+
export { OPSqliteOpenFactory, OPSQLiteOpenFactoryOptions } from './db/OPSqliteDBOpenFactory';
2+
3+
export * from './db/SqliteOptions';

0 commit comments

Comments
 (0)