xref: /expo/packages/expo-sqlite/src/SQLite.ts (revision c4573fff)
12ee87287SSzymon20000import './polyfillNextTick';
22ee87287SSzymon20000
392f6de58SWill Schurmanimport customOpenDatabase from '@expo/websql/custom';
474e0b8dfSAlan Hughesimport { requireNativeModule, EventEmitter } from 'expo-modules-core';
52ee87287SSzymon20000import { Platform } from 'react-native';
692f6de58SWill Schurman
74a7bfa1dSKudo Chienimport type {
84a7bfa1dSKudo Chien  Query,
94a7bfa1dSKudo Chien  ResultSet,
104a7bfa1dSKudo Chien  ResultSetError,
114a7bfa1dSKudo Chien  SQLiteCallback,
124a7bfa1dSKudo Chien  SQLTransactionAsyncCallback,
134a7bfa1dSKudo Chien  SQLTransactionAsync,
14c52e83e3SKudo Chien  SQLTransactionCallback,
15c52e83e3SKudo Chien  SQLTransactionErrorCallback,
164a7bfa1dSKudo Chien} from './SQLite.types';
172ee87287SSzymon20000
18c1d37355SAlan Hughesconst ExpoSQLite = requireNativeModule('ExpoSQLite');
1974e0b8dfSAlan Hughesconst emitter = new EventEmitter(ExpoSQLite);
202ee87287SSzymon20000
21e2a59be1SEvan Baconfunction zipObject(keys: string[], values: any[]) {
22e2a59be1SEvan Bacon  const result = {};
23e2a59be1SEvan Bacon  for (let i = 0; i < keys.length; i++) {
24e2a59be1SEvan Bacon    result[keys[i]] = values[i];
25e2a59be1SEvan Bacon  }
26e2a59be1SEvan Bacon  return result;
27e2a59be1SEvan Bacon}
28e2a59be1SEvan Bacon
294a7bfa1dSKudo Chien/** The database returned by `openDatabase()` */
304a7bfa1dSKudo Chienexport class SQLiteDatabase {
312ee87287SSzymon20000  _name: string;
322ee87287SSzymon20000  _closed: boolean = false;
332ee87287SSzymon20000
342ee87287SSzymon20000  constructor(name: string) {
352ee87287SSzymon20000    this._name = name;
362ee87287SSzymon20000  }
372ee87287SSzymon20000
384a7bfa1dSKudo Chien  /**
394a7bfa1dSKudo Chien   * Executes the SQL statement and returns a callback resolving with the result.
404a7bfa1dSKudo Chien   */
415cb26b36STomo Krajina  exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void {
422ee87287SSzymon20000    if (this._closed) {
432ee87287SSzymon20000      throw new Error(`The SQLite database is closed`);
442ee87287SSzymon20000    }
452ee87287SSzymon20000
46c1d37355SAlan Hughes    ExpoSQLite.exec(this._name, queries.map(_serializeQuery), readOnly).then(
4735f78160SBartosz Kaszubowski      (nativeResultSets) => {
482ee87287SSzymon20000        callback(null, nativeResultSets.map(_deserializeResultSet));
492ee87287SSzymon20000      },
5035f78160SBartosz Kaszubowski      (error) => {
512ee87287SSzymon20000        // TODO: make the native API consistently reject with an error, not a string or other type
522ee87287SSzymon20000        callback(error instanceof Error ? error : new Error(error));
532ee87287SSzymon20000      }
542ee87287SSzymon20000    );
552ee87287SSzymon20000  }
562ee87287SSzymon20000
574a7bfa1dSKudo Chien  /**
5850e16f8cSAlan Hughes   * Due to limitations on `Android` this function is provided to allow raw SQL queries to be
5950e16f8cSAlan Hughes   * executed on the database. This will be less efficient than using the `exec` function, please use
6050e16f8cSAlan Hughes   * only when necessary.
6150e16f8cSAlan Hughes   */
6250e16f8cSAlan Hughes  execRawQuery(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void {
6350e16f8cSAlan Hughes    if (Platform.OS === 'ios') {
6450e16f8cSAlan Hughes      return this.exec(queries, readOnly, callback);
6550e16f8cSAlan Hughes    }
6650e16f8cSAlan Hughes
6750e16f8cSAlan Hughes    ExpoSQLite.execRawQuery(this._name, queries.map(_serializeQuery), readOnly).then(
6850e16f8cSAlan Hughes      (nativeResultSets) => {
6950e16f8cSAlan Hughes        callback(null, nativeResultSets.map(_deserializeResultSet));
7050e16f8cSAlan Hughes      },
7150e16f8cSAlan Hughes      (error) => {
7250e16f8cSAlan Hughes        callback(error instanceof Error ? error : new Error(error));
7350e16f8cSAlan Hughes      }
7450e16f8cSAlan Hughes    );
7550e16f8cSAlan Hughes  }
7650e16f8cSAlan Hughes
7750e16f8cSAlan Hughes  /**
784a7bfa1dSKudo Chien   * Executes the SQL statement and returns a Promise resolving with the result.
794a7bfa1dSKudo Chien   */
80c52e83e3SKudo Chien  async execAsync(queries: Query[], readOnly: boolean): Promise<(ResultSetError | ResultSet)[]> {
814a7bfa1dSKudo Chien    if (this._closed) {
824a7bfa1dSKudo Chien      throw new Error(`The SQLite database is closed`);
834a7bfa1dSKudo Chien    }
844a7bfa1dSKudo Chien
854a7bfa1dSKudo Chien    const nativeResultSets = await ExpoSQLite.exec(
864a7bfa1dSKudo Chien      this._name,
874a7bfa1dSKudo Chien      queries.map(_serializeQuery),
884a7bfa1dSKudo Chien      readOnly
894a7bfa1dSKudo Chien    );
904a7bfa1dSKudo Chien    return nativeResultSets.map(_deserializeResultSet);
914a7bfa1dSKudo Chien  }
924a7bfa1dSKudo Chien
934a7bfa1dSKudo Chien  /**
944a7bfa1dSKudo Chien   * @deprecated Use `closeAsync()` instead.
954a7bfa1dSKudo Chien   */
964a7bfa1dSKudo Chien  close = this.closeAsync;
974a7bfa1dSKudo Chien
984a7bfa1dSKudo Chien  /**
994a7bfa1dSKudo Chien   * Close the database.
1004a7bfa1dSKudo Chien   */
1016c4baee8SAlan Hughes  closeAsync(): Promise<void> {
1022ee87287SSzymon20000    this._closed = true;
103c1d37355SAlan Hughes    return ExpoSQLite.close(this._name);
1043f4e5fe5SKudo Chien  }
1053f4e5fe5SKudo Chien
1064a7bfa1dSKudo Chien  /**
1076c4baee8SAlan Hughes   * Synchronously closes the database.
1086c4baee8SAlan Hughes   */
1096c4baee8SAlan Hughes  closeSync(): void {
1106c4baee8SAlan Hughes    this._closed = true;
1116c4baee8SAlan Hughes    return ExpoSQLite.closeSync(this._name);
1126c4baee8SAlan Hughes  }
1136c4baee8SAlan Hughes
1146c4baee8SAlan Hughes  /**
1154a7bfa1dSKudo Chien   * Delete the database file.
1164a7bfa1dSKudo Chien   * > The database has to be closed prior to deletion.
1174a7bfa1dSKudo Chien   */
1183f4e5fe5SKudo Chien  deleteAsync(): Promise<void> {
1193f4e5fe5SKudo Chien    if (!this._closed) {
1203f4e5fe5SKudo Chien      throw new Error(
1213f4e5fe5SKudo Chien        `Unable to delete '${this._name}' database that is currently open. Close it prior to deletion.`
1223f4e5fe5SKudo Chien      );
1233f4e5fe5SKudo Chien    }
1243f4e5fe5SKudo Chien
125c1d37355SAlan Hughes    return ExpoSQLite.deleteAsync(this._name);
1262ee87287SSzymon20000  }
1274a7bfa1dSKudo Chien
12850e16f8cSAlan Hughes  /**
12950e16f8cSAlan Hughes   * Used to listen to changes in the database.
13050e16f8cSAlan Hughes   * @param callback A function that receives the `tableName` and `rowId` of the modified data.
13150e16f8cSAlan Hughes   */
13274e0b8dfSAlan Hughes  onDatabaseChange(cb: (result: { tableName: string; rowId: number }) => void) {
13374e0b8dfSAlan Hughes    return emitter.addListener('onDatabaseChange', cb);
13474e0b8dfSAlan Hughes  }
13574e0b8dfSAlan Hughes
1364a7bfa1dSKudo Chien  /**
1374a7bfa1dSKudo Chien   * Creates a new transaction with Promise support.
1384a7bfa1dSKudo Chien   * @param asyncCallback A `SQLTransactionAsyncCallback` function that can perform SQL statements in a transaction.
1394a7bfa1dSKudo Chien   * @param readOnly true if all the SQL statements in the callback are read only.
1404a7bfa1dSKudo Chien   */
1414a7bfa1dSKudo Chien  async transactionAsync(
1424a7bfa1dSKudo Chien    asyncCallback: SQLTransactionAsyncCallback,
1434a7bfa1dSKudo Chien    readOnly: boolean = false
1444a7bfa1dSKudo Chien  ): Promise<void> {
1454a7bfa1dSKudo Chien    await this.execAsync([{ sql: 'BEGIN;', args: [] }], false);
1464a7bfa1dSKudo Chien    try {
1474a7bfa1dSKudo Chien      const transaction = new ExpoSQLTransactionAsync(this, readOnly);
1484a7bfa1dSKudo Chien      await asyncCallback(transaction);
1494a7bfa1dSKudo Chien      await this.execAsync([{ sql: 'END;', args: [] }], false);
1504a7bfa1dSKudo Chien    } catch (e: unknown) {
1514a7bfa1dSKudo Chien      await this.execAsync([{ sql: 'ROLLBACK;', args: [] }], false);
1524a7bfa1dSKudo Chien      throw e;
1534a7bfa1dSKudo Chien    }
1544a7bfa1dSKudo Chien  }
155c52e83e3SKudo Chien
156c52e83e3SKudo Chien  // @ts-expect-error: properties that are added from websql
157c52e83e3SKudo Chien  version: string;
158c52e83e3SKudo Chien
159c52e83e3SKudo Chien  /**
160c52e83e3SKudo Chien   * Execute a database transaction.
161c52e83e3SKudo Chien   * @param callback A function representing the transaction to perform. Takes a Transaction
162c52e83e3SKudo Chien   * (see below) as its only parameter, on which it can add SQL statements to execute.
163c52e83e3SKudo Chien   * @param errorCallback Called if an error occurred processing this transaction. Takes a single
164c52e83e3SKudo Chien   * parameter describing the error.
165c52e83e3SKudo Chien   * @param successCallback Called when the transaction has completed executing on the database.
166c52e83e3SKudo Chien   */
167c52e83e3SKudo Chien  // @ts-expect-error: properties that are added from websql
168c52e83e3SKudo Chien  transaction(
169c52e83e3SKudo Chien    callback: SQLTransactionCallback,
170c52e83e3SKudo Chien    errorCallback?: SQLTransactionErrorCallback,
171c52e83e3SKudo Chien    successCallback?: () => void
172c52e83e3SKudo Chien  ): void;
173c52e83e3SKudo Chien
174c52e83e3SKudo Chien  // @ts-expect-error: properties that are added from websql
175c52e83e3SKudo Chien  readTransaction(
176c52e83e3SKudo Chien    callback: SQLTransactionCallback,
177c52e83e3SKudo Chien    errorCallback?: SQLTransactionErrorCallback,
178c52e83e3SKudo Chien    successCallback?: () => void
179c52e83e3SKudo Chien  ): void;
1802ee87287SSzymon20000}
1812ee87287SSzymon20000
18209ee9580SAlan Hughesfunction _serializeQuery(query: Query): Query | [string, any[]] {
18309ee9580SAlan Hughes  return Platform.OS === 'android'
18409ee9580SAlan Hughes    ? {
18509ee9580SAlan Hughes        sql: query.sql,
18609ee9580SAlan Hughes        args: query.args.map(_escapeBlob),
18709ee9580SAlan Hughes      }
18809ee9580SAlan Hughes    : [query.sql, query.args];
1892ee87287SSzymon20000}
1902ee87287SSzymon20000
1915cb26b36STomo Krajinafunction _deserializeResultSet(nativeResult): ResultSet | ResultSetError {
192b91f886fSJames Ide  const [errorMessage, insertId, rowsAffected, columns, rows] = nativeResult;
1932ee87287SSzymon20000  // TODO: send more structured error information from the native module so we can better construct
1942ee87287SSzymon20000  // a SQLException object
1952ee87287SSzymon20000  if (errorMessage !== null) {
1965cb26b36STomo Krajina    return { error: new Error(errorMessage) } as ResultSetError;
1972ee87287SSzymon20000  }
1982ee87287SSzymon20000
1992ee87287SSzymon20000  return {
2002ee87287SSzymon20000    insertId,
2012ee87287SSzymon20000    rowsAffected,
20235f78160SBartosz Kaszubowski    rows: rows.map((row) => zipObject(columns, row)),
2032ee87287SSzymon20000  };
2042ee87287SSzymon20000}
2052ee87287SSzymon20000
2062ee87287SSzymon20000function _escapeBlob<T>(data: T): T {
2072ee87287SSzymon20000  if (typeof data === 'string') {
2082ee87287SSzymon20000    /* eslint-disable no-control-regex */
2092ee87287SSzymon20000    return data
2102ee87287SSzymon20000      .replace(/\u0002/g, '\u0002\u0002')
2112ee87287SSzymon20000      .replace(/\u0001/g, '\u0001\u0002')
2122ee87287SSzymon20000      .replace(/\u0000/g, '\u0001\u0001') as any;
2132ee87287SSzymon20000    /* eslint-enable no-control-regex */
2142ee87287SSzymon20000  } else {
2152ee87287SSzymon20000    return data;
2162ee87287SSzymon20000  }
2172ee87287SSzymon20000}
2182ee87287SSzymon20000
2192ee87287SSzymon20000const _openExpoSQLiteDatabase = customOpenDatabase(SQLiteDatabase);
2202ee87287SSzymon20000
221abded2bbSBartosz Kaszubowski// @needsAudit @docsMissing
222abded2bbSBartosz Kaszubowski/**
223abded2bbSBartosz Kaszubowski * Open a database, creating it if it doesn't exist, and return a `Database` object. On disk,
224a7112dadSBartosz Kaszubowski * the database will be created under the app's [documents directory](./filesystem), i.e.
225abded2bbSBartosz Kaszubowski * `${FileSystem.documentDirectory}/SQLite/${name}`.
226abded2bbSBartosz Kaszubowski * > The `version`, `description` and `size` arguments are ignored, but are accepted by the function
227abded2bbSBartosz Kaszubowski * for compatibility with the WebSQL specification.
228abded2bbSBartosz Kaszubowski * @param name Name of the database file to open.
229abded2bbSBartosz Kaszubowski * @param version
230abded2bbSBartosz Kaszubowski * @param description
231abded2bbSBartosz Kaszubowski * @param size
232abded2bbSBartosz Kaszubowski * @param callback
233abded2bbSBartosz Kaszubowski * @return
234abded2bbSBartosz Kaszubowski */
2352ee87287SSzymon20000export function openDatabase(
2362ee87287SSzymon20000  name: string,
2372ee87287SSzymon20000  version: string = '1.0',
2382ee87287SSzymon20000  description: string = name,
2392ee87287SSzymon20000  size: number = 1,
2404a7bfa1dSKudo Chien  callback?: (db: SQLiteDatabase) => void
2414a7bfa1dSKudo Chien): SQLiteDatabase {
2422ee87287SSzymon20000  if (name === undefined) {
2432ee87287SSzymon20000    throw new TypeError(`The database name must not be undefined`);
2442ee87287SSzymon20000  }
2450002abeaSSzymon20000  const db = _openExpoSQLiteDatabase(name, version, description, size, callback);
2463f4e5fe5SKudo Chien  db.exec = db._db.exec.bind(db._db);
24750e16f8cSAlan Hughes  db.execRawQuery = db._db.execRawQuery.bind(db._db);
2484a7bfa1dSKudo Chien  db.execAsync = db._db.execAsync.bind(db._db);
2494a7bfa1dSKudo Chien  db.closeAsync = db._db.closeAsync.bind(db._db);
2506c4baee8SAlan Hughes  db.closeSync = db._db.closeSync.bind(db._db);
25174e0b8dfSAlan Hughes  db.onDatabaseChange = db._db.onDatabaseChange.bind(db._db);
2523f4e5fe5SKudo Chien  db.deleteAsync = db._db.deleteAsync.bind(db._db);
2534a7bfa1dSKudo Chien  db.transactionAsync = db._db.transactionAsync.bind(db._db);
2543f4e5fe5SKudo Chien  return db;
2552ee87287SSzymon20000}
2564a7bfa1dSKudo Chien
2574a7bfa1dSKudo Chien/**
2584a7bfa1dSKudo Chien * Internal data structure for the async transaction API.
2594a7bfa1dSKudo Chien * @internal
2604a7bfa1dSKudo Chien */
2614a7bfa1dSKudo Chienexport class ExpoSQLTransactionAsync implements SQLTransactionAsync {
2628a424bebSJames Ide  constructor(
2638a424bebSJames Ide    private readonly db: SQLiteDatabase,
2648a424bebSJames Ide    private readonly readOnly: boolean
2658a424bebSJames Ide  ) {}
2664a7bfa1dSKudo Chien
267*c4573fffSKudo Chien  async executeSqlAsync(sqlStatement: string, args?: (number | string)[]): Promise<ResultSet> {
2684a7bfa1dSKudo Chien    const resultSets = await this.db.execAsync(
2694a7bfa1dSKudo Chien      [{ sql: sqlStatement, args: args ?? [] }],
2704a7bfa1dSKudo Chien      this.readOnly
2714a7bfa1dSKudo Chien    );
272*c4573fffSKudo Chien    const result = resultSets[0];
273*c4573fffSKudo Chien    if (isResultSetError(result)) {
274*c4573fffSKudo Chien      throw result.error;
2754a7bfa1dSKudo Chien    }
276*c4573fffSKudo Chien    return result;
277*c4573fffSKudo Chien  }
278*c4573fffSKudo Chien}
279*c4573fffSKudo Chien
280*c4573fffSKudo Chienfunction isResultSetError(result: ResultSet | ResultSetError): result is ResultSetError {
281*c4573fffSKudo Chien  return 'error' in result;
2824a7bfa1dSKudo Chien}
283