Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sound storage with SQLite #5157

Closed
wants to merge 106 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
ae42d72
wip storage [nfc]: Make src/storage/ for below-Redux storage layers
gnprice Dec 3, 2021
7458829
add TODO to get JS out of compression
gnprice Dec 3, 2021
5a52741
wip deps: Add expo-sqlite
gnprice Dec 3, 2021
f35a4de
deps: Add types for expo-sqlite
gnprice Dec 3, 2021
b92b823
wip expo-sqlite libdef: a ro array
gnprice Dec 3, 2021
e4a173e
wip promisify expo-sqlite
gnprice Dec 3, 2021
a3c3614
wip sqlite nfc: query clean up interface
gnprice Dec 3, 2021
22fbc6d
wip sketch AsyncStorage-on-SQLite
gnprice Dec 3, 2021
ef3148e
wip migrate from legacy
gnprice Dec 3, 2021
37d32a8
WIP switch to our new SQLite-based AsyncStorage
gnprice Dec 7, 2021
eeb2a4c
wip downgrade expo-sqlite, to stick for now with "unimodules"
gnprice Dec 7, 2021
260d588
wip expo-sqlite auto-added itself to generated file
gnprice Dec 7, 2021
d20d6bb
WIP start AsyncStorage tests
gnprice Dec 8, 2021
7c299bc
wip debugging
gnprice Dec 8, 2021
bf77f3c
wip test expo-sqlite directly
gnprice Dec 9, 2021
5db7d8b
and suspicion falls on custom immediate
gnprice Dec 9, 2021
a040ed5
wip deps: Add sqlite3
gnprice Dec 9, 2021
68c4491
smoke-test sqlite3
gnprice Dec 9, 2021
d5bc07f
WIP try mocking underlying native module
gnprice Dec 9, 2021
5a277ef
WIP resume mock-whole-library strategy
gnprice Dec 9, 2021
9538241
wip more mock
gnprice Dec 9, 2021
c679291
and it works!
gnprice Dec 9, 2021
ba0d502
and getItem and setItem work!
gnprice Dec 9, 2021
b696d01
cut debugging and detritus
gnprice Dec 9, 2021
41e6a42
reorder tests
gnprice Dec 9, 2021
165b8d0
complete a reasonable minimal AsyncStorage test suite
gnprice Dec 9, 2021
8f053d5
and fix the immediate issue
gnprice Dec 9, 2021
0be8aea
fixup expo-sqlite smoke test
gnprice Dec 9, 2021
d2fbb27
wip ZulipAsyncStorage tests mock the right AsyncStorage
gnprice Dec 9, 2021
cac1d43
wip fix ZulipAsyncStorage test re setItem return
gnprice Dec 9, 2021
79ffe6c
wip add fixmes and lint fixes
gnprice Dec 9, 2021
226834a
nfc clean up _exec a bit
gnprice Dec 9, 2021
893ca04
comment db in memory
gnprice Dec 9, 2021
88007bb
update comments in ZulipAsyncStorage test
gnprice Dec 9, 2021
d8c3252
add TODO comment to try sending mock-expo-sqlite upstream
gnprice Dec 9, 2021
3dc8acb
wip add tests for migration from legacy
gnprice Dec 17, 2021
8cad43e
AsyncStorage [nfc]: Convert to non-static methods
gnprice Dec 17, 2021
cd919d5
wip AsyncStorage: rework migration structure
gnprice Dec 17, 2021
cd7ea1c
wip AsyncStorage: generalize migrations scheme
gnprice Dec 17, 2021
b8f2a76
AsyncStorage [nfc]: Work around a Flow soundness bug
gnprice Dec 22, 2021
adeda02
storage [nfc]: Rename CompressedAsyncStorage <- ZulipAsyncStorage
gnprice Dec 21, 2021
718843c
wip storage [nfc]: Make CompressedAsyncStorage methods non-static
gnprice Dec 21, 2021
f88a1b4
storage [nfc]: Pass migrations to CompressedAsyncStorage
gnprice Dec 21, 2021
f3564bf
storage [nfc]: Factor out de/compression from getItem/setItem/etc
gnprice Dec 21, 2021
cf621b7
storage [nfc]: Add CompressedMigration
gnprice Dec 21, 2021
b13c7b3
WIP storage: Sketch migration for existing migrations
gnprice Dec 21, 2021
b9de3e6
wip sqlite types: Export type SQLTransaction
gnprice Dec 22, 2021
1cad19a
wip AsyncStorage: migrations get tx, not db
gnprice Dec 22, 2021
ec6977e
storage [nfc]: Pull our migrations into their own module
gnprice Dec 22, 2021
b2fa2a3
migrations: Handle old implicit migrations explicitly
gnprice Dec 22, 2021
2ba209f
wip move sketch to migrations.js
gnprice Dec 22, 2021
a156810
WIP CompressedMigration actually pass encode/decode to cb
gnprice Dec 22, 2021
566ae80
wip migrations add migrationLegacyRollup
gnprice Dec 22, 2021
79e8169
wip CompressedAsyncStorage nfc pull null-handling out of decode
gnprice Dec 22, 2021
b8154ce
wip CompressedAsyncStorage types: cleaner type export
gnprice Dec 22, 2021
aed3a8a
wip storage nfc: have store.js construct AsyncStorage instance
gnprice Dec 22, 2021
c809a4d
wip migrations: Run legacy-migration rollup as new-style migration
gnprice Dec 22, 2021
b26ab4c
wip migration [nfc]: Delete redux-persist-migrate
gnprice Dec 22, 2021
59362ab
persist: Drop complications from redux-persist-migrate
gnprice Dec 22, 2021
8923565
persist [nfc]: Cut now-disused "purge" feature
gnprice Dec 22, 2021
b8b1f50
wip migrations: Be sure to only act on storeKeys
gnprice Dec 22, 2021
d2c03bb
migrations [nfc]: Drop legacy dropCache migrations
gnprice Dec 22, 2021
9b0c4f6
wip migrations add mkMigration
gnprice Dec 22, 2021
4c88219
wip mkMigration add delete
gnprice Dec 22, 2021
cf6e965
WIP migrationSplitSettings, as mkMigration demo
gnprice Dec 22, 2021
f211c06
wip pull encode/decode up
gnprice Dec 22, 2021
1d4a164
wip migrations nfc: mkMigration handle serialize and encodeKey
gnprice Dec 22, 2021
5039a4c
wip add mkSimpleMigration
gnprice Dec 22, 2021
8fd4c47
WIP demo migration adding accountId
gnprice Dec 22, 2021
ca4015b
wip add devWipe
gnprice Dec 22, 2021
b495f50
WIP start testing migrationLegacyRollup
gnprice Dec 22, 2021
0a1bb91
wip debug expo-sqlite transactions
gnprice Dec 22, 2021
ace0bb1
sqlite [nfc]: Move sqlite tests to their own file
gnprice Dec 22, 2021
3f3aee2
wip sqlite add tests -- one failing, on transaction
gnprice Dec 22, 2021
7d1197f
and fix workaround
gnprice Dec 23, 2021
4370cfb
oops fix test expectations
gnprice Dec 23, 2021
37b9de8
Revert "wip debug expo-sqlite transactions"
gnprice Dec 23, 2021
2ce38dd
lolsob use the workaround generally; fixes test
gnprice Dec 23, 2021
921a558
WIP add failing test showing readTransaction needs workaround too
gnprice Dec 23, 2021
faa6d8c
use workaround for readTransaction, too
gnprice Dec 23, 2021
3ede788
sqlite tests [nfc]: Clean up db between test cases
gnprice Dec 23, 2021
be807b8
sqlite tests: Demo the broken asynchrony handling, in detail
gnprice Dec 23, 2021
b347de8
WIP try to make similar test for our version
gnprice Dec 23, 2021
f9f04d2
test now working, but messy
gnprice Dec 23, 2021
4cbe581
clean up that test
gnprice Dec 23, 2021
291271c
simplify the known-failure test a bit
gnprice Dec 23, 2021
311c6eb
WIP discover another expo-sqlite issue, in failing txn
gnprice Dec 23, 2021
e50e96c
fixup mock-expo-sqlite: add exec
gnprice Dec 23, 2021
3ff382d
wip devWipe avoid re-initializing first
gnprice Dec 23, 2021
001454d
wip sqlite txn failure: at least reject the outer promise
gnprice Dec 23, 2021
5a485e3
WIP attempt to handle mid-txn failure; partial success?
gnprice Dec 23, 2021
3651f6b
Revert "WIP attempt to handle mid-txn failure; partial success?"
gnprice Dec 23, 2021
632554c
migrations tests: Start on some substantive tests
gnprice Dec 23, 2021
3860be7
make that more compact
gnprice Dec 23, 2021
3612cec
migrations tests: Cover all legacy migrations
gnprice Dec 23, 2021
2fb8d0d
fixup! make that more compact
gnprice Dec 23, 2021
1014654
wip migration tests: organize a bit more
gnprice Dec 23, 2021
2a2dc37
wip migration tests: clean up base versions
gnprice Dec 23, 2021
6dfe97c
wip migration tests: clean up a bit more
gnprice Dec 23, 2021
57b6dce
wip migrations tests [nfc]: Reorganize toward testing future migrations
gnprice Dec 23, 2021
22bdb33
migrations tests [nfc]: Generalize prep and fetch
gnprice Dec 23, 2021
a2e3415
wip migration tests: Test demo migrations for split-settings and acco…
gnprice Dec 23, 2021
ab60821
WIP start exploring callback-style txns; but it's a real pain for mig…
gnprice Dec 23, 2021
a48834d
Revert "WIP start exploring callback-style txns; but it's a real pain…
gnprice Dec 23, 2021
e16b298
migrations: Fix buggy old migration 22, on PM narrow keys
gnprice Dec 24, 2021
d490f2a
squash mkSimpleMigration: simplify out startVersion
gnprice Dec 24, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public List<Package> getPackageList() {
new expo.modules.filesystem.FileSystemPackage(),
new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.screenorientation.ScreenOrientationPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);
}
Expand Down
7 changes: 0 additions & 7 deletions docs/THIRDPARTY
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ Files: *
Copyright: 2016-2021 Kandra Labs, Inc., and contributors
License: Apache-2

Files: src/redux-persist-migrate/index.js
Copyright: 2016-2017 Zack Story and contributors, 2018-2021 Kandra Labs, Inc.,
and contributors
License: Apache-2
Comment:
Upstream is licensed as MIT.

Files: src/third/react-native/*
Copyright: Facebook, Inc., and its affiliates
License: MIT
Expand Down
197 changes: 197 additions & 0 deletions flow-typed/expo-sqlite_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// `flowgen --no-inexact --interface-records node_modules/expo-sqlite/build/SQLite.types.d.ts`
// with s/export type/declare export type/g, and as noted with "Zulip fix" below
declare module 'expo-sqlite/build/SQLite.types' {
/**
* Flowtype definitions for SQLite.types
* Generated by Flowgen from a Typescript Definition
* Flowgen v1.14.1
*/

declare export type Window = {|
openDatabase?: (
name: string,
version: string,
displayName: string,
estimatedSize: number,
creationCallback?: DatabaseCallback
) => Database,
|};
declare export type DatabaseCallback = (database: Database) => void;
/**
* `Database` objects are returned by calls to `SQLite.openDatabase()`. Such an object represents a
* connection to a database on your device.
*/
declare export type Database = {|
version: string,

/**
* Execute a database transaction.
* @param callback A function representing the transaction to perform. Takes a Transaction
* (see below) as its only parameter, on which it can add SQL statements to execute.
* @param errorCallback Called if an error occurred processing this transaction. Takes a single
* parameter describing the error.
* @param successCallback Called when the transaction has completed executing on the database.
*/
transaction(
callback: SQLTransactionCallback,
errorCallback?: SQLTransactionErrorCallback,
successCallback?: () => void
): void,
readTransaction(
callback: SQLTransactionCallback,
errorCallback?: SQLTransactionErrorCallback,
successCallback?: () => void
): void,
|};
declare export type SQLTransactionCallback = (transaction: SQLTransaction) => void;
declare export type SQLTransactionErrorCallback = (error: SQLError) => void;
/**
* A `SQLTransaction` object is passed in as a parameter to the `callback` parameter for the
* `db.transaction()` method on a `Database` (see above). It allows enqueuing SQL statements to
* perform in a database transaction.
*/
declare export type SQLTransaction = {|
/**
* Enqueue a SQL statement to execute in the transaction. Authors are strongly recommended to make
* use of the `?` placeholder feature of the method to avoid against SQL injection attacks, and to
* never construct SQL statements on the fly.
* @param sqlStatement A string containing a database query to execute expressed as SQL. The string
* may contain `?` placeholders, with values to be substituted listed in the `arguments` parameter.
* @param args An array of values (numbers or strings) to substitute for `?` placeholders in the
* SQL statement.
* @param callback Called when the query is successfully completed during the transaction. Takes
* two parameters: the transaction itself, and a `ResultSet` object (see below) with the results
* of the query.
* @param errorCallback Called if an error occurred executing this particular query in the
* transaction. Takes two parameters: the transaction itself, and the error object.
*/
executeSql(
sqlStatement: string,
args?: $ReadOnlyArray<number | string>, // Zulip fix
callback?: SQLStatementCallback,
errorCallback?: SQLStatementErrorCallback
): void,
|};
declare export type SQLStatementCallback = (
transaction: SQLTransaction,
resultSet: SQLResultSet
) => void;
declare export type SQLStatementErrorCallback = (
transaction: SQLTransaction,
error: SQLError
) => boolean;
declare export type SQLResultSet = {|
/**
* The row ID of the row that the SQL statement inserted into the database, if a row was inserted.
*/
insertId?: number,

/**
* The number of rows that were changed by the SQL statement.
*/
rowsAffected: number,
rows: SQLResultSetRowList,
|};
declare export type SQLResultSetRowList = {|
/**
* The number of rows returned by the query.
*/
length: number,

/**
* Returns the row with the given `index`. If there is no such row, returns `null`.
* @param index Index of row to get.
*/
item(index: number): any,

/**
* The actual array of rows returned by the query. Can be used directly instead of
* getting rows through rows.item().
*/
_array: any[],
|};
declare export class SQLError {
static UNKNOWN_ERR: number;
static DATABASE_ERR: number;
static VERSION_ERR: number;
static TOO_LARGE_ERR: number;
static QUOTA_ERR: number;
static SYNTAX_ERR: number;
static CONSTRAINT_ERR: number;
static TIMEOUT_ERR: number;
code: number;
message: string;
}
declare export type WebSQLDatabase = {|
...$Exact<Database>,

exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void,
|};
declare export type Query = {|
sql: string,
args: mixed[],
|};
declare export type ResultSetError = {|
error: Error,
|};
/**
* `ResultSet` objects are returned through second parameter of the `success` callback for the
* `tx.executeSql()` method on a `SQLTransaction` (see above).
*/
declare export type ResultSet = {|
/**
* The row ID of the row that the SQL statement inserted into the database, if a row was inserted.
*/
insertId?: number,

/**
* The number of rows that were changed by the SQL statement.
*/
rowsAffected: number,
rows: {|
[column: string]: any,
|}[],
|};
declare export type SQLiteCallback = (
error?: Error | null,
resultSet?: (ResultSetError | ResultSet)[]
) => void;
}

// `flowgen --no-inexact --interface-records node_modules/expo-sqlite/build/SQLite.d.ts`
// with imports fixed up
declare module 'expo-sqlite/build/SQLite' {
/**
* Flowtype definitions for SQLite
* Generated by Flowgen from a Typescript Definition
* Flowgen v1.14.1
*/

import type { WebSQLDatabase } from "expo-sqlite/build/SQLite.types";

/**
* Open a database, creating it if it doesn't exist, and return a `Database` object. On disk,
* the database will be created under the app's [documents directory](../filesystem), i.e.
* `${FileSystem.documentDirectory}/SQLite/${name}`.
* > The `version`, `description` and `size` arguments are ignored, but are accepted by the function
* for compatibility with the WebSQL specification.
* @param name Name of the database file to open.
* @param version
* @param description
* @param size
* @param callback
* @return
*/
declare export function openDatabase(
name: string,
version?: string,
description?: string,
size?: number,
callback?: (db: WebSQLDatabase) => void
): WebSQLDatabase;
}

declare module 'expo-sqlite' {
declare export * from 'expo-sqlite/build/SQLite';
declare export * from 'expo-sqlite/build/SQLite.types';
}
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const transformModulesWhitelist = [
'expo-apple-authentication',
'expo-application',
'expo-sqlite',
'expo-web-browser',
'react-native',
'@react-native',
Expand Down
4 changes: 4 additions & 0 deletions jest/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ jest.mock('react-native', () => {
return ReactNative;
});

jest.mock('immediate', () => cb => Promise.resolve().then(cb));

jest.mock('expo-sqlite', () => require('./mock-expo-sqlite'));

/**
* Boring mocks
*
Expand Down
112 changes: 112 additions & 0 deletions jest/mock-expo-sqlite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// @flow strict-local
//
// TODO: this should probably go to expo-sqlite upstream.
//
// Not really so much a "mock" as a fourth platform-specific implementation:
// this one for Node (and therefore for Jest), complementing upstream's for
// Android, iOS, and web.
//
// OK, well, it's a mock in one respect: the databases are all in-memory.

import type { Query, SQLiteCallback, WebSQLDatabase } from 'expo-sqlite';

// We import this dependency of expo-sqlite directly, because we're
// substituting for the expo-sqlite implementation that uses it. (In a
// future where expo-sqlite no longer used this dependency, we'd want to
// make a similar change here, not keep using it.)
/* eslint-disable import/no-extraneous-dependencies */
// $FlowFixMe[untyped-import]
import customOpenDatabase from '@expo/websql/custom';

// $FlowFixMe[untyped-import]
import sqlite3 from 'sqlite3';

/* eslint-disable no-underscore-dangle */
/* eslint-disable no-void */

const dbs = new Map();

function openDb(name: string) {
const db = dbs.get(name);
if (db != null) {
return db;
}

const newDb = new sqlite3.Database(':memory:');
dbs.set(name, newDb);
return newDb;
}

class SQLiteDatabase {
_db: $FlowFixMe;
_closed: boolean = false;

constructor(name: string) {
this._db = openDb(name);
}

exec(queries: Query[], readOnly: boolean, callback: SQLiteCallback): void {
if (this._closed) {
throw new Error('already closed');
}

void this._exec(queries, callback);
}

async _exec(queries, callback) {
try {
const results = [];
for (const { sql, args } of queries) {
/* eslint-disable no-shadow */
const rows = await new Promise((resolve, reject) =>
this._db.all(sql, ...args, (err, rows) => (err ? reject(err) : resolve(rows))),
);

// TODO get rowsAffected and insertId. This will mean calling
// `this._db.run` instead of `this._db.all`:
// https://github.com/mapbox/node-sqlite3/wiki/API#databaserunsql-param--callback
// which on the other hand will mean it doesn't return rows.
//
// The way the Android implementation makes the equivalent choice
// is by checking if the statement looks like a SELECT.
// The iOS implementation uses the SQLite C API, and ends up looking
// more sensible.
// Given what node-sqlite3 gives us, our best strategy is probably
// the one in the Android implementation.
results.push({ rowsAffected: 0, rows });
}

callback(null, results);
} catch (e) {
callback(e);
}
}

close() {
this._closed = true;
this._db.close();
}
}

/** Not found in expo-sqlite itself, but helpful for tests. */
export function deleteDatabase(name: string) {
const db = dbs.get(name);
if (!db) {
return;
}
db.close();
dbs.delete(name);
}

export function openDatabase(
name: string,
version?: string,
description?: string,
size?: number,
callback?: (db: WebSQLDatabase) => void,
): WebSQLDatabase {
const db = customOpenDatabase(SQLiteDatabase)(name, version, description, size, callback);
db._db = new SQLiteDatabase(name);
db.exec = (...args) => db._db.exec(...args);
return db;
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"expo-apple-authentication": "^3.2.1",
"expo-application": "^3.2.0",
"expo-screen-orientation": "^3.3.0",
"expo-sqlite": "9.1.0",
"expo-web-browser": "^9.1.0",
"immutable": "^4.0.0-rc.12",
"invariant": "^2.2.4",
Expand Down Expand Up @@ -114,6 +115,7 @@
"react-test-renderer": "17.0.1",
"redux-mock-store": "^1.5.1",
"rollup": "^2.26.5",
"sqlite3": "^5.0.2",
"typescript": "~3.8.3",
"yarn-deduplicate": "^3.0.0"
}
Expand Down
Loading