@livestore/livestore 0.0.0-snapshot-29dc6acb4ddfcb70ac29c4ae18419710d194e555 → 0.0.0-snapshot-669b49b56c8abe87f4e11263af7cbf506deab38e

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.
Files changed (81) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/global-state.d.ts +1 -1
  3. package/dist/global-state.d.ts.map +1 -1
  4. package/dist/global-state.js +1 -1
  5. package/dist/global-state.js.map +1 -1
  6. package/dist/index.d.ts +6 -6
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/{live-queries → reactiveQueries}/base-class.d.ts +4 -8
  11. package/dist/reactiveQueries/base-class.d.ts.map +1 -0
  12. package/dist/{live-queries → reactiveQueries}/base-class.js +0 -2
  13. package/dist/reactiveQueries/base-class.js.map +1 -0
  14. package/dist/{live-queries → reactiveQueries}/computed.d.ts +13 -4
  15. package/dist/reactiveQueries/computed.d.ts.map +1 -0
  16. package/dist/{live-queries → reactiveQueries}/computed.js +23 -4
  17. package/dist/reactiveQueries/computed.js.map +1 -0
  18. package/dist/{live-queries → reactiveQueries}/graphql.d.ts +8 -4
  19. package/dist/reactiveQueries/graphql.d.ts.map +1 -0
  20. package/dist/{live-queries → reactiveQueries}/graphql.js +16 -2
  21. package/dist/reactiveQueries/graphql.js.map +1 -0
  22. package/dist/reactiveQueries/sql.d.ts +49 -0
  23. package/dist/reactiveQueries/sql.d.ts.map +1 -0
  24. package/dist/reactiveQueries/sql.js +130 -0
  25. package/dist/reactiveQueries/sql.js.map +1 -0
  26. package/dist/reactiveQueries/sql.test.d.ts +2 -0
  27. package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
  28. package/dist/reactiveQueries/sql.test.js +284 -0
  29. package/dist/reactiveQueries/sql.test.js.map +1 -0
  30. package/dist/row-query.d.ts +33 -0
  31. package/dist/row-query.d.ts.map +1 -0
  32. package/dist/row-query.js +80 -0
  33. package/dist/row-query.js.map +1 -0
  34. package/dist/store/create-store.d.ts +1 -1
  35. package/dist/store/create-store.d.ts.map +1 -1
  36. package/dist/store/devtools.d.ts +1 -1
  37. package/dist/store/devtools.d.ts.map +1 -1
  38. package/dist/store/devtools.js.map +1 -1
  39. package/dist/store/store-types.d.ts +2 -2
  40. package/dist/store/store-types.d.ts.map +1 -1
  41. package/dist/store/store.d.ts +3 -8
  42. package/dist/store/store.d.ts.map +1 -1
  43. package/dist/store/store.js +4 -32
  44. package/dist/store/store.js.map +1 -1
  45. package/dist/utils/tests/fixture.d.ts +132 -168
  46. package/dist/utils/tests/fixture.d.ts.map +1 -1
  47. package/package.json +5 -5
  48. package/src/global-state.ts +1 -1
  49. package/src/index.ts +5 -8
  50. package/src/{live-queries → reactiveQueries}/base-class.ts +5 -10
  51. package/src/{live-queries → reactiveQueries}/computed.ts +29 -5
  52. package/src/{live-queries → reactiveQueries}/graphql.ts +21 -6
  53. package/src/reactiveQueries/sql.test.ts +308 -0
  54. package/src/reactiveQueries/sql.ts +226 -0
  55. package/src/row-query.ts +196 -0
  56. package/src/store/create-store.ts +1 -1
  57. package/src/store/devtools.ts +1 -1
  58. package/src/store/store-types.ts +2 -2
  59. package/src/store/store.ts +7 -44
  60. package/dist/live-queries/base-class.d.ts.map +0 -1
  61. package/dist/live-queries/base-class.js.map +0 -1
  62. package/dist/live-queries/computed.d.ts.map +0 -1
  63. package/dist/live-queries/computed.js.map +0 -1
  64. package/dist/live-queries/db.d.ts +0 -66
  65. package/dist/live-queries/db.d.ts.map +0 -1
  66. package/dist/live-queries/db.js +0 -199
  67. package/dist/live-queries/db.js.map +0 -1
  68. package/dist/live-queries/db.test.d.ts +0 -2
  69. package/dist/live-queries/db.test.d.ts.map +0 -1
  70. package/dist/live-queries/db.test.js +0 -117
  71. package/dist/live-queries/db.test.js.map +0 -1
  72. package/dist/live-queries/graphql.d.ts.map +0 -1
  73. package/dist/live-queries/graphql.js.map +0 -1
  74. package/dist/row-query-utils.d.ts +0 -17
  75. package/dist/row-query-utils.d.ts.map +0 -1
  76. package/dist/row-query-utils.js +0 -30
  77. package/dist/row-query-utils.js.map +0 -1
  78. package/src/live-queries/__snapshots__/db.test.ts.snap +0 -301
  79. package/src/live-queries/db.test.ts +0 -153
  80. package/src/live-queries/db.ts +0 -350
  81. package/src/row-query-utils.ts +0 -65
@@ -1,4 +1,4 @@
1
- import type { QueryInfo } from '@livestore/common'
1
+ import type { QueryInfo, QueryInfoNone } from '@livestore/common'
2
2
  import * as otel from '@opentelemetry/api'
3
3
 
4
4
  import { globalReactivityGraph } from '../global-state.js'
@@ -8,7 +8,7 @@ import { getDurationMsFromSpan } from '../utils/otel.js'
8
8
  import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
9
9
  import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
10
10
 
11
- export const computed = <TResult, TQueryInfo extends QueryInfo = QueryInfo.None>(
11
+ export const computed = <TResult, TQueryInfo extends QueryInfo = QueryInfoNone>(
12
12
  fn: (get: GetAtomResult) => TResult,
13
13
  options?: {
14
14
  label: string
@@ -16,14 +16,14 @@ export const computed = <TResult, TQueryInfo extends QueryInfo = QueryInfo.None>
16
16
  queryInfo?: TQueryInfo
17
17
  },
18
18
  ): LiveQuery<TResult, TQueryInfo> =>
19
- new LiveStoreComputedQuery<TResult, TQueryInfo>({
19
+ new LiveStoreJSQuery<TResult, TQueryInfo>({
20
20
  fn,
21
21
  label: options?.label ?? fn.toString(),
22
22
  reactivityGraph: options?.reactivityGraph,
23
23
  queryInfo: options?.queryInfo,
24
24
  })
25
25
 
26
- export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = QueryInfo.None> extends LiveStoreQueryBase<
26
+ export class LiveStoreJSQuery<TResult, TQueryInfo extends QueryInfo = QueryInfoNone> extends LiveStoreQueryBase<
27
27
  TResult,
28
28
  TQueryInfo
29
29
  > {
@@ -38,19 +38,31 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
38
38
 
39
39
  queryInfo: TQueryInfo
40
40
 
41
+ /**
42
+ * Currently only used for "nested destruction" of piped queries
43
+ *
44
+ * i.e. when doing something like `const q = querySQL(...).pipe(...)`
45
+ * we need to also destory the SQL query when the JS query `q` is destroyed
46
+ */
47
+ private onDestroy: (() => void) | undefined
48
+
41
49
  constructor({
42
50
  fn,
43
51
  label,
52
+ onDestroy,
44
53
  reactivityGraph,
45
54
  queryInfo,
46
55
  }: {
47
56
  label: string
48
57
  fn: (get: GetAtomResult) => TResult
58
+ /** Currently only used for "nested destruction" of piped queries */
59
+ onDestroy?: () => void
49
60
  reactivityGraph?: ReactivityGraph
50
61
  queryInfo?: TQueryInfo
51
62
  }) {
52
63
  super()
53
64
 
65
+ this.onDestroy = onDestroy
54
66
  this.label = label
55
67
 
56
68
  this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
@@ -74,11 +86,23 @@ export class LiveStoreComputedQuery<TResult, TQueryInfo extends QueryInfo = Quer
74
86
 
75
87
  return res
76
88
  }),
77
- { label: queryLabel, meta: { liveStoreThunkType: 'computed' } },
89
+ { label: queryLabel, meta: { liveStoreThunkType: 'jsResults' } },
78
90
  )
79
91
  }
80
92
 
93
+ // pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
94
+ // new LiveStoreJSQuery({
95
+ // fn: (get) => {
96
+ // const results = get(this.results$)
97
+ // return fn(results, get)
98
+ // },
99
+ // label: `${this.label}:js`,
100
+ // onDestroy: () => this.destroy(),
101
+ // reactivityGraph: this.reactivityGraph,
102
+ // })
103
+
81
104
  destroy = () => {
82
105
  this.reactivityGraph.destroyNode(this.results$)
106
+ this.onDestroy?.()
83
107
  }
84
108
  }
@@ -1,5 +1,5 @@
1
1
  import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
2
- import type { QueryInfo } from '@livestore/common'
2
+ import type { QueryInfoNone } from '@livestore/common'
3
3
  import { shouldNeverHappen } from '@livestore/utils'
4
4
  import { Schema, TreeFormatter } from '@livestore/utils/effect'
5
5
  import * as otel from '@opentelemetry/api'
@@ -31,7 +31,7 @@ export const queryGraphQL = <
31
31
  reactivityGraph?: ReactivityGraph
32
32
  map?: MapResult<TResultMapped, TResult>
33
33
  } = {},
34
- ): LiveQuery<TResultMapped, QueryInfo.None> =>
34
+ ): LiveQuery<TResultMapped, QueryInfoNone> =>
35
35
  new LiveStoreGraphQLQuery({ document, genVariableValues, label, reactivityGraph, map })
36
36
 
37
37
  export class LiveStoreGraphQLQuery<
@@ -39,7 +39,7 @@ export class LiveStoreGraphQLQuery<
39
39
  TVariableValues extends Record<string, any>,
40
40
  TContext extends BaseGraphQLContext,
41
41
  TResultMapped extends Record<string, any> = TResult,
42
- > extends LiveStoreQueryBase<TResultMapped, QueryInfo.None> {
42
+ > extends LiveStoreQueryBase<TResultMapped, QueryInfoNone> {
43
43
  _tag: 'graphql' = 'graphql'
44
44
 
45
45
  /** The abstract GraphQL query */
@@ -54,7 +54,7 @@ export class LiveStoreGraphQLQuery<
54
54
 
55
55
  protected reactivityGraph: ReactivityGraph
56
56
 
57
- queryInfo: QueryInfo.None = { _tag: 'None' }
57
+ queryInfo: QueryInfoNone = { _tag: 'None' }
58
58
 
59
59
  private mapResult
60
60
 
@@ -105,7 +105,7 @@ export class LiveStoreGraphQLQuery<
105
105
  (get, _setDebugInfo, { rootOtelContext }, otelContext) => {
106
106
  return genVariableValues(makeGetAtomResult(get, otelContext ?? rootOtelContext))
107
107
  },
108
- { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphql.variables' } },
108
+ { label: `${labelWithDefault}:variableValues`, meta: { liveStoreThunkType: 'graphqlVariableValues' } },
109
109
  )
110
110
  this.variableValues$ = variableValues$OrvariableValues
111
111
  } else {
@@ -137,11 +137,26 @@ export class LiveStoreGraphQLQuery<
137
137
 
138
138
  return result
139
139
  },
140
- { label: resultsLabel, meta: { liveStoreThunkType: 'graphql.result' } },
140
+ { label: resultsLabel, meta: { liveStoreThunkType: 'graphqlResults' } },
141
141
  // otelContext,
142
142
  )
143
143
  }
144
144
 
145
+ /**
146
+ * Returns a new reactive query that contains the result of
147
+ * running an arbitrary JS computation on the results of this SQL query.
148
+ */
149
+ // pipe = <U>(fn: (result: TResult, get: GetAtomResult) => U): LiveStoreJSQuery<U> =>
150
+ // new LiveStoreJSQuery({
151
+ // fn: (get) => {
152
+ // const results = get(this.results$)
153
+ // return fn(results, get)
154
+ // },
155
+ // label: `${this.label}:js`,
156
+ // onDestroy: () => this.destroy(),
157
+ // reactivityGraph: this.reactivityGraph,
158
+ // })
159
+
145
160
  queryOnce = ({
146
161
  document,
147
162
  otelContext,
@@ -0,0 +1,308 @@
1
+ import { Effect, Schema } from '@livestore/utils/effect'
2
+ import * as otel from '@opentelemetry/api'
3
+ import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ import { computed, querySQL, rawSqlMutation, sql } from '../index.js'
7
+ import { makeTodoMvc, tables } from '../utils/tests/fixture.js'
8
+ import { getSimplifiedRootSpan } from '../utils/tests/otel.js'
9
+
10
+ /*
11
+ TODO write tests for:
12
+
13
+ - sql queries without and with `map` (incl. callback and schemas)
14
+ - optional and explicit `queriedTables` argument
15
+ */
16
+
17
+ describe('otel', () => {
18
+ let cachedProvider: BasicTracerProvider | undefined
19
+
20
+ const makeQuery = Effect.gen(function* () {
21
+ const exporter = new InMemorySpanExporter()
22
+
23
+ const provider = cachedProvider ?? new BasicTracerProvider()
24
+ cachedProvider = provider
25
+ provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
26
+ provider.register()
27
+
28
+ const otelTracer = otel.trace.getTracer('test')
29
+
30
+ const span = otelTracer.startSpan('test')
31
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
32
+
33
+ const { store } = yield* makeTodoMvc({ otelTracer, otelContext })
34
+
35
+ return {
36
+ store,
37
+ otelTracer,
38
+ exporter,
39
+ span,
40
+ provider,
41
+ }
42
+ })
43
+
44
+ it('otel', async () => {
45
+ const { exporter } = await Effect.gen(function* () {
46
+ const { store, exporter, span } = yield* makeQuery
47
+
48
+ const query = querySQL(`select * from todos`, {
49
+ schema: Schema.Array(tables.todos.schema),
50
+ queriedTables: new Set(['todos']),
51
+ })
52
+ expect(query.run()).toMatchInlineSnapshot('[]')
53
+
54
+ store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
55
+
56
+ expect(query.run()).toMatchInlineSnapshot(`
57
+ [
58
+ {
59
+ "completed": false,
60
+ "id": "t1",
61
+ "text": "buy milk",
62
+ },
63
+ ]
64
+ `)
65
+
66
+ query.destroy()
67
+ span.end()
68
+
69
+ return { exporter }
70
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
71
+
72
+ expect(getSimplifiedRootSpan(exporter)).toMatchInlineSnapshot(`
73
+ {
74
+ "_name": "test",
75
+ "children": [
76
+ {
77
+ "_name": "livestore.in-memory-db:execute",
78
+ "attributes": {
79
+ "sql.query": "
80
+ PRAGMA page_size=32768;
81
+ PRAGMA cache_size=10000;
82
+ PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
83
+ PRAGMA synchronous='OFF';
84
+ PRAGMA temp_store='MEMORY';
85
+ PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
86
+ ",
87
+ },
88
+ },
89
+ {
90
+ "_name": "LiveStore:mutations",
91
+ "children": [
92
+ {
93
+ "_name": "LiveStore:mutate",
94
+ "attributes": {
95
+ "livestore.mutateLabel": "mutate",
96
+ },
97
+ "children": [
98
+ {
99
+ "_name": "LiveStore:processWrites",
100
+ "attributes": {
101
+ "livestore.mutateLabel": "mutate",
102
+ },
103
+ "children": [
104
+ {
105
+ "_name": "LiveStore:mutateWithoutRefresh",
106
+ "attributes": {
107
+ "livestore.args": "{
108
+ "sql": "INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)"
109
+ }",
110
+ "livestore.mutation": "livestore.RawSql",
111
+ },
112
+ "children": [
113
+ {
114
+ "_name": "livestore.in-memory-db:execute",
115
+ "attributes": {
116
+ "sql.query": "INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)",
117
+ },
118
+ },
119
+ ],
120
+ },
121
+ ],
122
+ },
123
+ ],
124
+ },
125
+ ],
126
+ },
127
+ {
128
+ "_name": "LiveStore:queries",
129
+ "children": [
130
+ {
131
+ "_name": "sql:select * from todos",
132
+ "attributes": {
133
+ "sql.query": "select * from todos",
134
+ "sql.rowsCount": 0,
135
+ },
136
+ "children": [
137
+ {
138
+ "_name": "sql-in-memory-select",
139
+ "attributes": {
140
+ "sql.cached": false,
141
+ "sql.query": "select * from todos",
142
+ "sql.rowsCount": 0,
143
+ },
144
+ },
145
+ ],
146
+ },
147
+ {
148
+ "_name": "sql:select * from todos",
149
+ "attributes": {
150
+ "sql.query": "select * from todos",
151
+ "sql.rowsCount": 1,
152
+ },
153
+ "children": [
154
+ {
155
+ "_name": "sql-in-memory-select",
156
+ "attributes": {
157
+ "sql.cached": false,
158
+ "sql.query": "select * from todos",
159
+ "sql.rowsCount": 1,
160
+ },
161
+ },
162
+ ],
163
+ },
164
+ ],
165
+ },
166
+ ],
167
+ }
168
+ `)
169
+ })
170
+
171
+ it('with thunks', async () => {
172
+ const { exporter } = await Effect.gen(function* () {
173
+ const { store, exporter, span } = yield* makeQuery
174
+
175
+ const defaultTodo = { id: '', text: '', completed: false }
176
+
177
+ const filter = computed(() => `where completed = 0`, { label: 'where-filter' })
178
+ const query = querySQL((get) => `select * from todos ${get(filter)}`, {
179
+ label: 'all todos',
180
+ schema: Schema.Array(tables.todos.schema).pipe(Schema.headOrElse(() => defaultTodo)),
181
+ })
182
+
183
+ expect(query.run()).toMatchInlineSnapshot(`
184
+ {
185
+ "completed": false,
186
+ "id": "",
187
+ "text": "",
188
+ }
189
+ `)
190
+
191
+ store.mutate(rawSqlMutation({ sql: sql`INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)` }))
192
+
193
+ expect(query.run()).toMatchInlineSnapshot(`
194
+ {
195
+ "completed": false,
196
+ "id": "t1",
197
+ "text": "buy milk",
198
+ }
199
+ `)
200
+
201
+ query.destroy()
202
+ span.end()
203
+
204
+ return { exporter }
205
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.runPromise)
206
+
207
+ expect(getSimplifiedRootSpan(exporter)).toMatchInlineSnapshot(`
208
+ {
209
+ "_name": "test",
210
+ "children": [
211
+ {
212
+ "_name": "livestore.in-memory-db:execute",
213
+ "attributes": {
214
+ "sql.query": "
215
+ PRAGMA page_size=32768;
216
+ PRAGMA cache_size=10000;
217
+ PRAGMA journal_mode='MEMORY'; -- we don't flush to disk before committing a write
218
+ PRAGMA synchronous='OFF';
219
+ PRAGMA temp_store='MEMORY';
220
+ PRAGMA foreign_keys='ON'; -- we want foreign key constraints to be enforced
221
+ ",
222
+ },
223
+ },
224
+ {
225
+ "_name": "LiveStore:mutations",
226
+ "children": [
227
+ {
228
+ "_name": "LiveStore:mutate",
229
+ "attributes": {
230
+ "livestore.mutateLabel": "mutate",
231
+ },
232
+ "children": [
233
+ {
234
+ "_name": "LiveStore:processWrites",
235
+ "attributes": {
236
+ "livestore.mutateLabel": "mutate",
237
+ },
238
+ "children": [
239
+ {
240
+ "_name": "LiveStore:mutateWithoutRefresh",
241
+ "attributes": {
242
+ "livestore.args": "{
243
+ "sql": "INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)"
244
+ }",
245
+ "livestore.mutation": "livestore.RawSql",
246
+ },
247
+ "children": [
248
+ {
249
+ "_name": "livestore.in-memory-db:execute",
250
+ "attributes": {
251
+ "sql.query": "INSERT INTO todos (id, text, completed) VALUES ('t1', 'buy milk', 0)",
252
+ },
253
+ },
254
+ ],
255
+ },
256
+ ],
257
+ },
258
+ ],
259
+ },
260
+ ],
261
+ },
262
+ {
263
+ "_name": "LiveStore:queries",
264
+ "children": [
265
+ {
266
+ "_name": "sql:select * from todos where completed = 0",
267
+ "attributes": {
268
+ "sql.query": "select * from todos where completed = 0",
269
+ "sql.rowsCount": 0,
270
+ },
271
+ "children": [
272
+ {
273
+ "_name": "js:where-filter",
274
+ },
275
+ {
276
+ "_name": "sql-in-memory-select",
277
+ "attributes": {
278
+ "sql.cached": false,
279
+ "sql.query": "select * from todos where completed = 0",
280
+ "sql.rowsCount": 0,
281
+ },
282
+ },
283
+ ],
284
+ },
285
+ {
286
+ "_name": "sql:select * from todos where completed = 0",
287
+ "attributes": {
288
+ "sql.query": "select * from todos where completed = 0",
289
+ "sql.rowsCount": 1,
290
+ },
291
+ "children": [
292
+ {
293
+ "_name": "sql-in-memory-select",
294
+ "attributes": {
295
+ "sql.cached": false,
296
+ "sql.query": "select * from todos where completed = 0",
297
+ "sql.rowsCount": 1,
298
+ },
299
+ },
300
+ ],
301
+ },
302
+ ],
303
+ },
304
+ ],
305
+ }
306
+ `)
307
+ })
308
+ })
@@ -0,0 +1,226 @@
1
+ import { type Bindable, prepareBindValues, type QueryInfo, type QueryInfoNone } from '@livestore/common'
2
+ import { shouldNeverHappen } from '@livestore/utils'
3
+ import { Schema, TreeFormatter } from '@livestore/utils/effect'
4
+ import * as otel from '@opentelemetry/api'
5
+
6
+ import { globalReactivityGraph } from '../global-state.js'
7
+ import type { Thunk } from '../reactive.js'
8
+ import { NOT_REFRESHED_YET } from '../reactive.js'
9
+ import type { RefreshReason } from '../store/store-types.js'
10
+ import { getDurationMsFromSpan } from '../utils/otel.js'
11
+ import type { GetAtomResult, LiveQuery, QueryContext, ReactivityGraph } from './base-class.js'
12
+ import { LiveStoreQueryBase, makeGetAtomResult } from './base-class.js'
13
+
14
+ /**
15
+ * NOTE `querySQL` is only supposed to read data. Don't use it to insert/update/delete data but use mutations instead.
16
+ */
17
+ export const querySQL = <TResultSchema, TResult = TResultSchema>(
18
+ query: string | ((get: GetAtomResult) => string),
19
+ options: {
20
+ schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
21
+ map?: (rows: TResultSchema) => TResult
22
+ /**
23
+ * Can be provided explicitly to slightly speed up initial query performance
24
+ *
25
+ * NOTE In the future we want to do this automatically at build time
26
+ */
27
+ queriedTables?: Set<string>
28
+ bindValues?: Bindable
29
+ label?: string
30
+ reactivityGraph?: ReactivityGraph
31
+ },
32
+ ): LiveQuery<TResult, QueryInfoNone> =>
33
+ new LiveStoreSQLQuery<TResultSchema, TResult, QueryInfoNone>({
34
+ label: options?.label,
35
+ genQueryString: query,
36
+ queriedTables: options?.queriedTables,
37
+ bindValues: options?.bindValues,
38
+ reactivityGraph: options?.reactivityGraph,
39
+ map: options?.map,
40
+ schema: options.schema,
41
+ queryInfo: { _tag: 'None' },
42
+ })
43
+
44
+ /* An object encapsulating a reactive SQL query */
45
+ export class LiveStoreSQLQuery<
46
+ TResultSchema,
47
+ TResult = TResultSchema,
48
+ TQueryInfo extends QueryInfo = QueryInfoNone,
49
+ > extends LiveStoreQueryBase<TResult, TQueryInfo> {
50
+ _tag: 'sql' = 'sql'
51
+
52
+ /** A reactive thunk representing the query text */
53
+ queryString$: Thunk<string, QueryContext, RefreshReason> | undefined
54
+
55
+ /** A reactive thunk representing the query results */
56
+ results$: Thunk<TResult, QueryContext, RefreshReason>
57
+
58
+ label: string
59
+
60
+ protected reactivityGraph
61
+
62
+ /** Currently only used by `rowQuery` for lazy table migrations and eager default row insertion */
63
+ private execBeforeFirstRun
64
+
65
+ private mapResult: (rows: TResultSchema) => TResult
66
+ private schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
67
+
68
+ queryInfo: TQueryInfo
69
+
70
+ constructor({
71
+ genQueryString,
72
+ queriedTables,
73
+ bindValues,
74
+ label = genQueryString.toString(),
75
+ reactivityGraph,
76
+ schema,
77
+ map,
78
+ execBeforeFirstRun,
79
+ queryInfo,
80
+ }: {
81
+ label?: string
82
+ genQueryString: string | ((get: GetAtomResult, ctx: QueryContext) => string)
83
+ queriedTables?: Set<string>
84
+ bindValues?: Bindable
85
+ reactivityGraph?: ReactivityGraph
86
+ schema: Schema.Schema<TResultSchema, ReadonlyArray<any>>
87
+ map?: (rows: TResultSchema) => TResult
88
+ execBeforeFirstRun?: (ctx: QueryContext) => void
89
+ queryInfo?: TQueryInfo
90
+ }) {
91
+ super()
92
+
93
+ this.label = `sql(${label})`
94
+ this.reactivityGraph = reactivityGraph ?? globalReactivityGraph
95
+ this.execBeforeFirstRun = execBeforeFirstRun
96
+ this.queryInfo = queryInfo ?? ({ _tag: 'None' } as TQueryInfo)
97
+
98
+ this.schema = schema
99
+ this.mapResult = map === undefined ? (rows: any) => rows as TResult : map
100
+
101
+ let queryString$OrQueryString: string | Thunk<string, QueryContext, RefreshReason>
102
+ if (typeof genQueryString === 'function') {
103
+ queryString$OrQueryString = this.reactivityGraph.makeThunk(
104
+ (get, setDebugInfo, ctx, otelContext) => {
105
+ const startMs = performance.now()
106
+ const queryString = genQueryString(makeGetAtomResult(get, otelContext ?? ctx.rootOtelContext), ctx)
107
+ const durationMs = performance.now() - startMs
108
+ setDebugInfo({ _tag: 'computed', label: `${label}:queryString`, query: queryString, durationMs })
109
+ return queryString
110
+ },
111
+ {
112
+ label: `${label}:queryString`,
113
+ meta: { liveStoreThunkType: 'sqlQueryString' },
114
+ equal: (a, b) => a === b,
115
+ },
116
+ )
117
+
118
+ this.queryString$ = queryString$OrQueryString
119
+ } else {
120
+ queryString$OrQueryString = genQueryString
121
+ }
122
+
123
+ const queryLabel = `${label}:results`
124
+
125
+ const queriedTablesRef = { current: queriedTables }
126
+
127
+ const schemaEqual = Schema.equivalence(schema)
128
+ // TODO also support derived equality for `map` (probably will depend on having an easy way to transform a schema without an `encode` step)
129
+ // This would mean dropping the `map` option
130
+ const equal =
131
+ map === undefined
132
+ ? (a: TResult, b: TResult) =>
133
+ a === NOT_REFRESHED_YET || b === NOT_REFRESHED_YET ? false : schemaEqual(a as any, b as any)
134
+ : undefined
135
+
136
+ const results$ = this.reactivityGraph.makeThunk<TResult>(
137
+ (get, setDebugInfo, { store, otelTracer, rootOtelContext }, otelContext) =>
138
+ otelTracer.startActiveSpan(
139
+ 'sql:...', // NOTE span name will be overridden further down
140
+ {},
141
+ otelContext ?? rootOtelContext,
142
+ (span) => {
143
+ const otelContext = otel.trace.setSpan(otel.context.active(), span)
144
+
145
+ if (this.execBeforeFirstRun !== undefined) {
146
+ this.execBeforeFirstRun({ store, otelTracer, rootOtelContext, effectsWrapper: (run) => run() })
147
+ this.execBeforeFirstRun = undefined
148
+ }
149
+
150
+ const sqlString =
151
+ typeof queryString$OrQueryString === 'string'
152
+ ? queryString$OrQueryString
153
+ : get(queryString$OrQueryString, otelContext)
154
+
155
+ if (queriedTablesRef.current === undefined) {
156
+ queriedTablesRef.current = store.syncDbWrapper.getTablesUsed(sqlString)
157
+ }
158
+
159
+ // Establish a reactive dependency on the tables used in the query
160
+ for (const tableName of queriedTablesRef.current) {
161
+ const tableRef = store.tableRefs[tableName] ?? shouldNeverHappen(`No table ref found for ${tableName}`)
162
+ get(tableRef, otelContext)
163
+ }
164
+
165
+ span.setAttribute('sql.query', sqlString)
166
+ span.updateName(`sql:${sqlString.slice(0, 50)}`)
167
+
168
+ const rawResults = store.syncDbWrapper.select<any>(sqlString, {
169
+ queriedTables,
170
+ bindValues: bindValues ? prepareBindValues(bindValues, sqlString) : undefined,
171
+ otelContext,
172
+ })
173
+
174
+ span.setAttribute('sql.rowsCount', rawResults.length)
175
+
176
+ const parsedResult = Schema.decodeEither(this.schema)(rawResults)
177
+
178
+ if (parsedResult._tag === 'Left') {
179
+ const parseErrorStr = TreeFormatter.formatErrorSync(parsedResult.left)
180
+ const expectedSchemaStr = String(this.schema.ast)
181
+ const bindValuesStr = bindValues === undefined ? '' : `\nBind values: ${JSON.stringify(bindValues)}`
182
+
183
+ console.error(
184
+ `\
185
+ Error parsing SQL query result.
186
+
187
+ Query: ${sqlString}\
188
+ ${bindValuesStr}
189
+
190
+ Expected schema: ${expectedSchemaStr}
191
+
192
+ Error: ${parseErrorStr}
193
+
194
+ Result:`,
195
+ rawResults,
196
+ )
197
+ return shouldNeverHappen(`Error parsing SQL query result: ${parsedResult.left}`)
198
+ }
199
+
200
+ const result = this.mapResult(parsedResult.right)
201
+
202
+ span.end()
203
+
204
+ const durationMs = getDurationMsFromSpan(span)
205
+
206
+ this.executionTimes.push(durationMs)
207
+
208
+ setDebugInfo({ _tag: 'sql', label, query: sqlString, durationMs })
209
+
210
+ return result
211
+ },
212
+ ),
213
+ { label: queryLabel, equal },
214
+ )
215
+
216
+ this.results$ = results$
217
+ }
218
+
219
+ destroy = () => {
220
+ if (this.queryString$ !== undefined) {
221
+ this.reactivityGraph.destroyNode(this.queryString$)
222
+ }
223
+
224
+ this.reactivityGraph.destroyNode(this.results$)
225
+ }
226
+ }