@livestore/react 0.0.0-snapshot-3ea7644e665c5c8292d2309fb7f837b9146af912 → 0.0.0-snapshot-ba25981b6de87a90976fc39e1c2551844d384a05

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
21
21
  "_name": "sql-in-memory-select",
22
22
  "attributes": {
23
23
  "sql.cached": false,
24
- "sql.query": "SELECT 1 FROM 'UserInfo' WHERE id = ?",
24
+ "sql.query": "select 1 from UserInfo where id = 'u1'",
25
25
  "sql.rowsCount": 0,
26
26
  },
27
27
  },
@@ -29,7 +29,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
29
29
  "_name": "sql-in-memory-select",
30
30
  "attributes": {
31
31
  "sql.cached": false,
32
- "sql.query": "SELECT 1 FROM 'UserInfo' WHERE id = ?",
32
+ "sql.query": "select 1 from UserInfo where id = 'u2'",
33
33
  "sql.rowsCount": 1,
34
34
  },
35
35
  },
@@ -75,9 +75,9 @@ exports[`useRow > otel > should update the data based on component key > strictM
75
75
  "_name": "LiveStore:queries",
76
76
  "children": [
77
77
  {
78
- "_name": "db:SELECT * FROM 'UserInfo' WHERE id = ?",
78
+ "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
79
79
  "attributes": {
80
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
80
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
81
81
  "sql.rowsCount": 1,
82
82
  },
83
83
  "children": [
@@ -85,7 +85,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
85
85
  "_name": "sql-in-memory-select",
86
86
  "attributes": {
87
87
  "sql.cached": false,
88
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
88
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
89
89
  "sql.rowsCount": 1,
90
90
  },
91
91
  },
@@ -115,16 +115,16 @@ exports[`useRow > otel > should update the data based on component key > strictM
115
115
  ],
116
116
  },
117
117
  {
118
- "_name": "LiveStore:useQuery:db(row:UserInfo:u1)",
118
+ "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
119
119
  "attributes": {
120
- "label": "db(row:UserInfo:u1)",
120
+ "label": "sql(rowQuery:query:UserInfo:u1)",
121
121
  "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
122
122
  },
123
123
  "children": [
124
124
  {
125
- "_name": "db:SELECT * FROM 'UserInfo' WHERE id = ?",
125
+ "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
126
126
  "attributes": {
127
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
127
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
128
128
  "sql.rowsCount": 1,
129
129
  },
130
130
  "children": [
@@ -132,7 +132,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
132
132
  "_name": "sql-in-memory-select",
133
133
  "attributes": {
134
134
  "sql.cached": false,
135
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
135
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
136
136
  "sql.rowsCount": 1,
137
137
  },
138
138
  },
@@ -141,8 +141,8 @@ exports[`useRow > otel > should update the data based on component key > strictM
141
141
  {
142
142
  "_name": "LiveStore.subscribe",
143
143
  "attributes": {
144
- "label": "db(row:UserInfo:u1)",
145
- "queryLabel": "db(row:UserInfo:u1)",
144
+ "label": "sql(rowQuery:query:UserInfo:u1)",
145
+ "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
146
146
  },
147
147
  },
148
148
  ],
@@ -156,16 +156,16 @@ exports[`useRow > otel > should update the data based on component key > strictM
156
156
  },
157
157
  "children": [
158
158
  {
159
- "_name": "LiveStore:useQuery:db(row:UserInfo:u2)",
159
+ "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u2)",
160
160
  "attributes": {
161
- "label": "db(row:UserInfo:u2)",
161
+ "label": "sql(rowQuery:query:UserInfo:u2)",
162
162
  "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
163
163
  },
164
164
  "children": [
165
165
  {
166
- "_name": "db:SELECT * FROM 'UserInfo' WHERE id = ?",
166
+ "_name": "sql:select * from UserInfo where id = 'u2' limit 1",
167
167
  "attributes": {
168
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
168
+ "sql.query": "select * from UserInfo where id = 'u2' limit 1",
169
169
  "sql.rowsCount": 1,
170
170
  },
171
171
  "children": [
@@ -173,7 +173,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
173
173
  "_name": "sql-in-memory-select",
174
174
  "attributes": {
175
175
  "sql.cached": false,
176
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
176
+ "sql.query": "select * from UserInfo where id = 'u2' limit 1",
177
177
  "sql.rowsCount": 1,
178
178
  },
179
179
  },
@@ -182,8 +182,8 @@ exports[`useRow > otel > should update the data based on component key > strictM
182
182
  {
183
183
  "_name": "LiveStore.subscribe",
184
184
  "attributes": {
185
- "label": "db(row:UserInfo:u2)",
186
- "queryLabel": "db(row:UserInfo:u2)",
185
+ "label": "sql(rowQuery:query:UserInfo:u2)",
186
+ "queryLabel": "sql(rowQuery:query:UserInfo:u2)",
187
187
  },
188
188
  },
189
189
  ],
@@ -217,7 +217,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
217
217
  "_name": "sql-in-memory-select",
218
218
  "attributes": {
219
219
  "sql.cached": false,
220
- "sql.query": "SELECT 1 FROM 'UserInfo' WHERE id = ?",
220
+ "sql.query": "select 1 from UserInfo where id = 'u1'",
221
221
  "sql.rowsCount": 0,
222
222
  },
223
223
  },
@@ -225,7 +225,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
225
225
  "_name": "sql-in-memory-select",
226
226
  "attributes": {
227
227
  "sql.cached": false,
228
- "sql.query": "SELECT 1 FROM 'UserInfo' WHERE id = ?",
228
+ "sql.query": "select 1 from UserInfo where id = 'u2'",
229
229
  "sql.rowsCount": 1,
230
230
  },
231
231
  },
@@ -271,9 +271,9 @@ exports[`useRow > otel > should update the data based on component key > strictM
271
271
  "_name": "LiveStore:queries",
272
272
  "children": [
273
273
  {
274
- "_name": "db:SELECT * FROM 'UserInfo' WHERE id = ?",
274
+ "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
275
275
  "attributes": {
276
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
276
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
277
277
  "sql.rowsCount": 1,
278
278
  },
279
279
  "children": [
@@ -281,7 +281,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
281
281
  "_name": "sql-in-memory-select",
282
282
  "attributes": {
283
283
  "sql.cached": false,
284
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
284
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
285
285
  "sql.rowsCount": 1,
286
286
  },
287
287
  },
@@ -311,16 +311,16 @@ exports[`useRow > otel > should update the data based on component key > strictM
311
311
  ],
312
312
  },
313
313
  {
314
- "_name": "LiveStore:useQuery:db(row:UserInfo:u1)",
314
+ "_name": "LiveStore:useQuery:sql(rowQuery:query:UserInfo:u1)",
315
315
  "attributes": {
316
- "label": "db(row:UserInfo:u1)",
316
+ "label": "sql(rowQuery:query:UserInfo:u1)",
317
317
  "stackInfo": "{"frames":[{"name":"renderHook.wrapper","filePath":"__REPLACED_FOR_SNAPSHOT__"},{"name":"useRow","filePath":"__REPLACED_FOR_SNAPSHOT__"}]}",
318
318
  },
319
319
  "children": [
320
320
  {
321
- "_name": "db:SELECT * FROM 'UserInfo' WHERE id = ?",
321
+ "_name": "sql:select * from UserInfo where id = 'u1' limit 1",
322
322
  "attributes": {
323
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
323
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
324
324
  "sql.rowsCount": 1,
325
325
  },
326
326
  "children": [
@@ -328,7 +328,7 @@ exports[`useRow > otel > should update the data based on component key > strictM
328
328
  "_name": "sql-in-memory-select",
329
329
  "attributes": {
330
330
  "sql.cached": false,
331
- "sql.query": "SELECT * FROM 'UserInfo' WHERE id = ?",
331
+ "sql.query": "select * from UserInfo where id = 'u1' limit 1",
332
332
  "sql.rowsCount": 1,
333
333
  },
334
334
  },
@@ -337,15 +337,15 @@ exports[`useRow > otel > should update the data based on component key > strictM
337
337
  {
338
338
  "_name": "LiveStore.subscribe",
339
339
  "attributes": {
340
- "label": "db(row:UserInfo:u1)",
341
- "queryLabel": "db(row:UserInfo:u1)",
340
+ "label": "sql(rowQuery:query:UserInfo:u1)",
341
+ "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
342
342
  },
343
343
  },
344
344
  {
345
345
  "_name": "LiveStore.subscribe",
346
346
  "attributes": {
347
- "label": "db(row:UserInfo:u1)",
348
- "queryLabel": "db(row:UserInfo:u1)",
347
+ "label": "sql(rowQuery:query:UserInfo:u1)",
348
+ "queryLabel": "sql(rowQuery:query:UserInfo:u1)",
349
349
  },
350
350
  },
351
351
  ],
package/src/useAtom.ts CHANGED
@@ -1,6 +1,5 @@
1
- import type { DerivedMutationHelperFns, QueryInfo } from '@livestore/common'
1
+ import { type QueryInfoCol, type QueryInfoRow } from '@livestore/common'
2
2
  import type { DbSchema } from '@livestore/common/schema'
3
- import type { SqliteDsl } from '@livestore/db-schema'
4
3
  import type { LiveQuery } from '@livestore/livestore'
5
4
  import React from 'react'
6
5
 
@@ -9,8 +8,12 @@ import { useQueryRef } from './useQuery.js'
9
8
  import type { Dispatch, SetStateAction } from './useRow.js'
10
9
 
11
10
  export const useAtom = <
12
- // TODO also support colJsonValue
13
- TQuery extends LiveQuery<any, QueryInfo.Row | QueryInfo.Col>,
11
+ TQuery extends LiveQuery<any, QueryInfoRow<TTableDef> | QueryInfoCol<TTableDef, any>>,
12
+ TTableDef extends DbSchema.TableDef<
13
+ DbSchema.DefaultSqliteTableDefConstrained,
14
+ boolean,
15
+ DbSchema.TableOptions & { deriveMutations: { enabled: true } }
16
+ >,
14
17
  >(
15
18
  query$: TQuery,
16
19
  ): [value: TQuery['__result!'], setValue: Dispatch<SetStateAction<Partial<TQuery['__result!']>>>] => {
@@ -22,23 +25,23 @@ export const useAtom = <
22
25
  const setValue = React.useMemo<Dispatch<SetStateAction<TQuery['__result!']>>>(() => {
23
26
  return (newValueOrFn: any) => {
24
27
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
25
- const table = query$.queryInfo.table as DbSchema.TableDef &
26
- DerivedMutationHelperFns<SqliteDsl.Columns, DbSchema.TableOptions>
27
28
 
28
29
  if (query$.queryInfo._tag === 'Row') {
29
- if (table.options.isSingleton && table.options.isSingleColumn) {
30
- store.mutate(table.update(newValue))
31
- } else if (table.options.isSingleColumn) {
32
- store.mutate(table.update({ where: { id: query$.queryInfo.id }, values: { value: newValue } }))
30
+ if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
31
+ store.mutate(query$.queryInfo.table.update(newValue))
32
+ } else if (query$.queryInfo.table.options.isSingleColumn) {
33
+ store.mutate(
34
+ query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: { value: newValue } }),
35
+ )
33
36
  } else {
34
- store.mutate(table.update({ where: { id: query$.queryInfo.id }, values: newValue }))
37
+ store.mutate(query$.queryInfo.table.update({ where: { id: query$.queryInfo.id }, values: newValue }))
35
38
  }
36
39
  } else {
37
- if (table.options.isSingleton && table.options.isSingleColumn) {
38
- store.mutate(table.update({ [query$.queryInfo.column]: newValue }))
40
+ if (query$.queryInfo.table.options.isSingleton && query$.queryInfo.table.isSingleColumn) {
41
+ store.mutate(query$.queryInfo.table.update({ [query$.queryInfo.column]: newValue }))
39
42
  } else {
40
43
  store.mutate(
41
- table.update({
44
+ query$.queryInfo.table.update({
42
45
  where: { id: query$.queryInfo.id },
43
46
  values: { [query$.queryInfo.column]: newValue },
44
47
  }),
@@ -1,4 +1,4 @@
1
- import { queryDb } from '@livestore/livestore'
1
+ import { querySQL } from '@livestore/livestore'
2
2
  import { Effect, Schema } from '@livestore/utils/effect'
3
3
  import { renderHook } from '@testing-library/react'
4
4
  import React from 'react'
@@ -14,7 +14,7 @@ describe('useQuery', () => {
14
14
 
15
15
  const renderCount = makeRenderCount()
16
16
 
17
- const allTodos$ = queryDb({ query: `select * from todos`, schema: Schema.Array(tables.todos.schema) })
17
+ const allTodos$ = querySQL(`select * from todos`, { schema: Schema.Array(tables.todos.schema) })
18
18
 
19
19
  const { result } = renderHook(
20
20
  () => {
@@ -41,14 +41,14 @@ describe('useQuery', () => {
41
41
 
42
42
  const renderCount = makeRenderCount()
43
43
 
44
- const todo1$ = queryDb(
45
- { query: `select * from todos where id = 't1'`, schema: Schema.Array(tables.todos.schema) },
46
- { label: 'libraryTracksView1' },
47
- )
48
- const todo2$ = queryDb(
49
- { query: `select * from todos where id = 't2'`, schema: Schema.Array(tables.todos.schema) },
50
- { label: 'libraryTracksView2' },
51
- )
44
+ const todo1$ = querySQL(`select * from todos where id = 't1'`, {
45
+ label: 'libraryTracksView1',
46
+ schema: Schema.Array(tables.todos.schema),
47
+ })
48
+ const todo2$ = querySQL(`select * from todos where id = 't2'`, {
49
+ label: 'libraryTracksView2',
50
+ schema: Schema.Array(tables.todos.schema),
51
+ })
52
52
 
53
53
  store.mutate(
54
54
  todos.insert({ id: 't1', text: 'buy milk', completed: false }),
package/src/useQuery.ts CHANGED
@@ -101,10 +101,6 @@ Stack trace:
101
101
  React.useEffect(() => {
102
102
  query$.activeSubscriptions.add(stackInfo)
103
103
 
104
- // Dynamic queries only set their actual label after they've been run the first time,
105
- // so we're also updating the span name here.
106
- span.updateName(`LiveStore:useQuery:${query$.label}`)
107
-
108
104
  return store.subscribe(
109
105
  query$,
110
106
  (newValue) => {
@@ -121,10 +121,11 @@ describe('useRow', () => {
121
121
  useGlobalReactivityGraph: false,
122
122
  })
123
123
 
124
- const allTodos$ = LiveStore.queryDb(
125
- { query: `select * from todos`, schema: Schema.Array(tables.todos.schema) },
126
- { label: 'allTodos', reactivityGraph },
127
- )
124
+ const allTodos$ = LiveStore.querySQL(`select * from todos`, {
125
+ label: 'allTodos',
126
+ schema: Schema.Array(tables.todos.schema),
127
+ reactivityGraph,
128
+ })
128
129
 
129
130
  const appRouterRenderCount = makeRenderCount()
130
131
  let globalSetState: LiveStoreReact.StateSetters<typeof AppRouterSchema> | undefined
@@ -222,12 +223,13 @@ describe('useRow', () => {
222
223
  const [_row, _setRow, rowState$] = LiveStoreReact.useRow(AppComponentSchema, userId, { reactivityGraph })
223
224
  const todos = LiveStoreReact.useScopedQuery(
224
225
  () =>
225
- LiveStore.queryDb(
226
- (get) => ({
227
- query: LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
226
+ LiveStore.querySQL(
227
+ (get) => LiveStore.sql`select * from todos where text like '%${get(rowState$).text}%'`,
228
+ {
228
229
  schema: Schema.Array(tables.todos.schema),
229
- }),
230
- { reactivityGraph, label: 'todosFiltered' },
230
+ reactivityGraph,
231
+ label: 'todosFiltered',
232
+ },
231
233
  ),
232
234
  userId,
233
235
  )
package/src/useRow.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { QueryInfo, RowQuery } from '@livestore/common'
1
+ import type { QueryInfo } from '@livestore/common'
2
2
  import { SessionIdSymbol } from '@livestore/common'
3
3
  import { DbSchema } from '@livestore/common/schema'
4
4
  import type { SqliteDsl } from '@livestore/db-schema'
5
- import type { LiveQuery, ReactivityGraph } from '@livestore/livestore'
6
- import { queryDb } from '@livestore/livestore'
5
+ import type { LiveQuery, ReactivityGraph, RowResult } from '@livestore/livestore'
6
+ import { rowQuery } from '@livestore/livestore'
7
7
  import { shouldNeverHappen } from '@livestore/utils'
8
8
  import { ReadonlyRecord } from '@livestore/utils/effect'
9
9
  import React from 'react'
@@ -12,12 +12,16 @@ import { useStore } from './LiveStoreContext.js'
12
12
  import { useQueryRef } from './useQuery.js'
13
13
  import { useMakeScopedQuery } from './useScopedQuery.js'
14
14
 
15
- export type UseRowResult<TTableDef extends DbSchema.TableDefBase> = [
16
- row: RowQuery.Result<TTableDef>,
15
+ export type UseRowResult<TTableDef extends DbSchema.TableDef> = [
16
+ row: RowResult<TTableDef>,
17
17
  setRow: StateSetters<TTableDef>,
18
- query$: LiveQuery<RowQuery.Result<TTableDef>, QueryInfo>,
18
+ query$: LiveQuery<RowResult<TTableDef>, QueryInfo>,
19
19
  ]
20
20
 
21
+ export type UseRowOptionsDefaulValues<TTableDef extends DbSchema.TableDef> = {
22
+ defaultValues?: Partial<RowResult<TTableDef>>
23
+ }
24
+
21
25
  export type UseRowOptionsBase = {
22
26
  reactivityGraph?: ReactivityGraph
23
27
  }
@@ -35,6 +39,7 @@ export const useRow: {
35
39
  <
36
40
  TTableDef extends DbSchema.TableDef<
37
41
  DbSchema.DefaultSqliteTableDef,
42
+ boolean,
38
43
  DbSchema.TableOptions & { isSingleton: true; deriveMutations: { enabled: true } }
39
44
  >,
40
45
  >(
@@ -44,44 +49,31 @@ export const useRow: {
44
49
  <
45
50
  TTableDef extends DbSchema.TableDef<
46
51
  DbSchema.DefaultSqliteTableDef,
47
- DbSchema.TableOptions & {
48
- isSingleton: false
49
- requiredInsertColumnNames: 'id'
50
- deriveMutations: { enabled: true }
51
- }
52
- >,
53
- >(
54
- table: TTableDef,
55
- // TODO adjust so it works with arbitrary primary keys or unique constraints
56
- id: string | SessionIdSymbol,
57
- options?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
58
- ): UseRowResult<TTableDef>
59
- <
60
- TTableDef extends DbSchema.TableDef<
61
- DbSchema.DefaultSqliteTableDef,
52
+ boolean,
62
53
  DbSchema.TableOptions & { isSingleton: false; deriveMutations: { enabled: true } }
63
54
  >,
64
55
  >(
65
56
  table: TTableDef,
66
57
  // TODO adjust so it works with arbitrary primary keys or unique constraints
67
58
  id: string | SessionIdSymbol,
68
- options: UseRowOptionsBase & RowQuery.RequiredColumnsOptions<TTableDef>,
59
+ options?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
69
60
  ): UseRowResult<TTableDef>
70
61
  } = <
71
62
  TTableDef extends DbSchema.TableDef<
72
63
  DbSchema.DefaultSqliteTableDefConstrained,
64
+ boolean,
73
65
  DbSchema.TableOptions & { deriveMutations: { enabled: true } }
74
66
  >,
75
67
  >(
76
68
  table: TTableDef,
77
69
  idOrOptions?: string | SessionIdSymbol | UseRowOptionsBase,
78
- options_?: UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>,
70
+ options_?: UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>,
79
71
  ): UseRowResult<TTableDef> => {
80
72
  const sqliteTableDef = table.sqliteDef
81
73
  const id = typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? idOrOptions : undefined
82
- const options: (UseRowOptionsBase & Partial<RowQuery.RequiredColumnsOptions<TTableDef>>) | undefined =
74
+ const options: (UseRowOptionsBase & UseRowOptionsDefaulValues<TTableDef>) | undefined =
83
75
  typeof idOrOptions === 'string' || idOrOptions === SessionIdSymbol ? options_ : idOrOptions
84
- const { insertValues, reactivityGraph } = options ?? {}
76
+ const { defaultValues, reactivityGraph } = options ?? {}
85
77
 
86
78
  type TComponentState = SqliteDsl.FromColumns.RowDecoded<TTableDef['sqliteDef']['columns']>
87
79
 
@@ -103,14 +95,16 @@ export const useRow: {
103
95
  // console.debug('useRow', tableName, id)
104
96
 
105
97
  const idStr = id === SessionIdSymbol ? 'session' : id
106
- const rowQuery = table.query.row as any
107
98
 
108
- type Query$ = LiveQuery<RowQuery.Result<TTableDef>, QueryInfo.Row>
109
99
  const { query$, otelContext } = useMakeScopedQuery(
110
100
  (otelContext) =>
111
101
  DbSchema.tableIsSingleton(table)
112
- ? (queryDb(rowQuery(), { reactivityGraph, otelContext }) as any as Query$)
113
- : (queryDb(rowQuery(id!, { insertValues: insertValues! }), { reactivityGraph, otelContext }) as any as Query$),
102
+ ? (rowQuery(table, { otelContext, reactivityGraph }) as LiveQuery<RowResult<TTableDef>, QueryInfo>)
103
+ : (rowQuery(table as TTableDef & { options: { isSingleton: false } }, id!, {
104
+ otelContext,
105
+ defaultValues: defaultValues!,
106
+ reactivityGraph,
107
+ }) as any as LiveQuery<RowResult<TTableDef>, QueryInfo>),
114
108
  [idStr!, tableName],
115
109
  {
116
110
  otel: {
@@ -120,11 +114,11 @@ export const useRow: {
120
114
  },
121
115
  )
122
116
 
123
- const query$Ref = useQueryRef(query$, otelContext) as React.MutableRefObject<RowQuery.Result<TTableDef>>
117
+ const query$Ref = useQueryRef(query$, otelContext) as React.MutableRefObject<RowResult<TTableDef>>
124
118
 
125
119
  const setState = React.useMemo<StateSetters<TTableDef>>(() => {
126
- if (table.options.isSingleColumn) {
127
- return (newValueOrFn: RowQuery.Result<TTableDef>) => {
120
+ if (table.isSingleColumn) {
121
+ return (newValueOrFn: RowResult<TTableDef>) => {
128
122
  const newValue = typeof newValueOrFn === 'function' ? newValueOrFn(query$Ref.current) : newValueOrFn
129
123
  if (query$Ref.current === newValue) return
130
124
 
@@ -179,10 +173,10 @@ export const useRow: {
179
173
  export type Dispatch<A> = (action: A) => void
180
174
  export type SetStateAction<S> = S | ((previousValue: S) => S)
181
175
 
182
- export type StateSetters<TTableDef extends DbSchema.TableDefBase> = TTableDef['options']['isSingleColumn'] extends true
183
- ? Dispatch<SetStateAction<RowQuery.Result<TTableDef>>>
176
+ export type StateSetters<TTableDef extends DbSchema.TableDef> = TTableDef['isSingleColumn'] extends true
177
+ ? Dispatch<SetStateAction<RowResult<TTableDef>>>
184
178
  : {
185
- [K in keyof RowQuery.Result<TTableDef>]: Dispatch<SetStateAction<RowQuery.Result<TTableDef>[K]>>
179
+ [K in keyof RowResult<TTableDef>]: Dispatch<SetStateAction<RowResult<TTableDef>[K]>>
186
180
  } & {
187
- setMany: Dispatch<SetStateAction<Partial<RowQuery.Result<TTableDef>>>>
181
+ setMany: Dispatch<SetStateAction<Partial<RowResult<TTableDef>>>>
188
182
  }
@@ -1,5 +1,5 @@
1
1
  import * as LiveStore from '@livestore/livestore'
2
- import { queryDb } from '@livestore/livestore'
2
+ import { querySQL } from '@livestore/livestore'
3
3
  import { Effect, Schema } from '@livestore/utils/effect'
4
4
  import { render, renderHook } from '@testing-library/react'
5
5
  import React from 'react'
@@ -29,8 +29,7 @@ describe('useScopedQuery', () => {
29
29
  renderCount.inc()
30
30
 
31
31
  return LiveStoreReact.useScopedQuery(() => {
32
- const query$ = queryDb({
33
- query: `select * from todos where id = '${id}'`,
32
+ const query$ = querySQL(`select * from todos where id = '${id}'`, {
34
33
  schema: Schema.Array(tables.todos.schema),
35
34
  })
36
35
  queryMap.set(id, query$)
@@ -0,0 +1,131 @@
1
+ import type { QueryInfo } from '@livestore/common'
2
+ import type { LiveQuery } from '@livestore/livestore'
3
+ import * as otel from '@opentelemetry/api'
4
+ import React from 'react'
5
+
6
+ import { useStore } from './LiveStoreContext.js'
7
+ import { useQueryRef } from './useQuery.js'
8
+
9
+ // NOTE Given `useMemo` will be called multiple times (e.g. when using React Strict mode or Fast Refresh),
10
+ // we are using this cache to avoid starting multiple queries/spans for the same component.
11
+ // This is somewhat against some recommended React best practices, but it should be fine in our case below.
12
+ // Please definitely open an issue if you see or run into any problems with this approach!
13
+ const cache = new Map<
14
+ string,
15
+ | {
16
+ _tag: 'active'
17
+ rc: number
18
+ query$: LiveQuery<any, any>
19
+ span: otel.Span
20
+ otelContext: otel.Context
21
+ }
22
+ | {
23
+ _tag: 'destroyed'
24
+ }
25
+ >()
26
+
27
+ export type DepKey = string | number | ReadonlyArray<string | number>
28
+
29
+ /**
30
+ * Creates a query, subscribes and destroys it when the component unmounts.
31
+ *
32
+ * The `key` is used to determine whether the a new query should be created or if the existing one should be reused.
33
+ */
34
+ export const useScopedQuery = <TResult>(makeQuery: () => LiveQuery<TResult>, key: DepKey): TResult =>
35
+ useScopedQueryRef(makeQuery, key).current
36
+
37
+ export const useScopedQueryRef = <TResult>(
38
+ makeQuery: () => LiveQuery<TResult>,
39
+ key: DepKey,
40
+ ): React.MutableRefObject<TResult> => {
41
+ const { query$ } = useMakeScopedQuery(makeQuery, key)
42
+
43
+ return useQueryRef(query$)
44
+ }
45
+
46
+ export const useMakeScopedQuery = <TResult, TQueryInfo extends QueryInfo>(
47
+ makeQuery: (otelContext: otel.Context) => LiveQuery<TResult, TQueryInfo>,
48
+ key: DepKey,
49
+ options?: {
50
+ otel?: {
51
+ spanName?: string
52
+ attributes?: otel.Attributes
53
+ }
54
+ },
55
+ ): { query$: LiveQuery<TResult, TQueryInfo>; otelContext: otel.Context } => {
56
+ const { store } = useStore()
57
+ const fullKey = React.useMemo(
58
+ // NOTE We're using the `makeQuery` function body string to make sure the key is unique across the app
59
+ // TODO we should figure out whether this could cause some problems and/or if there's a better way to do this
60
+ () => (Array.isArray(key) ? key.join('-') : key) + '-' + store.reactivityGraph.id + '-' + makeQuery.toString(),
61
+ [key, makeQuery, store.reactivityGraph.id],
62
+ )
63
+ const fullKeyRef = React.useRef<string>()
64
+
65
+ const { query$, otelContext } = React.useMemo(() => {
66
+ if (fullKeyRef.current !== undefined && fullKeyRef.current !== fullKey) {
67
+ // console.debug('fullKey changed', 'prev', fullKeyRef.current.split('-')[0]!, '-> new', fullKey.split('-')[0]!)
68
+
69
+ const cachedItem = cache.get(fullKeyRef.current)
70
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
71
+ cachedItem.rc--
72
+
73
+ if (cachedItem.rc === 0) {
74
+ // console.debug('rc=0-changed', cachedItem.query$.id, cachedItem.query$.label)
75
+ cachedItem.query$.destroy()
76
+ cachedItem.span.end()
77
+ cache.set(fullKeyRef.current, { _tag: 'destroyed' })
78
+ }
79
+ }
80
+ }
81
+
82
+ const cachedItem = cache.get(fullKey)
83
+ if (cachedItem !== undefined && cachedItem._tag === 'active') {
84
+ // console.debug('rc++', cachedItem.query$.id, cachedItem.query$.label)
85
+ cachedItem.rc++
86
+
87
+ return cachedItem
88
+ }
89
+
90
+ const spanName = options?.otel?.spanName ?? `LiveStore:useScopedQuery:${key}`
91
+
92
+ const span = store.otel.tracer.startSpan(
93
+ spanName,
94
+ { attributes: options?.otel?.attributes },
95
+ store.otel.queriesSpanContext,
96
+ )
97
+
98
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
99
+
100
+ const query$ = makeQuery(otelContext)
101
+
102
+ cache.set(fullKey, { _tag: 'active', rc: 1, query$, span, otelContext })
103
+
104
+ return { query$, otelContext }
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [fullKey])
107
+
108
+ fullKeyRef.current = fullKey
109
+
110
+ React.useEffect(() => {
111
+ return () => {
112
+ const fullKey = fullKeyRef.current!
113
+ const cachedItem = cache.get(fullKey)
114
+ // NOTE in case the fullKey changed then the query was already destroyed in the useMemo above
115
+ if (cachedItem === undefined || cachedItem._tag === 'destroyed') return
116
+
117
+ // console.debug('rc--', cachedItem.query$.id, cachedItem.query$.label)
118
+
119
+ cachedItem.rc--
120
+
121
+ if (cachedItem.rc === 0) {
122
+ // console.debug('rc=0', cachedItem.query$.id, cachedItem.query$.label)
123
+ cachedItem.query$.destroy()
124
+ cachedItem.span.end()
125
+ cache.delete(fullKey)
126
+ }
127
+ }
128
+ }, [])
129
+
130
+ return { query$, otelContext }
131
+ }