@livestore/common 0.3.1-dev.0 → 0.3.2-dev.0
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/ClientSessionLeaderThreadProxy.d.ts +35 -0
- package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
- package/dist/ClientSessionLeaderThreadProxy.js +6 -0
- package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
- package/dist/adapter-types.d.ts +10 -156
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +5 -49
- package/dist/adapter-types.js.map +1 -1
- package/dist/defs.d.ts +20 -0
- package/dist/defs.d.ts.map +1 -0
- package/dist/defs.js +12 -0
- package/dist/defs.js.map +1 -0
- package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
- package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +26 -24
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/errors.d.ts +50 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +122 -123
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/eventlog.d.ts +17 -6
- package/dist/leader-thread/eventlog.d.ts.map +1 -1
- package/dist/leader-thread/eventlog.js +34 -17
- package/dist/leader-thread/eventlog.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +1 -2
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +37 -7
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/materialize-event.d.ts +3 -3
- package/dist/leader-thread/materialize-event.d.ts.map +1 -1
- package/dist/leader-thread/materialize-event.js +27 -10
- package/dist/leader-thread/materialize-event.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +2 -0
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +2 -0
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts +13 -6
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +1 -3
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +6 -7
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/make-client-session.d.ts +1 -1
- package/dist/make-client-session.d.ts.map +1 -1
- package/dist/make-client-session.js +1 -1
- package/dist/make-client-session.js.map +1 -1
- package/dist/materializer-helper.d.ts +13 -2
- package/dist/materializer-helper.d.ts.map +1 -1
- package/dist/materializer-helper.js +25 -11
- package/dist/materializer-helper.js.map +1 -1
- package/dist/rematerialize-from-eventlog.d.ts +1 -1
- package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
- package/dist/rematerialize-from-eventlog.js +12 -4
- package/dist/rematerialize-from-eventlog.js.map +1 -1
- package/dist/schema/EventDef.d.ts +8 -3
- package/dist/schema/EventDef.d.ts.map +1 -1
- package/dist/schema/EventDef.js +5 -2
- package/dist/schema/EventDef.js.map +1 -1
- package/dist/schema/EventSequenceNumber.d.ts +20 -2
- package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
- package/dist/schema/EventSequenceNumber.js +71 -19
- package/dist/schema/EventSequenceNumber.js.map +1 -1
- package/dist/schema/EventSequenceNumber.test.js +88 -3
- package/dist/schema/EventSequenceNumber.test.js.map +1 -1
- package/dist/schema/LiveStoreEvent.d.ts +56 -8
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
- package/dist/schema/LiveStoreEvent.js +34 -8
- package/dist/schema/LiveStoreEvent.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
- package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
- package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
- package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
- package/dist/schema/state/sqlite/mod.d.ts +1 -1
- package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.d.ts +36 -9
- package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
- package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -86
- package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
- package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
- package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
- package/dist/schema/state/sqlite/system-tables.d.ts +380 -432
- package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
- package/dist/schema/state/sqlite/system-tables.js +8 -17
- package/dist/schema/state/sqlite/system-tables.js.map +1 -1
- package/dist/schema/state/sqlite/table-def.d.ts +2 -2
- package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts +3 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/sql-queries/sql-queries.d.ts.map +1 -1
- package/dist/sql-queries/sql-queries.js +2 -0
- package/dist/sql-queries/sql-queries.js.map +1 -1
- package/dist/sqlite-db-helper.d.ts +7 -0
- package/dist/sqlite-db-helper.d.ts.map +1 -0
- package/dist/sqlite-db-helper.js +29 -0
- package/dist/sqlite-db-helper.js.map +1 -0
- package/dist/sqlite-types.d.ts +72 -0
- package/dist/sqlite-types.d.ts.map +1 -0
- package/dist/sqlite-types.js +5 -0
- package/dist/sqlite-types.js.map +1 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -3
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +37 -19
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/graphology.d.ts.map +1 -1
- package/dist/sync/next/graphology.js +0 -6
- package/dist/sync/next/graphology.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +1 -0
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +1 -1
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
- package/dist/sync/next/test/event-fixtures.js +12 -3
- package/dist/sync/next/test/event-fixtures.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -0
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js +3 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +13 -4
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +23 -10
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +17 -17
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/src/ClientSessionLeaderThreadProxy.ts +40 -0
- package/src/adapter-types.ts +19 -161
- package/src/defs.ts +17 -0
- package/src/errors.ts +49 -0
- package/src/index.ts +1 -0
- package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
- package/src/leader-thread/eventlog.ts +78 -54
- package/src/leader-thread/leader-worker-devtools.ts +1 -2
- package/src/leader-thread/make-leader-thread-layer.ts +52 -8
- package/src/leader-thread/materialize-event.ts +33 -12
- package/src/leader-thread/mod.ts +2 -0
- package/src/leader-thread/recreate-db.ts +99 -91
- package/src/leader-thread/types.ts +10 -12
- package/src/make-client-session.ts +2 -2
- package/src/materializer-helper.ts +45 -19
- package/src/rematerialize-from-eventlog.ts +12 -4
- package/src/schema/EventDef.ts +16 -4
- package/src/schema/EventSequenceNumber.test.ts +120 -3
- package/src/schema/EventSequenceNumber.ts +95 -23
- package/src/schema/LiveStoreEvent.ts +49 -8
- package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
- package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
- package/src/schema/state/sqlite/mod.ts +1 -1
- package/src/schema/state/sqlite/query-builder/api.ts +39 -9
- package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
- package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
- package/src/schema/state/sqlite/system-tables.ts +9 -22
- package/src/schema/state/sqlite/table-def.ts +2 -2
- package/src/schema-management/migrations.ts +3 -1
- package/src/sql-queries/sql-queries.ts +2 -0
- package/src/sqlite-db-helper.ts +41 -0
- package/src/sqlite-types.ts +76 -0
- package/src/sync/ClientSessionSyncProcessor.ts +51 -28
- package/src/sync/next/graphology.ts +0 -6
- package/src/sync/next/rebase-events.ts +1 -0
- package/src/sync/next/test/compact-events.test.ts +1 -1
- package/src/sync/next/test/event-fixtures.ts +12 -3
- package/src/sync/sync.ts +3 -0
- package/src/sync/syncstate.test.ts +17 -17
- package/src/sync/syncstate.ts +31 -10
- package/src/version.ts +1 -1
@@ -17,7 +17,7 @@ export const isColumnDefinition = (value: unknown): value is ColumnDefinition<an
|
|
17
17
|
typeof value === 'object' &&
|
18
18
|
value !== null &&
|
19
19
|
'columnType' in value &&
|
20
|
-
validColumnTypes.includes(value
|
20
|
+
validColumnTypes.includes(value.columnType as any)
|
21
21
|
)
|
22
22
|
}
|
23
23
|
|
@@ -36,7 +36,7 @@ export type SqlDefaultValue = {
|
|
36
36
|
}
|
37
37
|
|
38
38
|
export const isSqlDefaultValue = (value: unknown): value is SqlDefaultValue => {
|
39
|
-
return typeof value === 'object' && value !== null && 'sql' in value && typeof value
|
39
|
+
return typeof value === 'object' && value !== null && 'sql' in value && typeof value.sql === 'string'
|
40
40
|
}
|
41
41
|
|
42
42
|
export type ColDefFn<TColumnType extends FieldColumnType> = {
|
@@ -1,8 +1,8 @@
|
|
1
1
|
// Based on https://stackoverflow.com/a/7616484
|
2
2
|
export const hashCode = (str: string) => {
|
3
|
-
let hash = 0
|
4
|
-
|
5
|
-
|
3
|
+
let hash = 0
|
4
|
+
let i: number
|
5
|
+
let chr: number
|
6
6
|
if (str.length === 0) return hash
|
7
7
|
for (i = 0; i < str.length; i++) {
|
8
8
|
// eslint-disable-next-line unicorn/prefer-code-point
|
@@ -6,7 +6,7 @@ import type { InternalState } from '../../schema.js'
|
|
6
6
|
import { ClientDocumentTableDefSymbol, tableIsClientDocumentTable } from './client-document-def.js'
|
7
7
|
import { SqliteAst } from './db-schema/mod.js'
|
8
8
|
import { stateSystemTables } from './system-tables.js'
|
9
|
-
import {
|
9
|
+
import type { TableDef, TableDefBase } from './table-def.js'
|
10
10
|
|
11
11
|
export * from './table-def.js'
|
12
12
|
export {
|
@@ -19,7 +19,11 @@ export namespace QueryBuilderAst {
|
|
19
19
|
export interface SelectQuery {
|
20
20
|
readonly _tag: 'SelectQuery'
|
21
21
|
readonly columns: string[]
|
22
|
-
readonly pickFirst:
|
22
|
+
readonly pickFirst:
|
23
|
+
| { _tag: 'disabled' }
|
24
|
+
| { _tag: 'enabled'; behaviour: 'undefined' }
|
25
|
+
| { _tag: 'enabled'; behaviour: 'error' }
|
26
|
+
| { _tag: 'enabled'; behaviour: 'fallback'; fallback: () => any }
|
23
27
|
readonly select: {
|
24
28
|
columns: ReadonlyArray<string>
|
25
29
|
}
|
@@ -117,7 +121,7 @@ export type QueryBuilder<
|
|
117
121
|
> = {
|
118
122
|
readonly [QueryBuilderTypeId]: QueryBuilderTypeId
|
119
123
|
readonly [QueryBuilderAstSymbol]: QueryBuilderAst
|
120
|
-
readonly
|
124
|
+
readonly ResultType: TResult
|
121
125
|
readonly asSql: () => { query: string; bindValues: SqlValue[] }
|
122
126
|
readonly toString: () => string
|
123
127
|
} & Omit<QueryBuilder.ApiFull<TResult, TTableDef, TWithout>, TWithout>
|
@@ -167,6 +171,21 @@ export namespace QueryBuilder {
|
|
167
171
|
direction: 'asc' | 'desc'
|
168
172
|
}>
|
169
173
|
|
174
|
+
export type FirstQueryBehaviour<TResult, TFallback> =
|
175
|
+
| {
|
176
|
+
/** Will error if no matching row was found */
|
177
|
+
behaviour: 'error'
|
178
|
+
}
|
179
|
+
| {
|
180
|
+
/** Will return `undefined` if no matching row was found */
|
181
|
+
behaviour: 'undefined'
|
182
|
+
}
|
183
|
+
| {
|
184
|
+
/** Will return a fallback value if no matching row was found */
|
185
|
+
behaviour: 'fallback'
|
186
|
+
fallback: () => TResult | TFallback
|
187
|
+
}
|
188
|
+
|
170
189
|
export type ApiFull<TResult, TTableDef extends TableDefBase, TWithout extends ApiFeature> = {
|
171
190
|
/**
|
172
191
|
* `SELECT *` is the default
|
@@ -285,16 +304,27 @@ export namespace QueryBuilder {
|
|
285
304
|
* Example:
|
286
305
|
* ```ts
|
287
306
|
* db.todos.first()
|
288
|
-
* db.todos.where('id', '123').first()
|
307
|
+
* db.todos.where('id', '123').first() // will return `undefined` if no rows are returned
|
308
|
+
* db.todos.where('id', '123').first({ behaviour: 'error' }) // will throw if no rows are returned
|
309
|
+
* db.todos.first({ behaviour: 'fallback', fallback: () => ({ id: '123', text: 'Buy milk', status: 'active' }) })
|
289
310
|
* ```
|
290
311
|
*
|
291
|
-
*
|
312
|
+
* Behaviour:
|
313
|
+
* - `undefined`: Will return `undefined` if no rows are returned (default behaviour)
|
314
|
+
* - `error`: Will throw if no rows are returned
|
315
|
+
* - `fallback`: Will return a fallback value if no rows are returned
|
292
316
|
*/
|
293
|
-
readonly first: <
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
317
|
+
readonly first: <
|
318
|
+
TBehaviour extends QueryBuilder.FirstQueryBehaviour<GetSingle<TResult>, TFallback>,
|
319
|
+
TFallback = never,
|
320
|
+
>(
|
321
|
+
behaviour?: QueryBuilder.FirstQueryBehaviour<GetSingle<TResult>, TFallback> & TBehaviour,
|
322
|
+
) => QueryBuilder<
|
323
|
+
TBehaviour extends { behaviour: 'fallback' }
|
324
|
+
? ReturnType<TBehaviour['fallback']> | GetSingle<TResult>
|
325
|
+
: TBehaviour extends { behaviour: 'undefined' }
|
326
|
+
? undefined | GetSingle<TResult>
|
327
|
+
: GetSingle<TResult>,
|
298
328
|
TTableDef,
|
299
329
|
TWithout | 'row' | 'first' | 'orderBy' | 'select' | 'limit' | 'offset' | 'where' | 'returning' | 'onConflict'
|
300
330
|
>
|
@@ -55,7 +55,7 @@ const UiStateWithDefaultId = State.SQLite.clientDocument({
|
|
55
55
|
},
|
56
56
|
})
|
57
57
|
|
58
|
-
|
58
|
+
const issue = State.SQLite.table({
|
59
59
|
name: 'issue',
|
60
60
|
columns: {
|
61
61
|
id: State.SQLite.integer({ primaryKey: true }),
|
@@ -120,7 +120,19 @@ describe('query builder', () => {
|
|
120
120
|
}
|
121
121
|
`)
|
122
122
|
|
123
|
-
expect(dump(db.todos.select('id', 'text').first({
|
123
|
+
expect(dump(db.todos.select('id', 'text').first({ behaviour: 'error' }))).toMatchInlineSnapshot(`
|
124
|
+
{
|
125
|
+
"bindValues": [
|
126
|
+
1,
|
127
|
+
],
|
128
|
+
"query": "SELECT id, text FROM 'todos' LIMIT ?",
|
129
|
+
"schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> <-> { readonly id: string; readonly text: string })",
|
130
|
+
}
|
131
|
+
`)
|
132
|
+
|
133
|
+
expect(
|
134
|
+
dump(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined })),
|
135
|
+
).toMatchInlineSnapshot(`
|
124
136
|
{
|
125
137
|
"bindValues": [
|
126
138
|
1,
|
@@ -166,8 +178,9 @@ describe('query builder', () => {
|
|
166
178
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
167
179
|
}
|
168
180
|
`)
|
169
|
-
expect(
|
170
|
-
.
|
181
|
+
expect(
|
182
|
+
dump(db.todos.select('id', 'text').where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })),
|
183
|
+
).toMatchInlineSnapshot(`
|
171
184
|
{
|
172
185
|
"bindValues": [
|
173
186
|
"2024-01-01T00:00:00.000Z",
|
@@ -176,8 +189,9 @@ describe('query builder', () => {
|
|
176
189
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
177
190
|
}
|
178
191
|
`)
|
179
|
-
expect(
|
180
|
-
.
|
192
|
+
expect(
|
193
|
+
dump(db.todos.select('id', 'text').where({ status: { op: 'IN', value: ['active'] } })),
|
194
|
+
).toMatchInlineSnapshot(`
|
181
195
|
{
|
182
196
|
"bindValues": [
|
183
197
|
"active",
|
@@ -186,8 +200,9 @@ describe('query builder', () => {
|
|
186
200
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
187
201
|
}
|
188
202
|
`)
|
189
|
-
expect(
|
190
|
-
.
|
203
|
+
expect(
|
204
|
+
dump(db.todos.select('id', 'text').where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })),
|
205
|
+
).toMatchInlineSnapshot(`
|
191
206
|
{
|
192
207
|
"bindValues": [
|
193
208
|
"active",
|
@@ -197,6 +212,25 @@ describe('query builder', () => {
|
|
197
212
|
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
198
213
|
}
|
199
214
|
`)
|
215
|
+
|
216
|
+
expect(
|
217
|
+
dump(
|
218
|
+
db.todos
|
219
|
+
.select('id', 'text')
|
220
|
+
.where({ completed: false })
|
221
|
+
.where({ status: { op: 'IN', value: ['active'] } })
|
222
|
+
.where({ deletedAt: undefined }),
|
223
|
+
),
|
224
|
+
).toMatchInlineSnapshot(`
|
225
|
+
{
|
226
|
+
"bindValues": [
|
227
|
+
0,
|
228
|
+
"active",
|
229
|
+
],
|
230
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? AND status IN (?)",
|
231
|
+
"schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
|
232
|
+
}
|
233
|
+
`)
|
200
234
|
})
|
201
235
|
|
202
236
|
it('should handle OFFSET and LIMIT clauses', () => {
|
@@ -375,8 +409,9 @@ describe('query builder', () => {
|
|
375
409
|
})
|
376
410
|
|
377
411
|
it('should handle INSERT queries with undefined values', () => {
|
378
|
-
expect(
|
379
|
-
.
|
412
|
+
expect(
|
413
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined })),
|
414
|
+
).toMatchInlineSnapshot(`
|
380
415
|
{
|
381
416
|
"bindValues": [
|
382
417
|
"123",
|
@@ -443,8 +478,9 @@ describe('query builder', () => {
|
|
443
478
|
})
|
444
479
|
|
445
480
|
it('should handle UPDATE queries with undefined values', () => {
|
446
|
-
expect(
|
447
|
-
.
|
481
|
+
expect(
|
482
|
+
dump(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' })),
|
483
|
+
).toMatchInlineSnapshot(`
|
448
484
|
{
|
449
485
|
"bindValues": [
|
450
486
|
"some text",
|
@@ -483,8 +519,9 @@ describe('query builder', () => {
|
|
483
519
|
})
|
484
520
|
|
485
521
|
it('should handle INSERT with ON CONFLICT', () => {
|
486
|
-
expect(
|
487
|
-
.
|
522
|
+
expect(
|
523
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore')),
|
524
|
+
).toMatchInlineSnapshot(`
|
488
525
|
{
|
489
526
|
"bindValues": [
|
490
527
|
"123",
|
@@ -516,8 +553,9 @@ describe('query builder', () => {
|
|
516
553
|
}
|
517
554
|
`)
|
518
555
|
|
519
|
-
expect(
|
520
|
-
.
|
556
|
+
expect(
|
557
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace')),
|
558
|
+
).toMatchInlineSnapshot(`
|
521
559
|
{
|
522
560
|
"bindValues": [
|
523
561
|
"123",
|
@@ -547,8 +585,9 @@ describe('query builder', () => {
|
|
547
585
|
})
|
548
586
|
|
549
587
|
it('should handle RETURNING clause', () => {
|
550
|
-
expect(
|
551
|
-
.
|
588
|
+
expect(
|
589
|
+
dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')),
|
590
|
+
).toMatchInlineSnapshot(`
|
552
591
|
{
|
553
592
|
"bindValues": [
|
554
593
|
"123",
|
@@ -560,8 +599,9 @@ describe('query builder', () => {
|
|
560
599
|
}
|
561
600
|
`)
|
562
601
|
|
563
|
-
expect(
|
564
|
-
.
|
602
|
+
expect(
|
603
|
+
dump(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id')),
|
604
|
+
).toMatchInlineSnapshot(`
|
565
605
|
{
|
566
606
|
"bindValues": [
|
567
607
|
"completed",
|
@@ -1,3 +1,4 @@
|
|
1
|
+
/** biome-ignore-all lint/complexity/noArguments: using arguments is fine here */
|
1
2
|
import { casesHandled, shouldNeverHappen } from '@livestore/utils'
|
2
3
|
import { Match, Option, Predicate, Schema } from '@livestore/utils/effect'
|
3
4
|
|
@@ -36,7 +37,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
36
37
|
select: { columns },
|
37
38
|
}) as any
|
38
39
|
},
|
39
|
-
//
|
40
|
+
// biome-ignore lint/complexity/useArrowFunction: prefer function over arrow function for this case
|
40
41
|
where: function () {
|
41
42
|
if (ast._tag === 'InsertQuery') return invalidQueryBuilder('Cannot use where with insert')
|
42
43
|
if (ast._tag === 'RowQuery') return invalidQueryBuilder('Cannot use where with row')
|
@@ -136,7 +137,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
136
137
|
),
|
137
138
|
})
|
138
139
|
},
|
139
|
-
first: (
|
140
|
+
first: (behaviour) => {
|
140
141
|
assertSelectQueryBuilderAst(ast)
|
141
142
|
|
142
143
|
if (ast.limit._tag === 'Some') return invalidQueryBuilder(`.first() can't be called after .limit()`)
|
@@ -144,8 +145,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
144
145
|
return makeQueryBuilder(tableDef, {
|
145
146
|
...ast,
|
146
147
|
limit: Option.some(1),
|
147
|
-
pickFirst:
|
148
|
-
options?.fallback !== undefined && options.fallback !== 'throws' ? { fallback: options.fallback } : 'throws',
|
148
|
+
pickFirst: { _tag: 'enabled', ...(behaviour ?? { behaviour: 'undefined' }) },
|
149
149
|
})
|
150
150
|
},
|
151
151
|
// // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
@@ -249,7 +249,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
249
249
|
return {
|
250
250
|
[QueryBuilderTypeId]: QueryBuilderTypeId,
|
251
251
|
[QueryBuilderAstSymbol]: ast,
|
252
|
-
|
252
|
+
ResultType: 'only-for-type-inference' as TResult,
|
253
253
|
asSql: () => astToSql(ast),
|
254
254
|
toString: () => {
|
255
255
|
try {
|
@@ -266,7 +266,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends TableDefBase>(
|
|
266
266
|
const emptyAst = (tableDef: TableDefBase): QueryBuilderAst.SelectQuery => ({
|
267
267
|
_tag: 'SelectQuery',
|
268
268
|
columns: [],
|
269
|
-
pickFirst:
|
269
|
+
pickFirst: { _tag: 'disabled' },
|
270
270
|
select: { columns: [] },
|
271
271
|
orderBy: [],
|
272
272
|
offset: Option.none(),
|
@@ -280,28 +280,28 @@ const emptyAst = (tableDef: TableDefBase): QueryBuilderAst.SelectQuery => ({
|
|
280
280
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
281
281
|
function assertSelectQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.SelectQuery {
|
282
282
|
if (ast._tag !== 'SelectQuery') {
|
283
|
-
return shouldNeverHappen(
|
283
|
+
return shouldNeverHappen(`Expected SelectQuery but got ${ast._tag}`)
|
284
284
|
}
|
285
285
|
}
|
286
286
|
|
287
287
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
288
288
|
function assertInsertQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.InsertQuery {
|
289
289
|
if (ast._tag !== 'InsertQuery') {
|
290
|
-
return shouldNeverHappen(
|
290
|
+
return shouldNeverHappen(`Expected InsertQuery but got ${ast._tag}`)
|
291
291
|
}
|
292
292
|
}
|
293
293
|
|
294
294
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
295
295
|
function assertWriteQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.WriteQuery {
|
296
296
|
if (ast._tag !== 'InsertQuery' && ast._tag !== 'UpdateQuery' && ast._tag !== 'DeleteQuery') {
|
297
|
-
return shouldNeverHappen(
|
297
|
+
return shouldNeverHappen(`Expected WriteQuery but got ${ast._tag}`)
|
298
298
|
}
|
299
299
|
}
|
300
300
|
|
301
301
|
const isRowQuery = (ast: QueryBuilderAst): ast is QueryBuilderAst.RowQuery => ast._tag === 'RowQuery'
|
302
302
|
|
303
303
|
export const invalidQueryBuilder = (msg?: string) => {
|
304
|
-
return shouldNeverHappen(
|
304
|
+
return shouldNeverHappen(`Invalid query builder${msg ? `: ${msg}` : ''}`)
|
305
305
|
}
|
306
306
|
|
307
307
|
export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<any> => {
|
@@ -309,9 +309,12 @@ export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<
|
|
309
309
|
switch (queryAst._tag) {
|
310
310
|
case 'SelectQuery': {
|
311
311
|
const arraySchema = Schema.Array(queryAst.resultSchemaSingle)
|
312
|
-
if (queryAst.pickFirst ===
|
312
|
+
if (queryAst.pickFirst._tag === 'disabled') {
|
313
313
|
return arraySchema
|
314
|
-
} else if (queryAst.pickFirst === '
|
314
|
+
} else if (queryAst.pickFirst.behaviour === 'undefined') {
|
315
|
+
const arraySchema = Schema.Array(Schema.UndefinedOr(queryAst.resultSchemaSingle))
|
316
|
+
return arraySchema.pipe(Schema.headOrElse(() => undefined))
|
317
|
+
} else if (queryAst.pickFirst.behaviour === 'error') {
|
315
318
|
// Will throw if the array is empty
|
316
319
|
return arraySchema.pipe(Schema.headOrElse())
|
317
320
|
} else {
|
@@ -4,7 +4,7 @@ import * as EventSequenceNumber from '../../EventSequenceNumber.js'
|
|
4
4
|
import { SqliteDsl } from './db-schema/mod.js'
|
5
5
|
import { table } from './table-def.js'
|
6
6
|
|
7
|
-
///
|
7
|
+
/// State DB
|
8
8
|
|
9
9
|
export const SCHEMA_META_TABLE = '__livestore_schema'
|
10
10
|
|
@@ -46,6 +46,7 @@ export const sessionChangesetMetaTable = table({
|
|
46
46
|
// TODO bring back primary key
|
47
47
|
seqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
48
48
|
seqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
49
|
+
seqNumRebaseGeneration: SqliteDsl.integer({}),
|
49
50
|
changeset: SqliteDsl.blob({ nullable: true }),
|
50
51
|
debug: SqliteDsl.json({ nullable: true }),
|
51
52
|
},
|
@@ -54,25 +55,7 @@ export const sessionChangesetMetaTable = table({
|
|
54
55
|
|
55
56
|
export type SessionChangesetMetaRow = typeof sessionChangesetMetaTable.Type
|
56
57
|
|
57
|
-
export const
|
58
|
-
|
59
|
-
// TODO get rid of this table in favour of client-only merge generation
|
60
|
-
export const leaderMergeCounterTable = table({
|
61
|
-
name: LEADER_MERGE_COUNTER_TABLE,
|
62
|
-
columns: {
|
63
|
-
id: SqliteDsl.integer({ primaryKey: true, schema: Schema.Literal(0) }),
|
64
|
-
mergeCounter: SqliteDsl.integer({ primaryKey: true }),
|
65
|
-
},
|
66
|
-
})
|
67
|
-
|
68
|
-
export type LeaderMergeCounterRow = typeof leaderMergeCounterTable.Type
|
69
|
-
|
70
|
-
export const stateSystemTables = [
|
71
|
-
schemaMetaTable,
|
72
|
-
schemaEventDefsMetaTable,
|
73
|
-
sessionChangesetMetaTable,
|
74
|
-
leaderMergeCounterTable,
|
75
|
-
]
|
58
|
+
export const stateSystemTables = [schemaMetaTable, schemaEventDefsMetaTable, sessionChangesetMetaTable] as const
|
76
59
|
|
77
60
|
export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)
|
78
61
|
|
@@ -86,8 +69,11 @@ export const eventlogMetaTable = table({
|
|
86
69
|
// TODO Adjust modeling so a global event never needs a client id component
|
87
70
|
seqNumGlobal: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
88
71
|
seqNumClient: SqliteDsl.integer({ primaryKey: true, schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
72
|
+
seqNumRebaseGeneration: SqliteDsl.integer({ primaryKey: true }),
|
89
73
|
parentSeqNumGlobal: SqliteDsl.integer({ schema: EventSequenceNumber.GlobalEventSequenceNumber }),
|
90
74
|
parentSeqNumClient: SqliteDsl.integer({ schema: EventSequenceNumber.ClientEventSequenceNumber }),
|
75
|
+
parentSeqNumRebaseGeneration: SqliteDsl.integer({}),
|
76
|
+
/** Event definition name */
|
91
77
|
name: SqliteDsl.text({}),
|
92
78
|
argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
|
93
79
|
clientId: SqliteDsl.text({}),
|
@@ -97,7 +83,7 @@ export const eventlogMetaTable = table({
|
|
97
83
|
},
|
98
84
|
indexes: [
|
99
85
|
{ columns: ['seqNumGlobal'], name: 'idx_eventlog_seqNumGlobal' },
|
100
|
-
{ columns: ['seqNumGlobal', 'seqNumClient'], name: 'idx_eventlog_seqNum' },
|
86
|
+
{ columns: ['seqNumGlobal', 'seqNumClient', 'seqNumRebaseGeneration'], name: 'idx_eventlog_seqNum' },
|
101
87
|
],
|
102
88
|
})
|
103
89
|
|
@@ -105,6 +91,7 @@ export type EventlogMetaRow = typeof eventlogMetaTable.Type
|
|
105
91
|
|
106
92
|
export const SYNC_STATUS_TABLE = '__livestore_sync_status'
|
107
93
|
|
94
|
+
// TODO support sync backend identity (to detect if sync backend changes)
|
108
95
|
export const syncStatusTable = table({
|
109
96
|
name: SYNC_STATUS_TABLE,
|
110
97
|
columns: {
|
@@ -114,4 +101,4 @@ export const syncStatusTable = table({
|
|
114
101
|
|
115
102
|
export type SyncStatusRow = typeof syncStatusTable.Type
|
116
103
|
|
117
|
-
export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable]
|
104
|
+
export const eventlogSystemTables = [eventlogMetaTable, syncStatusTable] as const
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import type { Nullable } from '@livestore/utils'
|
2
2
|
import type { Schema, Types } from '@livestore/utils/effect'
|
3
3
|
|
4
4
|
import { SqliteDsl } from './db-schema/mod.js'
|
@@ -177,7 +177,7 @@ export namespace FromColumns {
|
|
177
177
|
export type InsertRowDecoded<TColumns extends SqliteDsl.Columns> = SqliteDsl.FromColumns.InsertRowDecoded<TColumns>
|
178
178
|
}
|
179
179
|
|
180
|
-
type SqliteTableDefForInput<
|
180
|
+
export type SqliteTableDefForInput<
|
181
181
|
TName extends string,
|
182
182
|
TColumns extends SqliteDsl.Columns | SqliteDsl.ColumnDefinition<any, any>,
|
183
183
|
> = SqliteDsl.TableDefinition<TName, PrettifyFlat<ToColumns<TColumns>>>
|
@@ -1,7 +1,9 @@
|
|
1
1
|
import { memoizeByStringifyArgs } from '@livestore/utils'
|
2
2
|
import { Effect, Schema as EffectSchema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import type {
|
4
|
+
import type { SqliteDb } from '../adapter-types.js'
|
5
|
+
import type { MigrationsReport, MigrationsReportEntry } from '../defs.js'
|
6
|
+
import type { UnexpectedError } from '../errors.js'
|
5
7
|
import type { LiveStoreSchema } from '../schema/mod.js'
|
6
8
|
import { SqliteAst, SqliteDsl } from '../schema/state/sqlite/db-schema/mod.js'
|
7
9
|
import type { SchemaEventDefsMetaRow, SchemaMetaRow } from '../schema/state/sqlite/system-tables.js'
|
@@ -106,6 +106,7 @@ export const insertRows = <TColumns extends SqliteDsl.Columns>({
|
|
106
106
|
|
107
107
|
const bindValues = valuesArray.reduce(
|
108
108
|
(acc, values, itemIndex) => ({
|
109
|
+
// biome-ignore lint/performance/noAccumulatingSpread: TODO improve this some day
|
109
110
|
...acc,
|
110
111
|
...makeBindValues({ columns, values, variablePrefix: `item_${itemIndex}_` }),
|
111
112
|
}),
|
@@ -292,6 +293,7 @@ Error: ${parseErrorStr}
|
|
292
293
|
Value:`,
|
293
294
|
value,
|
294
295
|
)
|
296
|
+
// biome-ignore lint/suspicious/noDebugger: debug
|
295
297
|
debugger
|
296
298
|
throw res.left
|
297
299
|
} else {
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import { Schema } from '@livestore/utils/effect'
|
2
|
+
|
3
|
+
import type { SqliteDb } from './adapter-types.js'
|
4
|
+
import { getResultSchema, isQueryBuilder } from './schema/state/sqlite/query-builder/mod.js'
|
5
|
+
import type { PreparedBindValues } from './util.js'
|
6
|
+
|
7
|
+
export const makeExecute = (
|
8
|
+
execute: (
|
9
|
+
queryStr: string,
|
10
|
+
bindValues: PreparedBindValues | undefined,
|
11
|
+
options?: { onRowsChanged?: (rowsChanged: number) => void },
|
12
|
+
) => void,
|
13
|
+
): SqliteDb['execute'] => {
|
14
|
+
return (...args: any[]) => {
|
15
|
+
const [queryStrOrQueryBuilder, bindValuesOrOptions, maybeOptions] = args
|
16
|
+
|
17
|
+
if (isQueryBuilder(queryStrOrQueryBuilder)) {
|
18
|
+
const { query, bindValues } = queryStrOrQueryBuilder.asSql()
|
19
|
+
return execute(query, bindValues as unknown as PreparedBindValues, bindValuesOrOptions)
|
20
|
+
} else {
|
21
|
+
return execute(queryStrOrQueryBuilder, bindValuesOrOptions, maybeOptions)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
export const makeSelect = <T>(
|
27
|
+
select: (queryStr: string, bindValues: PreparedBindValues | undefined) => ReadonlyArray<T>,
|
28
|
+
): SqliteDb['select'] => {
|
29
|
+
return (...args: any[]) => {
|
30
|
+
const [queryStrOrQueryBuilder, maybeBindValues] = args
|
31
|
+
|
32
|
+
if (isQueryBuilder(queryStrOrQueryBuilder)) {
|
33
|
+
const { query, bindValues } = queryStrOrQueryBuilder.asSql()
|
34
|
+
const resultSchema = getResultSchema(queryStrOrQueryBuilder)
|
35
|
+
const results = select(query, bindValues as unknown as PreparedBindValues)
|
36
|
+
return Schema.decodeSync(resultSchema)(results)
|
37
|
+
} else {
|
38
|
+
return select(queryStrOrQueryBuilder, maybeBindValues)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import { type Effect, Schema } from '@livestore/utils/effect'
|
2
|
+
import type { SqliteError, UnexpectedError } from './errors.js'
|
3
|
+
import type { EventSequenceNumber } from './schema/mod.js'
|
4
|
+
import type { QueryBuilder } from './schema/state/sqlite/query-builder/api.js'
|
5
|
+
import type { PreparedBindValues } from './util.js'
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Common interface for SQLite databases used by LiveStore to facilitate a consistent API across different platforms.
|
9
|
+
* Always assumes a synchronous SQLite build with the `bytecode` and `session` extensions enabled.
|
10
|
+
* Can be either in-memory or persisted to disk.
|
11
|
+
*/
|
12
|
+
export interface SqliteDb<TReq = any, TMetadata extends TReq = TReq> {
|
13
|
+
_tag: 'SqliteDb'
|
14
|
+
metadata: TMetadata
|
15
|
+
/** Debug information (currently not persisted and only available at runtime) */
|
16
|
+
debug: SqliteDebugInfo
|
17
|
+
prepare(queryStr: string): PreparedStatement
|
18
|
+
execute(
|
19
|
+
queryStr: string,
|
20
|
+
bindValues?: PreparedBindValues | undefined,
|
21
|
+
options?: { onRowsChanged?: (rowsChanged: number) => void },
|
22
|
+
): void
|
23
|
+
execute(queryBuilder: QueryBuilder.Any, options?: { onRowsChanged?: (rowsChanged: number) => void }): void
|
24
|
+
|
25
|
+
select<T>(queryStr: string, bindValues?: PreparedBindValues | undefined): ReadonlyArray<T>
|
26
|
+
select<T>(queryBuilder: QueryBuilder<T, any, any>): T
|
27
|
+
|
28
|
+
export(): Uint8Array
|
29
|
+
import: (data: Uint8Array | SqliteDb<TReq>) => void
|
30
|
+
close(): void
|
31
|
+
destroy(): void
|
32
|
+
session(): SqliteDbSession
|
33
|
+
makeChangeset: (data: Uint8Array) => SqliteDbChangeset
|
34
|
+
}
|
35
|
+
|
36
|
+
export type SqliteDebugInfo = { head: EventSequenceNumber.EventSequenceNumber }
|
37
|
+
|
38
|
+
// TODO refactor this helper type. It's quite cumbersome to use and should be revisited.
|
39
|
+
export type MakeSqliteDb<
|
40
|
+
TReq = { dbPointer: number; persistenceInfo: PersistenceInfo },
|
41
|
+
TInput_ extends { _tag: string } = { _tag: string },
|
42
|
+
TMetadata_ extends TReq = TReq,
|
43
|
+
R = never,
|
44
|
+
> = <
|
45
|
+
TInput extends TInput_,
|
46
|
+
TMetadata extends TMetadata_ & { _tag: TInput['_tag'] } = TMetadata_ & { _tag: TInput['_tag'] },
|
47
|
+
>(
|
48
|
+
input: TInput,
|
49
|
+
) => Effect.Effect<SqliteDb<TReq, Extract<TMetadata, { _tag: TInput['_tag'] }>>, SqliteError | UnexpectedError, R>
|
50
|
+
|
51
|
+
export interface PreparedStatement {
|
52
|
+
execute(bindValues: PreparedBindValues | undefined, options?: { onRowsChanged?: (rowsChanged: number) => void }): void
|
53
|
+
select<T>(bindValues: PreparedBindValues | undefined): ReadonlyArray<T>
|
54
|
+
finalize(): void
|
55
|
+
sql: string
|
56
|
+
}
|
57
|
+
|
58
|
+
export type SqliteDbSession = {
|
59
|
+
changeset: () => Uint8Array | undefined
|
60
|
+
finish: () => void
|
61
|
+
}
|
62
|
+
|
63
|
+
export type SqliteDbChangeset = {
|
64
|
+
// TODO combining changesets (requires changes in the SQLite WASM binding)
|
65
|
+
invert: () => SqliteDbChangeset
|
66
|
+
apply: () => void
|
67
|
+
}
|
68
|
+
|
69
|
+
export const PersistenceInfo = Schema.Struct(
|
70
|
+
{
|
71
|
+
fileName: Schema.String,
|
72
|
+
},
|
73
|
+
{ key: Schema.String, value: Schema.Any },
|
74
|
+
).annotations({ title: 'LiveStore.PersistenceInfo' })
|
75
|
+
|
76
|
+
export type PersistenceInfo<With extends {} = {}> = typeof PersistenceInfo.Type & With
|