Skip to content

Commit

Permalink
Merge pull request #2443 from drizzle-team/expo
Browse files Browse the repository at this point in the history
[SQLite]: Add live queries for expo-sqlite
  • Loading branch information
AndriiSherman committed Jun 4, 2024
2 parents e922211 + 7a4cc2d commit 3513d0a
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 3 deletions.
33 changes: 33 additions & 0 deletions changelogs/drizzle-orm/0.31.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# New Features

## Live Queries 馃帀

As of `v0.31.1` Drizzle ORM now has native support for Expo SQLite Live Queries!
We've implemented a native `useLiveQuery` React Hook which observes necessary database changes and automatically re-runs database queries. It works with both SQL-like and Drizzle Queries:

```tsx
import { useLiveQuery, drizzle } from 'drizzle-orm/expo-sqlite';
import { openDatabaseSync } from 'expo-sqlite/next';
import { users } from './schema';
import { Text } from 'react-native';

const expo = openDatabaseSync('db.db');
const db = drizzle(expo);

const App = () => {
// Re-renders automatically when data changes
const { data } = useLiveQuery(db.select().from(users));

// const { data, error, updatedAt } = useLiveQuery(db.query.users.findFirst());
// const { data, error, updatedAt } = useLiveQuery(db.query.users.findMany());


return <Text>{JSON.stringify(data)}</Text>;
};

export default App;
```

We've intentionally not changed the API of ORM itself to stay with conventional React Hook API, so we have `useLiveQuery(databaseQuery)` as opposed to `db.select().from(users).useLive()` or `db.query.users.useFindMany()`

We've also decided to provide `data`, `error` and `updatedAt` fields as a result of hook for concise explicit error handling following practices of `React Query` and `Electric SQL`
2 changes: 1 addition & 1 deletion drizzle-orm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "drizzle-orm",
"version": "0.31.0",
"version": "0.31.1",
"description": "Drizzle ORM package for SQL databases",
"type": "module",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions drizzle-orm/src/expo-sqlite/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './driver.ts';
export * from './query.ts';
export * from './session.ts';
53 changes: 53 additions & 0 deletions drizzle-orm/src/expo-sqlite/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { addDatabaseChangeListener } from 'expo-sqlite/next';
import { useEffect, useState } from 'react';
import { is, SQL, Subquery } from '~/index.ts';
import type { AnySQLiteSelect } from '~/sqlite-core/index.ts';
import { getTableConfig, getViewConfig, SQLiteTable, SQLiteView } from '~/sqlite-core/index.ts';
import { SQLiteRelationalQuery } from '~/sqlite-core/query-builders/query.ts';

export const useLiveQuery = <T extends Pick<AnySQLiteSelect, '_' | 'then'> | SQLiteRelationalQuery<'sync', unknown>>(
query: T,
) => {
const [data, setData] = useState<Awaited<T>>(
(is(query, SQLiteRelationalQuery) && query.mode === 'first' ? undefined : []) as Awaited<T>,
);
const [error, setError] = useState<Error>();
const [updatedAt, setUpdatedAt] = useState<Date>();

useEffect(() => {
const entity = is(query, SQLiteRelationalQuery) ? query.table : (query as AnySQLiteSelect).config.table;

if (is(entity, Subquery) || is(entity, SQL)) {
setError(new Error('Selecting from subqueries and SQL are not supported in useLiveQuery'));
return;
}

let listener: ReturnType<typeof addDatabaseChangeListener> | undefined;

const handleData = (data: any) => {
setData(data);
setUpdatedAt(new Date());
};

query.then(handleData).catch(setError);

if (is(entity, SQLiteTable) || is(entity, SQLiteView)) {
const config = is(entity, SQLiteTable) ? getTableConfig(entity) : getViewConfig(entity);
listener = addDatabaseChangeListener(({ tableName }) => {
if (config.name === tableName) {
query.then(handleData).catch(setError);
}
});
}

return () => {
listener?.remove();
};
}, []);

return {
data,
error,
updatedAt,
} as const;
};
3 changes: 2 additions & 1 deletion drizzle-orm/src/sqlite-core/query-builders/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ export class SQLiteRelationalQuery<TType extends 'sync' | 'async', TResult> exte
private fullSchema: Record<string, unknown>,
private schema: TablesRelationalConfig,
private tableNamesMap: Record<string, string>,
private table: SQLiteTable,
/** @internal */
public table: SQLiteTable,
private tableConfig: TableRelationalConfig,
private dialect: SQLiteDialect,
private session: SQLiteSession<'sync' | 'async', unknown, Record<string, unknown>, TablesRelationalConfig>,
Expand Down
5 changes: 4 additions & 1 deletion integration-tests/tests/imports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ it('dynamic imports check for CommonJS', async () => {
const promises: ProcessPromise[] = [];
for (const [i, key] of Object.keys(pj['exports']).entries()) {
const o1 = path.join('drizzle-orm', key);
if (o1.startsWith('drizzle-orm/pglite')) {
if (o1.startsWith('drizzle-orm/pglite') || o1.startsWith('drizzle-orm/expo-sqlite')) {
continue;
}
fs.writeFileSync(`${IMPORTS_FOLDER}/imports_${i}.cjs`, 'requ');
Expand All @@ -43,6 +43,9 @@ it('dynamic imports check for ESM', async () => {
const promises: ProcessPromise[] = [];
for (const [i, key] of Object.keys(pj['exports']).entries()) {
const o1 = path.join('drizzle-orm', key);
if (o1.startsWith('drizzle-orm/expo-sqlite')) {
continue;
}
fs.writeFileSync(`${IMPORTS_FOLDER}/imports_${i}.mjs`, 'imp');
fs.appendFileSync(`${IMPORTS_FOLDER}/imports_${i}.mjs`, 'ort "' + o1 + '"\n', {});
promises.push($`node ${IMPORTS_FOLDER}/imports_${i}.mjs`.nothrow());
Expand Down

0 comments on commit 3513d0a

Please sign in to comment.