xref: /expo/packages/expo-sqlite/build/SQLite.js (revision c4573fff)
1import './polyfillNextTick';
2import customOpenDatabase from '@expo/websql/custom';
3import { requireNativeModule, EventEmitter } from 'expo-modules-core';
4import { Platform } from 'react-native';
5const ExpoSQLite = requireNativeModule('ExpoSQLite');
6const emitter = new EventEmitter(ExpoSQLite);
7function zipObject(keys, values) {
8    const result = {};
9    for (let i = 0; i < keys.length; i++) {
10        result[keys[i]] = values[i];
11    }
12    return result;
13}
14/** The database returned by `openDatabase()` */
15export class SQLiteDatabase {
16    _name;
17    _closed = false;
18    constructor(name) {
19        this._name = name;
20    }
21    /**
22     * Executes the SQL statement and returns a callback resolving with the result.
23     */
24    exec(queries, readOnly, callback) {
25        if (this._closed) {
26            throw new Error(`The SQLite database is closed`);
27        }
28        ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then((nativeResultSets) => {
29            callback(null, nativeResultSets.map(_deserializeResultSet));
30        }, (error) => {
31            // TODO: make the native API consistently reject with an error, not a string or other type
32            callback(error instanceof Error ? error : new Error(error));
33        });
34    }
35    /**
36     * Due to limitations on `Android` this function is provided to allow raw SQL queries to be
37     * executed on the database. This will be less efficient than using the `exec` function, please use
38     * only when necessary.
39     */
40    execRawQuery(queries, readOnly, callback) {
41        if (Platform.OS === 'ios') {
42            return this.exec(queries, readOnly, callback);
43        }
44        ExpoSQLite.execRawQuery(this._name, queries.map(_serializeQuery), readOnly).then((nativeResultSets) => {
45            callback(null, nativeResultSets.map(_deserializeResultSet));
46        }, (error) => {
47            callback(error instanceof Error ? error : new Error(error));
48        });
49    }
50    /**
51     * Executes the SQL statement and returns a Promise resolving with the result.
52     */
53    async execAsync(queries, readOnly) {
54        if (this._closed) {
55            throw new Error(`The SQLite database is closed`);
56        }
57        const nativeResultSets = await ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly);
58        return nativeResultSets.map(_deserializeResultSet);
59    }
60    /**
61     * @deprecated Use `closeAsync()` instead.
62     */
63    close = this.closeAsync;
64    /**
65     * Close the database.
66     */
67    closeAsync() {
68        this._closed = true;
69        return ExpoSQLite.close(this._name);
70    }
71    /**
72     * Synchronously closes the database.
73     */
74    closeSync() {
75        this._closed = true;
76        return ExpoSQLite.closeSync(this._name);
77    }
78    /**
79     * Delete the database file.
80     * > The database has to be closed prior to deletion.
81     */
82    deleteAsync() {
83        if (!this._closed) {
84            throw new Error(`Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.`);
85        }
86        return ExpoSQLite.deleteAsync(this._name);
87    }
88    /**
89     * Used to listen to changes in the database.
90     * @param callback A function that receives the `tableName` and `rowId` of the modified data.
91     */
92    onDatabaseChange(cb) {
93        return emitter.addListener('onDatabaseChange', cb);
94    }
95    /**
96     * Creates a new transaction with Promise support.
97     * @param asyncCallback A `SQLTransactionAsyncCallback` function that can perform SQL statements in a transaction.
98     * @param readOnly true if all the SQL statements in the callback are read only.
99     */
100    async transactionAsync(asyncCallback, readOnly = false) {
101        await this.execAsync([{ sql: 'BEGIN;', args: [] }], false);
102        try {
103            const transaction = new ExpoSQLTransactionAsync(this, readOnly);
104            await asyncCallback(transaction);
105            await this.execAsync([{ sql: 'END;', args: [] }], false);
106        }
107        catch (e) {
108            await this.execAsync([{ sql: 'ROLLBACK;', args: [] }], false);
109            throw e;
110        }
111    }
112    // @ts-expect-error: properties that are added from websql
113    version;
114}
115function _serializeQuery(query) {
116    return Platform.OS === 'android'
117        ? {
118            sql: query.sql,
119            args: query.args.map(_escapeBlob),
120        }
121        : [query.sql, query.args];
122}
123function _deserializeResultSet(nativeResult) {
124    const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult;
125    // TODO: send more structured error information from the native module so we can better construct
126    // a SQLException object
127    if (errorMessage !== null) {
128        return { error: new Error(errorMessage) };
129    }
130    return {
131        insertId,
132        rowsAffected,
133        rows: rows.map((row) => zipObject(columns, row)),
134    };
135}
136function _escapeBlob(data) {
137    if (typeof data === 'string') {
138        /* eslint-disable no-control-regex */
139        return data
140            .replace(/\u0002/g, '\u0002\u0002')
141            .replace(/\u0001/g, '\u0001\u0002')
142            .replace(/\u0000/g, '\u0001\u0001');
143        /* eslint-enable no-control-regex */
144    }
145    else {
146        return data;
147    }
148}
149const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase);
150// @needsAudit @docsMissing
151/**
152 * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk,
153 * the database will be created under the app's [documents directory](./filesystem), i.e.
154 * `${FileSystem.documentDirectory}/SQLite/${name}`.
155 * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function
156 * for compatibility with the WebSQL specification.
157 * @param name Name of the database file to open.
158 * @param version
159 * @param description
160 * @param size
161 * @param callback
162 * @return
163 */
164export function openDatabase(name, version = '1.0', description = name, size = 1, callback) {
165    if (name === undefined) {
166        throw new TypeError(`The database name must not be undefined`);
167    }
168    const db = _openExpoSQLiteDatabase(name, version, description, size, callback);
169    db.exec = db._db.exec.bind(db._db);
170    db.execRawQuery = db._db.execRawQuery.bind(db._db);
171    db.execAsync = db._db.execAsync.bind(db._db);
172    db.closeAsync = db._db.closeAsync.bind(db._db);
173    db.closeSync = db._db.closeSync.bind(db._db);
174    db.onDatabaseChange = db._db.onDatabaseChange.bind(db._db);
175    db.deleteAsync = db._db.deleteAsync.bind(db._db);
176    db.transactionAsync = db._db.transactionAsync.bind(db._db);
177    return db;
178}
179/**
180 * Internal data structure for the async transaction API.
181 * @internal
182 */
183export class ExpoSQLTransactionAsync {
184    db;
185    readOnly;
186    constructor(db, readOnly) {
187        this.db = db;
188        this.readOnly = readOnly;
189    }
190    async executeSqlAsync(sqlStatement, args) {
191        const resultSets = await this.db.execAsync([{ sql: sqlStatement, args: args ?? [] }], this.readOnly);
192        const result = resultSets[0];
193        if (isResultSetError(result)) {
194            throw result.error;
195        }
196        return result;
197    }
198}
199function isResultSetError(result) {
200    return 'error' in result;
201}
202//# sourceMappingURL=SQLite.js.map