@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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/build/angular/index.d.ts +2 -0
  4. package/build/angular/index.js +2 -0
  5. package/build/angular/sqlite.service.d.ts +41 -0
  6. package/build/angular/sqlite.service.js +127 -0
  7. package/build/core/index.d.ts +10 -0
  8. package/build/core/index.js +6 -0
  9. package/build/core/native-database.d.ts +65 -0
  10. package/build/core/native-database.js +13 -0
  11. package/build/core/native-module.d.ts +25 -0
  12. package/build/core/native-module.js +17 -0
  13. package/build/core/native-session.d.ts +20 -0
  14. package/build/core/native-session.js +1 -0
  15. package/build/core/native-statement.d.ts +45 -0
  16. package/build/core/native-statement.js +6 -0
  17. package/build/core/param-utils.d.ts +28 -0
  18. package/build/core/param-utils.js +126 -0
  19. package/build/core/path-utils.d.ts +6 -0
  20. package/build/core/path-utils.js +29 -0
  21. package/build/core/query-utils.d.ts +17 -0
  22. package/build/core/query-utils.js +37 -0
  23. package/build/core/sqlite-database.d.ts +246 -0
  24. package/build/core/sqlite-database.js +427 -0
  25. package/build/core/sqlite-session.d.ts +84 -0
  26. package/build/core/sqlite-session.js +121 -0
  27. package/build/core/sqlite-statement.d.ts +96 -0
  28. package/build/core/sqlite-statement.js +321 -0
  29. package/build/core/sqlite-tagged-query.d.ts +73 -0
  30. package/build/core/sqlite-tagged-query.js +119 -0
  31. package/build/core/storage.d.ts +107 -0
  32. package/build/core/storage.js +382 -0
  33. package/build/core/types.d.ts +30 -0
  34. package/build/core/types.js +1 -0
  35. package/build/react/index.d.ts +2 -0
  36. package/build/react/index.js +2 -0
  37. package/build/react/sqlite-context.d.ts +25 -0
  38. package/build/react/sqlite-context.js +129 -0
  39. package/build/solid/index.d.ts +2 -0
  40. package/build/solid/index.js +2 -0
  41. package/build/solid/sqlite-context.d.ts +23 -0
  42. package/build/solid/sqlite-context.js +106 -0
  43. package/build/svelte/SQLiteProvider.svelte +88 -0
  44. package/build/svelte/SQLiteProvider.svelte.d.ts +13 -0
  45. package/build/svelte/index.d.ts +3 -0
  46. package/build/svelte/index.js +5 -0
  47. package/build/svelte/sqlite-context.d.ts +11 -0
  48. package/build/svelte/sqlite-context.js +23 -0
  49. package/build/vue/index.d.ts +2 -0
  50. package/build/vue/index.js +2 -0
  51. package/build/vue/sqlite-context.d.ts +21 -0
  52. package/build/vue/sqlite-context.js +69 -0
  53. package/build-ngc/angular/index.d.ts +2 -0
  54. package/build-ngc/angular/index.js +3 -0
  55. package/build-ngc/angular/index.js.map +1 -0
  56. package/build-ngc/angular/sqlite.service.d.ts +44 -0
  57. package/build-ngc/angular/sqlite.service.js +85 -0
  58. package/build-ngc/angular/sqlite.service.js.map +1 -0
  59. package/build-ngc/core/index.d.ts +10 -0
  60. package/build-ngc/core/index.js +7 -0
  61. package/build-ngc/core/index.js.map +1 -0
  62. package/build-ngc/core/native-database.d.ts +65 -0
  63. package/build-ngc/core/native-database.js +14 -0
  64. package/build-ngc/core/native-database.js.map +1 -0
  65. package/build-ngc/core/native-module.d.ts +25 -0
  66. package/build-ngc/core/native-module.js +18 -0
  67. package/build-ngc/core/native-module.js.map +1 -0
  68. package/build-ngc/core/native-session.d.ts +20 -0
  69. package/build-ngc/core/native-session.js +2 -0
  70. package/build-ngc/core/native-session.js.map +1 -0
  71. package/build-ngc/core/native-statement.d.ts +45 -0
  72. package/build-ngc/core/native-statement.js +7 -0
  73. package/build-ngc/core/native-statement.js.map +1 -0
  74. package/build-ngc/core/param-utils.d.ts +28 -0
  75. package/build-ngc/core/param-utils.js +127 -0
  76. package/build-ngc/core/param-utils.js.map +1 -0
  77. package/build-ngc/core/path-utils.d.ts +6 -0
  78. package/build-ngc/core/path-utils.js +30 -0
  79. package/build-ngc/core/path-utils.js.map +1 -0
  80. package/build-ngc/core/query-utils.d.ts +17 -0
  81. package/build-ngc/core/query-utils.js +38 -0
  82. package/build-ngc/core/query-utils.js.map +1 -0
  83. package/build-ngc/core/sqlite-database.d.ts +246 -0
  84. package/build-ngc/core/sqlite-database.js +428 -0
  85. package/build-ngc/core/sqlite-database.js.map +1 -0
  86. package/build-ngc/core/sqlite-session.d.ts +84 -0
  87. package/build-ngc/core/sqlite-session.js +122 -0
  88. package/build-ngc/core/sqlite-session.js.map +1 -0
  89. package/build-ngc/core/sqlite-statement.d.ts +96 -0
  90. package/build-ngc/core/sqlite-statement.js +322 -0
  91. package/build-ngc/core/sqlite-statement.js.map +1 -0
  92. package/build-ngc/core/sqlite-tagged-query.d.ts +73 -0
  93. package/build-ngc/core/sqlite-tagged-query.js +120 -0
  94. package/build-ngc/core/sqlite-tagged-query.js.map +1 -0
  95. package/build-ngc/core/storage.d.ts +107 -0
  96. package/build-ngc/core/storage.js +383 -0
  97. package/build-ngc/core/storage.js.map +1 -0
  98. package/build-ngc/core/types.d.ts +30 -0
  99. package/build-ngc/core/types.js +2 -0
  100. package/build-ngc/core/types.js.map +1 -0
  101. package/native-link.json +12 -0
  102. package/package.json +141 -0
  103. package/src/angular/index.ts +2 -0
  104. package/src/angular/sqlite.service.ts +114 -0
  105. package/src/core/index.ts +40 -0
  106. package/src/core/native-database.ts +118 -0
  107. package/src/core/native-module.ts +57 -0
  108. package/src/core/native-session.ts +64 -0
  109. package/src/core/native-statement.ts +85 -0
  110. package/src/core/param-utils.ts +163 -0
  111. package/src/core/path-utils.ts +38 -0
  112. package/src/core/query-utils.ts +45 -0
  113. package/src/core/sqlite-database.ts +676 -0
  114. package/src/core/sqlite-session.ts +165 -0
  115. package/src/core/sqlite-statement.ts +578 -0
  116. package/src/core/sqlite-tagged-query.ts +160 -0
  117. package/src/core/storage.ts +492 -0
  118. package/src/core/types.ts +33 -0
  119. package/src/react/index.ts +2 -0
  120. package/src/react/sqlite-context.tsx +244 -0
  121. package/src/solid/index.ts +2 -0
  122. package/src/solid/sqlite-context.ts +156 -0
  123. package/src/svelte/SQLiteProvider.svelte +88 -0
  124. package/src/svelte/index.ts +6 -0
  125. package/src/svelte/sqlite-context.ts +32 -0
  126. package/src/svelte/svelte-compile.test-helper.ts +135 -0
  127. package/src/vue/index.ts +2 -0
  128. package/src/vue/sqlite-context.ts +105 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # @symbiote-native/sqlite
2
+
3
+ A wrapper package for [SymbioteNative](../../README.md) that makes
4
+ [`expo-sqlite`](https://github.com/expo/expo/tree/main/packages/expo-sqlite) usable from every
5
+ adapter — `Database`, `Statement`, `Session` (transactions and changesets), a Bun-style SQL
6
+ tagged-template helper, and a SQLite-backed key-value store. Built the same way as
7
+ [`@symbiote-native/secure-store`](../secure-store): an `expo-modules-core`-based wrapper (see the
8
+ `symbiote-expo-native-module` project skill for the full mechanism — why `expo-modules-core` is
9
+ depended on directly and never the `expo` meta-package, why the upstream JS is hand-ported into
10
+ `core/` rather than imported, and how autolinking picks up the native module).
11
+
12
+ Every adapter — React, Vue, Svelte, Solid, Angular — ships its own idiomatic `<SQLiteProvider>` /
13
+ `useSQLiteContext` equivalent (see "Provider / context" below), reachable from its own subpath.
14
+ Each subpath re-exports the full `src/core/` barrel too, so importing from `@symbiote-native/sqlite/react`
15
+ (etc.) is a strict superset of importing from the package root.
16
+
17
+ expo-sqlite's native classes — `NativeDatabase`, `NativeStatement`, `NativeSession` — are **plain
18
+ constructors** exposed as properties on the required native module (`new
19
+ ExpoSQLite.NativeDatabase(...)`), unlike `@symbiote-native/audio`'s JSI-backed `SharedObject`
20
+ subclasses. There is nothing to subclass here: this package's `SQLiteDatabase` /
21
+ `SQLiteStatement` / `SQLiteSession` wrapper classes hold a native instance and forward to it,
22
+ exactly like upstream's own.
23
+
24
+ ## Install
25
+
26
+ **New app:**
27
+
28
+ ```bash
29
+ npx @symbiote-native/cli new my-app --sqlite
30
+ ```
31
+
32
+ **Existing SymbioteNative app:**
33
+
34
+ ```bash
35
+ npx @symbiote-native/cli add --sqlite
36
+ ```
37
+
38
+ Either way: installs `@symbiote-native/sqlite` and wires the native autolinking automatically — see
39
+ [`@symbiote-native/cli`](../cli).
40
+
41
+ <details>
42
+ <summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
43
+
44
+ ```bash
45
+ npm install @symbiote-native/sqlite
46
+ ```
47
+
48
+ `expo-sqlite` and `expo-modules-core` come along as regular dependencies, pinned to exact
49
+ versions — never install either yourself, and never add the `expo` meta-package to your project
50
+ (it bundles its own Metro/Babel pipeline, which conflicts with this project's own). `await-lock`
51
+ is a real runtime dependency of the kv-store (see "Shape" below), not a native-only placeholder.
52
+
53
+ ### Required one-time step: native autolinking wiring
54
+
55
+ Same one-time step as every other `expo-modules-core` package this project ships — see
56
+ [`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
57
+ and the `symbiote-expo-native-module` project skill. Nothing package-specific: iOS autolinks the
58
+ `Expo` pod automatically; Android's Gradle project + native-module map entry is generated by
59
+ [`@symbiote-native/expo-modules-link`](../expo-modules-link)'s aggregator from this package's
60
+ `native-link.json` on every install.
61
+
62
+ expo-sqlite compiles its own vendored SQLite amalgamation from source per platform (it does not
63
+ link the OS's system `libsqlite3`) — expect `pod install`/the first Android build touching this
64
+ package to be noticeably heavier than a package that only links a prebuilt framework. That is
65
+ expected, not a broken build.
66
+
67
+ </details>
68
+
69
+ ## Shape
70
+
71
+ ```
72
+ src/core/ native-database.ts / native-statement.ts / native-session.ts — the ambient
73
+ `declare class` surface expo-sqlite's native module exposes, ported type-only.
74
+ native-module.ts — resolves `ExpoSQLite` via expo-modules-core's requireNativeModule.
75
+ sqlite-database.ts / sqlite-statement.ts / sqlite-session.ts — the real wrapper
76
+ classes, holding a native instance and forwarding to it.
77
+ sqlite-tagged-query.ts — the `db.sql`...`` tagged-template helper.
78
+ storage.ts — SQLiteStorage, the SQLite-backed key-value store (its own subpath, see
79
+ below). query-utils.ts / path-utils.ts / param-utils.ts — framework/native-free
80
+ helpers (SQL-shape parsing, database-path joining, bind-param normalization).
81
+ ```
82
+
83
+ `@symbiote-native/sqlite/kv-store` is a **separate subpath**, not re-exported from the package
84
+ root — `SQLiteStorage`/`AsyncStorage`/`Storage` pull in `await-lock` and open their own database
85
+ lazily on first use; an app that only wants `Database`/`Statement`/`sql` should not pay for that
86
+ by importing it transitively.
87
+
88
+ ## Use it
89
+
90
+ ```ts
91
+ import { openDatabaseAsync } from '@symbiote-native/sqlite';
92
+
93
+ const db = await openDatabaseAsync('app.db');
94
+ await db.execAsync('CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, title TEXT)');
95
+ await db.runAsync('INSERT INTO todos (title) VALUES (?)', 'Buy milk');
96
+ const todos = await db.getAllAsync<{ id: number; title: string }>('SELECT * FROM todos');
97
+ ```
98
+
99
+ The SQL tagged-template helper (Bun-style, automatically parameterized):
100
+
101
+ ```ts
102
+ const users = await db.sql<User>`SELECT * FROM users WHERE age > ${21}`;
103
+ const rows = await db.sql`SELECT name, age FROM users`.values(); // [["Alice", 30], ...]
104
+ const user = await db.sql<User>`SELECT * FROM users WHERE id = ${id}`.first();
105
+ for await (const user of db.sql<User>`SELECT * FROM users`.each()) {
106
+ /* ... */
107
+ }
108
+ ```
109
+
110
+ Transactions:
111
+
112
+ ```ts
113
+ await db.withTransactionAsync(async () => {
114
+ await db.runAsync('UPDATE accounts SET balance = balance - ? WHERE id = ?', 100, from);
115
+ await db.runAsync('UPDATE accounts SET balance = balance + ? WHERE id = ?', 100, to);
116
+ }); // rolls back and re-throws if the task throws
117
+ ```
118
+
119
+ The key-value store, from its own subpath:
120
+
121
+ ```ts
122
+ import { AsyncStorage } from '@symbiote-native/sqlite/kv-store';
123
+
124
+ await AsyncStorage.setItem('token', 'secret');
125
+ const token = await AsyncStorage.getItem('token'); // string | null
126
+ ```
127
+
128
+ ### `onInit` — a deliberate deviation from upstream
129
+
130
+ Upstream only exposes `onInit` as a `SQLiteProviderProps` field, called by its React
131
+ `<SQLiteProvider>` after opening, before rendering children. Here `onInit` is exposed directly on
132
+ `openDatabaseAsync`/`openDatabaseSync` instead — called once, right after the native handle opens,
133
+ before the function returns:
134
+
135
+ ```ts
136
+ const db = await openDatabaseAsync('app.db', {
137
+ onInit: async db => {
138
+ await db.execAsync('CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, title TEXT)');
139
+ },
140
+ });
141
+ ```
142
+
143
+ This gives an adapter with no view-tree context — a plain module-scope call, or Angular's
144
+ `SqliteService` — a "run this once right after open" hook without needing a Provider to exist
145
+ first. Every Provider above forwards its own `onInit` prop straight into `openDatabaseAsync`
146
+ rather than calling it a second time. `openDatabaseSync` rejects (throws) if its `onInit` returns
147
+ a `Promise`, rather than silently racing it.
148
+
149
+ ## Provider / context (per adapter)
150
+
151
+ Each adapter's Provider opens the database once (closing and reopening if its config props
152
+ change), renders nothing until it resolves, and hands the open `SQLiteDatabase` to descendants.
153
+ On failure it calls `onError` if given, else the failure propagates through whatever error
154
+ channel is idiomatic to that framework (a rethrow, an unhandled rejection, a resource-read
155
+ throw — see each adapter's own source comment for the exact mechanism). `onInit` is forwarded to
156
+ `openDatabaseAsync` (which already runs it — see below), not called a second time.
157
+
158
+ **React** (`@symbiote-native/sqlite/react`):
159
+
160
+ ```tsx
161
+ import { SQLiteProvider, useSQLiteContext } from '@symbiote-native/sqlite/react';
162
+
163
+ <SQLiteProvider databaseName="app.db" onInit={migrate}>
164
+ <Main />
165
+ </SQLiteProvider>;
166
+
167
+ function Main() {
168
+ const db = useSQLiteContext();
169
+ // ...
170
+ }
171
+ ```
172
+
173
+ `useSuspense` is also supported (React's own primitive — `<Suspense>` shows its fallback instead
174
+ of the Provider rendering `null`).
175
+
176
+ **Vue** (`@symbiote-native/sqlite/vue`):
177
+
178
+ ```ts
179
+ import { SQLiteProvider, useSQLiteContext } from '@symbiote-native/sqlite/vue';
180
+ // <SQLiteProvider database-name="app.db"><Main /></SQLiteProvider> in a template
181
+ // useSQLiteContext() inside a descendant's setup()
182
+ ```
183
+
184
+ **Svelte** (`@symbiote-native/sqlite/svelte`):
185
+
186
+ ```svelte
187
+ <script>
188
+ import { SQLiteProvider, useSQLiteContext } from '@symbiote-native/sqlite/svelte';
189
+ </script>
190
+
191
+ <SQLiteProvider databaseName="app.db">
192
+ <Main />
193
+ </SQLiteProvider>
194
+ ```
195
+
196
+ **Solid** (`@symbiote-native/sqlite/solid`):
197
+
198
+ ```tsx
199
+ import { SQLiteProvider, useSQLiteContext } from '@symbiote-native/sqlite/solid';
200
+
201
+ <SQLiteProvider databaseName="app.db">
202
+ <Main />
203
+ </SQLiteProvider>;
204
+ ```
205
+
206
+ **Angular** (`@symbiote-native/sqlite/angular`) — DI, not a template Provider (this project's
207
+ established Angular idiom, see `@symbiote-native/navigation`'s own context service):
208
+
209
+ ```ts
210
+ import { SqliteService, provideSqliteDatabase } from '@symbiote-native/sqlite/angular';
211
+
212
+ // app config / module providers:
213
+ provideSqliteDatabase({ databaseName: 'app.db', onInit: migrate });
214
+
215
+ // any component/service:
216
+ constructor(private readonly sqlite: SqliteService) {}
217
+ // this.sqlite.database() — an Angular `resource()`-backed signal
218
+ ```
219
+
220
+ None of the five ports upstream's `assetSource` option (see "Deliberately out of scope" below).
221
+ React's `useSuspense` has no equivalent on the other four — Vue/Solid have their own native
222
+ `<Suspense>`/`createResource` primitives an app can compose around the Provider itself if wanted;
223
+ Svelte and Angular have none, and none of the four non-React Providers builds a second opt-in
224
+ code path for it (a deliberate framework-idiom divergence, not a gap — each adapter's own source
225
+ file says so at the top).
226
+
227
+ ## Error handling
228
+
229
+ Native errors surface as a plain thrown `Error`/rejected `Promise` — upstream's ~12 native
230
+ `Exception`/`GenericException` JS classes are not ported, same convention as every other
231
+ module-only package this project ships. Trigger conditions, from the vendored native source
232
+ (`.vendors/expo` @ `origin/sdk-57`, `packages/expo-sqlite/{android,ios}`):
233
+
234
+ | Message contains | Triggered by |
235
+ | --------------------------------------------------- | --------------------------------------------------------------------------------- |
236
+ | `Could not open database` / invalid path | The native `sqlite3_open` call for the resolved path failed. |
237
+ | `Database '<name>' not found` | Deleting a database that was never opened / does not exist on disk. |
238
+ | `Unable to delete database '<name>' that is currently open` | `deleteDatabaseAsync`/`Sync` on a database with a still-open connection. Close it first. |
239
+ | `Unable to delete the database file for '<name>' database` | The file delete itself failed after the open-connection check passed. |
240
+ | `Access to closed resource` | Any call on a `SQLiteDatabase`/`SQLiteStatement`/`SQLiteSession` after `close*`/`finalize*`. |
241
+ | `Invalid bind parameter` | A bind value's type SQLite's C binding layer cannot marshal. |
242
+ | `Invalid arguments: ...` | A malformed call into the native layer (wrong argument shape). |
243
+ | `ERR_INTERNAL_SQLITE_ERROR` (code) / raw SQLite message | SQLite itself rejected the statement — malformed SQL, constraint violation, etc. |
244
+ | `Unsupported operations` | An operation the current build (e.g. a non-libSQL build calling `syncLibSQL()`) does not support. |
245
+
246
+ ## Deliberately out of scope
247
+
248
+ - **The `assetSource` bundled-database-file option**
249
+ (`openDatabaseAsync(name, options, { assetSource })`) — needs `expo-asset`'s
250
+ `Asset.fromModule`, which this project deliberately does not depend on (same exclusion class as
251
+ `@symbiote-native/audio`'s `Asset`-instance form of `AudioSource` — see that package's README).
252
+ A file-copy-based alternative through [`@symbiote-native/file-system`](../file-system)'s
253
+ `File`/`Directory` API is possible but not wired here.
254
+ - **The `expo-sqlite/plugin` config plugin** (libSQL / `sqlite-vec` extension bundling config) —
255
+ manual per-app native configuration, same convention as
256
+ [`@symbiote-native/notifications`'s README](../notifications/README.md) documenting manual
257
+ Firebase/entitlement setup. `bundledExtensions`/`loadExtensionAsync`/`loadExtensionSync` are
258
+ still exported and work against whatever extensions your app's own native build actually
259
+ bundles — they just require you to wire that bundling yourself.
260
+ - **`importAssetDatabaseAsync`** — copies a bundled asset database into place at first launch;
261
+ layers on top of the `assetSource` exclusion above, so it is out of scope for the same reason.
262
+ - **Web-only surfaces** (`ExpoSQLite.web.ts`, `WebStorage.ts`) — this project targets iOS +
263
+ Android only.
264
+ - **expo-sqlite's own DevTools-browser-extension wiring** (`SQLiteDevToolsClient.ts`) — this
265
+ project has no equivalent of Expo's DevTools plugin.
266
+
267
+ ## Test it
268
+
269
+ ```bash
270
+ pnpm vitest run packages/sqlite
271
+ ```
272
+
273
+ The core tests fake the native module in place of `requireNativeModule`'s runtime resolution —
274
+ `ExpoSQLite` only exists on a device, so a headless run would otherwise throw at import. Unlike
275
+ this project's other `expo-modules-core` wrappers, the fake here (`src/core/native-fakes.ts`)
276
+ wraps a **real [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) database** rather
277
+ than a hand-rolled stub, so tests exercise genuine SQL behavior — real transactions, real
278
+ constraint errors, real column shapes — not a guess at what SQLite would do. `better-sqlite3` is
279
+ a devDependency only; it is never shipped to a consumer and is excluded from the published
280
+ tarball alongside the fakes themselves.
281
+
282
+ Real database-file persistence across app restarts, native extension loading, and libSQL sync can
283
+ only be verified on a device or emulator.
@@ -0,0 +1,2 @@
1
+ export * from '../core';
2
+ export * from './sqlite.service';
@@ -0,0 +1,2 @@
1
+ export * from '../core/index.js';
2
+ export * from './sqlite.service.js';
@@ -0,0 +1,41 @@
1
+ import { InjectionToken, type OnDestroy, type Provider } from '@angular/core';
2
+ import type { IOpenDatabaseOptions } from '../core';
3
+ export interface ISqliteConfig {
4
+ /** The name of the database file to open. */
5
+ databaseName: string;
6
+ /** The directory the database file is located in. Defaults to `defaultDatabaseDirectory`. */
7
+ directory?: string;
8
+ /** Open options — `onInit` runs once, right after the native handle opens. */
9
+ options?: IOpenDatabaseOptions;
10
+ /**
11
+ * Called instead of the failure sitting quietly in `SqliteService#error` — mirrors upstream's
12
+ * `SQLiteProviderProps.onError`. Omit it to read the failure off `error()` instead.
13
+ */
14
+ onError?: (error: Error) => void;
15
+ }
16
+ export declare const SQLITE_CONFIG: InjectionToken<ISqliteConfig>;
17
+ /**
18
+ * Declarative config for `SqliteService`'s app-wide database — pass to `bootstrapApplication`'s
19
+ * (or any environment injector's) `providers`, mirroring mounting `<SQLiteProvider>` once near
20
+ * the app root.
21
+ */
22
+ export declare function provideSqliteDatabase(config: ISqliteConfig): Provider;
23
+ export declare class SqliteService implements OnDestroy {
24
+ private readonly config;
25
+ private readonly onErrorHandler;
26
+ private readonly dbResource;
27
+ /**
28
+ * The resolved database, or `undefined` before `open()` has resolved. `resource()`'s own
29
+ * contract: reading this while `error()` is set RE-THROWS that error instead of returning —
30
+ * this IS the "propagates" half of upstream's `onError` contract for a caller that reads it
31
+ * directly (e.g. from a template's `@if (sqlite.database(); as db)`).
32
+ */
33
+ readonly database: import("@angular/core").WritableSignal<import(".").SQLiteDatabase | undefined>;
34
+ /** The open failure, if any — reading `error()` itself never throws, only `database()` does. */
35
+ readonly error: import("@angular/core").Signal<Error | undefined>;
36
+ readonly isLoading: import("@angular/core").Signal<boolean>;
37
+ constructor();
38
+ /** Opens (or re-opens, for a new config) the app-wide database. */
39
+ open(config: ISqliteConfig): void;
40
+ ngOnDestroy(): void;
41
+ }
@@ -0,0 +1,127 @@
1
+ // Angular's DI twin of upstream's `<SQLiteProvider>`/`useSQLiteContext`
2
+ // (.vendors/expo@sdk-57 packages/expo-sqlite/src/hooks.tsx): ONE database instance app-wide by
3
+ // default, opened once and read through DI instead of a React context.
4
+ //
5
+ // Declarative config goes through `provideSqliteDatabase()` — the
6
+ // `providers: [{ provide: TOKEN, useValue }]` pattern this repo already uses for
7
+ // gate-demand.ts's `provideGateDemand()` — rather than a scoping directive like navigation's
8
+ // `symbioteNavigationScope`. That directive pattern deliberately mints a FRESH service instance
9
+ // per usage (one per route); wrong here, since a database is a singleton to open once, not
10
+ // something to re-scope per screen.
11
+ //
12
+ // Async state rides Angular's own `resource()` (adapters/angular/src/render/index.ts already
13
+ // leans on it for TransferState) rather than a hand-rolled Promise/Observable — it is the
14
+ // signals-first primitive this Angular adapter already uses for "async value + error + loading",
15
+ // so `database`/`error`/`isLoading` come straight off it instead of a second, parallel state
16
+ // machine. Its own contract already matches upstream's non-Suspense provider: a failure PARKS in
17
+ // `error()` rather than throwing, exactly like `SQLiteProviderNonSuspense`'s `onError` branch
18
+ // parks it in React state instead of re-throwing.
19
+ //
20
+ // Deliberately excluded, same as `../core`: `assetSource` (needs `expo-asset`, out of scope).
21
+ // No Suspense equivalent — Angular has none.
22
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
23
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
24
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
25
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
26
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
27
+ var _, done = false;
28
+ for (var i = decorators.length - 1; i >= 0; i--) {
29
+ var context = {};
30
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
31
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
32
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
33
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
34
+ if (kind === "accessor") {
35
+ if (result === void 0) continue;
36
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
37
+ if (_ = accept(result.get)) descriptor.get = _;
38
+ if (_ = accept(result.set)) descriptor.set = _;
39
+ if (_ = accept(result.init)) initializers.unshift(_);
40
+ }
41
+ else if (_ = accept(result)) {
42
+ if (kind === "field") initializers.unshift(_);
43
+ else descriptor[key] = _;
44
+ }
45
+ }
46
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
47
+ done = true;
48
+ };
49
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
50
+ var useValue = arguments.length > 2;
51
+ for (var i = 0; i < initializers.length; i++) {
52
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
53
+ }
54
+ return useValue ? value : void 0;
55
+ };
56
+ import { Injectable, InjectionToken, effect, inject, resource, signal, } from '@angular/core';
57
+ import { openDatabaseAsync } from '../core/index.js';
58
+ export const SQLITE_CONFIG = new InjectionToken('symbiote.sqlite-config');
59
+ /**
60
+ * Declarative config for `SqliteService`'s app-wide database — pass to `bootstrapApplication`'s
61
+ * (or any environment injector's) `providers`, mirroring mounting `<SQLiteProvider>` once near
62
+ * the app root.
63
+ */
64
+ export function provideSqliteDatabase(config) {
65
+ return { provide: SQLITE_CONFIG, useValue: config };
66
+ }
67
+ let SqliteService = (() => {
68
+ let _classDecorators = [Injectable({ providedIn: 'root' })];
69
+ let _classDescriptor;
70
+ let _classExtraInitializers = [];
71
+ let _classThis;
72
+ var SqliteService = class {
73
+ static { _classThis = this; }
74
+ static {
75
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
76
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
77
+ SqliteService = _classThis = _classDescriptor.value;
78
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
79
+ __runInitializers(_classThis, _classExtraInitializers);
80
+ }
81
+ config = signal(undefined);
82
+ onErrorHandler = signal(undefined);
83
+ dbResource = resource({
84
+ params: () => this.config(),
85
+ loader: ({ params }) => openDatabaseAsync(params.databaseName, params.options, params.directory),
86
+ });
87
+ /**
88
+ * The resolved database, or `undefined` before `open()` has resolved. `resource()`'s own
89
+ * contract: reading this while `error()` is set RE-THROWS that error instead of returning —
90
+ * this IS the "propagates" half of upstream's `onError` contract for a caller that reads it
91
+ * directly (e.g. from a template's `@if (sqlite.database(); as db)`).
92
+ */
93
+ database = this.dbResource.value;
94
+ /** The open failure, if any — reading `error()` itself never throws, only `database()` does. */
95
+ error = this.dbResource.error;
96
+ isLoading = this.dbResource.isLoading;
97
+ constructor() {
98
+ const injected = inject(SQLITE_CONFIG, { optional: true });
99
+ if (injected)
100
+ this.open(injected);
101
+ effect(() => {
102
+ const raw = this.dbResource.error();
103
+ if (raw === undefined)
104
+ return;
105
+ const error = raw instanceof Error ? raw : new Error(String(raw));
106
+ this.onErrorHandler()?.(error);
107
+ });
108
+ }
109
+ /** Opens (or re-opens, for a new config) the app-wide database. */
110
+ open(config) {
111
+ this.onErrorHandler.set(config.onError);
112
+ this.config.set(config);
113
+ }
114
+ // ponytail: closes only the CURRENTLY resolved handle, on the service's own teardown
115
+ // (providedIn: 'root' → root injector destruction). Re-opening with a different config does
116
+ // not close the previous handle first, unlike upstream's `getDatabaseAsync` memo — add that if
117
+ // an app actually calls `open()` more than once per lifetime.
118
+ ngOnDestroy() {
119
+ // `hasValue()` never throws, unlike reading `.value()` directly on an errored resource.
120
+ if (!this.dbResource.hasValue())
121
+ return;
122
+ void this.dbResource.value().closeAsync();
123
+ }
124
+ };
125
+ return SqliteService = _classThis;
126
+ })();
127
+ export { SqliteService };
@@ -0,0 +1,10 @@
1
+ export { SQLiteDatabase, defaultDatabaseDirectory, bundledExtensions, openDatabaseAsync, openDatabaseSync, deserializeDatabaseAsync, deserializeDatabaseSync, deleteDatabaseAsync, deleteDatabaseSync, backupDatabaseAsync, backupDatabaseSync, addDatabaseChangeListener, } from './sqlite-database';
2
+ export type { ISQLiteOpenOptions, IDatabaseChangeEvent, IOnInitCallback, IOpenDatabaseOptions, } from './sqlite-database';
3
+ export { SQLiteStatement } from './sqlite-statement';
4
+ export type { ISQLiteBindParams, ISQLiteBindValue, ISQLiteExecuteAsyncResult, ISQLiteExecuteSyncResult, ISQLiteRunResult, ISQLiteVariadicBindParams, } from './sqlite-statement';
5
+ export { SQLiteSession } from './sqlite-session';
6
+ export type { IChangeset } from './sqlite-session';
7
+ export { SQLiteTaggedQuery } from './sqlite-tagged-query';
8
+ export { parseSQLQuery } from './query-utils';
9
+ export type { ISQLParsedInfo } from './query-utils';
10
+ export { createDatabasePath, basename } from './path-utils';
@@ -0,0 +1,6 @@
1
+ export { SQLiteDatabase, defaultDatabaseDirectory, bundledExtensions, openDatabaseAsync, openDatabaseSync, deserializeDatabaseAsync, deserializeDatabaseSync, deleteDatabaseAsync, deleteDatabaseSync, backupDatabaseAsync, backupDatabaseSync, addDatabaseChangeListener, } from './sqlite-database.js';
2
+ export { SQLiteStatement } from './sqlite-statement.js';
3
+ export { SQLiteSession } from './sqlite-session.js';
4
+ export { SQLiteTaggedQuery } from './sqlite-tagged-query.js';
5
+ export { parseSQLQuery } from './query-utils.js';
6
+ export { createDatabasePath, basename } from './path-utils.js';
@@ -0,0 +1,65 @@
1
+ import type { NativeSession } from './native-session';
2
+ import type { NativeStatement } from './native-statement';
3
+ /** An instance of the native SQLite database handle. */
4
+ export declare class NativeDatabase {
5
+ constructor(databasePath: string, options?: ISQLiteOpenOptions, serializedData?: Uint8Array);
6
+ initAsync(): Promise<void>;
7
+ isInTransactionAsync(): Promise<boolean>;
8
+ closeAsync(): Promise<void>;
9
+ execAsync(source: string): Promise<void>;
10
+ serializeAsync(databaseName: string): Promise<Uint8Array>;
11
+ prepareAsync(nativeStatement: NativeStatement, source: string): Promise<NativeStatement>;
12
+ createSessionAsync(nativeSession: NativeSession, dbName: string): Promise<NativeSession>;
13
+ loadExtensionAsync(libPath: string, entryPoint?: string): Promise<void>;
14
+ initSync(): void;
15
+ isInTransactionSync(): boolean;
16
+ closeSync(): void;
17
+ execSync(source: string): void;
18
+ serializeSync(databaseName: string): Uint8Array;
19
+ prepareSync(nativeStatement: NativeStatement, source: string): NativeStatement;
20
+ createSessionSync(nativeSession: NativeSession, dbName: string): NativeSession;
21
+ loadExtensionSync(libPath: string, entryPoint?: string): void;
22
+ /** Only available when the native module was built against a libSQL-backed SQLite. */
23
+ syncLibSQL(): Promise<void>;
24
+ }
25
+ /** Options for opening a database. */
26
+ export type ISQLiteOpenOptions = {
27
+ /**
28
+ * Whether to call the [`sqlite3_update_hook()`](https://www.sqlite.org/c3ref/update_hook.html)
29
+ * function and enable `onDatabaseChange` events.
30
+ * @default false
31
+ */
32
+ enableChangeListener?: boolean;
33
+ /**
34
+ * Whether to create a new connection even if a connection with the same database name exists
35
+ * in cache.
36
+ * @default false
37
+ */
38
+ useNewConnection?: boolean;
39
+ /**
40
+ * Finalize unclosed statements automatically when the database is closed.
41
+ * @default true
42
+ * @hidden
43
+ */
44
+ finalizeUnusedStatementsBeforeClosing?: boolean;
45
+ /** Options for libSQL integration. */
46
+ libSQLOptions?: {
47
+ /** The URL of the libSQL server. */
48
+ url: string;
49
+ /** The auth token for the libSQL server. */
50
+ authToken: string;
51
+ /**
52
+ * Whether to use remote-only without syncing to a local database.
53
+ * @default false
54
+ */
55
+ remoteOnly?: boolean;
56
+ };
57
+ };
58
+ type IFlattenedOpenOptions = Omit<ISQLiteOpenOptions, 'libSQLOptions'> & {
59
+ libSQLUrl?: string;
60
+ libSQLAuthToken?: string;
61
+ libSQLRemoteOnly?: boolean;
62
+ };
63
+ /** Flattens `ISQLiteOpenOptions` into the shape the native module expects. */
64
+ export declare function flattenOpenOptions(options: ISQLiteOpenOptions): IFlattenedOpenOptions;
65
+ export {};
@@ -0,0 +1,13 @@
1
+ /** Flattens `ISQLiteOpenOptions` into the shape the native module expects. */
2
+ export function flattenOpenOptions(options) {
3
+ const { libSQLOptions, ...restOptions } = options;
4
+ const result = { ...restOptions };
5
+ if (libSQLOptions) {
6
+ Object.assign(result, {
7
+ libSQLUrl: libSQLOptions.url,
8
+ libSQLAuthToken: libSQLOptions.authToken,
9
+ libSQLRemoteOnly: libSQLOptions.remoteOnly,
10
+ });
11
+ }
12
+ return result;
13
+ }
@@ -0,0 +1,25 @@
1
+ import type { EventSubscription } from 'expo-modules-core';
2
+ import { NativeDatabase } from './native-database';
3
+ import { NativeSession } from './native-session';
4
+ import { NativeStatement } from './native-statement';
5
+ import type { IDatabaseChangeEvent } from './types';
6
+ export type INativeSQLiteModule = {
7
+ readonly NativeDatabase: typeof NativeDatabase;
8
+ readonly NativeStatement: typeof NativeStatement;
9
+ readonly NativeSession: typeof NativeSession;
10
+ /** The directory new databases are created in when no explicit directory is given. */
11
+ readonly defaultDatabaseDirectory: string;
12
+ /** Pre-bundled SQLite extensions (e.g. `sqlite-vec`), keyed by extension name. */
13
+ readonly bundledExtensions: Record<string, {
14
+ libPath: string;
15
+ entryPoint: string;
16
+ } | undefined>;
17
+ ensureDatabasePathExistsAsync(databasePath: string): Promise<void>;
18
+ ensureDatabasePathExistsSync(databasePath: string): void;
19
+ deleteDatabaseAsync(databasePath: string): Promise<void>;
20
+ deleteDatabaseSync(databasePath: string): void;
21
+ backupDatabaseAsync(destDatabase: NativeDatabase, destDatabaseName: string, sourceDatabase: NativeDatabase, sourceDatabaseName: string): Promise<void>;
22
+ backupDatabaseSync(destDatabase: NativeDatabase, destDatabaseName: string, sourceDatabase: NativeDatabase, sourceDatabaseName: string): void;
23
+ addListener(eventName: 'onDatabaseChange', listener: (event: IDatabaseChangeEvent) => void): EventSubscription;
24
+ };
25
+ export declare const expoSQLite: INativeSQLiteModule;
@@ -0,0 +1,17 @@
1
+ // Resolves expo-sqlite's native module by the same name its own `ExpoSQLite.ts` one-liner uses
2
+ // (`.vendors/expo` @ origin/sdk-57, packages/expo-sqlite/src/ExpoSQLite.ts:
3
+ // `requireNativeModule('ExpoSQLite')`) — through `expo-modules-core` directly, never the `expo`
4
+ // meta-package (root CLAUDE.md's dependency-scope invariant). Verified against the Android
5
+ // module's `Name("ExpoSQLite")` declaration
6
+ // (packages/expo-sqlite/android/.../SQLiteModule.kt) — see this package's `native-link.json`.
7
+ //
8
+ // `NativeDatabase`/`NativeStatement`/`NativeSession` are plain constructors exposed as CLASS
9
+ // PROPERTIES on the native module (`new ExpoSQLite.NativeDatabase(...)`), not `SharedObject`
10
+ // subclasses — see native-database.ts's header for why that makes this simpler than
11
+ // `@symbiote-native/audio`'s port.
12
+ import { requireNativeModule } from 'expo-modules-core';
13
+ import { NativeDatabase } from './native-database.js';
14
+ import { NativeSession } from './native-session.js';
15
+ import { NativeStatement } from './native-statement.js';
16
+ const EXPO_SQLITE_MODULE_NAME = 'ExpoSQLite';
17
+ export const expoSQLite = requireNativeModule(EXPO_SQLITE_MODULE_NAME);
@@ -0,0 +1,20 @@
1
+ import type { ISQLiteAnyDatabase } from './native-statement';
2
+ /** A changeset produced by the session extension. */
3
+ export type IChangeset = Uint8Array;
4
+ export type INativeChangeset = ArrayBuffer;
5
+ export declare class NativeSession {
6
+ attachAsync(database: ISQLiteAnyDatabase, table: string | null): Promise<void>;
7
+ enableAsync(database: ISQLiteAnyDatabase, enabled: boolean): Promise<void>;
8
+ closeAsync(database: ISQLiteAnyDatabase): Promise<void>;
9
+ createChangesetAsync(database: ISQLiteAnyDatabase): Promise<INativeChangeset>;
10
+ createInvertedChangesetAsync(database: ISQLiteAnyDatabase): Promise<INativeChangeset>;
11
+ applyChangesetAsync(database: ISQLiteAnyDatabase, changeset: IChangeset | INativeChangeset): Promise<void>;
12
+ invertChangesetAsync(database: ISQLiteAnyDatabase, changeset: IChangeset | INativeChangeset): Promise<INativeChangeset>;
13
+ attachSync(database: ISQLiteAnyDatabase, table: string | null): void;
14
+ enableSync(database: ISQLiteAnyDatabase, enabled: boolean): void;
15
+ closeSync(database: ISQLiteAnyDatabase): void;
16
+ createChangesetSync(database: ISQLiteAnyDatabase): INativeChangeset;
17
+ createInvertedChangesetSync(database: ISQLiteAnyDatabase): INativeChangeset;
18
+ applyChangesetSync(database: ISQLiteAnyDatabase, changeset: IChangeset | INativeChangeset): void;
19
+ invertChangesetSync(database: ISQLiteAnyDatabase, changeset: IChangeset | INativeChangeset): INativeChangeset;
20
+ }
@@ -0,0 +1 @@
1
+ export {};