Skip to content

Commit e731bd5

Browse files
committed
Merge branch 'main' into poc-sqljs
2 parents dabf474 + c7d2b53 commit e731bd5

90 files changed

Lines changed: 5466 additions & 1289 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/little-bananas-fetch.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@powersync/common': minor
3+
---
4+
5+
- Added additional listeners for `closing` and `closed` events in `AbstractPowerSyncDatabase`.
6+
- Added `query` and `customQuery` APIs for enhanced watched queries.
7+
- Added `triggerImmediate` option to the `onChange` API. This allows emitting an initial event which can be useful for downstream use cases.

‎.changeset/nine-pens-ring.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@powersync/vue': minor
3+
---
4+
5+
[Potentially breaking change] The `useQuery` hook results are now explicitly defined as readonly. These values should not be mutated.
6+
7+
- Added the ability to limit re-renders by specifying a `rowComparator` for query results. The `useQuery` hook will only emit `data` changes when the data has changed.
8+
9+
```javascript
10+
// The data here will maintain previous object references for unchanged items.
11+
const { data } = useQuery('SELECT * FROM lists WHERE name = ?', ['aname'], {
12+
rowComparator: {
13+
keyBy: (item) => item.id,
14+
compareBy: (item) => JSON.stringify(item)
15+
}
16+
});
17+
```
18+
19+
- Added the ability to subscribe to an existing instance of a `WatchedQuery`
20+
21+
```vue
22+
<script setup>
23+
import { useWatchedQuerySubscription } from '@powersync/vue';
24+
25+
const listsQuery = powerSync
26+
.query({
27+
sql: `SELECT * FROM lists`
28+
})
29+
.differentialWatch();
30+
31+
const { data, isLoading, isFetching, error } = useWatchedQuerySubscription(listsQuery);
32+
</script>
33+
34+
<template>
35+
<div v-if="isLoading">Loading...</div>
36+
<div v-else-if="isFetching">Updating results...</div>
37+
38+
<div v-if="error">{{ error }}</div>
39+
<ul v-else>
40+
<li v-for="l in data" :key="l.id">{{ l.name }}</li>
41+
</ul>
42+
</template>
43+
```

‎.changeset/plenty-rice-protect.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@powersync/react': minor
3+
'@powersync/vue': minor
4+
---
5+
6+
- [Internal] Updated implementation to use shared `WatchedQuery` implementation.

‎.changeset/stale-dots-jog.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/web': minor
3+
---
4+
5+
Improved query behaviour when client is closed. Pending requests will be aborted, future requests will be rejected with an Error. Fixed read and write lock requests not respecting timeout parameter.

‎.changeset/swift-guests-explain.md‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
'@powersync/react': minor
3+
---
4+
5+
- Added the ability to limit re-renders by specifying a `rowComparator` for query results. The `useQuery` hook will only emit `data` changes when the data has changed.
6+
7+
```javascript
8+
// The data here will maintain previous object references for unchanged items.
9+
const { data } = useQuery('SELECT * FROM lists WHERE name = ?', ['aname'], {
10+
rowComparator: {
11+
keyBy: (item) => item.id,
12+
compareBy: (item) => JSON.stringify(item)
13+
}
14+
});
15+
```
16+
17+
- Added the ability to subscribe to an existing instance of a `WatchedQuery`
18+
19+
```jsx
20+
import { useWatchedQuerySubscription } from '@powersync/react';
21+
22+
const listsQuery = powerSync
23+
.query({
24+
sql: `SELECT * FROM lists`
25+
})
26+
.differentialWatch();
27+
28+
export const ListsWidget = (props) => {
29+
const { data: lists } = useWatchedQuerySubscription(listsQuery);
30+
31+
return (
32+
<div>
33+
{lists.map((list) => (
34+
<div key={list.id}>{list.name}</div>
35+
))}
36+
</div>
37+
);
38+
};
39+
```

‎.prettierrc‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"jsxBracketSameLine": true,
66
"useTabs": false,
77
"printWidth": 120,
8-
"trailingComma": "none"
8+
"trailingComma": "none",
9+
"plugins": ["prettier-plugin-embed", "prettier-plugin-sql"]
910
}

‎demos/react-supabase-todolist/src/app/views/sql-console/page.tsx‎

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,73 @@
1-
import React from 'react';
2-
import { useQuery } from '@powersync/react';
3-
import { Box, Button, Grid, TextField, styled } from '@mui/material';
4-
import { DataGrid } from '@mui/x-data-grid';
51
import { NavigationPage } from '@/components/navigation/NavigationPage';
2+
import { Alert, Box, Button, Grid, TextField, styled } from '@mui/material';
3+
import { DataGrid } from '@mui/x-data-grid';
4+
import { useQuery } from '@powersync/react';
5+
import React from 'react';
66

77
export type LoginFormParams = {
88
email: string;
99
password: string;
1010
};
1111

12-
const DEFAULT_QUERY = 'SELECT * FROM lists';
13-
14-
export default function SQLConsolePage() {
15-
const inputRef = React.useRef<HTMLInputElement>();
16-
const [query, setQuery] = React.useState(DEFAULT_QUERY);
17-
const { data: querySQLResult } = useQuery(query);
12+
const DEFAULT_QUERY = /* sql */ `
13+
SELECT
14+
*
15+
FROM
16+
lists
17+
`;
1818

19+
const TableDisplay = React.memo(({ data }: { data: ReadonlyArray<any> }) => {
1920
const queryDataGridResult = React.useMemo(() => {
20-
const firstItem = querySQLResult?.[0];
21-
21+
const firstItem = data?.[0];
2222
return {
2323
columns: firstItem
2424
? Object.keys(firstItem).map((field) => ({
2525
field,
2626
flex: 1
2727
}))
2828
: [],
29-
rows: querySQLResult
29+
rows: data
3030
};
31-
}, [querySQLResult]);
31+
}, [data]);
32+
33+
return (
34+
<S.QueryResultContainer>
35+
<DataGrid
36+
autoHeight={true}
37+
rows={queryDataGridResult.rows.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
38+
columns={queryDataGridResult.columns}
39+
initialState={{
40+
pagination: {
41+
paginationModel: {
42+
pageSize: 20
43+
}
44+
}
45+
}}
46+
pageSizeOptions={[20]}
47+
disableRowSelectionOnClick
48+
/>
49+
</S.QueryResultContainer>
50+
);
51+
});
52+
53+
export default function SQLConsolePage() {
54+
const inputRef = React.useRef<HTMLInputElement>();
55+
const [query, setQuery] = React.useState(DEFAULT_QUERY);
56+
57+
const { data, error } = useQuery(query, [], {
58+
/**
59+
* We don't use the isFetching status here, we can avoid re-renders if we don't report on it.
60+
*/
61+
reportFetching: false,
62+
/**
63+
* The query here will only emit results when the query data set changes.
64+
* Result sets are compared by serializing each item to JSON and comparing the strings.
65+
*/
66+
rowComparator: {
67+
keyBy: (item: any) => JSON.stringify(item),
68+
compareBy: (item: any) => JSON.stringify(item)
69+
}
70+
});
3271

3372
return (
3473
<NavigationPage title="SQL Console">
@@ -57,33 +96,13 @@ export default function SQLConsolePage() {
5796
if (queryInput) {
5897
setQuery(queryInput);
5998
}
60-
}}
61-
>
99+
}}>
62100
Execute Query
63101
</Button>
64102
</S.CenteredGrid>
65103
</S.CenteredGrid>
66-
67-
{queryDataGridResult ? (
68-
<S.QueryResultContainer>
69-
{queryDataGridResult.columns ? (
70-
<DataGrid
71-
autoHeight={true}
72-
rows={queryDataGridResult.rows?.map((r, index) => ({ ...r, id: r.id ?? index })) ?? []}
73-
columns={queryDataGridResult.columns}
74-
initialState={{
75-
pagination: {
76-
paginationModel: {
77-
pageSize: 20
78-
}
79-
}
80-
}}
81-
pageSizeOptions={[20]}
82-
disableRowSelectionOnClick
83-
/>
84-
) : null}
85-
</S.QueryResultContainer>
86-
) : null}
104+
{error ? <Alert severity="error">{error.message}</Alert> : null}
105+
<TableDisplay data={data} />
87106
</S.MainContainer>
88107
</NavigationPage>
89108
);

‎demos/react-supabase-todolist/src/app/views/todo-lists/edit/page.tsx‎

Lines changed: 35 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { usePowerSync, useQuery } from '@powersync/react';
1+
import { NavigationPage } from '@/components/navigation/NavigationPage';
2+
import { useSupabase } from '@/components/providers/SystemProvider';
3+
import { TodoItemWidget } from '@/components/widgets/TodoItemWidget';
4+
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
25
import AddIcon from '@mui/icons-material/Add';
36
import {
47
Box,
@@ -15,12 +18,9 @@ import {
1518
styled
1619
} from '@mui/material';
1720
import Fab from '@mui/material/Fab';
21+
import { usePowerSync, useQuery } from '@powersync/react';
1822
import React, { Suspense } from 'react';
1923
import { useParams } from 'react-router-dom';
20-
import { useSupabase } from '@/components/providers/SystemProvider';
21-
import { LISTS_TABLE, TODOS_TABLE, TodoRecord } from '@/library/powersync/AppSchema';
22-
import { NavigationPage } from '@/components/navigation/NavigationPage';
23-
import { TodoItemWidget } from '@/components/widgets/TodoItemWidget';
2424

2525
/**
2626
* useSearchParams causes the entire element to fall back to client side rendering
@@ -34,61 +34,53 @@ const TodoEditSection = () => {
3434

3535
const {
3636
data: [listRecord]
37-
} = useQuery<{ name: string }>(`SELECT name FROM ${LISTS_TABLE} WHERE id = ?`, [listID]);
37+
} = useQuery<{ name: string }>(
38+
/* sql */ `
39+
SELECT
40+
name
41+
FROM
42+
${LISTS_TABLE}
43+
WHERE
44+
id = ?
45+
`,
46+
[listID]
47+
);
3848

3949
const { data: todos } = useQuery<TodoRecord>(
40-
`SELECT * FROM ${TODOS_TABLE} WHERE list_id=? ORDER BY created_at DESC, id`,
50+
/* sql */ `
51+
SELECT
52+
*
53+
FROM
54+
${TODOS_TABLE}
55+
WHERE
56+
list_id = ?
57+
ORDER BY
58+
created_at DESC,
59+
id
60+
`,
4161
[listID]
4262
);
4363

4464
const [showPrompt, setShowPrompt] = React.useState(false);
4565
const nameInputRef = React.createRef<HTMLInputElement>();
4666

47-
const toggleCompletion = async (record: TodoRecord, completed: boolean) => {
48-
const updatedRecord = { ...record, completed: completed };
49-
if (completed) {
50-
const userID = supabase?.currentSession?.user.id;
51-
if (!userID) {
52-
throw new Error(`Could not get user ID.`);
53-
}
54-
updatedRecord.completed_at = new Date().toISOString();
55-
updatedRecord.completed_by = userID;
56-
} else {
57-
updatedRecord.completed_at = null;
58-
updatedRecord.completed_by = null;
59-
}
60-
await powerSync.execute(
61-
`UPDATE ${TODOS_TABLE}
62-
SET completed = ?,
63-
completed_at = ?,
64-
completed_by = ?
65-
WHERE id = ?`,
66-
[completed, updatedRecord.completed_at, updatedRecord.completed_by, record.id]
67-
);
68-
};
69-
7067
const createNewTodo = async (description: string) => {
7168
const userID = supabase?.currentSession?.user.id;
7269
if (!userID) {
7370
throw new Error(`Could not get user ID.`);
7471
}
7572

7673
await powerSync.execute(
77-
`INSERT INTO
78-
${TODOS_TABLE}
79-
(id, created_at, created_by, description, list_id)
80-
VALUES
81-
(uuid(), datetime(), ?, ?, ?)`,
74+
/* sql */ `
75+
INSERT INTO
76+
${TODOS_TABLE} (id, created_at, created_by, description, list_id)
77+
VALUES
78+
(uuid (), datetime (), ?, ?, ?)
79+
`,
8280
[userID, description, listID!]
8381
);
8482
};
8583

86-
const deleteTodo = async (id: string) => {
87-
await powerSync.writeTransaction(async (tx) => {
88-
await tx.execute(`DELETE FROM ${TODOS_TABLE} WHERE id = ?`, [id]);
89-
});
90-
};
91-
9284
if (!listRecord) {
9385
return (
9486
<Box>
@@ -106,13 +98,7 @@ const TodoEditSection = () => {
10698
<Box>
10799
<List dense={false}>
108100
{todos.map((r) => (
109-
<TodoItemWidget
110-
key={r.id}
111-
description={r.description}
112-
onDelete={() => deleteTodo(r.id)}
113-
isComplete={r.completed == 1}
114-
toggleCompletion={() => toggleCompletion(r, !r.completed)}
115-
/>
101+
<TodoItemWidget key={r.id} id={r.id} description={r.description} isComplete={r.completed == 1} />
116102
))}
117103
</List>
118104
</Box>
@@ -129,8 +115,7 @@ const TodoEditSection = () => {
129115
await createNewTodo(nameInputRef.current!.value);
130116
setShowPrompt(false);
131117
}
132-
}}
133-
>
118+
}}>
134119
<DialogTitle id="alert-dialog-title">{'Create Todo Item'}</DialogTitle>
135120
<DialogContent>
136121
<DialogContentText id="alert-dialog-description">Enter a description for a new todo item</DialogContentText>

0 commit comments

Comments
 (0)