@livestore/common 0.3.0-dev.23 → 0.3.0-dev.25

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 (117) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/adapter-types.d.ts +4 -2
  3. package/dist/adapter-types.d.ts.map +1 -1
  4. package/dist/adapter-types.js +1 -1
  5. package/dist/adapter-types.js.map +1 -1
  6. package/dist/derived-mutations.d.ts +8 -8
  7. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  8. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  9. package/dist/devtools/devtools-messages-leader.d.ts +25 -24
  10. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  11. package/dist/leader-thread/LeaderSyncProcessor.d.ts +2 -1
  12. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  13. package/dist/leader-thread/LeaderSyncProcessor.js +16 -12
  14. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  15. package/dist/leader-thread/apply-mutation.js +1 -1
  16. package/dist/leader-thread/apply-mutation.js.map +1 -1
  17. package/dist/leader-thread/leader-worker-devtools.js +2 -2
  18. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  19. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  20. package/dist/leader-thread/make-leader-thread-layer.js +3 -2
  21. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  22. package/dist/leader-thread/mutationlog.d.ts +1 -0
  23. package/dist/leader-thread/mutationlog.d.ts.map +1 -1
  24. package/dist/leader-thread/mutationlog.js +2 -1
  25. package/dist/leader-thread/mutationlog.js.map +1 -1
  26. package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
  27. package/dist/leader-thread/types.d.ts +1 -1
  28. package/dist/leader-thread/types.d.ts.map +1 -1
  29. package/dist/mutation.d.ts.map +1 -1
  30. package/dist/mutation.js +13 -2
  31. package/dist/mutation.js.map +1 -1
  32. package/dist/query-builder/api.d.ts +118 -20
  33. package/dist/query-builder/api.d.ts.map +1 -1
  34. package/dist/query-builder/api.js.map +1 -1
  35. package/dist/query-builder/astToSql.d.ts +7 -0
  36. package/dist/query-builder/astToSql.d.ts.map +1 -0
  37. package/dist/query-builder/astToSql.js +168 -0
  38. package/dist/query-builder/astToSql.js.map +1 -0
  39. package/dist/query-builder/impl.d.ts +1 -5
  40. package/dist/query-builder/impl.d.ts.map +1 -1
  41. package/dist/query-builder/impl.js +130 -96
  42. package/dist/query-builder/impl.js.map +1 -1
  43. package/dist/query-builder/impl.test.js +94 -0
  44. package/dist/query-builder/impl.test.js.map +1 -1
  45. package/dist/query-builder/mod.d.ts +7 -0
  46. package/dist/query-builder/mod.d.ts.map +1 -1
  47. package/dist/query-builder/mod.js +7 -0
  48. package/dist/query-builder/mod.js.map +1 -1
  49. package/dist/query-info.d.ts +4 -1
  50. package/dist/query-info.d.ts.map +1 -1
  51. package/dist/query-info.js.map +1 -1
  52. package/dist/rehydrate-from-mutationlog.js +1 -1
  53. package/dist/rehydrate-from-mutationlog.js.map +1 -1
  54. package/dist/schema/MutationEvent.d.ts +27 -10
  55. package/dist/schema/MutationEvent.d.ts.map +1 -1
  56. package/dist/schema/MutationEvent.js +24 -8
  57. package/dist/schema/MutationEvent.js.map +1 -1
  58. package/dist/schema/db-schema/dsl/mod.d.ts +7 -5
  59. package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
  60. package/dist/schema/db-schema/dsl/mod.js +6 -0
  61. package/dist/schema/db-schema/dsl/mod.js.map +1 -1
  62. package/dist/schema/mutations.d.ts +12 -3
  63. package/dist/schema/mutations.d.ts.map +1 -1
  64. package/dist/schema/mutations.js.map +1 -1
  65. package/dist/schema/system-tables.d.ts +5 -5
  66. package/dist/schema/system-tables.d.ts.map +1 -1
  67. package/dist/schema/system-tables.js +1 -2
  68. package/dist/schema/system-tables.js.map +1 -1
  69. package/dist/schema/table-def.d.ts +7 -3
  70. package/dist/schema/table-def.d.ts.map +1 -1
  71. package/dist/schema/table-def.js +7 -1
  72. package/dist/schema/table-def.js.map +1 -1
  73. package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -0
  74. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  75. package/dist/sync/ClientSessionSyncProcessor.js +8 -5
  76. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  77. package/dist/sync/next/rebase-events.d.ts +1 -1
  78. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  79. package/dist/sync/sync.d.ts +19 -1
  80. package/dist/sync/sync.d.ts.map +1 -1
  81. package/dist/sync/sync.js.map +1 -1
  82. package/dist/sync/syncstate.d.ts +26 -4
  83. package/dist/sync/syncstate.d.ts.map +1 -1
  84. package/dist/sync/syncstate.js +95 -25
  85. package/dist/sync/syncstate.js.map +1 -1
  86. package/dist/sync/syncstate.test.js +60 -29
  87. package/dist/sync/syncstate.test.js.map +1 -1
  88. package/dist/version.d.ts +1 -1
  89. package/dist/version.js +1 -1
  90. package/package.json +2 -2
  91. package/src/adapter-types.ts +4 -2
  92. package/src/leader-thread/LeaderSyncProcessor.ts +19 -13
  93. package/src/leader-thread/apply-mutation.ts +2 -2
  94. package/src/leader-thread/leader-worker-devtools.ts +2 -2
  95. package/src/leader-thread/make-leader-thread-layer.ts +3 -2
  96. package/src/leader-thread/mutationlog.ts +2 -1
  97. package/src/leader-thread/types.ts +1 -1
  98. package/src/mutation.ts +20 -3
  99. package/src/query-builder/api.ts +192 -15
  100. package/src/query-builder/astToSql.ts +203 -0
  101. package/src/query-builder/impl.test.ts +104 -0
  102. package/src/query-builder/impl.ts +157 -113
  103. package/src/query-builder/mod.ts +7 -0
  104. package/src/query-info.ts +6 -1
  105. package/src/rehydrate-from-mutationlog.ts +1 -1
  106. package/src/schema/MutationEvent.ts +28 -12
  107. package/src/schema/db-schema/dsl/mod.ts +30 -2
  108. package/src/schema/mutations.ts +12 -1
  109. package/src/schema/system-tables.ts +1 -2
  110. package/src/schema/table-def.ts +14 -4
  111. package/src/sync/ClientSessionSyncProcessor.ts +10 -4
  112. package/src/sync/next/rebase-events.ts +1 -1
  113. package/src/sync/sync.ts +19 -3
  114. package/src/sync/syncstate.test.ts +66 -32
  115. package/src/sync/syncstate.ts +116 -34
  116. package/src/version.ts +1 -1
  117. package/tmp/pack.tgz +0 -0
@@ -1,9 +1,11 @@
1
- import { Option, Predicate, Schema } from '@livestore/utils/effect'
1
+ import { casesHandled } from '@livestore/utils'
2
+ import { Match, Option, Predicate, Schema } from '@livestore/utils/effect'
2
3
 
3
4
  import type { QueryInfo } from '../query-info.js'
4
5
  import type { DbSchema } from '../schema/mod.js'
5
6
  import type { QueryBuilder, QueryBuilderAst } from './api.js'
6
7
  import { QueryBuilderAstSymbol, TypeId } from './api.js'
8
+ import { astToSql } from './astToSql.js'
7
9
 
8
10
  export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBase>(
9
11
  tableDef: TTableDef,
@@ -12,7 +14,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
12
14
  const api = {
13
15
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
14
16
  select() {
15
- assertQueryBuilderAst(ast)
17
+ assertSelectQueryBuilderAst(ast)
16
18
 
17
19
  // eslint-disable-next-line prefer-rest-params
18
20
  const params = [...arguments]
@@ -36,8 +38,9 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
36
38
  }) as any
37
39
  },
38
40
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
39
- where() {
40
- if (isRowQuery(ast)) return invalidQueryBuilder()
41
+ where: function () {
42
+ if (ast._tag === 'InsertQuery') return invalidQueryBuilder('Cannot use where with insert')
43
+ if (ast._tag === 'RowQuery') return invalidQueryBuilder('Cannot use where with row')
41
44
 
42
45
  if (arguments.length === 1) {
43
46
  // eslint-disable-next-line prefer-rest-params
@@ -50,24 +53,45 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
50
53
  : { col, op: '=', value },
51
54
  )
52
55
 
53
- return makeQueryBuilder(tableDef, {
54
- ...ast,
55
- where: [...ast.where, ...newOps],
56
- }) as any
56
+ switch (ast._tag) {
57
+ case 'CountQuery':
58
+ case 'SelectQuery':
59
+ case 'UpdateQuery':
60
+ case 'DeleteQuery': {
61
+ return makeQueryBuilder(tableDef, {
62
+ ...ast,
63
+ where: [...ast.where, ...newOps],
64
+ }) as any
65
+ }
66
+ default: {
67
+ return casesHandled(ast)
68
+ }
69
+ }
57
70
  }
58
71
 
59
72
  // eslint-disable-next-line prefer-rest-params
60
73
  const [col, opOrValue, valueOrUndefined] = arguments
61
74
  const op = valueOrUndefined === undefined ? '=' : opOrValue
62
75
  const value = valueOrUndefined === undefined ? opOrValue : valueOrUndefined
63
- return makeQueryBuilder(tableDef, {
64
- ...ast,
65
- where: [...ast.where, { col, op, value }],
66
- })
76
+
77
+ switch (ast._tag) {
78
+ case 'CountQuery':
79
+ case 'SelectQuery':
80
+ case 'UpdateQuery':
81
+ case 'DeleteQuery': {
82
+ return makeQueryBuilder(tableDef, {
83
+ ...ast,
84
+ where: [...ast.where, { col, op, value }],
85
+ }) as any
86
+ }
87
+ default: {
88
+ return casesHandled(ast)
89
+ }
90
+ }
67
91
  },
68
92
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
69
93
  orderBy() {
70
- assertQueryBuilderAst(ast)
94
+ assertSelectQueryBuilderAst(ast)
71
95
 
72
96
  if (arguments.length === 0 || arguments.length > 2) return invalidQueryBuilder()
73
97
 
@@ -89,12 +113,12 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
89
113
  }) as any
90
114
  },
91
115
  limit: (limit) => {
92
- assertQueryBuilderAst(ast)
116
+ assertSelectQueryBuilderAst(ast)
93
117
 
94
118
  return makeQueryBuilder(tableDef, { ...ast, limit: Option.some(limit) })
95
119
  },
96
120
  offset: (offset) => {
97
- assertQueryBuilderAst(ast)
121
+ assertSelectQueryBuilderAst(ast)
98
122
 
99
123
  return makeQueryBuilder(tableDef, { ...ast, offset: Option.some(offset) })
100
124
  },
@@ -102,17 +126,18 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
102
126
  if (isRowQuery(ast)) return invalidQueryBuilder()
103
127
 
104
128
  return makeQueryBuilder(tableDef, {
105
- ...ast,
129
+ _tag: 'CountQuery',
130
+ tableDef,
131
+ where: [],
106
132
  resultSchema: Schema.Struct({ count: Schema.Number }).pipe(
107
133
  Schema.pluck('count'),
108
134
  Schema.Array,
109
135
  Schema.headOrElse(),
110
136
  ),
111
- _tag: 'CountQuery',
112
137
  })
113
138
  },
114
139
  first: (options) => {
115
- assertQueryBuilderAst(ast)
140
+ assertSelectQueryBuilderAst(ast)
116
141
 
117
142
  if (ast.limit._tag === 'Some') return invalidQueryBuilder(`.first() can't be called after .limit()`)
118
143
 
@@ -149,6 +174,65 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
149
174
  insertValues,
150
175
  }) as any
151
176
  },
177
+ insert: (values) => {
178
+ return makeQueryBuilder(tableDef, {
179
+ _tag: 'InsertQuery',
180
+ tableDef,
181
+ values: values as any,
182
+ onConflict: undefined,
183
+ returning: undefined,
184
+ resultSchema: Schema.Void,
185
+ }) as any
186
+ },
187
+ onConflict: (target: string, action: 'ignore' | 'replace' | 'update', updateValues?: Record<string, unknown>) => {
188
+ assertInsertQueryBuilderAst(ast)
189
+
190
+ const onConflict = Match.value(action).pipe(
191
+ Match.when('ignore', () => ({ target, action: { _tag: 'ignore' } }) satisfies QueryBuilderAst.OnConflict),
192
+ Match.when('replace', () => ({ target, action: { _tag: 'replace' } }) satisfies QueryBuilderAst.OnConflict),
193
+ Match.when(
194
+ 'update',
195
+ () => ({ target, action: { _tag: 'update', update: updateValues! } }) satisfies QueryBuilderAst.OnConflict,
196
+ ),
197
+ Match.exhaustive,
198
+ )
199
+
200
+ return makeQueryBuilder(tableDef, {
201
+ ...ast,
202
+ onConflict,
203
+ }) as any
204
+ },
205
+
206
+ returning: (...columns) => {
207
+ assertWriteQueryBuilderAst(ast)
208
+
209
+ return makeQueryBuilder(tableDef, {
210
+ ...ast,
211
+ returning: columns,
212
+ resultSchema: tableDef.schema.pipe(Schema.pick(...columns), Schema.Array),
213
+ }) as any
214
+ },
215
+
216
+ update: (values) => {
217
+ return makeQueryBuilder(tableDef, {
218
+ _tag: 'UpdateQuery',
219
+ tableDef,
220
+ values: values as any,
221
+ where: [],
222
+ returning: undefined,
223
+ resultSchema: Schema.Void,
224
+ }) as any
225
+ },
226
+
227
+ delete: () => {
228
+ return makeQueryBuilder(tableDef, {
229
+ _tag: 'DeleteQuery',
230
+ tableDef,
231
+ where: [],
232
+ returning: undefined,
233
+ resultSchema: Schema.Void,
234
+ }) as any
235
+ },
152
236
  } satisfies QueryBuilder.ApiFull<TResult, TTableDef, never, QueryInfo.None>
153
237
 
154
238
  return {
@@ -167,94 +251,38 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
167
251
  } satisfies QueryBuilder<TResult, TTableDef>
168
252
  }
169
253
 
170
- const emptyAst = (tableDef: DbSchema.TableDefBase) =>
171
- ({
172
- _tag: 'SelectQuery',
173
- columns: [],
174
- pickFirst: false,
175
- select: { columns: [] },
176
- orderBy: [],
177
- offset: Option.none(),
178
- limit: Option.none(),
179
- tableDef,
180
- where: [],
181
- resultSchemaSingle: tableDef.schema,
182
- }) satisfies QueryBuilderAst
183
-
184
- const astToSql = (ast: QueryBuilderAst) => {
185
- if (isRowQuery(ast)) {
186
- // TODO
187
- return { query: `SELECT * FROM '${ast.tableDef.sqliteDef.name}' WHERE id = ?`, bindValues: [ast.id as TODO] }
254
+ const emptyAst = (tableDef: DbSchema.TableDefBase): QueryBuilderAst.SelectQuery => ({
255
+ _tag: 'SelectQuery',
256
+ columns: [],
257
+ pickFirst: false,
258
+ select: { columns: [] },
259
+ orderBy: [],
260
+ offset: Option.none(),
261
+ limit: Option.none(),
262
+ tableDef,
263
+ where: [],
264
+ resultSchemaSingle: tableDef.schema,
265
+ })
266
+
267
+ // Helper functions
268
+ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
269
+ function assertSelectQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.SelectQuery {
270
+ if (ast._tag !== 'SelectQuery') {
271
+ throw new Error('Expected SelectQuery but got ' + ast._tag)
188
272
  }
273
+ }
189
274
 
190
- const bindValues: unknown[] = []
191
-
192
- // TODO bind values
193
- const whereStmt =
194
- ast.where.length > 0
195
- ? `WHERE ${ast.where
196
- .map(({ col, op, value }) => {
197
- if (value === null) {
198
- if (op !== '=' && op !== '!=') {
199
- throw new Error(`Unsupported operator for NULL value: ${op}`)
200
- }
201
- const opStmt = op === '=' ? 'IS' : 'IS NOT'
202
- return `${col} ${opStmt} NULL`
203
- } else {
204
- const colDef = ast.tableDef.sqliteDef.columns[col]
205
- if (colDef === undefined) {
206
- throw new Error(`Column ${col} not found`)
207
- }
208
- const isArray = op === 'IN' || op === 'NOT IN'
209
- const colSchema = isArray ? Schema.Array(colDef.schema) : colDef.schema
210
- const encodedValue = Schema.encodeSync(colSchema)(value)
211
-
212
- if (isArray) {
213
- bindValues.push(...encodedValue)
214
- const placeholders = Array.from({ length: encodedValue.length }, () => '?').join(', ')
215
- return `${col} ${op} (${placeholders})`
216
- } else {
217
- bindValues.push(encodedValue)
218
- return `${col} ${op} ?`
219
- }
220
- }
221
- })
222
- .join(' AND ')}`
223
- : ''
224
-
225
- if (ast._tag === 'CountQuery') {
226
- const selectFromStmt = `SELECT COUNT(*) as count FROM '${ast.tableDef.sqliteDef.name}'`
227
- const query = [selectFromStmt, whereStmt].filter((_) => _.length > 0).join(' ')
228
- return { query, bindValues }
275
+ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
276
+ function assertInsertQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.InsertQuery {
277
+ if (ast._tag !== 'InsertQuery') {
278
+ throw new Error('Expected InsertQuery but got ' + ast._tag)
229
279
  }
230
- const columnsStmt = ast.select.columns.length === 0 ? '*' : ast.select.columns.join(', ')
231
- const selectStmt = `SELECT ${columnsStmt}`
232
- const fromStmt = `FROM '${ast.tableDef.sqliteDef.name}'`
233
-
234
- const orderByStmt =
235
- ast.orderBy.length > 0
236
- ? `ORDER BY ${ast.orderBy.map(({ col, direction }) => `${col} ${direction}`).join(', ')}`
237
- : ''
238
-
239
- const limitStmt = ast.limit._tag === 'Some' ? `LIMIT ?` : ''
240
- if (ast.limit._tag === 'Some') bindValues.push(ast.limit.value)
241
-
242
- const offsetStmt = ast.offset._tag === 'Some' ? `OFFSET ?` : ''
243
- if (ast.offset._tag === 'Some') bindValues.push(ast.offset.value)
244
-
245
- const query = [selectStmt, fromStmt, whereStmt, orderByStmt, offsetStmt, limitStmt]
246
- .map((_) => _.trim())
247
- .filter((_) => _.length > 0)
248
- .join(' ')
249
-
250
- // TODO bind values
251
- return { query, bindValues }
252
280
  }
253
281
 
254
282
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
255
- function assertQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.SelectQuery {
256
- if (ast._tag !== 'SelectQuery') {
257
- throw new Error('Expected SelectQuery but got ' + ast._tag)
283
+ function assertWriteQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.WriteQuery {
284
+ if (ast._tag !== 'InsertQuery' && ast._tag !== 'UpdateQuery' && ast._tag !== 'DeleteQuery') {
285
+ throw new Error('Expected WriteQuery but got ' + ast._tag)
258
286
  }
259
287
  }
260
288
 
@@ -264,22 +292,38 @@ export const invalidQueryBuilder = (msg?: string) => {
264
292
  throw new Error('Invalid query builder' + (msg ? `: ${msg}` : ''))
265
293
  }
266
294
 
267
- export const getResultSchema = (qb: QueryBuilder<any, any, any>) => {
295
+ export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<any> => {
268
296
  const queryAst = qb[QueryBuilderAstSymbol]
269
- if (queryAst._tag === 'SelectQuery') {
270
- const arraySchema = Schema.Array(queryAst.resultSchemaSingle)
271
- if (queryAst.pickFirst !== false) {
272
- return arraySchema.pipe(Schema.headOrElse(queryAst.pickFirst.fallback))
297
+ switch (queryAst._tag) {
298
+ case 'SelectQuery': {
299
+ const arraySchema = Schema.Array(queryAst.resultSchemaSingle)
300
+ if (queryAst.pickFirst !== false) {
301
+ return arraySchema.pipe(Schema.headOrElse(queryAst.pickFirst.fallback))
302
+ }
303
+
304
+ return arraySchema
273
305
  }
306
+ case 'CountQuery': {
307
+ return Schema.Struct({ count: Schema.Number }).pipe(Schema.pluck('count'), Schema.Array, Schema.headOrElse())
308
+ }
309
+ case 'InsertQuery':
310
+ case 'UpdateQuery':
311
+ case 'DeleteQuery': {
312
+ // For write operations with RETURNING clause, we need to return the appropriate schema
313
+ if (queryAst.returning && queryAst.returning.length > 0) {
314
+ // Create a schema for the returned columns
315
+ return queryAst.tableDef.schema.pipe(Schema.pick(...queryAst.returning), Schema.Array)
316
+ }
274
317
 
275
- return arraySchema
276
- } else if (queryAst._tag === 'CountQuery') {
277
- return Schema.Struct({ count: Schema.Number }).pipe(Schema.pluck('count'), Schema.Array, Schema.headOrElse())
278
- } else {
279
- if (queryAst.tableDef.options.isSingleColumn) {
280
- return queryAst.tableDef.schema.pipe(Schema.pluck('value'), Schema.Array, Schema.headOrElse())
281
- } else {
282
- return queryAst.tableDef.schema.pipe(Schema.Array, Schema.headOrElse())
318
+ // For write operations without RETURNING, the result is the number of affected rows
319
+ return Schema.Number
320
+ }
321
+ default: {
322
+ if (queryAst.tableDef.options.isSingleColumn) {
323
+ return queryAst.tableDef.schema.pipe(Schema.pluck('value'), Schema.Array, Schema.headOrElse())
324
+ } else {
325
+ return queryAst.tableDef.schema.pipe(Schema.Array, Schema.headOrElse())
326
+ }
283
327
  }
284
328
  }
285
329
  }
@@ -7,4 +7,11 @@ export * from './impl.js'
7
7
  * - Close abstraction to SQLite to provide a simple & convenient API with predictable behaviour
8
8
  * - Use table schema definitions to parse, map & validate query results
9
9
  * - Implementation detail: Separate type-level & AST-based runtime implementation
10
+ *
11
+ * Currently not supported (not exhaustive list):
12
+ * - Assumes a `id` column as primary key
13
+ * - Composite primary keys
14
+ *
15
+ * Other known limitations
16
+ * - Doesn't exclude all invalid query patterns on type level `e.g. `db.todos.returning('id')`
10
17
  */
package/src/query-info.ts CHANGED
@@ -9,7 +9,7 @@ import type { DbSchema } from './schema/mod.js'
9
9
  *
10
10
  * This information is currently only used for derived mutations.
11
11
  */
12
- export type QueryInfo = QueryInfo.None | QueryInfo.Row | QueryInfo.Col | QueryInfo.ColJsonValue
12
+ export type QueryInfo = QueryInfo.None | QueryInfo.Row | QueryInfo.Col | QueryInfo.ColJsonValue | QueryInfo.Write
13
13
  // export type QueryInfo<TTableDef extends DbSchema.TableDefBase = DbSchema.TableDefBase> =
14
14
  // | QueryInfo.None
15
15
  // | QueryInfo.Row<TTableDef>
@@ -45,6 +45,11 @@ export namespace QueryInfo {
45
45
  jsonPath: string
46
46
  }
47
47
 
48
+ // NOTE Not yet used but we might want to use this in order to avoid write queries in read-only situations
49
+ export type Write = {
50
+ _tag: 'Write'
51
+ }
52
+
48
53
  // NOTE maybe we want to bring back type-params back like below
49
54
  // export type Row<TTableDef extends DbSchema.TableDefBase> = {
50
55
  // _tag: 'Row'
@@ -65,7 +65,7 @@ This likely means the schema has changed in an incompatible way.
65
65
  mutation: row.mutation,
66
66
  args,
67
67
  clientId: row.clientId,
68
- sessionId: row.sessionId ?? undefined,
68
+ sessionId: row.sessionId,
69
69
  } satisfies MutationEvent.AnyEncoded
70
70
 
71
71
  yield* applyMutation(mutationEventEncoded, { skipMutationLog: true })
@@ -21,7 +21,7 @@ export type MutationEvent<TMutationsDef extends MutationDef.Any> = {
21
21
  id: EventId.EventId
22
22
  parentId: EventId.EventId
23
23
  clientId: string
24
- sessionId: string | undefined
24
+ sessionId: string
25
25
  }
26
26
 
27
27
  export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
@@ -30,7 +30,7 @@ export type MutationEventEncoded<TMutationsDef extends MutationDef.Any> = {
30
30
  id: EventId.EventId
31
31
  parentId: EventId.EventId
32
32
  clientId: string
33
- sessionId: string | undefined
33
+ sessionId: string
34
34
  }
35
35
 
36
36
  export type AnyDecoded = MutationEvent<MutationDef.Any>
@@ -40,7 +40,7 @@ export const AnyDecoded = Schema.Struct({
40
40
  id: EventId.EventId,
41
41
  parentId: EventId.EventId,
42
42
  clientId: Schema.String,
43
- sessionId: Schema.UndefinedOr(Schema.String),
43
+ sessionId: Schema.String,
44
44
  }).annotations({ title: 'MutationEvent.AnyDecoded' })
45
45
 
46
46
  export type AnyEncoded = MutationEventEncoded<MutationDef.Any>
@@ -50,7 +50,7 @@ export const AnyEncoded = Schema.Struct({
50
50
  id: EventId.EventId,
51
51
  parentId: EventId.EventId,
52
52
  clientId: Schema.String,
53
- sessionId: Schema.UndefinedOr(Schema.String),
53
+ sessionId: Schema.String,
54
54
  }).annotations({ title: 'MutationEvent.AnyEncoded' })
55
55
 
56
56
  export const AnyEncodedGlobal = Schema.Struct({
@@ -59,6 +59,7 @@ export const AnyEncodedGlobal = Schema.Struct({
59
59
  id: EventId.GlobalEventId,
60
60
  parentId: EventId.GlobalEventId,
61
61
  clientId: Schema.String,
62
+ sessionId: Schema.String,
62
63
  }).annotations({ title: 'MutationEvent.AnyEncodedGlobal' })
63
64
  export type AnyEncodedGlobal = typeof AnyEncodedGlobal.Type
64
65
 
@@ -90,7 +91,7 @@ export type ForMutationDefRecord<TMutationsDefRecord extends MutationDefRecord>
90
91
  id: EventId.EventId
91
92
  parentId: EventId.EventId
92
93
  clientId: string
93
- sessionId: string | undefined
94
+ sessionId: string
94
95
  }
95
96
  }[keyof TMutationsDefRecord],
96
97
  {
@@ -100,7 +101,7 @@ export type ForMutationDefRecord<TMutationsDefRecord extends MutationDefRecord>
100
101
  id: EventId.EventId
101
102
  parentId: EventId.EventId
102
103
  clientId: string
103
- sessionId: string | undefined
104
+ sessionId: string
104
105
  }
105
106
  }[keyof TMutationsDefRecord]
106
107
  >
@@ -131,7 +132,7 @@ export const makeMutationEventSchema = <TSchema extends LiveStoreSchema>(
131
132
  id: EventId.EventId,
132
133
  parentId: EventId.EventId,
133
134
  clientId: Schema.String,
134
- sessionId: Schema.UndefinedOr(Schema.String),
135
+ sessionId: Schema.String,
135
136
  }),
136
137
  ),
137
138
  ).annotations({ title: 'MutationEvent' }) as any
@@ -157,7 +158,7 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
157
158
  id: EventId.EventId,
158
159
  parentId: EventId.EventId,
159
160
  clientId: Schema.String,
160
- sessionId: Schema.UndefinedOr(Schema.String),
161
+ sessionId: Schema.String,
161
162
  // TODO get rid of `meta` again by cleaning up the usage implementations
162
163
  meta: Schema.optionalWith(
163
164
  Schema.Any as Schema.Schema<{
@@ -177,10 +178,26 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
177
178
  }
178
179
  }
179
180
 
180
- rebase = (parentId: EventId.EventId, isLocal: boolean) =>
181
+ /**
182
+ * Example: (global event)
183
+ * For event id (2,0) → (1,0) which should be rebased on event id (3,1) → (3,0)
184
+ * the resulting event id will be (4,0) → (3,0)
185
+ *
186
+ * Example: (client event)
187
+ * For event id (2,1) → (2,0) which should be rebased on event id (3,0) → (2,0)
188
+ * the resulting event id will be (3,1) → (3,0)
189
+ *
190
+ * Syntax: (2,1) → (2,0)
191
+ * ^ ^ ^ ^
192
+ * | | | +- client parent id
193
+ * | | +--- global parent id
194
+ * | +-- client id
195
+ * +---- global id
196
+ */
197
+ rebase = (parentId: EventId.EventId, isClient: boolean) =>
181
198
  new EncodedWithMeta({
182
199
  ...this,
183
- ...EventId.nextPair(parentId, isLocal),
200
+ ...EventId.nextPair(parentId, isClient),
184
201
  })
185
202
 
186
203
  static fromGlobal = (mutationEvent: AnyEncodedGlobal) =>
@@ -188,7 +205,6 @@ export class EncodedWithMeta extends Schema.Class<EncodedWithMeta>('MutationEven
188
205
  ...mutationEvent,
189
206
  id: { global: mutationEvent.id, client: EventId.clientDefault },
190
207
  parentId: { global: mutationEvent.parentId, client: EventId.clientDefault },
191
- sessionId: undefined,
192
208
  })
193
209
 
194
210
  toGlobal = (): AnyEncodedGlobal => ({
@@ -203,6 +219,6 @@ export const isEqualEncoded = (a: AnyEncoded, b: AnyEncoded) =>
203
219
  a.id.client === b.id.client &&
204
220
  a.mutation === b.mutation &&
205
221
  a.clientId === b.clientId &&
206
- // a.sessionId === b.sessionId &&
222
+ a.sessionId === b.sessionId &&
207
223
  // TODO use schema equality here
208
224
  JSON.stringify(a.args) === JSON.stringify(b.args)
@@ -52,8 +52,13 @@ export type AnyIfConstained<In, Out> = '__constrained' extends keyof In ? any :
52
52
  export type EmptyObjIfConstained<In> = '__constrained' extends keyof In ? {} : In
53
53
 
54
54
  export type StructSchemaForColumns<TCols extends ConstraintColumns> = Schema.Schema<
55
- AnyIfConstained<TCols, { readonly [K in keyof TCols]: TCols[K]['schema']['Type'] }>,
56
- AnyIfConstained<TCols, { readonly [K in keyof TCols]: TCols[K]['schema']['Encoded'] }>
55
+ AnyIfConstained<TCols, FromColumns.RowDecoded<TCols>>,
56
+ AnyIfConstained<TCols, FromColumns.RowEncoded<TCols>>
57
+ >
58
+
59
+ export type InsertStructSchemaForColumns<TCols extends ConstraintColumns> = Schema.Schema<
60
+ AnyIfConstained<TCols, FromColumns.InsertRowDecoded<TCols>>,
61
+ AnyIfConstained<TCols, FromColumns.InsertRowEncoded<TCols>>
57
62
  >
58
63
 
59
64
  export const structSchemaForTable = <TTableDefinition extends TableDefinition<any, any>>(
@@ -63,6 +68,20 @@ export const structSchemaForTable = <TTableDefinition extends TableDefinition<an
63
68
  title: tableDef.name,
64
69
  }) as any
65
70
 
71
+ export const insertStructSchemaForTable = <TTableDefinition extends TableDefinition<any, any>>(
72
+ tableDef: TTableDefinition,
73
+ ): InsertStructSchemaForColumns<TTableDefinition['columns']> =>
74
+ Schema.Struct(
75
+ Object.fromEntries(
76
+ tableDef.ast.columns.map((column) => [
77
+ column.name,
78
+ column.nullable === true || column.default._tag === 'Some' ? Schema.optional(column.schema) : column.schema,
79
+ ]),
80
+ ),
81
+ ).annotations({
82
+ title: tableDef.name,
83
+ }) as any
84
+
66
85
  const columsToAst = (columns: Columns): ReadonlyArray<SqliteAst.Column> => {
67
86
  return Object.entries(columns).map(([name, column]) => {
68
87
  return {
@@ -161,6 +180,10 @@ export namespace FromColumns {
161
180
  readonly [K in keyof TColumns]: Schema.Schema.Type<TColumns[K]['schema']>
162
181
  }
163
182
 
183
+ export type RowEncodedAll<TColumns extends Columns> = {
184
+ readonly [K in keyof TColumns]: Schema.Schema.Encoded<TColumns[K]['schema']>
185
+ }
186
+
164
187
  export type RowEncoded<TColumns extends Columns> = Types.Simplify<
165
188
  Nullable<Pick<RowEncodeNonNullable<TColumns>, NullableColumnNames<TColumns>>> &
166
189
  Omit<RowEncodeNonNullable<TColumns>, NullableColumnNames<TColumns>>
@@ -192,4 +215,9 @@ export namespace FromColumns {
192
215
  Pick<RowDecodedAll<TColumns>, RequiredInsertColumnNames<TColumns>> &
193
216
  Partial<Omit<RowDecodedAll<TColumns>, RequiredInsertColumnNames<TColumns>>>
194
217
  >
218
+
219
+ export type InsertRowEncoded<TColumns extends Columns> = Types.Simplify<
220
+ Pick<RowEncodedAll<TColumns>, RequiredInsertColumnNames<TColumns>> &
221
+ Partial<Omit<RowEncodedAll<TColumns>, RequiredInsertColumnNames<TColumns>>>
222
+ >
195
223
  }
@@ -1,5 +1,6 @@
1
1
  import { Schema } from '@livestore/utils/effect'
2
2
 
3
+ import type { QueryBuilder } from '../query-builder/mod.js'
3
4
  import type { BindValues } from '../sql-queries/sql-queries.js'
4
5
 
5
6
  export type MutationDefMap = {
@@ -20,7 +21,10 @@ export type InternalMutationSchema<TRecord extends MutationDefRecord = MutationD
20
21
 
21
22
  export type MutationDefSqlResult<TTo> =
22
23
  | SingleOrReadonlyArray<string>
23
- | ((args: TTo) => SingleOrReadonlyArray<
24
+ | ((
25
+ args: TTo,
26
+ context: { currentFacts: MutationEventFacts; clientOnly: boolean },
27
+ ) => SingleOrReadonlyArray<
24
28
  | string
25
29
  | {
26
30
  sql: string
@@ -28,8 +32,15 @@ export type MutationDefSqlResult<TTo> =
28
32
  bindValues: BindValues
29
33
  writeTables?: ReadonlySet<string>
30
34
  }
35
+ | QueryBuilder.Any
31
36
  >)
32
37
 
38
+ export type MutationHandlerResult = {
39
+ sql: string
40
+ bindValues: BindValues
41
+ writeTables?: ReadonlySet<string>
42
+ }
43
+
33
44
  export type SingleOrReadonlyArray<T> = T | ReadonlyArray<T>
34
45
 
35
46
  export type MutationDef<TName extends string, TFrom, TTo> = {
@@ -79,8 +79,7 @@ export const mutationLogMetaTable = table(
79
79
  mutation: SqliteDsl.text({}),
80
80
  argsJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Any) }),
81
81
  clientId: SqliteDsl.text({}),
82
- /** Only available for mutations which were executed in this client */
83
- sessionId: SqliteDsl.text({ nullable: true }),
82
+ sessionId: SqliteDsl.text({}),
84
83
  schemaHash: SqliteDsl.integer({}),
85
84
  syncMetadataJson: SqliteDsl.text({ schema: Schema.parseJson(Schema.Option(Schema.JsonValue)) }),
86
85
  },
@@ -19,12 +19,12 @@ export type DefaultSqliteTableDefConstrained = SqliteDsl.TableDefinition<string,
19
19
  export type TableDefBase<
20
20
  TSqliteDef extends DefaultSqliteTableDef = DefaultSqliteTableDefConstrained,
21
21
  TOptions extends TableOptions = TableOptions,
22
- TSchema = SqliteDsl.StructSchemaForColumns<TSqliteDef['columns']>,
23
22
  > = {
24
23
  sqliteDef: TSqliteDef
25
24
  options: TOptions
26
25
  // Derived from `sqliteDef`, so only exposed for convenience
27
- schema: TSchema
26
+ schema: SqliteDsl.StructSchemaForColumns<TSqliteDef['columns']>
27
+ insertSchema: SqliteDsl.InsertStructSchemaForColumns<TSqliteDef['columns']>
28
28
  }
29
29
 
30
30
  export type TableDef<
@@ -47,7 +47,10 @@ export type TableDef<
47
47
  options: TOptions
48
48
  // Derived from `sqliteDef`, so only exposed for convenience
49
49
  schema: TSchema
50
- query: QueryBuilder<ReadonlyArray<Schema.Schema.Type<TSchema>>, TableDef<TSqliteDef & {}, TOptions>>
50
+ insertSchema: SqliteDsl.InsertStructSchemaForColumns<TSqliteDef['columns']>
51
+ query: QueryBuilder<ReadonlyArray<Schema.Schema.Type<TSchema>>, TableDefBase<TSqliteDef & {}, TOptions>>
52
+ readonly Type: Schema.Schema.Type<TSchema>
53
+ readonly Encoded: Schema.Schema.Encoded<TSchema>
51
54
  } & (TOptions['deriveMutations']['enabled'] extends true
52
55
  ? DerivedMutationHelperFns<TSqliteDef['columns'], TOptions>
53
56
  : {})
@@ -195,7 +198,14 @@ export const table = <
195
198
  const isSingleColumn = SqliteDsl.isColumnDefinition(columnOrColumns) === true
196
199
 
197
200
  const schema = SqliteDsl.structSchemaForTable(sqliteDef)
198
- const tableDef = { sqliteDef, options: options_, schema } satisfies TableDefBase
201
+ const insertSchema = SqliteDsl.insertStructSchemaForTable(sqliteDef)
202
+ const tableDef = {
203
+ sqliteDef,
204
+ options: options_,
205
+ schema,
206
+ insertSchema,
207
+ } satisfies TableDefBase
208
+
199
209
  const query = makeQueryBuilder(tableDef)
200
210
  // const tableDef = { ...tableDefBase, query } satisfies TableDef
201
211