@symbiote-native/sqlite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +2 -0
- package/build/angular/sqlite.service.d.ts +41 -0
- package/build/angular/sqlite.service.js +127 -0
- package/build/core/index.d.ts +10 -0
- package/build/core/index.js +6 -0
- package/build/core/native-database.d.ts +65 -0
- package/build/core/native-database.js +13 -0
- package/build/core/native-module.d.ts +25 -0
- package/build/core/native-module.js +17 -0
- package/build/core/native-session.d.ts +20 -0
- package/build/core/native-session.js +1 -0
- package/build/core/native-statement.d.ts +45 -0
- package/build/core/native-statement.js +6 -0
- package/build/core/param-utils.d.ts +28 -0
- package/build/core/param-utils.js +126 -0
- package/build/core/path-utils.d.ts +6 -0
- package/build/core/path-utils.js +29 -0
- package/build/core/query-utils.d.ts +17 -0
- package/build/core/query-utils.js +37 -0
- package/build/core/sqlite-database.d.ts +246 -0
- package/build/core/sqlite-database.js +427 -0
- package/build/core/sqlite-session.d.ts +84 -0
- package/build/core/sqlite-session.js +121 -0
- package/build/core/sqlite-statement.d.ts +96 -0
- package/build/core/sqlite-statement.js +321 -0
- package/build/core/sqlite-tagged-query.d.ts +73 -0
- package/build/core/sqlite-tagged-query.js +119 -0
- package/build/core/storage.d.ts +107 -0
- package/build/core/storage.js +382 -0
- package/build/core/types.d.ts +30 -0
- package/build/core/types.js +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +2 -0
- package/build/react/sqlite-context.d.ts +25 -0
- package/build/react/sqlite-context.js +129 -0
- package/build/solid/index.d.ts +2 -0
- package/build/solid/index.js +2 -0
- package/build/solid/sqlite-context.d.ts +23 -0
- package/build/solid/sqlite-context.js +106 -0
- package/build/svelte/SQLiteProvider.svelte +88 -0
- package/build/svelte/SQLiteProvider.svelte.d.ts +13 -0
- package/build/svelte/index.d.ts +3 -0
- package/build/svelte/index.js +5 -0
- package/build/svelte/sqlite-context.d.ts +11 -0
- package/build/svelte/sqlite-context.js +23 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +2 -0
- package/build/vue/sqlite-context.d.ts +21 -0
- package/build/vue/sqlite-context.js +69 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +3 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/sqlite.service.d.ts +44 -0
- package/build-ngc/angular/sqlite.service.js +85 -0
- package/build-ngc/angular/sqlite.service.js.map +1 -0
- package/build-ngc/core/index.d.ts +10 -0
- package/build-ngc/core/index.js +7 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-database.d.ts +65 -0
- package/build-ngc/core/native-database.js +14 -0
- package/build-ngc/core/native-database.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +25 -0
- package/build-ngc/core/native-module.js +18 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/native-session.d.ts +20 -0
- package/build-ngc/core/native-session.js +2 -0
- package/build-ngc/core/native-session.js.map +1 -0
- package/build-ngc/core/native-statement.d.ts +45 -0
- package/build-ngc/core/native-statement.js +7 -0
- package/build-ngc/core/native-statement.js.map +1 -0
- package/build-ngc/core/param-utils.d.ts +28 -0
- package/build-ngc/core/param-utils.js +127 -0
- package/build-ngc/core/param-utils.js.map +1 -0
- package/build-ngc/core/path-utils.d.ts +6 -0
- package/build-ngc/core/path-utils.js +30 -0
- package/build-ngc/core/path-utils.js.map +1 -0
- package/build-ngc/core/query-utils.d.ts +17 -0
- package/build-ngc/core/query-utils.js +38 -0
- package/build-ngc/core/query-utils.js.map +1 -0
- package/build-ngc/core/sqlite-database.d.ts +246 -0
- package/build-ngc/core/sqlite-database.js +428 -0
- package/build-ngc/core/sqlite-database.js.map +1 -0
- package/build-ngc/core/sqlite-session.d.ts +84 -0
- package/build-ngc/core/sqlite-session.js +122 -0
- package/build-ngc/core/sqlite-session.js.map +1 -0
- package/build-ngc/core/sqlite-statement.d.ts +96 -0
- package/build-ngc/core/sqlite-statement.js +322 -0
- package/build-ngc/core/sqlite-statement.js.map +1 -0
- package/build-ngc/core/sqlite-tagged-query.d.ts +73 -0
- package/build-ngc/core/sqlite-tagged-query.js +120 -0
- package/build-ngc/core/sqlite-tagged-query.js.map +1 -0
- package/build-ngc/core/storage.d.ts +107 -0
- package/build-ngc/core/storage.js +383 -0
- package/build-ngc/core/storage.js.map +1 -0
- package/build-ngc/core/types.d.ts +30 -0
- package/build-ngc/core/types.js +2 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +141 -0
- package/src/angular/index.ts +2 -0
- package/src/angular/sqlite.service.ts +114 -0
- package/src/core/index.ts +40 -0
- package/src/core/native-database.ts +118 -0
- package/src/core/native-module.ts +57 -0
- package/src/core/native-session.ts +64 -0
- package/src/core/native-statement.ts +85 -0
- package/src/core/param-utils.ts +163 -0
- package/src/core/path-utils.ts +38 -0
- package/src/core/query-utils.ts +45 -0
- package/src/core/sqlite-database.ts +676 -0
- package/src/core/sqlite-session.ts +165 -0
- package/src/core/sqlite-statement.ts +578 -0
- package/src/core/sqlite-tagged-query.ts +160 -0
- package/src/core/storage.ts +492 -0
- package/src/core/types.ts +33 -0
- package/src/react/index.ts +2 -0
- package/src/react/sqlite-context.tsx +244 -0
- package/src/solid/index.ts +2 -0
- package/src/solid/sqlite-context.ts +156 -0
- package/src/svelte/SQLiteProvider.svelte +88 -0
- package/src/svelte/index.ts +6 -0
- package/src/svelte/sqlite-context.ts +32 -0
- package/src/svelte/svelte-compile.test-helper.ts +135 -0
- package/src/vue/index.ts +2 -0
- package/src/vue/sqlite-context.ts +105 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Ported from expo-sqlite's SQLiteTaggedQuery.ts (.vendors/expo @ origin/sdk-57,
|
|
2
|
+
// packages/expo-sqlite/src/SQLiteTaggedQuery.ts), renamed with this repo's `I`-prefix convention.
|
|
3
|
+
import type { ISQLiteBindValue, ISQLiteRunResult } from './native-statement';
|
|
4
|
+
import type { SQLiteDatabase } from './sqlite-database';
|
|
5
|
+
import { parseSQLQuery } from './query-utils';
|
|
6
|
+
import type { ISQLParsedInfo } from './query-utils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns `T[]` when a type parameter is explicitly given, or a union of the possible shapes
|
|
10
|
+
* when relying on the default `unknown` type.
|
|
11
|
+
*/
|
|
12
|
+
type ISQLiteTaggedQueryResult<T> = [unknown] extends [T]
|
|
13
|
+
? unknown[] | ISQLiteRunResult
|
|
14
|
+
: T[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A SQL query built from a tagged template literal, awaitable directly (returns an array of
|
|
18
|
+
* objects by default) or reshaped via `.values()` / `.first()` / `.each()`. Bun's `sql` API is
|
|
19
|
+
* the inspiration (credited upstream).
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const users = await sql`SELECT * FROM users WHERE age > ${21}`;
|
|
24
|
+
* const values = await sql`SELECT name, age FROM users`.values(); // [["Alice", 30], ...]
|
|
25
|
+
* const user = await sql`SELECT * FROM users WHERE id = ${1}`.first();
|
|
26
|
+
* const users = await sql<User>`SELECT * FROM users`; // typed
|
|
27
|
+
* const result = (await sql`INSERT INTO users (name) VALUES (${'Alice'})`) as ISQLiteRunResult;
|
|
28
|
+
* const users = sql<User>`SELECT * FROM users WHERE age > ${21}`.allSync();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export class SQLiteTaggedQuery<T = unknown> implements PromiseLike<
|
|
32
|
+
ISQLiteTaggedQueryResult<T>
|
|
33
|
+
> {
|
|
34
|
+
private readonly source: string;
|
|
35
|
+
private readonly params: ISQLiteBindValue[];
|
|
36
|
+
private readonly parsedInfo: ISQLParsedInfo;
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private readonly database: SQLiteDatabase,
|
|
40
|
+
strings: TemplateStringsArray,
|
|
41
|
+
values: unknown[],
|
|
42
|
+
) {
|
|
43
|
+
const sql = strings.join('?');
|
|
44
|
+
this.source = sql;
|
|
45
|
+
// I/O edge: a tagged-template interpolation site (`${...}`) is only constrained to
|
|
46
|
+
// ISQLiteBindValue by the public overload of `SQLiteDatabase.sql`; nothing narrower can be
|
|
47
|
+
// inferred from `TemplateStringsArray`'s own interpolated-value type (`unknown[]` by design).
|
|
48
|
+
this.params = values as ISQLiteBindValue[];
|
|
49
|
+
this.parsedInfo = parseSQLQuery(sql);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Makes the query awaitable, returning rows or metadata depending on the query's shape — called
|
|
54
|
+
* automatically when the tagged query is `await`ed.
|
|
55
|
+
*/
|
|
56
|
+
then<TResult1 = ISQLiteTaggedQueryResult<T>, TResult2 = never>(
|
|
57
|
+
onfulfilled?:
|
|
58
|
+
| ((
|
|
59
|
+
value: ISQLiteTaggedQueryResult<T>,
|
|
60
|
+
) => TResult1 | PromiseLike<TResult1>)
|
|
61
|
+
| null,
|
|
62
|
+
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
|
63
|
+
): PromiseLike<TResult1 | TResult2> {
|
|
64
|
+
if (this.parsedInfo.canReturnRows) {
|
|
65
|
+
return this.database
|
|
66
|
+
.getAllAsync<T>(this.source, this.params)
|
|
67
|
+
.then(rows => rows as ISQLiteTaggedQueryResult<T>)
|
|
68
|
+
.then(onfulfilled, onrejected);
|
|
69
|
+
}
|
|
70
|
+
return this.database
|
|
71
|
+
.runAsync(this.source, this.params)
|
|
72
|
+
.then(result => result as ISQLiteTaggedQueryResult<T>)
|
|
73
|
+
.then(onfulfilled, onrejected);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Executes the query and returns rows as arrays of values (Bun-style) instead of objects.
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const rows = await sql`SELECT name, age FROM users`.values(); // [["Alice", 30], ...]
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
async values(): Promise<unknown[][]> {
|
|
84
|
+
const statement = await this.database.prepareAsync(this.source);
|
|
85
|
+
try {
|
|
86
|
+
const result = await statement.executeForRawResultAsync(this.params);
|
|
87
|
+
return await result.getAllAsync();
|
|
88
|
+
} finally {
|
|
89
|
+
await statement.finalizeAsync();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Executes the query and returns the first row only, or `null` if no rows match. */
|
|
94
|
+
async first(): Promise<T | null> {
|
|
95
|
+
return this.database.getFirstAsync<T>(this.source, this.params);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Executes the query and returns an async iterator over the rows.
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* for await (const user of sql`SELECT * FROM users`.each()) { console.log(user.name); }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
each(): AsyncIterableIterator<T> {
|
|
106
|
+
return this.database.getEachAsync<T>(this.source, this.params);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#region Synchronous variants
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Executes the query synchronously, returning rows or metadata depending on the query's shape.
|
|
113
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
114
|
+
*/
|
|
115
|
+
allSync(): ISQLiteTaggedQueryResult<T> {
|
|
116
|
+
// I/O edge: see `ISQLiteTaggedQueryResult`'s own definition — the conditional type cannot be
|
|
117
|
+
// narrowed from `canReturnRows`, a plain runtime boolean.
|
|
118
|
+
return this.parsedInfo.canReturnRows
|
|
119
|
+
? (this.database.getAllSync<T>(
|
|
120
|
+
this.source,
|
|
121
|
+
this.params,
|
|
122
|
+
) as ISQLiteTaggedQueryResult<T>)
|
|
123
|
+
: (this.database.runSync(
|
|
124
|
+
this.source,
|
|
125
|
+
this.params,
|
|
126
|
+
) as ISQLiteTaggedQueryResult<T>);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Executes the query synchronously and returns rows as arrays of values.
|
|
131
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
132
|
+
*/
|
|
133
|
+
valuesSync(): unknown[][] {
|
|
134
|
+
const statement = this.database.prepareSync(this.source);
|
|
135
|
+
try {
|
|
136
|
+
const result = statement.executeForRawResultSync(this.params);
|
|
137
|
+
return result.getAllSync();
|
|
138
|
+
} finally {
|
|
139
|
+
statement.finalizeSync();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Executes the query synchronously and returns the first row.
|
|
145
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
146
|
+
*/
|
|
147
|
+
firstSync(): T | null {
|
|
148
|
+
return this.database.getFirstSync<T>(this.source, this.params);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Executes the query synchronously and returns an iterator.
|
|
153
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
154
|
+
*/
|
|
155
|
+
eachSync(): IterableIterator<T> {
|
|
156
|
+
return this.database.getEachSync<T>(this.source, this.params);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Ported from expo-sqlite's Storage.ts (.vendors/expo @ origin/sdk-57,
|
|
2
|
+
// packages/expo-sqlite/src/Storage.ts), renamed with this repo's `I`-prefix convention.
|
|
3
|
+
//
|
|
4
|
+
// Published on its own subpath (`@symbiote-native/sqlite/kv-store`) rather than the package
|
|
5
|
+
// root — see the README for why.
|
|
6
|
+
import AwaitLock from 'await-lock';
|
|
7
|
+
|
|
8
|
+
import { openDatabaseAsync, openDatabaseSync } from './sqlite-database';
|
|
9
|
+
import type { SQLiteDatabase } from './sqlite-database';
|
|
10
|
+
import { normalizeStorageIndex } from './param-utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Update function for `setItemAsync()`/`setItemSync()`. Computes the new value from the
|
|
14
|
+
* previous one (`null` when the key was unset) and returns the value to store.
|
|
15
|
+
*/
|
|
16
|
+
export type ISQLiteStorageSetItemUpdateFunction = (
|
|
17
|
+
prevValue: string | null,
|
|
18
|
+
) => string;
|
|
19
|
+
|
|
20
|
+
const DATABASE_VERSION = 1;
|
|
21
|
+
const STATEMENT_GET = 'SELECT value FROM storage WHERE key = ?;';
|
|
22
|
+
const STATEMENT_SET =
|
|
23
|
+
'INSERT INTO storage (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;';
|
|
24
|
+
const STATEMENT_REMOVE = 'DELETE FROM storage WHERE key = ?;';
|
|
25
|
+
const STATEMENT_GET_ALL_KEYS = 'SELECT key FROM storage;';
|
|
26
|
+
const STATEMENT_CLEAR = 'DELETE FROM storage;';
|
|
27
|
+
const STATEMENT_LENGTH = 'SELECT COUNT(*) as count FROM storage;';
|
|
28
|
+
const STATEMENT_GET_KEY_BY_INDEX = 'SELECT key FROM storage LIMIT 1 OFFSET ?;';
|
|
29
|
+
|
|
30
|
+
const MIGRATION_STATEMENT_0 =
|
|
31
|
+
'CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY NOT NULL, value TEXT);';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A key-value store backed by SQLite. The constructor's `databaseName` is the database file
|
|
35
|
+
* name used for storage.
|
|
36
|
+
*/
|
|
37
|
+
export class SQLiteStorage {
|
|
38
|
+
private db: SQLiteDatabase | null = null;
|
|
39
|
+
private readonly awaitLock = new AwaitLock();
|
|
40
|
+
|
|
41
|
+
constructor(private readonly databaseName: string) {}
|
|
42
|
+
|
|
43
|
+
//#region Asynchronous API
|
|
44
|
+
|
|
45
|
+
/** Retrieves the value for the given key. */
|
|
46
|
+
async getItemAsync(key: string): Promise<string | null> {
|
|
47
|
+
this.checkValidInput(key);
|
|
48
|
+
const db = await this.getDbAsync();
|
|
49
|
+
const result = await db.getFirstAsync<{ value: string }>(
|
|
50
|
+
STATEMENT_GET,
|
|
51
|
+
key,
|
|
52
|
+
);
|
|
53
|
+
return result?.value ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
58
|
+
*/
|
|
59
|
+
async setItemAsync(
|
|
60
|
+
key: string,
|
|
61
|
+
value: string | ISQLiteStorageSetItemUpdateFunction,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
this.checkValidInput(key, value);
|
|
64
|
+
const db = await this.getDbAsync();
|
|
65
|
+
|
|
66
|
+
if (typeof value === 'function') {
|
|
67
|
+
await db.withExclusiveTransactionAsync(async tx => {
|
|
68
|
+
const prevResult = await tx.getFirstAsync<{ value: string }>(
|
|
69
|
+
STATEMENT_GET,
|
|
70
|
+
key,
|
|
71
|
+
);
|
|
72
|
+
const prevValue = prevResult?.value ?? null;
|
|
73
|
+
const nextValue = value(prevValue);
|
|
74
|
+
this.checkValidInput(key, nextValue);
|
|
75
|
+
await tx.runAsync(STATEMENT_SET, key, nextValue);
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await db.runAsync(STATEMENT_SET, key, value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Removes the value for the given key. Returns whether a row was actually removed. */
|
|
84
|
+
async removeItemAsync(key: string): Promise<boolean> {
|
|
85
|
+
this.checkValidInput(key);
|
|
86
|
+
const db = await this.getDbAsync();
|
|
87
|
+
const result = await db.runAsync(STATEMENT_REMOVE, key);
|
|
88
|
+
return result.changes > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Retrieves every key stored. */
|
|
92
|
+
async getAllKeysAsync(): Promise<string[]> {
|
|
93
|
+
const db = await this.getDbAsync();
|
|
94
|
+
const result = await db.getAllAsync<{ key: string }>(
|
|
95
|
+
STATEMENT_GET_ALL_KEYS,
|
|
96
|
+
);
|
|
97
|
+
return result.map(({ key }) => key);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Clears every key-value pair. Returns whether anything was actually cleared. */
|
|
101
|
+
async clearAsync(): Promise<boolean> {
|
|
102
|
+
const db = await this.getDbAsync();
|
|
103
|
+
const result = await db.runAsync(STATEMENT_CLEAR);
|
|
104
|
+
return result.changes > 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Closes the database connection. */
|
|
108
|
+
async closeAsync(): Promise<void> {
|
|
109
|
+
await this.awaitLock.acquireAsync();
|
|
110
|
+
try {
|
|
111
|
+
if (this.db) {
|
|
112
|
+
await this.db.closeAsync();
|
|
113
|
+
this.db = null;
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
this.awaitLock.release();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Retrieves the number of key-value pairs stored. */
|
|
121
|
+
async getLengthAsync(): Promise<number> {
|
|
122
|
+
const db = await this.getDbAsync();
|
|
123
|
+
const result = await db.getFirstAsync<{ count: number }>(STATEMENT_LENGTH);
|
|
124
|
+
return result?.count ?? 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Retrieves the key at the given index. */
|
|
128
|
+
async getKeyByIndexAsync(index: number): Promise<string | null> {
|
|
129
|
+
const db = await this.getDbAsync();
|
|
130
|
+
const offset = normalizeStorageIndex(index);
|
|
131
|
+
if (offset == null) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const result = await db.getFirstAsync<{ key: string }>(
|
|
135
|
+
STATEMENT_GET_KEY_BY_INDEX,
|
|
136
|
+
offset,
|
|
137
|
+
);
|
|
138
|
+
return result?.key ?? null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
|
|
143
|
+
//#region Synchronous API
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Retrieves the value for the given key.
|
|
147
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
148
|
+
*/
|
|
149
|
+
getItemSync(key: string): string | null {
|
|
150
|
+
this.checkValidInput(key);
|
|
151
|
+
const db = this.getDbSync();
|
|
152
|
+
const result = db.getFirstSync<{ value: string }>(STATEMENT_GET, key);
|
|
153
|
+
return result?.value ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Sets the value for the given key. A function computes the new value from the previous one.
|
|
158
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
159
|
+
*/
|
|
160
|
+
setItemSync(
|
|
161
|
+
key: string,
|
|
162
|
+
value: string | ISQLiteStorageSetItemUpdateFunction,
|
|
163
|
+
): void {
|
|
164
|
+
this.checkValidInput(key, value);
|
|
165
|
+
const db = this.getDbSync();
|
|
166
|
+
|
|
167
|
+
if (typeof value === 'function') {
|
|
168
|
+
db.withTransactionSync(() => {
|
|
169
|
+
const prevResult = db.getFirstSync<{ value: string }>(
|
|
170
|
+
STATEMENT_GET,
|
|
171
|
+
key,
|
|
172
|
+
);
|
|
173
|
+
const prevValue = prevResult?.value ?? null;
|
|
174
|
+
const nextValue = value(prevValue);
|
|
175
|
+
this.checkValidInput(key, nextValue);
|
|
176
|
+
db.runSync(STATEMENT_SET, key, nextValue);
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
db.runSync(STATEMENT_SET, key, value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Removes the value for the given key. Returns whether a row was actually removed.
|
|
186
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
187
|
+
*/
|
|
188
|
+
removeItemSync(key: string): boolean {
|
|
189
|
+
this.checkValidInput(key);
|
|
190
|
+
const db = this.getDbSync();
|
|
191
|
+
const result = db.runSync(STATEMENT_REMOVE, key);
|
|
192
|
+
return result.changes > 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Retrieves every key stored.
|
|
197
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
198
|
+
*/
|
|
199
|
+
getAllKeysSync(): string[] {
|
|
200
|
+
const db = this.getDbSync();
|
|
201
|
+
const result = db.getAllSync<{ key: string }>(STATEMENT_GET_ALL_KEYS);
|
|
202
|
+
return result.map(({ key }) => key);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clears every key-value pair. Returns whether anything was actually cleared.
|
|
207
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
208
|
+
*/
|
|
209
|
+
clearSync(): boolean {
|
|
210
|
+
const db = this.getDbSync();
|
|
211
|
+
const result = db.runSync(STATEMENT_CLEAR);
|
|
212
|
+
return result.changes > 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Closes the database connection.
|
|
217
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
218
|
+
*/
|
|
219
|
+
closeSync(): void {
|
|
220
|
+
if (this.db) {
|
|
221
|
+
this.db.closeSync();
|
|
222
|
+
this.db = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Retrieves the number of key-value pairs stored.
|
|
228
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
229
|
+
*/
|
|
230
|
+
getLengthSync(): number {
|
|
231
|
+
const db = this.getDbSync();
|
|
232
|
+
const result = db.getFirstSync<{ count: number }>(STATEMENT_LENGTH);
|
|
233
|
+
return result?.count ?? 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Retrieves the key at the given index.
|
|
238
|
+
* > **Note:** Running heavy tasks with this function can block the JavaScript thread.
|
|
239
|
+
*/
|
|
240
|
+
getKeyByIndexSync(index: number): string | null {
|
|
241
|
+
const db = this.getDbSync();
|
|
242
|
+
const offset = normalizeStorageIndex(index);
|
|
243
|
+
if (offset == null) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const result = db.getFirstSync<{ key: string }>(
|
|
247
|
+
STATEMENT_GET_KEY_BY_INDEX,
|
|
248
|
+
offset,
|
|
249
|
+
);
|
|
250
|
+
return result?.key ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//#endregion
|
|
254
|
+
|
|
255
|
+
//#region react-native-async-storage compatible API
|
|
256
|
+
|
|
257
|
+
/** Alias for `getItemAsync()`. */
|
|
258
|
+
async getItem(key: string): Promise<string | null> {
|
|
259
|
+
return this.getItemAsync(key);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Alias for `setItemAsync()`. */
|
|
263
|
+
async setItem(
|
|
264
|
+
key: string,
|
|
265
|
+
value: string | ISQLiteStorageSetItemUpdateFunction,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
await this.setItemAsync(key, value);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Alias for `removeItemAsync()`. */
|
|
271
|
+
async removeItem(key: string): Promise<void> {
|
|
272
|
+
await this.removeItemAsync(key);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Alias for `getAllKeysAsync()`. */
|
|
276
|
+
async getAllKeys(): Promise<string[]> {
|
|
277
|
+
return this.getAllKeysAsync();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Alias for `clearAsync()`. */
|
|
281
|
+
async clear(): Promise<void> {
|
|
282
|
+
await this.clearAsync();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Merges the given value with the existing value for the key — a deep merge for JSON. */
|
|
286
|
+
async mergeItem(key: string, value: string): Promise<void> {
|
|
287
|
+
this.checkValidInput(key, value);
|
|
288
|
+
await this.setItemAsync(key, prevValue => {
|
|
289
|
+
if (prevValue == null) {
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
const prevJSON: unknown = JSON.parse(prevValue);
|
|
293
|
+
const newJSON: unknown = JSON.parse(value);
|
|
294
|
+
const mergedJSON = mergeDeep(prevJSON, newJSON);
|
|
295
|
+
return JSON.stringify(mergedJSON);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Retrieves the values for the given keys. */
|
|
300
|
+
async multiGet(keys: string[]): Promise<[string, string | null][]> {
|
|
301
|
+
return Promise.all(
|
|
302
|
+
keys.map(async (key): Promise<[string, string | null]> => {
|
|
303
|
+
this.checkValidInput(key);
|
|
304
|
+
return [key, await this.getItemAsync(key)];
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Sets multiple key-value pairs. */
|
|
310
|
+
async multiSet(keyValuePairs: [string, string][]): Promise<void> {
|
|
311
|
+
const db = await this.getDbAsync();
|
|
312
|
+
await db.withExclusiveTransactionAsync(async tx => {
|
|
313
|
+
for (const [key, value] of keyValuePairs) {
|
|
314
|
+
this.checkValidInput(key, value);
|
|
315
|
+
await tx.runAsync(STATEMENT_SET, key, value);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Removes the values for the given keys. */
|
|
321
|
+
async multiRemove(keys: string[]): Promise<void> {
|
|
322
|
+
const db = await this.getDbAsync();
|
|
323
|
+
await db.withExclusiveTransactionAsync(async tx => {
|
|
324
|
+
for (const key of keys) {
|
|
325
|
+
this.checkValidInput(key);
|
|
326
|
+
await tx.runAsync(STATEMENT_REMOVE, key);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Merges multiple key-value pairs — a deep merge for JSON existing values. */
|
|
332
|
+
async multiMerge(keyValuePairs: [string, string][]): Promise<void> {
|
|
333
|
+
const db = await this.getDbAsync();
|
|
334
|
+
await db.withExclusiveTransactionAsync(async tx => {
|
|
335
|
+
for (const [key, value] of keyValuePairs) {
|
|
336
|
+
this.checkValidInput(key, value);
|
|
337
|
+
const prevValue = await tx.getFirstAsync<{ value: string }>(
|
|
338
|
+
STATEMENT_GET,
|
|
339
|
+
key,
|
|
340
|
+
);
|
|
341
|
+
if (prevValue == null) {
|
|
342
|
+
await tx.runAsync(STATEMENT_SET, key, value);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const prevJSON: unknown = JSON.parse(prevValue.value);
|
|
346
|
+
const newJSON: unknown = JSON.parse(value);
|
|
347
|
+
const mergedJSON = mergeDeep(prevJSON, newJSON);
|
|
348
|
+
await tx.runAsync(STATEMENT_SET, key, JSON.stringify(mergedJSON));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Alias for `closeAsync()`. */
|
|
354
|
+
async close(): Promise<void> {
|
|
355
|
+
await this.closeAsync();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//#endregion
|
|
359
|
+
|
|
360
|
+
//#region Internals
|
|
361
|
+
|
|
362
|
+
private async getDbAsync(): Promise<SQLiteDatabase> {
|
|
363
|
+
await this.awaitLock.acquireAsync();
|
|
364
|
+
try {
|
|
365
|
+
if (!this.db) {
|
|
366
|
+
const db = await openDatabaseAsync(this.databaseName);
|
|
367
|
+
await this.maybeMigrateDbAsync(db);
|
|
368
|
+
this.db = db;
|
|
369
|
+
}
|
|
370
|
+
} finally {
|
|
371
|
+
this.awaitLock.release();
|
|
372
|
+
}
|
|
373
|
+
return this.db;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private getDbSync(): SQLiteDatabase {
|
|
377
|
+
// Unlike getDbAsync(), this cannot take `awaitLock` — it is promise-based and this method is
|
|
378
|
+
// synchronous. The migration is idempotent instead, so the next open repairs a database a
|
|
379
|
+
// race left without a `storage` table.
|
|
380
|
+
if (!this.db) {
|
|
381
|
+
const db = openDatabaseSync(this.databaseName);
|
|
382
|
+
this.maybeMigrateDbSync(db);
|
|
383
|
+
this.db = db;
|
|
384
|
+
}
|
|
385
|
+
return this.db;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private maybeMigrateDbAsync(db: SQLiteDatabase): Promise<void> {
|
|
389
|
+
return db.withTransactionAsync(async () => {
|
|
390
|
+
const result = await db.getFirstAsync<{ user_version: number }>(
|
|
391
|
+
'PRAGMA user_version',
|
|
392
|
+
);
|
|
393
|
+
const currentDbVersion = result?.user_version ?? 0;
|
|
394
|
+
|
|
395
|
+
// Baseline schema, deliberately outside the version ladder below — `CREATE TABLE IF NOT
|
|
396
|
+
// EXISTS` is a no-op on a healthy database and also repairs one a raced first-run
|
|
397
|
+
// migration left at `user_version >= 1` with no `storage` table. A new column has to be
|
|
398
|
+
// added here too, so fresh and repaired databases both get it.
|
|
399
|
+
await db.execAsync(MIGRATION_STATEMENT_0);
|
|
400
|
+
|
|
401
|
+
// Version ladder: add each new migration below, gated on `currentDbVersion`. Since the
|
|
402
|
+
// baseline above already carries every column, a ladder entry adding one has to tolerate
|
|
403
|
+
// it already being there — gate it on `PRAGMA table_info(storage)`, or a fresh database
|
|
404
|
+
// fails the migration with `duplicate column name`.
|
|
405
|
+
if (currentDbVersion >= DATABASE_VERSION) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private maybeMigrateDbSync(db: SQLiteDatabase): void {
|
|
413
|
+
db.withTransactionSync(() => {
|
|
414
|
+
const result = db.getFirstSync<{ user_version: number }>(
|
|
415
|
+
'PRAGMA user_version',
|
|
416
|
+
);
|
|
417
|
+
const currentDbVersion = result?.user_version ?? 0;
|
|
418
|
+
|
|
419
|
+
// Keep in sync with maybeMigrateDbAsync(), which documents the two steps below.
|
|
420
|
+
db.execSync(MIGRATION_STATEMENT_0);
|
|
421
|
+
|
|
422
|
+
if (currentDbVersion >= DATABASE_VERSION) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
db.execSync(`PRAGMA user_version = ${DATABASE_VERSION}`);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private checkValidInput(...input: unknown[]): void {
|
|
430
|
+
const [key, value] = input;
|
|
431
|
+
|
|
432
|
+
if (typeof key !== 'string') {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`[SQLiteStorage] Using ${typeof key} type for key is not supported. Use string instead. Key passed: ${String(key)}`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (
|
|
439
|
+
input.length > 1 &&
|
|
440
|
+
typeof value !== 'string' &&
|
|
441
|
+
typeof value !== 'function'
|
|
442
|
+
) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`[SQLiteStorage] Using ${typeof value} type for value is not supported. Use string instead. Key passed: ${key}. Value passed : ${String(value)}`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
//#endregion
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Recursively merges two JSON-decoded values, concatenating arrays and merging objects. */
|
|
453
|
+
function mergeDeep(target: unknown, source: unknown): unknown {
|
|
454
|
+
if (typeof target !== 'object' || target === null || Array.isArray(target)) {
|
|
455
|
+
return source;
|
|
456
|
+
}
|
|
457
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) {
|
|
458
|
+
return target;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// I/O edge: both are JSON.parse() output narrowed only to "non-null, non-array object" above —
|
|
462
|
+
// JSON itself carries no further static shape to check against.
|
|
463
|
+
const targetRecord = target as Record<string, unknown>;
|
|
464
|
+
const sourceRecord = source as Record<string, unknown>;
|
|
465
|
+
const output: Record<string, unknown> = { ...targetRecord };
|
|
466
|
+
|
|
467
|
+
for (const key of Object.keys(sourceRecord)) {
|
|
468
|
+
const sourceValue = sourceRecord[key];
|
|
469
|
+
if (Array.isArray(sourceValue)) {
|
|
470
|
+
const existing = output[key];
|
|
471
|
+
output[key] = (Array.isArray(existing) ? existing : []).concat(
|
|
472
|
+
sourceValue,
|
|
473
|
+
);
|
|
474
|
+
} else if (typeof sourceValue === 'object' && sourceValue !== null) {
|
|
475
|
+
output[key] = mergeDeep(targetRecord[key], sourceValue);
|
|
476
|
+
} else {
|
|
477
|
+
output[key] = sourceValue;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return output;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A drop-in replacement for `AsyncStorage` from `@react-native-async-storage/async-storage`.
|
|
486
|
+
*/
|
|
487
|
+
export const AsyncStorage = new SQLiteStorage('ExpoSQLiteStorage');
|
|
488
|
+
|
|
489
|
+
export default AsyncStorage;
|
|
490
|
+
|
|
491
|
+
/** Alias for `AsyncStorage`, given the storage offers more than asynchronous methods. */
|
|
492
|
+
export const Storage = AsyncStorage;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SQLiteDatabase } from './sqlite-database';
|
|
2
|
+
import type { ISQLiteOpenOptions } from './native-database';
|
|
3
|
+
|
|
4
|
+
/** The event payload for `addDatabaseChangeListener`. */
|
|
5
|
+
export type IDatabaseChangeEvent = {
|
|
6
|
+
/** The database name — `main` by default, or another name set via `ATTACH DATABASE`. */
|
|
7
|
+
databaseName: string;
|
|
8
|
+
/** The absolute file path to the database. */
|
|
9
|
+
databaseFilePath: string;
|
|
10
|
+
/** The table name. */
|
|
11
|
+
tableName: string;
|
|
12
|
+
/** The changed row ID. */
|
|
13
|
+
rowId: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Called once, right after the native handle opens and before `openDatabaseAsync`/
|
|
18
|
+
* `openDatabaseSync` returns the database — e.g. to run `execAsync`/`execSync` migrations.
|
|
19
|
+
*
|
|
20
|
+
* **Deviation from upstream:** expo-sqlite only exposes this as a `SQLiteProviderProps.onInit`
|
|
21
|
+
* field, called by its React `<SQLiteProvider>` after opening, before rendering children. This
|
|
22
|
+
* package has no Provider yet (per-adapter Provider/hook wrappers are a later, separate pass —
|
|
23
|
+
* see the README), so `onInit` is exposed directly on `openDatabaseAsync`/`openDatabaseSync`
|
|
24
|
+
* instead, giving any adapter — or plain module-scope code with no view tree at all — the same
|
|
25
|
+
* "run this once right after open" hook without needing a Provider to exist first. A future
|
|
26
|
+
* Provider can still accept its own `onInit` prop and either forward it here or call the
|
|
27
|
+
* callback itself; that choice is left to that later pass.
|
|
28
|
+
*/
|
|
29
|
+
export type IOnInitCallback = (db: SQLiteDatabase) => Promise<void> | void;
|
|
30
|
+
|
|
31
|
+
export type IOpenDatabaseOptions = ISQLiteOpenOptions & {
|
|
32
|
+
onInit?: IOnInitCallback;
|
|
33
|
+
};
|