@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapter-types.d.ts +4 -2
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +1 -1
- package/dist/adapter-types.js.map +1 -1
- package/dist/derived-mutations.d.ts +8 -8
- package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
- package/dist/devtools/devtools-messages-common.d.ts +6 -6
- package/dist/devtools/devtools-messages-leader.d.ts +25 -24
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +2 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +16 -12
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/apply-mutation.js +1 -1
- package/dist/leader-thread/apply-mutation.js.map +1 -1
- package/dist/leader-thread/leader-worker-devtools.js +2 -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 +3 -2
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mutationlog.d.ts +1 -0
- package/dist/leader-thread/mutationlog.d.ts.map +1 -1
- package/dist/leader-thread/mutationlog.js +2 -1
- package/dist/leader-thread/mutationlog.js.map +1 -1
- package/dist/leader-thread/pull-queue-set.d.ts.map +1 -1
- package/dist/leader-thread/types.d.ts +1 -1
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +13 -2
- package/dist/mutation.js.map +1 -1
- package/dist/query-builder/api.d.ts +118 -20
- package/dist/query-builder/api.d.ts.map +1 -1
- package/dist/query-builder/api.js.map +1 -1
- package/dist/query-builder/astToSql.d.ts +7 -0
- package/dist/query-builder/astToSql.d.ts.map +1 -0
- package/dist/query-builder/astToSql.js +168 -0
- package/dist/query-builder/astToSql.js.map +1 -0
- package/dist/query-builder/impl.d.ts +1 -5
- package/dist/query-builder/impl.d.ts.map +1 -1
- package/dist/query-builder/impl.js +130 -96
- package/dist/query-builder/impl.js.map +1 -1
- package/dist/query-builder/impl.test.js +94 -0
- package/dist/query-builder/impl.test.js.map +1 -1
- package/dist/query-builder/mod.d.ts +7 -0
- package/dist/query-builder/mod.d.ts.map +1 -1
- package/dist/query-builder/mod.js +7 -0
- package/dist/query-builder/mod.js.map +1 -1
- package/dist/query-info.d.ts +4 -1
- package/dist/query-info.d.ts.map +1 -1
- package/dist/query-info.js.map +1 -1
- package/dist/rehydrate-from-mutationlog.js +1 -1
- package/dist/rehydrate-from-mutationlog.js.map +1 -1
- package/dist/schema/MutationEvent.d.ts +27 -10
- package/dist/schema/MutationEvent.d.ts.map +1 -1
- package/dist/schema/MutationEvent.js +24 -8
- package/dist/schema/MutationEvent.js.map +1 -1
- package/dist/schema/db-schema/dsl/mod.d.ts +7 -5
- package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/db-schema/dsl/mod.js +6 -0
- package/dist/schema/db-schema/dsl/mod.js.map +1 -1
- package/dist/schema/mutations.d.ts +12 -3
- package/dist/schema/mutations.d.ts.map +1 -1
- package/dist/schema/mutations.js.map +1 -1
- package/dist/schema/system-tables.d.ts +5 -5
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +1 -2
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema/table-def.d.ts +7 -3
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +7 -1
- package/dist/schema/table-def.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +2 -0
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +8 -5
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +1 -1
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/sync.d.ts +19 -1
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/syncstate.d.ts +26 -4
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +95 -25
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +60 -29
- 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 +2 -2
- package/src/adapter-types.ts +4 -2
- package/src/leader-thread/LeaderSyncProcessor.ts +19 -13
- package/src/leader-thread/apply-mutation.ts +2 -2
- package/src/leader-thread/leader-worker-devtools.ts +2 -2
- package/src/leader-thread/make-leader-thread-layer.ts +3 -2
- package/src/leader-thread/mutationlog.ts +2 -1
- package/src/leader-thread/types.ts +1 -1
- package/src/mutation.ts +20 -3
- package/src/query-builder/api.ts +192 -15
- package/src/query-builder/astToSql.ts +203 -0
- package/src/query-builder/impl.test.ts +104 -0
- package/src/query-builder/impl.ts +157 -113
- package/src/query-builder/mod.ts +7 -0
- package/src/query-info.ts +6 -1
- package/src/rehydrate-from-mutationlog.ts +1 -1
- package/src/schema/MutationEvent.ts +28 -12
- package/src/schema/db-schema/dsl/mod.ts +30 -2
- package/src/schema/mutations.ts +12 -1
- package/src/schema/system-tables.ts +1 -2
- package/src/schema/table-def.ts +14 -4
- package/src/sync/ClientSessionSyncProcessor.ts +10 -4
- package/src/sync/next/rebase-events.ts +1 -1
- package/src/sync/sync.ts +19 -3
- package/src/sync/syncstate.test.ts +66 -32
- package/src/sync/syncstate.ts +116 -34
- package/src/version.ts +1 -1
- package/tmp/pack.tgz +0 -0
@@ -1,9 +1,11 @@
|
|
1
|
-
import {
|
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
|
-
|
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 (
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
116
|
+
assertSelectQueryBuilderAst(ast)
|
93
117
|
|
94
118
|
return makeQueryBuilder(tableDef, { ...ast, limit: Option.some(limit) })
|
95
119
|
},
|
96
120
|
offset: (offset) => {
|
97
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
256
|
-
if (ast._tag !== '
|
257
|
-
throw new Error('Expected
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
}
|
package/src/query-builder/mod.ts
CHANGED
@@ -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
|
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
|
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
|
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.
|
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.
|
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
|
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
|
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.
|
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.
|
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
|
-
|
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,
|
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
|
-
|
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,
|
56
|
-
AnyIfConstained<TCols,
|
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
|
}
|
package/src/schema/mutations.ts
CHANGED
@@ -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
|
-
| ((
|
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
|
-
|
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
|
},
|
package/src/schema/table-def.ts
CHANGED
@@ -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:
|
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
|
-
|
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
|
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
|
|