@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.
Files changed (185) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +35 -0
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -0
  4. package/dist/ClientSessionLeaderThreadProxy.js +6 -0
  5. package/dist/ClientSessionLeaderThreadProxy.js.map +1 -0
  6. package/dist/adapter-types.d.ts +10 -156
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +5 -49
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/defs.d.ts +20 -0
  11. package/dist/defs.d.ts.map +1 -0
  12. package/dist/defs.js +12 -0
  13. package/dist/defs.js.map +1 -0
  14. package/dist/devtools/devtools-messages-client-session.d.ts +23 -21
  15. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  16. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  17. package/dist/devtools/devtools-messages-leader.d.ts +26 -24
  18. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  19. package/dist/errors.d.ts +50 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +36 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/leader-thread/LeaderSyncProcessor.d.ts +6 -7
  28. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  29. package/dist/leader-thread/LeaderSyncProcessor.js +122 -123
  30. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  31. package/dist/leader-thread/eventlog.d.ts +17 -6
  32. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  33. package/dist/leader-thread/eventlog.js +34 -17
  34. package/dist/leader-thread/eventlog.js.map +1 -1
  35. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  36. package/dist/leader-thread/leader-worker-devtools.js +1 -2
  37. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  38. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  39. package/dist/leader-thread/make-leader-thread-layer.js +37 -7
  40. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  41. package/dist/leader-thread/materialize-event.d.ts +3 -3
  42. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  43. package/dist/leader-thread/materialize-event.js +27 -10
  44. package/dist/leader-thread/materialize-event.js.map +1 -1
  45. package/dist/leader-thread/mod.d.ts +2 -0
  46. package/dist/leader-thread/mod.d.ts.map +1 -1
  47. package/dist/leader-thread/mod.js +2 -0
  48. package/dist/leader-thread/mod.js.map +1 -1
  49. package/dist/leader-thread/recreate-db.d.ts +13 -6
  50. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  51. package/dist/leader-thread/recreate-db.js +1 -3
  52. package/dist/leader-thread/recreate-db.js.map +1 -1
  53. package/dist/leader-thread/types.d.ts +6 -7
  54. package/dist/leader-thread/types.d.ts.map +1 -1
  55. package/dist/make-client-session.d.ts +1 -1
  56. package/dist/make-client-session.d.ts.map +1 -1
  57. package/dist/make-client-session.js +1 -1
  58. package/dist/make-client-session.js.map +1 -1
  59. package/dist/materializer-helper.d.ts +13 -2
  60. package/dist/materializer-helper.d.ts.map +1 -1
  61. package/dist/materializer-helper.js +25 -11
  62. package/dist/materializer-helper.js.map +1 -1
  63. package/dist/rematerialize-from-eventlog.d.ts +1 -1
  64. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  65. package/dist/rematerialize-from-eventlog.js +12 -4
  66. package/dist/rematerialize-from-eventlog.js.map +1 -1
  67. package/dist/schema/EventDef.d.ts +8 -3
  68. package/dist/schema/EventDef.d.ts.map +1 -1
  69. package/dist/schema/EventDef.js +5 -2
  70. package/dist/schema/EventDef.js.map +1 -1
  71. package/dist/schema/EventSequenceNumber.d.ts +20 -2
  72. package/dist/schema/EventSequenceNumber.d.ts.map +1 -1
  73. package/dist/schema/EventSequenceNumber.js +71 -19
  74. package/dist/schema/EventSequenceNumber.js.map +1 -1
  75. package/dist/schema/EventSequenceNumber.test.js +88 -3
  76. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  77. package/dist/schema/LiveStoreEvent.d.ts +56 -8
  78. package/dist/schema/LiveStoreEvent.d.ts.map +1 -1
  79. package/dist/schema/LiveStoreEvent.js +34 -8
  80. package/dist/schema/LiveStoreEvent.js.map +1 -1
  81. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -2
  82. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  83. package/dist/schema/state/sqlite/db-schema/hash.js +3 -1
  84. package/dist/schema/state/sqlite/db-schema/hash.js.map +1 -1
  85. package/dist/schema/state/sqlite/mod.d.ts +1 -1
  86. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  87. package/dist/schema/state/sqlite/query-builder/api.d.ts +36 -9
  88. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  89. package/dist/schema/state/sqlite/query-builder/api.js.map +1 -1
  90. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  91. package/dist/schema/state/sqlite/query-builder/impl.js +16 -11
  92. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  93. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts +1 -86
  94. package/dist/schema/state/sqlite/query-builder/impl.test.d.ts.map +1 -1
  95. package/dist/schema/state/sqlite/query-builder/impl.test.js +34 -20
  96. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  97. package/dist/schema/state/sqlite/system-tables.d.ts +380 -432
  98. package/dist/schema/state/sqlite/system-tables.d.ts.map +1 -1
  99. package/dist/schema/state/sqlite/system-tables.js +8 -17
  100. package/dist/schema/state/sqlite/system-tables.js.map +1 -1
  101. package/dist/schema/state/sqlite/table-def.d.ts +2 -2
  102. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  103. package/dist/schema-management/migrations.d.ts +3 -1
  104. package/dist/schema-management/migrations.d.ts.map +1 -1
  105. package/dist/schema-management/migrations.js.map +1 -1
  106. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  107. package/dist/sql-queries/sql-queries.js +2 -0
  108. package/dist/sql-queries/sql-queries.js.map +1 -1
  109. package/dist/sqlite-db-helper.d.ts +7 -0
  110. package/dist/sqlite-db-helper.d.ts.map +1 -0
  111. package/dist/sqlite-db-helper.js +29 -0
  112. package/dist/sqlite-db-helper.js.map +1 -0
  113. package/dist/sqlite-types.d.ts +72 -0
  114. package/dist/sqlite-types.d.ts.map +1 -0
  115. package/dist/sqlite-types.js +5 -0
  116. package/dist/sqlite-types.js.map +1 -0
  117. package/dist/sync/ClientSessionSyncProcessor.d.ts +12 -3
  118. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  119. package/dist/sync/ClientSessionSyncProcessor.js +37 -19
  120. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  121. package/dist/sync/next/graphology.d.ts.map +1 -1
  122. package/dist/sync/next/graphology.js +0 -6
  123. package/dist/sync/next/graphology.js.map +1 -1
  124. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  125. package/dist/sync/next/rebase-events.js +1 -0
  126. package/dist/sync/next/rebase-events.js.map +1 -1
  127. package/dist/sync/next/test/compact-events.test.js +1 -1
  128. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  129. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  130. package/dist/sync/next/test/event-fixtures.js +12 -3
  131. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  132. package/dist/sync/sync.d.ts +2 -0
  133. package/dist/sync/sync.d.ts.map +1 -1
  134. package/dist/sync/sync.js +3 -0
  135. package/dist/sync/sync.js.map +1 -1
  136. package/dist/sync/syncstate.d.ts +13 -4
  137. package/dist/sync/syncstate.d.ts.map +1 -1
  138. package/dist/sync/syncstate.js +23 -10
  139. package/dist/sync/syncstate.js.map +1 -1
  140. package/dist/sync/syncstate.test.js +17 -17
  141. package/dist/sync/syncstate.test.js.map +1 -1
  142. package/dist/version.d.ts +1 -1
  143. package/dist/version.js +1 -1
  144. package/package.json +7 -6
  145. package/src/ClientSessionLeaderThreadProxy.ts +40 -0
  146. package/src/adapter-types.ts +19 -161
  147. package/src/defs.ts +17 -0
  148. package/src/errors.ts +49 -0
  149. package/src/index.ts +1 -0
  150. package/src/leader-thread/LeaderSyncProcessor.ts +157 -181
  151. package/src/leader-thread/eventlog.ts +78 -54
  152. package/src/leader-thread/leader-worker-devtools.ts +1 -2
  153. package/src/leader-thread/make-leader-thread-layer.ts +52 -8
  154. package/src/leader-thread/materialize-event.ts +33 -12
  155. package/src/leader-thread/mod.ts +2 -0
  156. package/src/leader-thread/recreate-db.ts +99 -91
  157. package/src/leader-thread/types.ts +10 -12
  158. package/src/make-client-session.ts +2 -2
  159. package/src/materializer-helper.ts +45 -19
  160. package/src/rematerialize-from-eventlog.ts +12 -4
  161. package/src/schema/EventDef.ts +16 -4
  162. package/src/schema/EventSequenceNumber.test.ts +120 -3
  163. package/src/schema/EventSequenceNumber.ts +95 -23
  164. package/src/schema/LiveStoreEvent.ts +49 -8
  165. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +2 -2
  166. package/src/schema/state/sqlite/db-schema/hash.ts +3 -3
  167. package/src/schema/state/sqlite/mod.ts +1 -1
  168. package/src/schema/state/sqlite/query-builder/api.ts +39 -9
  169. package/src/schema/state/sqlite/query-builder/impl.test.ts +60 -20
  170. package/src/schema/state/sqlite/query-builder/impl.ts +15 -12
  171. package/src/schema/state/sqlite/system-tables.ts +9 -22
  172. package/src/schema/state/sqlite/table-def.ts +2 -2
  173. package/src/schema-management/migrations.ts +3 -1
  174. package/src/sql-queries/sql-queries.ts +2 -0
  175. package/src/sqlite-db-helper.ts +41 -0
  176. package/src/sqlite-types.ts +76 -0
  177. package/src/sync/ClientSessionSyncProcessor.ts +51 -28
  178. package/src/sync/next/graphology.ts +0 -6
  179. package/src/sync/next/rebase-events.ts +1 -0
  180. package/src/sync/next/test/compact-events.test.ts +1 -1
  181. package/src/sync/next/test/event-fixtures.ts +12 -3
  182. package/src/sync/sync.ts +3 -0
  183. package/src/sync/syncstate.test.ts +17 -17
  184. package/src/sync/syncstate.ts +31 -10
  185. 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['columnType'] as any)
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['sql'] === 'string'
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
- i,
5
- chr
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 { type TableDef, type TableDefBase } from './table-def.js'
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: false | { fallback: () => any } | 'throws'
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 ['ResultType']: TResult
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
- * Query will throw if no rows are returned and no fallback is provided.
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: <TFallback = never>(options?: {
294
- /** @default 'throws' */
295
- fallback?: (() => TFallback | GetSingle<TResult>) | 'throws'
296
- }) => QueryBuilder<
297
- TFallback | GetSingle<TResult>,
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
- export const issue = State.SQLite.table({
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({ fallback: () => undefined }))).toMatchInlineSnapshot(`
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(dump(db.todos.select('id', 'text').where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })))
170
- .toMatchInlineSnapshot(`
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(dump(db.todos.select('id', 'text').where({ status: { op: 'IN', value: ['active'] } })))
180
- .toMatchInlineSnapshot(`
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(dump(db.todos.select('id', 'text').where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })))
190
- .toMatchInlineSnapshot(`
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(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined })))
379
- .toMatchInlineSnapshot(`
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(dump(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' })))
447
- .toMatchInlineSnapshot(`
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(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore')))
487
- .toMatchInlineSnapshot(`
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(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace')))
520
- .toMatchInlineSnapshot(`
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(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')))
551
- .toMatchInlineSnapshot(`
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(dump(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id')))
564
- .toMatchInlineSnapshot(`
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
- // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
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: (options) => {
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
- ['ResultType']: 'only-for-type-inference' as TResult,
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: false,
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('Expected SelectQuery but got ' + ast._tag)
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('Expected InsertQuery but got ' + ast._tag)
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('Expected WriteQuery but got ' + ast._tag)
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('Invalid query builder' + (msg ? `: ${msg}` : ''))
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 === false) {
312
+ if (queryAst.pickFirst._tag === 'disabled') {
313
313
  return arraySchema
314
- } else if (queryAst.pickFirst === 'throws') {
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
- /// Read model DB
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 LEADER_MERGE_COUNTER_TABLE = '__livestore_leader_merge_counter'
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 { type Nullable } from '@livestore/utils'
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 { MigrationsReport, MigrationsReportEntry, SqliteDb, UnexpectedError } from '../adapter-types.js'
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