Skip to content

Commit 05da697

Browse files
committed
WIP: PR feedback
1 parent f43150e commit 05da697

File tree

4 files changed

+159
-117
lines changed

4 files changed

+159
-117
lines changed

packages/powersync-op-sqlite/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ _[PowerSync](https://www.powersync.com) is a sync engine for building local-firs
88

99
This package (`packages/powersync-op-sqlite`) enables using OP-SQLite with PowerSync. It is an extension of `packages/common`.
1010

11+
## Alpha release
12+
13+
This package is currently in an alpha release. If you find a bug or issue, please open a [GitHub issue](https://github.com/powersync-ja/powersync-js/issues). Questions or feedback can be posted on our [community Discord](https://discord.gg/powersync) - we'd love to hear from you.
14+
1115
# Installation
1216

1317
## Install Package
@@ -18,7 +22,7 @@ npx expo install @powersync/op-sqlite
1822

1923
## Install Peer Dependency: SQLite
2024

21-
This SDK currently requires `@op-engineering/op-sqlite` as a dependency.
25+
This SDK currently requires `@op-engineering/op-sqlite` as a peer dependency.
2226

2327
Install it in your app with:
2428

@@ -28,6 +32,19 @@ npx expo install @op-engineering/op-sqlite
2832

2933
**Note**: This package cannot be installed alongside `@journeyapps/react-native-quick-sqlite`. Please ensure you do **not** install both packages at the same time.
3034

35+
## Usage
36+
37+
```typescript
38+
import { OPSqliteOpenFactory } from '@powersync/op-sqlite';
39+
import { PowerSyncDatabase } from '@powersync/react-native';
40+
41+
const factory = new OPSqliteOpenFactory({
42+
dbFilename: 'sqlite.db'
43+
});
44+
45+
this.powersync = new PowerSyncDatabase({ database: factory, schema: AppSchema });
46+
```
47+
3148
# Native Projects
3249

3350
This package uses native libraries. Create native Android and iOS projects (if not created already) by running:

packages/powersync-op-sqlite/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,18 @@
6161
"registry": "https://registry.npmjs.org/"
6262
},
6363
"peerDependencies": {
64+
"@op-engineering/op-sqlite": "^9.1.3",
6465
"@powersync/common": "workspace:^1.18.0",
6566
"react": "*",
6667
"react-native": "*"
6768
},
6869
"dependencies": {
69-
"@op-engineering/op-sqlite": "^9.1.3",
7070
"@powersync/common": "workspace:*",
7171
"react": "18.3.1",
7272
"react-native": "0.75.3"
7373
},
7474
"devDependencies": {
75+
"@op-engineering/op-sqlite": "^9.1.3",
7576
"@react-native/eslint-config": "^0.73.1",
7677
"@types/async-lock": "^1.4.0",
7778
"@types/react": "^18.2.44",

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

Lines changed: 136 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,165 @@
1-
import { BaseObserver, DBAdapter, DBAdapterListener, DBLockOptions, QueryResult, Transaction } from '@powersync/common';
1+
import {
2+
BaseObserver,
3+
DBAdapter,
4+
DBAdapterListener,
5+
DBLockOptions,
6+
QueryResult,
7+
SQLOpenOptions,
8+
Transaction
9+
} from '@powersync/common';
10+
import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engineering/op-sqlite';
211
import Lock from 'async-lock';
312
import { OPSQLiteConnection } from './OPSQLiteConnection';
13+
import { NativeModules, Platform } from 'react-native';
14+
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
415

516
/**
617
* Adapter for React Native Quick SQLite
718
*/
819
export type OPSQLiteAdapterOptions = {
9-
writeConnection: OPSQLiteConnection;
10-
readConnections: OPSQLiteConnection[];
1120
name: string;
21+
dbLocation?: string;
22+
sqliteOptions?: SqliteOptions;
1223
};
1324

1425
enum LockType {
1526
READ = 'read',
1627
WRITE = 'write'
1728
}
1829

30+
const READ_CONNECTIONS = 5;
31+
1932
export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implements DBAdapter {
2033
name: string;
2134
protected locks: Lock;
35+
36+
protected initialized: Promise<void>;
37+
38+
protected readConnections: OPSQLiteConnection[] | null;
39+
40+
protected writeConnection: OPSQLiteConnection | null;
41+
2242
constructor(protected options: OPSQLiteAdapterOptions) {
2343
super();
2444
this.name = this.options.name;
45+
46+
this.locks = new Lock();
47+
this.readConnections = null;
48+
this.writeConnection = null;
49+
this.initialized = this.init();
50+
}
51+
52+
protected async init() {
53+
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.options.sqliteOptions;
54+
// const { dbFilename, dbLocation } = this.options;
55+
const dbFilename = this.options.name;
56+
//This is needed because an undefined dbLocation will cause the open function to fail
57+
const location = this.getDbLocation(this.options.dbLocation);
58+
const DB: DB = open({
59+
name: dbFilename,
60+
location: location
61+
});
62+
63+
const statements: string[] = [
64+
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
65+
`PRAGMA journal_mode = ${journalMode}`,
66+
`PRAGMA journal_size_limit = ${journalSizeLimit}`,
67+
`PRAGMA synchronous = ${synchronous}`
68+
];
69+
70+
for (const statement of statements) {
71+
for (let tries = 0; tries < 30; tries++) {
72+
try {
73+
await DB.execute(statement);
74+
break;
75+
} catch (e) {
76+
//TODO better error handling for SQLITE_BUSY(5)
77+
console.error('Error executing pragma statement', statement, e);
78+
// if (e.errorCode === 5 && tries < 29) {
79+
// continue;
80+
// } else {
81+
// throw e;
82+
// }
83+
}
84+
}
85+
}
86+
87+
this.loadExtension(DB);
88+
89+
await DB.execute('SELECT powersync_init()');
90+
91+
this.readConnections = [];
92+
for (let i = 0; i < READ_CONNECTIONS; i++) {
93+
// Workaround to create read-only connections
94+
let dbName = './'.repeat(i + 1) + dbFilename;
95+
const conn = await this.openConnection(location, dbName);
96+
await conn.execute('PRAGMA query_only = true');
97+
this.readConnections.push(conn);
98+
}
99+
100+
this.writeConnection = new OPSQLiteConnection({
101+
baseDB: DB
102+
});
103+
25104
// Changes should only occur in the write connection
26-
options.writeConnection.registerListener({
105+
this.writeConnection!.registerListener({
27106
tablesUpdated: (notification) => this.iterateListeners((cb) => cb.tablesUpdated?.(notification))
28107
});
29-
this.locks = new Lock();
108+
}
109+
110+
protected async openConnection(dbLocation: string, filenameOverride?: string): Promise<OPSQLiteConnection> {
111+
const DB: DB = open({
112+
name: filenameOverride ?? this.options.name,
113+
location: dbLocation
114+
});
115+
116+
//Load extension for all connections
117+
this.loadExtension(DB);
118+
119+
await DB.execute('SELECT powersync_init()');
120+
121+
return new OPSQLiteConnection({
122+
baseDB: DB
123+
});
124+
}
125+
126+
private getDbLocation(dbLocation?: string): string {
127+
if (Platform.OS === 'ios') {
128+
return dbLocation ?? IOS_LIBRARY_PATH;
129+
} else {
130+
return dbLocation ?? ANDROID_DATABASE_PATH;
131+
}
132+
}
133+
134+
private loadExtension(DB: DB) {
135+
if (Platform.OS === 'ios') {
136+
const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();
137+
const libPath = `${bundlePath}/Frameworks/powersync-sqlite-core.framework/powersync-sqlite-core`;
138+
DB.loadExtension(libPath, 'sqlite3_powersync_init');
139+
} else {
140+
DB.loadExtension('libpowersync', 'sqlite3_powersync_init');
141+
}
30142
}
31143

32144
close() {
33-
this.options.writeConnection.close();
34-
this.options.readConnections.forEach((c) => c.close());
145+
this.initialized.then(() => {
146+
this.writeConnection!.close();
147+
this.readConnections!.forEach((c) => c.close());
148+
});
35149
}
36150

37151
async readLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
38-
// TODO better
39-
const sortedConnections = this.options.readConnections
40-
.map((connection, index) => ({
41-
lockKey: `${LockType.READ}-${index}`,
42-
connection
43-
}))
44-
.sort((a, b) => {
45-
const aBusy = this.locks.isBusy(a.lockKey);
46-
const bBusy = this.locks.isBusy(b.lockKey);
47-
// Sort by ones which are not busy
48-
return aBusy > bBusy ? 1 : 0;
49-
});
152+
await this.initialized;
153+
// TODO: Use async queues to handle multiple read connections
154+
const sortedConnections = this.readConnections!.map((connection, index) => ({
155+
lockKey: `${LockType.READ}-${index}`,
156+
connection
157+
})).sort((a, b) => {
158+
const aBusy = this.locks.isBusy(a.lockKey);
159+
const bBusy = this.locks.isBusy(b.lockKey);
160+
// Sort by ones which are not busy
161+
return aBusy > bBusy ? 1 : 0;
162+
});
50163

51164
return new Promise(async (resolve, reject) => {
52165
try {
@@ -63,13 +176,15 @@ export class OPSQLiteDBAdapter extends BaseObserver<DBAdapterListener> implement
63176
});
64177
}
65178

66-
writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
179+
async writeLock<T>(fn: (tx: OPSQLiteConnection) => Promise<T>, options?: DBLockOptions): Promise<T> {
180+
await this.initialized;
181+
67182
return new Promise(async (resolve, reject) => {
68183
try {
69184
await this.locks.acquire(
70185
LockType.WRITE,
71186
async () => {
72-
resolve(await fn(this.options.writeConnection));
187+
resolve(await fn(this.writeConnection!));
73188
},
74189
{ timeout: options?.timeoutMs }
75190
);
Lines changed: 3 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,10 @@
1-
import { ANDROID_DATABASE_PATH, IOS_LIBRARY_PATH, open, type DB } from '@op-engineering/op-sqlite';
21
import { DBAdapter, SQLOpenFactory, SQLOpenOptions } from '@powersync/common';
3-
import { NativeModules, Platform } from 'react-native';
42
import { OPSQLiteDBAdapter } from './OPSqliteAdapter';
5-
import { OPSQLiteConnection } from './OPSQLiteConnection';
63
import { DEFAULT_SQLITE_OPTIONS, SqliteOptions } from './SqliteOptions';
74

85
export interface OPSQLiteOpenFactoryOptions extends SQLOpenOptions {
96
sqliteOptions?: SqliteOptions;
107
}
11-
12-
const READ_CONNECTIONS = 5;
13-
148
export class OPSqliteOpenFactory implements SQLOpenFactory {
159
private sqliteOptions: Required<SqliteOptions>;
1610

@@ -22,95 +16,10 @@ export class OPSqliteOpenFactory implements SQLOpenFactory {
2216
}
2317

2418
openDB(): DBAdapter {
25-
const { lockTimeoutMs, journalMode, journalSizeLimit, synchronous } = this.sqliteOptions;
26-
const { dbFilename, dbLocation } = this.options;
27-
//This is needed because an undefined dbLocation will cause the open function to fail
28-
const location = this.getDbLocation(dbLocation);
29-
const DB: DB = open({
30-
name: dbFilename,
31-
location: location
32-
});
33-
34-
const statements: string[] = [
35-
`PRAGMA busy_timeout = ${lockTimeoutMs}`,
36-
`PRAGMA journal_mode = ${journalMode}`,
37-
`PRAGMA journal_size_limit = ${journalSizeLimit}`,
38-
`PRAGMA synchronous = ${synchronous}`
39-
];
40-
41-
for (const statement of statements) {
42-
for (let tries = 0; tries < 30; tries++) {
43-
try {
44-
DB.execute(statement);
45-
break;
46-
} catch (e) {
47-
//TODO better error handling for SQLITE_BUSY(5)
48-
console.error('Error executing pragma statement', statement, e);
49-
// if (e.errorCode === 5 && tries < 29) {
50-
// continue;
51-
// } else {
52-
// throw e;
53-
// }
54-
}
55-
}
56-
}
57-
58-
this.loadExtension(DB);
59-
60-
DB.execute('SELECT powersync_init()');
61-
62-
const readConnections: OPSQLiteConnection[] = [];
63-
for (let i = 0; i < READ_CONNECTIONS; i++) {
64-
// Workaround to create read-only connections
65-
let dbName = './'.repeat(i + 1) + dbFilename;
66-
const conn = this.openConnection(location, dbName);
67-
conn.execute('PRAGMA query_only = true');
68-
readConnections.push(conn);
69-
}
70-
71-
const writeConnection = new OPSQLiteConnection({
72-
baseDB: DB
73-
});
74-
7519
return new OPSQLiteDBAdapter({
76-
name: dbFilename,
77-
readConnections: readConnections,
78-
writeConnection: writeConnection
20+
name: this.options.dbFilename,
21+
dbLocation: this.options.dbLocation,
22+
sqliteOptions: this.sqliteOptions
7923
});
8024
}
81-
82-
protected openConnection(dbLocation: string, filenameOverride?: string): OPSQLiteConnection {
83-
const { dbFilename } = this.options;
84-
const DB: DB = open({
85-
name: filenameOverride ?? dbFilename,
86-
location: dbLocation
87-
});
88-
89-
//Load extension for all connections
90-
this.loadExtension(DB);
91-
92-
DB.execute('SELECT powersync_init()');
93-
94-
return new OPSQLiteConnection({
95-
baseDB: DB
96-
});
97-
}
98-
99-
private getDbLocation(dbLocation?: string): string {
100-
if (Platform.OS === 'ios') {
101-
return dbLocation ?? IOS_LIBRARY_PATH;
102-
} else {
103-
return dbLocation ?? ANDROID_DATABASE_PATH;
104-
}
105-
}
106-
107-
private loadExtension(DB: DB) {
108-
if (Platform.OS === 'ios') {
109-
const bundlePath: string = NativeModules.PowerSyncOpSqlite.getBundlePath();
110-
const libPath = `${bundlePath}/Frameworks/powersync-sqlite-core.framework/powersync-sqlite-core`;
111-
DB.loadExtension(libPath, 'sqlite3_powersync_init');
112-
} else {
113-
DB.loadExtension('libpowersync', 'sqlite3_powersync_init');
114-
}
115-
}
11625
}

0 commit comments

Comments
 (0)