@livestore/livestore 0.0.0-snapshot-7438d57f493eb9f78c301e784856d60182aeb884 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/global-state.d.ts +1 -1
- package/dist/global-state.d.ts.map +1 -1
- package/dist/global-state.js +1 -1
- package/dist/global-state.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/{live-queries → reactiveQueries}/base-class.d.ts +4 -8
- package/dist/reactiveQueries/base-class.d.ts.map +1 -0
- package/dist/{live-queries → reactiveQueries}/base-class.js +0 -2
- package/dist/reactiveQueries/base-class.js.map +1 -0
- package/dist/{live-queries → reactiveQueries}/computed.d.ts +13 -4
- package/dist/reactiveQueries/computed.d.ts.map +1 -0
- package/dist/{live-queries → reactiveQueries}/computed.js +23 -4
- package/dist/reactiveQueries/computed.js.map +1 -0
- package/dist/{live-queries → reactiveQueries}/graphql.d.ts +8 -4
- package/dist/reactiveQueries/graphql.d.ts.map +1 -0
- package/dist/{live-queries → reactiveQueries}/graphql.js +16 -2
- package/dist/reactiveQueries/graphql.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 +80 -0
- package/dist/row-query.js.map +1 -0
- package/dist/store/create-store.d.ts +1 -1
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/devtools.d.ts +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +2 -2
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store.d.ts +3 -8
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +4 -32
- package/dist/store/store.js.map +1 -1
- package/dist/utils/tests/fixture.d.ts +132 -168
- package/dist/utils/tests/fixture.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/global-state.ts +1 -1
- package/src/index.ts +5 -8
- package/src/{live-queries → reactiveQueries}/base-class.ts +5 -10
- package/src/{live-queries → reactiveQueries}/computed.ts +29 -5
- package/src/{live-queries → reactiveQueries}/graphql.ts +21 -6
- package/src/reactiveQueries/sql.test.ts +308 -0
- package/src/reactiveQueries/sql.ts +226 -0
- package/src/row-query.ts +196 -0
- package/src/store/create-store.ts +1 -1
- package/src/store/devtools.ts +1 -1
- package/src/store/store-types.ts +2 -2
- package/src/store/store.ts +7 -44
- package/dist/live-queries/base-class.d.ts.map +0 -1
- package/dist/live-queries/base-class.js.map +0 -1
- package/dist/live-queries/computed.d.ts.map +0 -1
- package/dist/live-queries/computed.js.map +0 -1
- package/dist/live-queries/db.d.ts +0 -66
- package/dist/live-queries/db.d.ts.map +0 -1
- package/dist/live-queries/db.js +0 -199
- package/dist/live-queries/db.js.map +0 -1
- package/dist/live-queries/db.test.d.ts +0 -2
- package/dist/live-queries/db.test.d.ts.map +0 -1
- package/dist/live-queries/db.test.js +0 -117
- package/dist/live-queries/db.test.js.map +0 -1
- package/dist/live-queries/graphql.d.ts.map +0 -1
- package/dist/live-queries/graphql.js.map +0 -1
- package/dist/row-query-utils.d.ts +0 -17
- package/dist/row-query-utils.d.ts.map +0 -1
- package/dist/row-query-utils.js +0 -30
- package/dist/row-query-utils.js.map +0 -1
- package/src/live-queries/__snapshots__/db.test.ts.snap +0 -301
- package/src/live-queries/db.test.ts +0 -153
- package/src/live-queries/db.ts +0 -350
- 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 =
|
|
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
|
|
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
|
|
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: '
|
|
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 {
|
|
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,
|
|
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,
|
|
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:
|
|
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: '
|
|
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: '
|
|
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
|
+
}
|