@livestore/livestore 0.0.0-snapshot-909cdd1ac2fd591945c2be2b0f53e14d87f3c9d4
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.
- package/README.md +1 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/QueryCache.d.ts +20 -0
- package/dist/QueryCache.d.ts.map +1 -0
- package/dist/QueryCache.js +61 -0
- package/dist/QueryCache.js.map +1 -0
- package/dist/SynchronousDatabaseWrapper.d.ts +36 -0
- package/dist/SynchronousDatabaseWrapper.d.ts.map +1 -0
- package/dist/SynchronousDatabaseWrapper.js +176 -0
- package/dist/SynchronousDatabaseWrapper.js.map +1 -0
- package/dist/effect/LiveStore.d.ts +38 -0
- package/dist/effect/LiveStore.d.ts.map +1 -0
- package/dist/effect/LiveStore.js +38 -0
- package/dist/effect/LiveStore.js.map +1 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.d.ts.map +1 -0
- package/dist/effect/index.js +2 -0
- package/dist/effect/index.js.map +1 -0
- package/dist/global-state.d.ts +14 -0
- package/dist/global-state.d.ts.map +1 -0
- package/dist/global-state.js +16 -0
- package/dist/global-state.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/reactive.d.ts +163 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +382 -0
- package/dist/reactive.js.map +1 -0
- package/dist/reactive.test.d.ts +2 -0
- package/dist/reactive.test.d.ts.map +1 -0
- package/dist/reactive.test.js +345 -0
- package/dist/reactive.test.js.map +1 -0
- package/dist/reactiveQueries/base-class.d.ts +59 -0
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/reactiveQueries/base-class.js +29 -0
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/reactiveQueries/graphql.d.ts +52 -0
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/reactiveQueries/graphql.js +136 -0
- package/dist/reactiveQueries/graphql.js.map +1 -0
- package/dist/reactiveQueries/js.d.ts +35 -0
- package/dist/reactiveQueries/js.d.ts.map +1 -0
- package/dist/reactiveQueries/js.js +57 -0
- package/dist/reactiveQueries/js.js.map +1 -0
- package/dist/reactiveQueries/sql.d.ts +49 -0
- package/dist/reactiveQueries/sql.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.js +130 -0
- package/dist/reactiveQueries/sql.js.map +1 -0
- package/dist/reactiveQueries/sql.test.d.ts +2 -0
- package/dist/reactiveQueries/sql.test.d.ts.map +1 -0
- package/dist/reactiveQueries/sql.test.js +284 -0
- package/dist/reactiveQueries/sql.test.js.map +1 -0
- package/dist/row-query.d.ts +33 -0
- package/dist/row-query.d.ts.map +1 -0
- package/dist/row-query.js +84 -0
- package/dist/row-query.js.map +1 -0
- package/dist/store-context.d.ts +26 -0
- package/dist/store-context.d.ts.map +1 -0
- package/dist/store-context.js +6 -0
- package/dist/store-context.js.map +1 -0
- package/dist/store-devtools.d.ts +19 -0
- package/dist/store-devtools.d.ts.map +1 -0
- package/dist/store-devtools.js +141 -0
- package/dist/store-devtools.js.map +1 -0
- package/dist/store.d.ts +175 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +507 -0
- package/dist/store.js.map +1 -0
- package/dist/utils/data-structures.d.ts +10 -0
- package/dist/utils/data-structures.d.ts.map +1 -0
- package/dist/utils/data-structures.js +32 -0
- package/dist/utils/data-structures.js.map +1 -0
- package/dist/utils/dev.d.ts +3 -0
- package/dist/utils/dev.d.ts.map +1 -0
- package/dist/utils/dev.js +17 -0
- package/dist/utils/dev.js.map +1 -0
- package/dist/utils/otel.d.ts +4 -0
- package/dist/utils/otel.d.ts.map +1 -0
- package/dist/utils/otel.js +6 -0
- package/dist/utils/otel.js.map +1 -0
- package/dist/utils/stack-info.d.ts +10 -0
- package/dist/utils/stack-info.d.ts.map +1 -0
- package/dist/utils/stack-info.js +41 -0
- package/dist/utils/stack-info.js.map +1 -0
- package/dist/utils/stack-info.test.d.ts +2 -0
- package/dist/utils/stack-info.test.d.ts.map +1 -0
- package/dist/utils/stack-info.test.js +75 -0
- package/dist/utils/stack-info.test.js.map +1 -0
- package/dist/utils/tests/fixture.d.ts +259 -0
- package/dist/utils/tests/fixture.d.ts.map +1 -0
- package/dist/utils/tests/fixture.js +33 -0
- package/dist/utils/tests/fixture.js.map +1 -0
- package/dist/utils/tests/mod.d.ts +3 -0
- package/dist/utils/tests/mod.d.ts.map +1 -0
- package/dist/utils/tests/mod.js +3 -0
- package/dist/utils/tests/mod.js.map +1 -0
- package/dist/utils/tests/otel.d.ts +10 -0
- package/dist/utils/tests/otel.d.ts.map +1 -0
- package/dist/utils/tests/otel.js +42 -0
- package/dist/utils/tests/otel.js.map +1 -0
- package/package.json +60 -0
- package/src/QueryCache.ts +81 -0
- package/src/SynchronousDatabaseWrapper.ts +256 -0
- package/src/ambient.d.ts +10 -0
- package/src/effect/LiveStore.ts +112 -0
- package/src/effect/index.ts +8 -0
- package/src/global-state.ts +20 -0
- package/src/index.ts +64 -0
- package/src/reactive.test.ts +426 -0
- package/src/reactive.ts +661 -0
- package/src/reactiveQueries/base-class.ts +115 -0
- package/src/reactiveQueries/graphql.ts +233 -0
- package/src/reactiveQueries/js.ts +108 -0
- package/src/reactiveQueries/sql.test.ts +308 -0
- package/src/reactiveQueries/sql.ts +226 -0
- package/src/row-query.ts +200 -0
- package/src/store-context.ts +23 -0
- package/src/store-devtools.ts +217 -0
- package/src/store.ts +920 -0
- package/src/utils/data-structures.ts +36 -0
- package/src/utils/dev.ts +24 -0
- package/src/utils/otel.ts +9 -0
- package/src/utils/stack-info.test.ts +79 -0
- package/src/utils/stack-info.ts +54 -0
- package/src/utils/tests/fixture.ts +77 -0
- package/src/utils/tests/mod.ts +2 -0
- package/src/utils/tests/otel.ts +61 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +9 -0
|
@@ -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.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: 'js', 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
|
+
}
|