Skip to content

Commit

Permalink
[sqlite] add experimental promise support (#23109)
Browse files Browse the repository at this point in the history
# Why

fixes #13357
close ENG-8685

# How

- originally, the callbacks for websql doesn't support Promise. when people using either 'async/async` or '.then` inside the callback, the statement will be executed after the "transaction end" statement.
- we should we low level control without websql at all.
- introduce low-level `execAsync`
- introduce `transactionAsync`
  usage
  ```tsx
  const db = SQLite.openDatabase('dbName', version);

  const readOnly = true;
  await db.transactionAsync(async tx => {
    const result = await tx.executeSqlAsync('SELECT COUNT(*) FROM USERS', []);
    console.log('Count:', result.rows[0]['COUNT(*)']);
  }, readOnly);
  ```
  note that the result is the [`ResultSet` type](https://github.com/expo/expo/blob/065419647694cf9341261bc7ac614d05e4bac27d/packages/expo-sqlite/src/SQLite.types.ts#L167-L177) but not the [`SQLResultSet` type](https://github.com/expo/expo/blob/065419647694cf9341261bc7ac614d05e4bac27d/packages/expo-sqlite/src/SQLite.types.ts#L93C18-L121). people can access the result items by `rows[0]` rather than `rows.item(0)`. i was thinking to deprecate websql somehow and it doesn't make sense to wrap the result by the [`WebSQLResultSet` again](https://github.com/nolanlawson/node-websql/blob/b3e48284572108feff1cd019dc7f13c1d8aa34b2/lib/websql/WebSQLTransaction.js#L12-L36)

# Test Plan

add some SQLite Async unit tests and test suite ci should be passed
  • Loading branch information
Kudo committed Jun 27, 2023
1 parent b067ef4 commit 4a7bfa1
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 20 deletions.
155 changes: 155 additions & 0 deletions apps/test-suite/tests/SQLite.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,4 +455,159 @@ export function test(t) {
});
}
});

if (Platform.OS !== 'web') {
t.describe('SQLiteAsync', () => {
const throws = async (run) => {
let error = null;
try {
await run();
} catch (e) {
error = e;
}
t.expect(error).toBeTruthy();
};

t.it('should support async transaction', async () => {
const db = SQLite.openDatabase('test.db');

// create table
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('DROP TABLE IF EXISTS Users;', []);
await tx.executeSqlAsync(
'CREATE TABLE IF NOT EXISTS Users (user_id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(64));',
[]
);
});

// fetch data from network
async function fakeUserFetcher(userID) {
switch (userID) {
case 1: {
return Promise.resolve('Tim Duncan');
}
case 2: {
return Promise.resolve('Manu Ginobili');
}
case 3: {
return Promise.resolve('Nikhilesh Sigatapu');
}
default: {
return null;
}
}
}

const userName = await fakeUserFetcher(1);
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', [userName]);

const currentUser = (await tx.executeSqlAsync('SELECT * FROM Users LIMIT 1')).rows[0]
.name;
t.expect(currentUser).toEqual('Tim Duncan');
});
});

t.it('should support Promise.all', async () => {
const db = SQLite.openDatabase('test.db');

// create table
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('DROP TABLE IF EXISTS Users;', []);
await tx.executeSqlAsync(
'CREATE TABLE IF NOT EXISTS Users (user_id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(64));',
[]
);
});

await db.transactionAsync(async (tx) => {
await Promise.all([
tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['aaa']),
tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['bbb']),
tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['ccc']),
]);

const recordCount = (await tx.executeSqlAsync('SELECT COUNT(*) FROM Users')).rows[0][
'COUNT(*)'
];
t.expect(recordCount).toEqual(3);
});
});

t.it(
'should return `could not prepare ...` error when having write statements in readOnly transaction',
async () => {
const db = SQLite.openDatabase('test.db');

// create table in readOnly transaction
await db.transactionAsync(async (tx) => {
const result = await tx.executeSqlAsync('DROP TABLE IF EXISTS Users;', []);
t.expect(result.error).toBeDefined();
t.expect(result.error.message).toContain('could not prepare ');
}, true);
}
);

t.it('should rollback transaction when exception happens inside a transaction', async () => {
const db = SQLite.openDatabase('test.db');

// create table
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('DROP TABLE IF EXISTS Users;', []);
await tx.executeSqlAsync(
'CREATE TABLE IF NOT EXISTS Users (user_id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(64));',
[]
);
});
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['aaa']);
});
await db.transactionAsync(async (tx) => {
const recordCount = (await tx.executeSqlAsync('SELECT COUNT(*) FROM Users')).rows[0][
'COUNT(*)'
];
t.expect(recordCount).toEqual(1);
}, true);

await throws(() =>
db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['bbb']);
await tx.executeSqlAsync('INSERT INTO Users (name) VALUES (?)', ['ccc']);
// exeuting invalid sql statement will throw an exception
await tx.executeSqlAsync(undefined);
})
);

await db.transactionAsync(async (tx) => {
const recordCount = (await tx.executeSqlAsync('SELECT COUNT(*) FROM Users')).rows[0][
'COUNT(*)'
];
t.expect(recordCount).toEqual(1);
}, true);
});

t.it('should support async PRAGMA statements', async () => {
const db = SQLite.openDatabase('test.db');
await db.transactionAsync(async (tx) => {
await tx.executeSqlAsync('DROP TABLE IF EXISTS SomeTable;', []);
await tx.executeSqlAsync(
'CREATE TABLE IF NOT EXISTS SomeTable (id INTEGER PRIMARY KEY NOT NULL, name VARCHAR(64));',
[]
);
// a result-returning pragma
let results = await tx.executeSqlAsync('PRAGMA table_info(SomeTable);', []);
t.expect(results.rows.length).toEqual(2);
t.expect(results.rows[0].name).toEqual('id');
t.expect(results.rows[1].name).toEqual('name');
// a no-result pragma
await tx.executeSqlAsync('PRAGMA case_sensitive_like = true;', []);
// a setter/getter pragma
await tx.executeSqlAsync('PRAGMA user_version = 123;', []);
results = await tx.executeSqlAsync('PRAGMA user_version;', []);
t.expect(results.rows.length).toEqual(1);
t.expect(results.rows[0].user_version).toEqual(123);
});
});
}); // t.describe('SQLiteAsync')
}
}
17 changes: 14 additions & 3 deletions docs/pages/versions/unversioned/sdk/sqlite.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,27 @@ async function openDatabase(pathToDatabaseFile: string): Promise<SQLite.WebSQLDa

</Step>

### Executing statements with an async transaction

```js
const db = SQLite.openDatabase('dbName', version);

const readOnly = true;
await db.transactionAsync(async tx => {
const result = await tx.executeSqlAsync('SELECT COUNT(*) FROM USERS', []);
console.log('Count:', result.rows[0]['COUNT(*)']);
}, readOnly);
```

### Executing statements outside of a transaction

> You should use this kind of execution only when it is necessary. For instance, when code is a no-op within transactions. Example: `PRAGMA foreign_keys = ON;`.
```js
const db = SQLite.openDatabase('dbName', version);

db.exec([{ sql: 'PRAGMA foreign_keys = ON;', args: [] }], false, () =>
console.log('Foreign keys turned on')
);
await db.execAsync([{ sql: 'PRAGMA foreign_keys = ON;', args: [] }], false);
console.log('Foreign keys turned on');
```

## API
Expand Down
2 changes: 1 addition & 1 deletion docs/public/static/data/unversioned/expo-sqlite.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/expo-sqlite/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### 🎉 New features

- Migrated Android codebase to Expo Modules API. ([#23115](https://github.com/expo/expo/pull/23115) by [@alanjhughes](https://github.com/alanjhughes))
- Added experimental `Promise` based `execAsync` and `transactionAsync` functions. ([#23109](https://github.com/expo/expo/pull/23109) by [@kudo](https://github.com/kudo))

### 🐛 Bug fixes

Expand Down
47 changes: 45 additions & 2 deletions packages/expo-sqlite/build/SQLite.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo-sqlite/build/SQLite.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 63 additions & 3 deletions packages/expo-sqlite/build/SQLite.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4a7bfa1

Please sign in to comment.