expo-sqlite 12.2.1 → 13.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 (68) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/android/CMakeLists.txt +1 -1
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/cpp/NativeDatabaseBinding.cpp +4 -4
  5. package/android/src/main/cpp/NativeDatabaseBinding.h +2 -2
  6. package/android/src/main/cpp/NativeStatementBinding.cpp +22 -15
  7. package/android/src/main/cpp/NativeStatementBinding.h +1 -0
  8. package/android/src/main/java/expo/modules/sqlite/NativeDatabase.kt +3 -1
  9. package/android/src/main/java/expo/modules/sqlite/NativeDatabaseBinding.kt +4 -4
  10. package/android/src/main/java/expo/modules/sqlite/NativeStatement.kt +2 -0
  11. package/android/src/main/java/expo/modules/sqlite/NativeStatementBinding.kt +5 -4
  12. package/android/src/main/java/expo/modules/sqlite/SQLExceptions.kt +6 -0
  13. package/android/src/main/java/expo/modules/sqlite/SQLiteModule.kt +21 -21
  14. package/android/src/main/java/expo/modules/sqlite/SQLiteModuleNext.kt +122 -132
  15. package/build/next/ExpoSQLiteNext.d.ts +4 -4
  16. package/build/next/ExpoSQLiteNext.d.ts.map +1 -1
  17. package/build/next/ExpoSQLiteNext.js +3 -3
  18. package/build/next/ExpoSQLiteNext.js.map +1 -1
  19. package/build/next/NativeDatabase.d.ts +3 -3
  20. package/build/next/NativeDatabase.d.ts.map +1 -1
  21. package/build/next/NativeDatabase.js.map +1 -1
  22. package/build/next/NativeStatement.d.ts +37 -35
  23. package/build/next/NativeStatement.d.ts.map +1 -1
  24. package/build/next/NativeStatement.js.map +1 -1
  25. package/build/next/SQLiteDatabase.d.ts +266 -0
  26. package/build/next/SQLiteDatabase.d.ts.map +1 -0
  27. package/build/next/{Database.js → SQLiteDatabase.js} +69 -59
  28. package/build/next/SQLiteDatabase.js.map +1 -0
  29. package/build/next/SQLiteStatement.d.ts +190 -0
  30. package/build/next/SQLiteStatement.d.ts.map +1 -0
  31. package/build/next/SQLiteStatement.js +275 -0
  32. package/build/next/SQLiteStatement.js.map +1 -0
  33. package/build/next/hooks.d.ts +26 -14
  34. package/build/next/hooks.d.ts.map +1 -1
  35. package/build/next/hooks.js +121 -33
  36. package/build/next/hooks.js.map +1 -1
  37. package/build/next/index.d.ts +2 -2
  38. package/build/next/index.d.ts.map +1 -1
  39. package/build/next/index.js +2 -2
  40. package/build/next/index.js.map +1 -1
  41. package/build/next/paramUtils.d.ts +18 -0
  42. package/build/next/paramUtils.d.ts.map +1 -0
  43. package/build/next/paramUtils.js +72 -0
  44. package/build/next/paramUtils.js.map +1 -0
  45. package/ios/Exceptions.swift +12 -0
  46. package/ios/NativeDatabase.swift +4 -3
  47. package/ios/NativeStatement.swift +1 -0
  48. package/ios/SQLiteModule.swift +20 -21
  49. package/ios/SQLiteModuleNext.swift +126 -131
  50. package/ios/crsqlite.xcframework/Info.plist +4 -0
  51. package/ios/crsqlite.xcframework/ios-arm64/crsqlite.framework/Info.plist +4 -0
  52. package/package.json +4 -3
  53. package/src/next/ExpoSQLiteNext.ts +4 -4
  54. package/src/next/NativeDatabase.ts +3 -3
  55. package/src/next/NativeStatement.ts +43 -48
  56. package/src/next/{Database.ts → SQLiteDatabase.ts} +134 -112
  57. package/src/next/SQLiteStatement.ts +528 -0
  58. package/src/next/hooks.tsx +202 -51
  59. package/src/next/index.ts +2 -2
  60. package/src/next/paramUtils.ts +94 -0
  61. package/build/next/Database.d.ts +0 -272
  62. package/build/next/Database.d.ts.map +0 -1
  63. package/build/next/Database.js.map +0 -1
  64. package/build/next/Statement.d.ts +0 -142
  65. package/build/next/Statement.d.ts.map +0 -1
  66. package/build/next/Statement.js +0 -184
  67. package/build/next/Statement.js.map +0 -1
  68. package/src/next/Statement.ts +0 -310
@@ -1,18 +1,18 @@
1
1
  import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
2
2
 
3
- import { openDatabaseAsync, type Database } from './Database';
4
- import type { OpenOptions } from './NativeDatabase';
3
+ import type { SQLiteOpenOptions } from './NativeDatabase';
4
+ import { openDatabaseAsync, type SQLiteDatabase } from './SQLiteDatabase';
5
5
 
6
6
  export interface SQLiteProviderProps {
7
7
  /**
8
8
  * The name of the database file to open.
9
9
  */
10
- dbName: string;
10
+ databaseName: string;
11
11
 
12
12
  /**
13
13
  * Open options.
14
14
  */
15
- options?: OpenOptions;
15
+ options?: SQLiteOpenOptions;
16
16
 
17
17
  /**
18
18
  * The children to render.
@@ -23,49 +23,130 @@ export interface SQLiteProviderProps {
23
23
  * A custom initialization handler to run before rendering the children.
24
24
  * You can use this to run database migrations or other setup tasks.
25
25
  */
26
- initHandler?: (db: Database) => Promise<void>;
26
+ onInit?: (db: SQLiteDatabase) => Promise<void>;
27
27
 
28
28
  /**
29
- * A custom loading fallback to render before the database is ready.
30
- * @default null
29
+ * Handle errors from SQLiteProvider.
30
+ * @default rethrow the error
31
31
  */
32
- loadingFallback?: React.ReactNode;
32
+ onError?: (error: Error) => void;
33
33
 
34
34
  /**
35
- * Handle errors from SQLiteProvider.
36
- * @default rethrow the error
35
+ * Enable [`React.Suspense`](https://react.dev/reference/react/Suspense) integration.
36
+ * @default false
37
+ * @example
38
+ * ```tsx
39
+ * export default function App() {
40
+ * return (
41
+ * <Suspense fallback={<Text>Loading...</Text>}>
42
+ * <SQLiteProvider databaseName="test.db" useSuspense={true}>
43
+ * <Main />
44
+ * </SQLiteProvider>
45
+ * </Suspense>
46
+ * );
47
+ * }
48
+ * ```
37
49
  */
38
- errorHandler?: (error: Error) => void;
50
+ useSuspense?: boolean;
39
51
  }
40
52
 
41
53
  /**
42
54
  * Create a context for the SQLite database
43
55
  */
44
- const SQLiteContext = createContext<Database | null>(null);
56
+ const SQLiteContext = createContext<SQLiteDatabase | null>(null);
45
57
 
46
58
  /**
47
59
  * Context.Provider component that provides a SQLite database to all children.
48
60
  * All descendants of this component will be able to access the database using the [`useSQLiteContext`](#usesqlitecontext) hook.
49
61
  */
50
62
  export function SQLiteProvider({
51
- dbName,
52
- options,
53
63
  children,
54
- initHandler,
55
- loadingFallback,
56
- errorHandler,
64
+ onError,
65
+ useSuspense = false,
66
+ ...props
57
67
  }: SQLiteProviderProps) {
58
- const databaseRef = useRef<Database | null>(null);
68
+ if (onError != null && useSuspense) {
69
+ throw new Error('Cannot use `onError` with `useSuspense`, use error boundaries instead.');
70
+ }
71
+
72
+ if (useSuspense) {
73
+ return <SQLiteProviderSuspense {...props}>{children}</SQLiteProviderSuspense>;
74
+ }
75
+
76
+ return (
77
+ <SQLiteProviderNonSuspense {...props} onError={onError}>
78
+ {children}
79
+ </SQLiteProviderNonSuspense>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * A global hook for accessing the SQLite database across components.
85
+ * This hook should only be used within a [`<SQLiteProvider>`](#sqliteprovider) component.
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * export default function App() {
90
+ * return (
91
+ * <SQLiteProvider databaseName="test.db">
92
+ * <Main />
93
+ * </SQLiteProvider>
94
+ * );
95
+ * }
96
+ *
97
+ * export function Main() {
98
+ * const db = useSQLiteContext();
99
+ * console.log('sqlite version', db.getSync('SELECT sqlite_version()'));
100
+ * return <View />
101
+ * }
102
+ * ```
103
+ */
104
+ export function useSQLiteContext(): SQLiteDatabase {
105
+ const context = useContext(SQLiteContext);
106
+ if (context == null) {
107
+ throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
108
+ }
109
+ return context;
110
+ }
111
+
112
+ //#region Internals
113
+
114
+ type DatabaseInstanceType = Pick<SQLiteProviderProps, 'databaseName' | 'options' | 'onInit'> & {
115
+ promise: Promise<SQLiteDatabase> | null;
116
+ };
117
+
118
+ let databaseInstance: DatabaseInstanceType | null = null;
119
+
120
+ function SQLiteProviderSuspense({
121
+ databaseName,
122
+ options,
123
+ children,
124
+ onInit,
125
+ }: Omit<SQLiteProviderProps, 'onError' | 'useSuspense'>) {
126
+ const databasePromise = getDatabaseAsync({
127
+ databaseName,
128
+ options,
129
+ onInit,
130
+ });
131
+ const database = use(databasePromise);
132
+ return <SQLiteContext.Provider value={database}>{children}</SQLiteContext.Provider>;
133
+ }
134
+
135
+ function SQLiteProviderNonSuspense({
136
+ databaseName,
137
+ options,
138
+ children,
139
+ onInit,
140
+ onError,
141
+ }: Omit<SQLiteProviderProps, 'useSuspense'>) {
142
+ const databaseRef = useRef<SQLiteDatabase | null>(null);
59
143
  const [loading, setLoading] = useState(true);
60
144
  const [error, setError] = useState<Error | null>(null);
61
145
 
62
146
  useEffect(() => {
63
147
  async function setup() {
64
148
  try {
65
- const db = await openDatabaseAsync(dbName, options);
66
- if (initHandler != null) {
67
- await initHandler(db);
68
- }
149
+ const db = await openDatabaseWithInitAsync({ databaseName, options, onInit });
69
150
  databaseRef.current = db;
70
151
  setLoading(false);
71
152
  } catch (e) {
@@ -73,7 +154,7 @@ export function SQLiteProvider({
73
154
  }
74
155
  }
75
156
 
76
- async function teardown(db: Database | null) {
157
+ async function teardown(db: SQLiteDatabase | null) {
77
158
  try {
78
159
  await db?.closeAsync();
79
160
  } catch (e) {
@@ -89,48 +170,118 @@ export function SQLiteProvider({
89
170
  databaseRef.current = null;
90
171
  setLoading(true);
91
172
  };
92
- }, [dbName, options, initHandler]);
173
+ }, [databaseName, options, onInit]);
93
174
 
94
175
  if (error != null) {
95
176
  const handler =
96
- errorHandler ??
177
+ onError ??
97
178
  ((e) => {
98
179
  throw e;
99
180
  });
100
181
  handler(error);
101
182
  }
102
-
103
183
  if (loading || !databaseRef.current) {
104
- return loadingFallback != null ? <>{loadingFallback}</> : null;
184
+ return null;
105
185
  }
106
186
  return <SQLiteContext.Provider value={databaseRef.current}>{children}</SQLiteContext.Provider>;
107
187
  }
108
188
 
189
+ function getDatabaseAsync({
190
+ databaseName,
191
+ options,
192
+ onInit,
193
+ }: Pick<SQLiteProviderProps, 'databaseName' | 'options' | 'onInit'>): Promise<SQLiteDatabase> {
194
+ if (
195
+ databaseInstance?.promise != null &&
196
+ databaseInstance?.databaseName === databaseName &&
197
+ databaseInstance?.options === options &&
198
+ databaseInstance?.onInit === onInit
199
+ ) {
200
+ return databaseInstance.promise;
201
+ }
202
+
203
+ let promise: Promise<SQLiteDatabase>;
204
+ if (databaseInstance?.promise != null) {
205
+ promise = databaseInstance.promise
206
+ .then((db) => {
207
+ db.closeAsync();
208
+ })
209
+ .then(() => {
210
+ return openDatabaseWithInitAsync({ databaseName, options, onInit });
211
+ });
212
+ } else {
213
+ promise = openDatabaseWithInitAsync({ databaseName, options, onInit });
214
+ }
215
+ databaseInstance = {
216
+ databaseName,
217
+ options,
218
+ onInit,
219
+ promise,
220
+ };
221
+ return promise;
222
+ }
223
+
224
+ async function openDatabaseWithInitAsync({
225
+ databaseName,
226
+ options,
227
+ onInit,
228
+ }: Pick<SQLiteProviderProps, 'databaseName' | 'options' | 'onInit'>): Promise<SQLiteDatabase> {
229
+ const database = await openDatabaseAsync(databaseName, options);
230
+ if (onInit != null) {
231
+ await onInit(database);
232
+ }
233
+ return database;
234
+ }
235
+
236
+ //#endregion
237
+
238
+ //#region Private Suspense API similar to `React.use`
239
+
240
+ // Referenced from https://github.com/vercel/swr/blob/1d8110900d1aee3747199bfb377b149b7ff6848e/_internal/src/types.ts#L27-L31
241
+ type ReactUsePromise<T, E extends Error = Error> = Promise<T> & {
242
+ status?: 'pending' | 'fulfilled' | 'rejected';
243
+ value?: T;
244
+ reason?: E;
245
+ };
246
+
247
+ // Referenced from https://github.com/reactjs/react.dev/blob/6570e6cd79a16ac3b1a2902632eddab7e6abb9ad/src/content/reference/react/Suspense.md
109
248
  /**
110
- * A global hook for accessing the SQLite database across components.
111
- * This hook should only be used within a [`<SQLiteProvider>`](#sqliteprovider) component.
112
- *
113
- * @example
114
- * ```tsx
115
- * export default function App() {
116
- * return (
117
- * <SQLiteProvider dbName="test.db">
118
- * <Main />
119
- * </SQLiteProvider>
120
- * );
121
- * }
122
- *
123
- * export function Main() {
124
- * const db = useSQLiteContext();
125
- * console.log('sqlite version', db.getSync('SELECT sqlite_version()'));
126
- * return <View />
127
- * }
128
- * ```
249
+ * A custom hook like [`React.use`](https://react.dev/reference/react/use) hook using private Suspense implementation.
129
250
  */
130
- export function useSQLiteContext(): Database {
131
- const context = useContext(SQLiteContext);
132
- if (context == null) {
133
- throw new Error('useSQLiteContext must be used within a <SQLiteProvider>');
251
+ function use<T>(promise: Promise<T> | ReactUsePromise<T>) {
252
+ if (isReactUsePromise(promise)) {
253
+ if (promise.status === 'fulfilled') {
254
+ if (promise.value === undefined) {
255
+ throw new Error('[use] Unexpected undefined value from promise');
256
+ }
257
+ return promise.value;
258
+ } else if (promise.status === 'rejected') {
259
+ throw promise.reason;
260
+ } else if (promise.status === 'pending') {
261
+ throw promise;
262
+ }
263
+ throw new Error('[use] Promise is in an invalid state');
134
264
  }
135
- return context;
265
+
266
+ const suspensePromise = promise as ReactUsePromise<T>;
267
+ suspensePromise.status = 'pending';
268
+ suspensePromise.then(
269
+ (result: T) => {
270
+ suspensePromise.status = 'fulfilled';
271
+ suspensePromise.value = result;
272
+ },
273
+ (reason) => {
274
+ suspensePromise.status = 'rejected';
275
+ suspensePromise.reason = reason;
276
+ }
277
+ );
278
+ throw suspensePromise;
136
279
  }
280
+
281
+ function isReactUsePromise<T>(
282
+ promise: Promise<T> | ReactUsePromise<T>
283
+ ): promise is ReactUsePromise<T> {
284
+ return typeof promise === 'object' && promise !== null && 'status' in promise;
285
+ }
286
+
287
+ //#endregion
package/src/next/index.ts CHANGED
@@ -1,3 +1,3 @@
1
- export * from './Database';
2
- export * from './Statement';
1
+ export * from './SQLiteDatabase';
2
+ export * from './SQLiteStatement';
3
3
  export * from './hooks';
@@ -0,0 +1,94 @@
1
+ import {
2
+ SQLiteBindBlobParams,
3
+ SQLiteBindParams,
4
+ SQLiteBindPrimitiveParams,
5
+ SQLiteBindValue,
6
+ type SQLiteColumnNames,
7
+ type SQLiteColumnValues,
8
+ } from './NativeStatement';
9
+
10
+ /**
11
+ * Normalize the bind params to data structure that can be passed to native module.
12
+ * The data structure is a tuple of [primitiveParams, blobParams, shouldPassAsArray].
13
+ * @hidden
14
+ */
15
+ export function normalizeParams(
16
+ ...params: any[]
17
+ ): [SQLiteBindPrimitiveParams, SQLiteBindBlobParams, boolean] {
18
+ let bindParams = params.length > 1 ? params : (params[0] as SQLiteBindParams);
19
+ if (bindParams == null) {
20
+ bindParams = [];
21
+ }
22
+ if (
23
+ typeof bindParams !== 'object' ||
24
+ bindParams instanceof ArrayBuffer ||
25
+ ArrayBuffer.isView(bindParams)
26
+ ) {
27
+ bindParams = [bindParams];
28
+ }
29
+ const shouldPassAsArray = Array.isArray(bindParams);
30
+ if (Array.isArray(bindParams)) {
31
+ bindParams = bindParams.reduce<Record<string, SQLiteBindValue>>((acc, value, index) => {
32
+ acc[index] = value;
33
+ return acc;
34
+ }, {});
35
+ }
36
+
37
+ const primitiveParams: SQLiteBindPrimitiveParams = {};
38
+ const blobParams: SQLiteBindBlobParams = {};
39
+ for (const key in bindParams) {
40
+ const value = bindParams[key];
41
+ if (value instanceof Uint8Array) {
42
+ blobParams[key] = value;
43
+ } else {
44
+ primitiveParams[key] = value;
45
+ }
46
+ }
47
+
48
+ return [primitiveParams, blobParams, shouldPassAsArray];
49
+ }
50
+
51
+ /**
52
+ * Compose `columnNames` and `columnValues` to an row object.
53
+ * @hidden
54
+ */
55
+ export function composeRow<T>(columnNames: SQLiteColumnNames, columnValues: SQLiteColumnValues): T {
56
+ const row = {};
57
+ if (columnNames.length !== columnValues.length) {
58
+ throw new Error(
59
+ `Column names and values count mismatch. Names: ${columnNames.length}, Values: ${columnValues.length}`
60
+ );
61
+ }
62
+ for (let i = 0; i < columnNames.length; i++) {
63
+ row[columnNames[i]] = columnValues[i];
64
+ }
65
+ return row as T;
66
+ }
67
+
68
+ /**
69
+ * Compose `columnNames` and `columnValuesList` to an array of row objects.
70
+ * @hidden
71
+ */
72
+ export function composeRows<T>(
73
+ columnNames: SQLiteColumnNames,
74
+ columnValuesList: SQLiteColumnValues[]
75
+ ): T[] {
76
+ if (columnValuesList.length === 0) {
77
+ return [];
78
+ }
79
+ if (columnNames.length !== columnValuesList[0].length) {
80
+ // We only check the first row because SQLite returns the same column count for all rows.
81
+ throw new Error(
82
+ `Column names and values count mismatch. Names: ${columnNames.length}, Values: ${columnValuesList[0].length}`
83
+ );
84
+ }
85
+ const results: T[] = [];
86
+ for (const columnValues of columnValuesList) {
87
+ const row = {};
88
+ for (let i = 0; i < columnNames.length; i++) {
89
+ row[columnNames[i]] = columnValues[i];
90
+ }
91
+ results.push(row as T);
92
+ }
93
+ return results;
94
+ }