@livestore/common 0.3.0-dev.27 → 0.3.0-dev.29
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/__tests__/fixture.d.ts +83 -221
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +33 -11
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/adapter-types.d.ts +22 -15
- package/dist/adapter-types.d.ts.map +1 -1
- package/dist/adapter-types.js +15 -2
- package/dist/adapter-types.js.map +1 -1
- package/dist/bounded-collections.d.ts +1 -1
- package/dist/bounded-collections.d.ts.map +1 -1
- package/dist/debug-info.d.ts.map +1 -1
- package/dist/debug-info.js +1 -0
- package/dist/debug-info.js.map +1 -1
- 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 +45 -45
- package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
- package/dist/devtools/devtools-messages-leader.js +11 -11
- package/dist/devtools/devtools-messages-leader.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.d.ts +25 -12
- package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
- package/dist/leader-thread/LeaderSyncProcessor.js +125 -89
- package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
- package/dist/leader-thread/{apply-mutation.d.ts → apply-event.d.ts} +7 -7
- package/dist/leader-thread/apply-event.d.ts.map +1 -0
- package/dist/leader-thread/apply-event.js +103 -0
- package/dist/leader-thread/apply-event.js.map +1 -0
- package/dist/leader-thread/eventlog.d.ts +27 -0
- package/dist/leader-thread/eventlog.d.ts.map +1 -0
- package/dist/leader-thread/eventlog.js +123 -0
- package/dist/leader-thread/eventlog.js.map +1 -0
- package/dist/leader-thread/leader-worker-devtools.js +18 -18
- package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.d.ts +16 -4
- package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
- package/dist/leader-thread/make-leader-thread-layer.js +23 -16
- package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
- package/dist/leader-thread/mod.d.ts +1 -1
- package/dist/leader-thread/mod.d.ts.map +1 -1
- package/dist/leader-thread/mod.js +1 -1
- package/dist/leader-thread/mod.js.map +1 -1
- package/dist/leader-thread/recreate-db.d.ts.map +1 -1
- package/dist/leader-thread/recreate-db.js +6 -8
- package/dist/leader-thread/recreate-db.js.map +1 -1
- package/dist/leader-thread/types.d.ts +11 -11
- package/dist/leader-thread/types.d.ts.map +1 -1
- package/dist/materializer-helper.d.ts +23 -0
- package/dist/materializer-helper.d.ts.map +1 -0
- package/dist/materializer-helper.js +70 -0
- package/dist/materializer-helper.js.map +1 -0
- package/dist/query-builder/api.d.ts +58 -53
- package/dist/query-builder/api.d.ts.map +1 -1
- package/dist/query-builder/api.js +3 -5
- package/dist/query-builder/api.js.map +1 -1
- package/dist/query-builder/astToSql.d.ts.map +1 -1
- package/dist/query-builder/astToSql.js +59 -37
- package/dist/query-builder/astToSql.js.map +1 -1
- package/dist/query-builder/impl.d.ts +2 -3
- package/dist/query-builder/impl.d.ts.map +1 -1
- package/dist/query-builder/impl.js +48 -46
- package/dist/query-builder/impl.js.map +1 -1
- package/dist/query-builder/impl.test.d.ts +86 -1
- package/dist/query-builder/impl.test.d.ts.map +1 -1
- package/dist/query-builder/impl.test.js +244 -36
- package/dist/query-builder/impl.test.js.map +1 -1
- package/dist/rehydrate-from-eventlog.d.ts +14 -0
- package/dist/rehydrate-from-eventlog.d.ts.map +1 -0
- package/dist/{rehydrate-from-mutationlog.js → rehydrate-from-eventlog.js} +25 -26
- package/dist/rehydrate-from-eventlog.js.map +1 -0
- package/dist/schema/EventDef.d.ts +136 -0
- package/dist/schema/EventDef.d.ts.map +1 -0
- package/dist/schema/EventDef.js +58 -0
- package/dist/schema/EventDef.js.map +1 -0
- package/dist/schema/EventId.d.ts +2 -2
- package/dist/schema/EventId.d.ts.map +1 -1
- package/dist/schema/EventId.js +8 -2
- package/dist/schema/EventId.js.map +1 -1
- package/dist/schema/{MutationEvent.d.ts → LiveStoreEvent.d.ts} +56 -56
- package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
- package/dist/schema/{MutationEvent.js → LiveStoreEvent.js} +25 -25
- package/dist/schema/LiveStoreEvent.js.map +1 -0
- package/dist/schema/client-document-def.d.ts +223 -0
- package/dist/schema/client-document-def.d.ts.map +1 -0
- package/dist/schema/client-document-def.js +170 -0
- package/dist/schema/client-document-def.js.map +1 -0
- package/dist/schema/client-document-def.test.d.ts +2 -0
- package/dist/schema/client-document-def.test.d.ts.map +1 -0
- package/dist/schema/client-document-def.test.js +201 -0
- package/dist/schema/client-document-def.test.js.map +1 -0
- package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
- package/dist/schema/events.d.ts +2 -0
- package/dist/schema/events.d.ts.map +1 -0
- package/dist/schema/events.js +2 -0
- package/dist/schema/events.js.map +1 -0
- package/dist/schema/mod.d.ts +4 -3
- package/dist/schema/mod.d.ts.map +1 -1
- package/dist/schema/mod.js +4 -3
- package/dist/schema/mod.js.map +1 -1
- package/dist/schema/schema.d.ts +27 -23
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +45 -43
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/sqlite-state.d.ts +12 -0
- package/dist/schema/sqlite-state.d.ts.map +1 -0
- package/dist/schema/sqlite-state.js +36 -0
- package/dist/schema/sqlite-state.js.map +1 -0
- package/dist/schema/system-tables.d.ts +67 -98
- package/dist/schema/system-tables.d.ts.map +1 -1
- package/dist/schema/system-tables.js +62 -48
- package/dist/schema/system-tables.js.map +1 -1
- package/dist/schema/table-def.d.ts +26 -96
- package/dist/schema/table-def.d.ts.map +1 -1
- package/dist/schema/table-def.js +16 -64
- package/dist/schema/table-def.js.map +1 -1
- package/dist/schema/view.d.ts +3 -0
- package/dist/schema/view.d.ts.map +1 -0
- package/dist/schema/view.js +3 -0
- package/dist/schema/view.js.map +1 -0
- package/dist/schema-management/common.d.ts +4 -4
- package/dist/schema-management/common.d.ts.map +1 -1
- package/dist/schema-management/migrations.d.ts.map +1 -1
- package/dist/schema-management/migrations.js +6 -6
- package/dist/schema-management/migrations.js.map +1 -1
- package/dist/schema-management/validate-mutation-defs.d.ts +3 -3
- package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
- package/dist/schema-management/validate-mutation-defs.js +17 -17
- package/dist/schema-management/validate-mutation-defs.js.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.d.ts +7 -7
- package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
- package/dist/sync/ClientSessionSyncProcessor.js +31 -30
- package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
- package/dist/sync/next/facts.d.ts +19 -19
- package/dist/sync/next/facts.d.ts.map +1 -1
- package/dist/sync/next/facts.js +2 -2
- package/dist/sync/next/facts.js.map +1 -1
- package/dist/sync/next/history-dag-common.d.ts +3 -3
- package/dist/sync/next/history-dag-common.d.ts.map +1 -1
- package/dist/sync/next/history-dag-common.js +1 -1
- package/dist/sync/next/history-dag-common.js.map +1 -1
- package/dist/sync/next/history-dag.js +1 -1
- package/dist/sync/next/history-dag.js.map +1 -1
- package/dist/sync/next/rebase-events.d.ts +7 -7
- package/dist/sync/next/rebase-events.d.ts.map +1 -1
- package/dist/sync/next/rebase-events.js +5 -5
- package/dist/sync/next/rebase-events.js.map +1 -1
- package/dist/sync/next/test/compact-events.calculator.test.js +38 -33
- package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
- package/dist/sync/next/test/compact-events.test.js +71 -71
- package/dist/sync/next/test/compact-events.test.js.map +1 -1
- package/dist/sync/next/test/{mutation-fixtures.d.ts → event-fixtures.d.ts} +29 -29
- package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
- package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +60 -25
- package/dist/sync/next/test/event-fixtures.js.map +1 -0
- package/dist/sync/next/test/mod.d.ts +1 -1
- package/dist/sync/next/test/mod.d.ts.map +1 -1
- package/dist/sync/next/test/mod.js +1 -1
- package/dist/sync/next/test/mod.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -3
- package/dist/sync/sync.d.ts.map +1 -1
- package/dist/sync/syncstate.d.ts +32 -32
- package/dist/sync/syncstate.d.ts.map +1 -1
- package/dist/sync/syncstate.js +31 -25
- package/dist/sync/syncstate.js.map +1 -1
- package/dist/sync/syncstate.test.js +165 -175
- package/dist/sync/syncstate.test.js.map +1 -1
- package/dist/sync/validate-push-payload.d.ts +2 -2
- package/dist/sync/validate-push-payload.d.ts.map +1 -1
- package/dist/sync/validate-push-payload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/__tests__/fixture.ts +36 -15
- package/src/adapter-types.ts +23 -16
- package/src/debug-info.ts +1 -0
- package/src/devtools/devtools-messages-leader.ts +13 -13
- package/src/index.ts +2 -5
- package/src/leader-thread/LeaderSyncProcessor.ts +183 -122
- package/src/leader-thread/{apply-mutation.ts → apply-event.ts} +50 -74
- package/src/leader-thread/eventlog.ts +199 -0
- package/src/leader-thread/leader-worker-devtools.ts +18 -18
- package/src/leader-thread/make-leader-thread-layer.ts +51 -29
- package/src/leader-thread/mod.ts +1 -1
- package/src/leader-thread/recreate-db.ts +6 -9
- package/src/leader-thread/types.ts +12 -12
- package/src/materializer-helper.ts +110 -0
- package/src/query-builder/api.ts +79 -105
- package/src/query-builder/astToSql.ts +68 -39
- package/src/query-builder/impl.test.ts +264 -42
- package/src/query-builder/impl.ts +72 -56
- package/src/{rehydrate-from-mutationlog.ts → rehydrate-from-eventlog.ts} +33 -40
- package/src/schema/EventDef.ts +216 -0
- package/src/schema/EventId.ts +11 -3
- package/src/schema/{MutationEvent.ts → LiveStoreEvent.ts} +68 -69
- package/src/schema/client-document-def.test.ts +239 -0
- package/src/schema/client-document-def.ts +444 -0
- package/src/schema/db-schema/dsl/mod.ts +0 -1
- package/src/schema/events.ts +1 -0
- package/src/schema/mod.ts +4 -3
- package/src/schema/schema.ts +79 -69
- package/src/schema/sqlite-state.ts +62 -0
- package/src/schema/system-tables.ts +42 -53
- package/src/schema/table-def.ts +53 -209
- package/src/schema/view.ts +2 -0
- package/src/schema-management/common.ts +4 -4
- package/src/schema-management/migrations.ts +8 -9
- package/src/schema-management/validate-mutation-defs.ts +22 -24
- package/src/sync/ClientSessionSyncProcessor.ts +37 -36
- package/src/sync/next/facts.ts +31 -32
- package/src/sync/next/history-dag-common.ts +4 -4
- package/src/sync/next/history-dag.ts +1 -1
- package/src/sync/next/rebase-events.ts +13 -13
- package/src/sync/next/test/compact-events.calculator.test.ts +45 -45
- package/src/sync/next/test/compact-events.test.ts +73 -73
- package/src/sync/next/test/event-fixtures.ts +219 -0
- package/src/sync/next/test/mod.ts +1 -1
- package/src/sync/sync.ts +3 -3
- package/src/sync/syncstate.test.ts +168 -179
- package/src/sync/syncstate.ts +48 -38
- package/src/sync/validate-push-payload.ts +2 -2
- package/src/version.ts +1 -1
- package/tmp/pack.tgz +0 -0
- package/tsconfig.json +1 -0
- package/dist/derived-mutations.d.ts +0 -109
- package/dist/derived-mutations.d.ts.map +0 -1
- package/dist/derived-mutations.js +0 -54
- package/dist/derived-mutations.js.map +0 -1
- package/dist/derived-mutations.test.d.ts +0 -2
- package/dist/derived-mutations.test.d.ts.map +0 -1
- package/dist/derived-mutations.test.js +0 -93
- package/dist/derived-mutations.test.js.map +0 -1
- package/dist/init-singleton-tables.d.ts +0 -4
- package/dist/init-singleton-tables.d.ts.map +0 -1
- package/dist/init-singleton-tables.js +0 -16
- package/dist/init-singleton-tables.js.map +0 -1
- package/dist/leader-thread/apply-mutation.d.ts.map +0 -1
- package/dist/leader-thread/apply-mutation.js +0 -122
- package/dist/leader-thread/apply-mutation.js.map +0 -1
- package/dist/leader-thread/mutationlog.d.ts +0 -27
- package/dist/leader-thread/mutationlog.d.ts.map +0 -1
- package/dist/leader-thread/mutationlog.js +0 -124
- package/dist/leader-thread/mutationlog.js.map +0 -1
- package/dist/leader-thread/pull-queue-set.d.ts +0 -7
- package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
- package/dist/leader-thread/pull-queue-set.js +0 -38
- package/dist/leader-thread/pull-queue-set.js.map +0 -1
- package/dist/mutation.d.ts +0 -20
- package/dist/mutation.d.ts.map +0 -1
- package/dist/mutation.js +0 -68
- package/dist/mutation.js.map +0 -1
- package/dist/query-info.d.ts +0 -41
- package/dist/query-info.d.ts.map +0 -1
- package/dist/query-info.js +0 -7
- package/dist/query-info.js.map +0 -1
- package/dist/rehydrate-from-mutationlog.d.ts +0 -15
- package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
- package/dist/rehydrate-from-mutationlog.js.map +0 -1
- package/dist/schema/MutationEvent.d.ts.map +0 -1
- package/dist/schema/MutationEvent.js.map +0 -1
- package/dist/schema/mutations.d.ts +0 -115
- package/dist/schema/mutations.d.ts.map +0 -1
- package/dist/schema/mutations.js +0 -42
- package/dist/schema/mutations.js.map +0 -1
- package/dist/sync/next/test/mutation-fixtures.d.ts.map +0 -1
- package/dist/sync/next/test/mutation-fixtures.js.map +0 -1
- package/src/derived-mutations.test.ts +0 -101
- package/src/derived-mutations.ts +0 -170
- package/src/init-singleton-tables.ts +0 -24
- package/src/leader-thread/mutationlog.ts +0 -202
- package/src/mutation.ts +0 -108
- package/src/query-info.ts +0 -83
- package/src/schema/mutations.ts +0 -193
- package/src/sync/next/test/mutation-fixtures.ts +0 -228
@@ -1,40 +1,78 @@
|
|
1
1
|
import { Schema } from '@livestore/utils/effect'
|
2
2
|
import { describe, expect, it } from 'vitest'
|
3
3
|
|
4
|
-
import {
|
4
|
+
import { State } from '../schema/mod.js'
|
5
5
|
import { getResultSchema } from './impl.js'
|
6
6
|
|
7
|
-
const todos =
|
8
|
-
'todos',
|
9
|
-
{
|
10
|
-
id:
|
11
|
-
text:
|
12
|
-
completed:
|
13
|
-
status:
|
14
|
-
deletedAt:
|
7
|
+
const todos = State.SQLite.table({
|
8
|
+
name: 'todos',
|
9
|
+
columns: {
|
10
|
+
id: State.SQLite.text({ primaryKey: true }),
|
11
|
+
text: State.SQLite.text({ default: '', nullable: false }),
|
12
|
+
completed: State.SQLite.boolean({ default: false, nullable: false }),
|
13
|
+
status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
|
14
|
+
deletedAt: State.SQLite.datetime({ nullable: true }),
|
15
15
|
// TODO consider leaning more into Effect schema
|
16
|
-
// other: Schema.Number.pipe(
|
16
|
+
// other: Schema.Number.pipe(State.SQLite.asInteger),
|
17
17
|
},
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
status: DbSchema.text({ schema: Schema.Literal('active', 'completed') }),
|
18
|
+
})
|
19
|
+
|
20
|
+
const todosWithIntId = State.SQLite.table({
|
21
|
+
name: 'todos_with_int_id',
|
22
|
+
columns: {
|
23
|
+
id: State.SQLite.integer({ primaryKey: true }),
|
24
|
+
text: State.SQLite.text({ default: '', nullable: false }),
|
25
|
+
status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
|
27
26
|
},
|
28
|
-
|
29
|
-
)
|
27
|
+
})
|
30
28
|
|
31
|
-
const comments =
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
const comments = State.SQLite.table({
|
30
|
+
name: 'comments',
|
31
|
+
columns: {
|
32
|
+
id: State.SQLite.text({ primaryKey: true }),
|
33
|
+
text: State.SQLite.text({ default: '', nullable: false }),
|
34
|
+
todoId: State.SQLite.text({}),
|
35
|
+
},
|
35
36
|
})
|
36
37
|
|
37
|
-
const
|
38
|
+
const UiState = State.SQLite.clientDocument({
|
39
|
+
name: 'UiState',
|
40
|
+
schema: Schema.Struct({
|
41
|
+
filter: Schema.Literal('all', 'active', 'completed'),
|
42
|
+
}),
|
43
|
+
default: { value: { filter: 'all' } },
|
44
|
+
})
|
45
|
+
|
46
|
+
const UiStateWithDefaultId = State.SQLite.clientDocument({
|
47
|
+
name: 'UiState',
|
48
|
+
schema: Schema.Struct({
|
49
|
+
filter: Schema.Literal('all', 'active', 'completed'),
|
50
|
+
}),
|
51
|
+
default: {
|
52
|
+
id: 'static',
|
53
|
+
value: { filter: 'all' },
|
54
|
+
},
|
55
|
+
})
|
56
|
+
|
57
|
+
export const issue = State.SQLite.table({
|
58
|
+
name: 'issue',
|
59
|
+
columns: {
|
60
|
+
id: State.SQLite.integer({ primaryKey: true }),
|
61
|
+
title: State.SQLite.text({ default: '' }),
|
62
|
+
creator: State.SQLite.text({ default: '' }),
|
63
|
+
priority: State.SQLite.integer({ schema: Schema.Literal(0, 1, 2, 3, 4), default: 0 }),
|
64
|
+
created: State.SQLite.integer({ schema: Schema.DateFromNumber }),
|
65
|
+
deleted: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
|
66
|
+
modified: State.SQLite.integer({ schema: Schema.DateFromNumber }),
|
67
|
+
kanbanorder: State.SQLite.text({ nullable: false, default: '' }),
|
68
|
+
},
|
69
|
+
indexes: [
|
70
|
+
{ name: 'issue_kanbanorder', columns: ['kanbanorder'] },
|
71
|
+
{ name: 'issue_created', columns: ['created'] },
|
72
|
+
],
|
73
|
+
})
|
74
|
+
|
75
|
+
const db = { todos, todosWithIntId, comments, issue, UiState, UiStateWithDefaultId }
|
38
76
|
|
39
77
|
describe('query builder', () => {
|
40
78
|
describe('result schema', () => {
|
@@ -52,6 +90,13 @@ describe('query builder', () => {
|
|
52
90
|
}
|
53
91
|
`)
|
54
92
|
|
93
|
+
expect(db.todos.select('id').asSql()).toMatchInlineSnapshot(`
|
94
|
+
{
|
95
|
+
"bindValues": [],
|
96
|
+
"query": "SELECT id FROM 'todos'",
|
97
|
+
}
|
98
|
+
`)
|
99
|
+
|
55
100
|
expect(db.todos.select('id', 'text').asSql()).toMatchInlineSnapshot(`
|
56
101
|
{
|
57
102
|
"bindValues": [],
|
@@ -60,6 +105,31 @@ describe('query builder', () => {
|
|
60
105
|
`)
|
61
106
|
})
|
62
107
|
|
108
|
+
it('should handle .first()', () => {
|
109
|
+
expect(db.todos.select('id', 'text').first().asSql()).toMatchInlineSnapshot(`
|
110
|
+
{
|
111
|
+
"bindValues": [
|
112
|
+
1,
|
113
|
+
],
|
114
|
+
"query": "SELECT id, text FROM 'todos' LIMIT ?",
|
115
|
+
}
|
116
|
+
`)
|
117
|
+
|
118
|
+
expect(
|
119
|
+
db.todos
|
120
|
+
.select('id', 'text')
|
121
|
+
.first({ fallback: () => undefined })
|
122
|
+
.asSql(),
|
123
|
+
).toMatchInlineSnapshot(`
|
124
|
+
{
|
125
|
+
"bindValues": [
|
126
|
+
1,
|
127
|
+
],
|
128
|
+
"query": "SELECT id, text FROM 'todos' LIMIT ?",
|
129
|
+
}
|
130
|
+
`)
|
131
|
+
})
|
132
|
+
|
63
133
|
it('should handle WHERE clauses', () => {
|
64
134
|
expect(db.todos.select('id', 'text').where('completed', true).asSql()).toMatchInlineSnapshot(`
|
65
135
|
{
|
@@ -147,6 +217,42 @@ describe('query builder', () => {
|
|
147
217
|
`)
|
148
218
|
})
|
149
219
|
|
220
|
+
it('should handle OFFSET and LIMIT clauses correctly', () => {
|
221
|
+
// Test with both offset and limit
|
222
|
+
expect(db.todos.select('id', 'text').where('completed', true).offset(5).limit(10).asSql()).toMatchInlineSnapshot(`
|
223
|
+
{
|
224
|
+
"bindValues": [
|
225
|
+
1,
|
226
|
+
5,
|
227
|
+
10,
|
228
|
+
],
|
229
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? OFFSET ? LIMIT ?",
|
230
|
+
}
|
231
|
+
`)
|
232
|
+
|
233
|
+
// Test with only offset
|
234
|
+
expect(db.todos.select('id', 'text').where('completed', true).offset(5).asSql()).toMatchInlineSnapshot(`
|
235
|
+
{
|
236
|
+
"bindValues": [
|
237
|
+
1,
|
238
|
+
5,
|
239
|
+
],
|
240
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? OFFSET ?",
|
241
|
+
}
|
242
|
+
`)
|
243
|
+
|
244
|
+
// Test with only limit
|
245
|
+
expect(db.todos.select('id', 'text').where('completed', true).limit(10).asSql()).toMatchInlineSnapshot(`
|
246
|
+
{
|
247
|
+
"bindValues": [
|
248
|
+
1,
|
249
|
+
10,
|
250
|
+
],
|
251
|
+
"query": "SELECT id, text FROM 'todos' WHERE completed = ? LIMIT ?",
|
252
|
+
}
|
253
|
+
`)
|
254
|
+
})
|
255
|
+
|
150
256
|
it('should handle COUNT queries', () => {
|
151
257
|
expect(db.todos.count().asSql()).toMatchInlineSnapshot(`
|
152
258
|
{
|
@@ -203,40 +309,92 @@ describe('query builder', () => {
|
|
203
309
|
})
|
204
310
|
})
|
205
311
|
|
206
|
-
describe('
|
207
|
-
|
208
|
-
|
312
|
+
// describe('getOrCreate queries', () => {
|
313
|
+
// it('should handle getOrCreate queries', () => {
|
314
|
+
// expect(db.UiState.getOrCreate('sessionid-1').asSql()).toMatchInlineSnapshot(`
|
315
|
+
// {
|
316
|
+
// "bindValues": [
|
317
|
+
// "sessionid-1",
|
318
|
+
// ],
|
319
|
+
// "query": "SELECT * FROM 'UiState' WHERE id = ?",
|
320
|
+
// }
|
321
|
+
// `)
|
322
|
+
// })
|
323
|
+
|
324
|
+
// it('should handle getOrCreate queries with default id', () => {
|
325
|
+
// expect(db.UiStateWithDefaultId.getOrCreate().asSql()).toMatchInlineSnapshot(`
|
326
|
+
// {
|
327
|
+
// "bindValues": [],
|
328
|
+
// "query": "SELECT * FROM 'UiState' WHERE id = ?",
|
329
|
+
// }
|
330
|
+
// `)
|
331
|
+
// })
|
332
|
+
// // it('should handle row queries with numbers', () => {
|
333
|
+
// // expect(db.todosWithIntId.getOrCreate(123, { insertValues: { status: 'active' } }).asSql()).toMatchInlineSnapshot(`
|
334
|
+
// // {
|
335
|
+
// // "bindValues": [
|
336
|
+
// // 123,
|
337
|
+
// // ],
|
338
|
+
// // "query": "SELECT * FROM 'todos_with_int_id' WHERE id = ?",
|
339
|
+
// // }
|
340
|
+
// // `)
|
341
|
+
// // })
|
342
|
+
// })
|
343
|
+
|
344
|
+
describe('write operations', () => {
|
345
|
+
it('should handle INSERT queries', () => {
|
346
|
+
expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toMatchInlineSnapshot(`
|
209
347
|
{
|
210
348
|
"bindValues": [
|
211
349
|
"123",
|
350
|
+
"Buy milk",
|
351
|
+
"active",
|
212
352
|
],
|
213
|
-
"query": "
|
353
|
+
"query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?)",
|
214
354
|
}
|
215
355
|
`)
|
216
356
|
})
|
217
357
|
|
218
|
-
it('should handle
|
219
|
-
expect(db.
|
358
|
+
it('should handle INSERT queries with undefined values', () => {
|
359
|
+
expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active', completed: undefined }).asSql())
|
360
|
+
.toMatchInlineSnapshot(`
|
220
361
|
{
|
221
362
|
"bindValues": [
|
222
|
-
123,
|
363
|
+
"123",
|
364
|
+
"Buy milk",
|
365
|
+
"active",
|
223
366
|
],
|
224
|
-
"query": "
|
367
|
+
"query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?)",
|
225
368
|
}
|
226
369
|
`)
|
227
370
|
})
|
228
|
-
})
|
229
371
|
|
230
|
-
|
231
|
-
it('should handle INSERT queries', () => {
|
232
|
-
expect(
|
372
|
+
// Test helped to catch a bindValues ordering bug
|
373
|
+
it('should handle INSERT queries (issue)', () => {
|
374
|
+
expect(
|
375
|
+
db.issue
|
376
|
+
.insert({
|
377
|
+
id: 1,
|
378
|
+
title: 'Revert the user profile page',
|
379
|
+
priority: 2,
|
380
|
+
created: new Date('2024-08-01T17:15:20.507Z'),
|
381
|
+
modified: new Date('2024-12-29T17:15:20.507Z'),
|
382
|
+
kanbanorder: 'a2',
|
383
|
+
creator: 'John Doe',
|
384
|
+
})
|
385
|
+
.asSql(),
|
386
|
+
).toMatchInlineSnapshot(`
|
233
387
|
{
|
234
388
|
"bindValues": [
|
235
|
-
|
236
|
-
"
|
237
|
-
|
389
|
+
1,
|
390
|
+
"Revert the user profile page",
|
391
|
+
2,
|
392
|
+
1722532520507,
|
393
|
+
1735492520507,
|
394
|
+
"a2",
|
395
|
+
"John Doe",
|
238
396
|
],
|
239
|
-
"query": "INSERT INTO '
|
397
|
+
"query": "INSERT INTO 'issue' (id, title, priority, created, modified, kanbanorder, creator) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
240
398
|
}
|
241
399
|
`)
|
242
400
|
})
|
@@ -251,6 +409,40 @@ describe('query builder', () => {
|
|
251
409
|
"query": "UPDATE 'todos' SET status = ? WHERE id = ?",
|
252
410
|
}
|
253
411
|
`)
|
412
|
+
|
413
|
+
// empty update set
|
414
|
+
expect(db.todos.update({}).where({ id: '123' }).asSql()).toMatchInlineSnapshot(`
|
415
|
+
{
|
416
|
+
"bindValues": [],
|
417
|
+
"query": "SELECT 1",
|
418
|
+
}
|
419
|
+
`)
|
420
|
+
})
|
421
|
+
|
422
|
+
it('should handle UPDATE queries with undefined values', () => {
|
423
|
+
expect(db.todos.update({ status: undefined, text: 'some text' }).where({ id: '123' }).asSql())
|
424
|
+
.toMatchInlineSnapshot(`
|
425
|
+
{
|
426
|
+
"bindValues": [
|
427
|
+
"some text",
|
428
|
+
"123",
|
429
|
+
],
|
430
|
+
"query": "UPDATE 'todos' SET text = ? WHERE id = ?",
|
431
|
+
}
|
432
|
+
`)
|
433
|
+
})
|
434
|
+
|
435
|
+
it('should handle UPDATE queries with undefined values (issue)', () => {
|
436
|
+
expect(db.issue.update({ priority: 2, creator: 'John Doe' }).where({ id: 1 }).asSql()).toMatchInlineSnapshot(`
|
437
|
+
{
|
438
|
+
"bindValues": [
|
439
|
+
2,
|
440
|
+
"John Doe",
|
441
|
+
1,
|
442
|
+
],
|
443
|
+
"query": "UPDATE 'issue' SET priority = ?, creator = ? WHERE id = ?",
|
444
|
+
}
|
445
|
+
`)
|
254
446
|
})
|
255
447
|
|
256
448
|
it('should handle DELETE queries', () => {
|
@@ -294,6 +486,36 @@ describe('query builder', () => {
|
|
294
486
|
"query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET text = ?, status = ?",
|
295
487
|
}
|
296
488
|
`)
|
489
|
+
|
490
|
+
expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace').asSql())
|
491
|
+
.toMatchInlineSnapshot(`
|
492
|
+
{
|
493
|
+
"bindValues": [
|
494
|
+
"123",
|
495
|
+
"Buy milk",
|
496
|
+
"active",
|
497
|
+
],
|
498
|
+
"query": "INSERT OR REPLACE INTO 'todos' (id, text, status) VALUES (?, ?, ?)",
|
499
|
+
}
|
500
|
+
`)
|
501
|
+
})
|
502
|
+
|
503
|
+
it('should handle ON CONFLICT with multiple columns', () => {
|
504
|
+
expect(
|
505
|
+
db.todos
|
506
|
+
.insert({ id: '123', text: 'Buy milk', status: 'active' })
|
507
|
+
.onConflict(['id', 'status'], 'ignore')
|
508
|
+
.asSql(),
|
509
|
+
).toMatchInlineSnapshot(`
|
510
|
+
{
|
511
|
+
"bindValues": [
|
512
|
+
"123",
|
513
|
+
"Buy milk",
|
514
|
+
"active",
|
515
|
+
],
|
516
|
+
"query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?) ON CONFLICT (id, status) DO NOTHING",
|
517
|
+
}
|
518
|
+
`)
|
297
519
|
})
|
298
520
|
|
299
521
|
it('should handle RETURNING clause', () => {
|
@@ -1,16 +1,15 @@
|
|
1
|
-
import { casesHandled } from '@livestore/utils'
|
1
|
+
import { casesHandled, shouldNeverHappen } from '@livestore/utils'
|
2
2
|
import { Match, Option, Predicate, Schema } from '@livestore/utils/effect'
|
3
3
|
|
4
|
-
import type {
|
5
|
-
import type { DbSchema } from '../schema/mod.js'
|
4
|
+
import type { State } from '../schema/mod.js'
|
6
5
|
import type { QueryBuilder, QueryBuilderAst } from './api.js'
|
7
|
-
import { QueryBuilderAstSymbol,
|
6
|
+
import { QueryBuilderAstSymbol, QueryBuilderTypeId } from './api.js'
|
8
7
|
import { astToSql } from './astToSql.js'
|
9
8
|
|
10
|
-
export const makeQueryBuilder = <TResult, TTableDef extends
|
9
|
+
export const makeQueryBuilder = <TResult, TTableDef extends State.SQLite.TableDefBase>(
|
11
10
|
tableDef: TTableDef,
|
12
11
|
ast: QueryBuilderAst = emptyAst(tableDef),
|
13
|
-
): QueryBuilder<TResult, TTableDef, never
|
12
|
+
): QueryBuilder<TResult, TTableDef, never> => {
|
14
13
|
const api = {
|
15
14
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
16
15
|
select() {
|
@@ -19,11 +18,12 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
19
18
|
// eslint-disable-next-line prefer-rest-params
|
20
19
|
const params = [...arguments]
|
21
20
|
|
22
|
-
|
23
|
-
|
21
|
+
// Pluck if there's only one column selected
|
22
|
+
if (params.length === 1) {
|
23
|
+
const [col] = params as any as [string]
|
24
24
|
return makeQueryBuilder(tableDef, {
|
25
25
|
...ast,
|
26
|
-
resultSchemaSingle:
|
26
|
+
resultSchemaSingle: ast.resultSchemaSingle.pipe(Schema.pluck(col)),
|
27
27
|
select: { columns: [col] },
|
28
28
|
})
|
29
29
|
}
|
@@ -148,51 +148,60 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
148
148
|
pickFirst: options?.fallback ? { fallback: options.fallback } : { fallback: () => undefined },
|
149
149
|
})
|
150
150
|
},
|
151
|
-
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
}
|
151
|
+
// // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
152
|
+
// getOrCreate() {
|
153
|
+
// if (tableDef.options.isClientDocumentTable === false) {
|
154
|
+
// return invalidQueryBuilder(`getOrCreate() is not allowed when table is not a client document table`)
|
155
|
+
// }
|
156
|
+
|
157
|
+
// // eslint-disable-next-line prefer-rest-params
|
158
|
+
// const params = [...arguments]
|
159
|
+
|
160
|
+
// let id: string | number
|
161
|
+
|
162
|
+
// // TODO refactor to handle default id
|
163
|
+
// id = params[0] as string | number
|
164
|
+
// if (id === undefined) {
|
165
|
+
// invalidQueryBuilder(`Id missing for row query on non-singleton table ${tableDef.sqliteDef.name}`)
|
166
|
+
// }
|
167
|
+
|
168
|
+
// // TODO validate all required columns are present and values are matching the schema
|
169
|
+
// const insertValues: Record<string, unknown> = params[1]?.insertValues ?? {}
|
170
|
+
|
171
|
+
// return makeQueryBuilder(tableDef, {
|
172
|
+
// _tag: 'RowQuery',
|
173
|
+
// id,
|
174
|
+
// tableDef,
|
175
|
+
// insertValues,
|
176
|
+
// }) as any
|
177
|
+
// },
|
177
178
|
insert: (values) => {
|
179
|
+
const filteredValues = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined))
|
180
|
+
|
178
181
|
return makeQueryBuilder(tableDef, {
|
179
182
|
_tag: 'InsertQuery',
|
180
183
|
tableDef,
|
181
|
-
values:
|
184
|
+
values: filteredValues,
|
182
185
|
onConflict: undefined,
|
183
186
|
returning: undefined,
|
184
187
|
resultSchema: Schema.Void,
|
185
188
|
}) as any
|
186
189
|
},
|
187
|
-
onConflict: (
|
190
|
+
onConflict: (
|
191
|
+
targetOrTargets: string | string[],
|
192
|
+
action: 'ignore' | 'replace' | 'update',
|
193
|
+
updateValues?: Record<string, unknown>,
|
194
|
+
) => {
|
195
|
+
const targets = Array.isArray(targetOrTargets) ? targetOrTargets : [targetOrTargets]
|
196
|
+
|
188
197
|
assertInsertQueryBuilderAst(ast)
|
189
198
|
|
190
199
|
const onConflict = Match.value(action).pipe(
|
191
|
-
Match.when('ignore', () => ({
|
192
|
-
Match.when('replace', () => ({
|
200
|
+
Match.when('ignore', () => ({ targets, action: { _tag: 'ignore' } }) satisfies QueryBuilderAst.OnConflict),
|
201
|
+
Match.when('replace', () => ({ targets, action: { _tag: 'replace' } }) satisfies QueryBuilderAst.OnConflict),
|
193
202
|
Match.when(
|
194
203
|
'update',
|
195
|
-
() => ({
|
204
|
+
() => ({ targets, action: { _tag: 'update', update: updateValues! } }) satisfies QueryBuilderAst.OnConflict,
|
196
205
|
),
|
197
206
|
Match.exhaustive,
|
198
207
|
)
|
@@ -209,15 +218,17 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
209
218
|
return makeQueryBuilder(tableDef, {
|
210
219
|
...ast,
|
211
220
|
returning: columns,
|
212
|
-
resultSchema: tableDef.
|
221
|
+
resultSchema: tableDef.rowSchema.pipe(Schema.pick(...columns), Schema.Array),
|
213
222
|
}) as any
|
214
223
|
},
|
215
224
|
|
216
225
|
update: (values) => {
|
226
|
+
const filteredValues = Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined))
|
227
|
+
|
217
228
|
return makeQueryBuilder(tableDef, {
|
218
229
|
_tag: 'UpdateQuery',
|
219
230
|
tableDef,
|
220
|
-
values:
|
231
|
+
values: filteredValues,
|
221
232
|
where: [],
|
222
233
|
returning: undefined,
|
223
234
|
resultSchema: Schema.Void,
|
@@ -233,11 +244,12 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
233
244
|
resultSchema: Schema.Void,
|
234
245
|
}) as any
|
235
246
|
},
|
236
|
-
} satisfies QueryBuilder.ApiFull<TResult, TTableDef, never
|
247
|
+
} satisfies QueryBuilder.ApiFull<TResult, TTableDef, never>
|
237
248
|
|
238
249
|
return {
|
239
|
-
[
|
250
|
+
[QueryBuilderTypeId]: QueryBuilderTypeId,
|
240
251
|
[QueryBuilderAstSymbol]: ast,
|
252
|
+
['ResultType']: 'only-for-type-inference' as TResult,
|
241
253
|
asSql: () => astToSql(ast),
|
242
254
|
toString: () => {
|
243
255
|
try {
|
@@ -251,7 +263,7 @@ export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBas
|
|
251
263
|
} satisfies QueryBuilder<TResult, TTableDef>
|
252
264
|
}
|
253
265
|
|
254
|
-
const emptyAst = (tableDef:
|
266
|
+
const emptyAst = (tableDef: State.SQLite.TableDefBase): QueryBuilderAst.SelectQuery => ({
|
255
267
|
_tag: 'SelectQuery',
|
256
268
|
columns: [],
|
257
269
|
pickFirst: false,
|
@@ -261,35 +273,35 @@ const emptyAst = (tableDef: DbSchema.TableDefBase): QueryBuilderAst.SelectQuery
|
|
261
273
|
limit: Option.none(),
|
262
274
|
tableDef,
|
263
275
|
where: [],
|
264
|
-
resultSchemaSingle: tableDef.
|
276
|
+
resultSchemaSingle: tableDef.rowSchema,
|
265
277
|
})
|
266
278
|
|
267
279
|
// Helper functions
|
268
280
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
269
281
|
function assertSelectQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.SelectQuery {
|
270
282
|
if (ast._tag !== 'SelectQuery') {
|
271
|
-
|
283
|
+
return shouldNeverHappen('Expected SelectQuery but got ' + ast._tag)
|
272
284
|
}
|
273
285
|
}
|
274
286
|
|
275
287
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
276
288
|
function assertInsertQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.InsertQuery {
|
277
289
|
if (ast._tag !== 'InsertQuery') {
|
278
|
-
|
290
|
+
return shouldNeverHappen('Expected InsertQuery but got ' + ast._tag)
|
279
291
|
}
|
280
292
|
}
|
281
293
|
|
282
294
|
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions
|
283
295
|
function assertWriteQueryBuilderAst(ast: QueryBuilderAst): asserts ast is QueryBuilderAst.WriteQuery {
|
284
296
|
if (ast._tag !== 'InsertQuery' && ast._tag !== 'UpdateQuery' && ast._tag !== 'DeleteQuery') {
|
285
|
-
|
297
|
+
return shouldNeverHappen('Expected WriteQuery but got ' + ast._tag)
|
286
298
|
}
|
287
299
|
}
|
288
300
|
|
289
301
|
const isRowQuery = (ast: QueryBuilderAst): ast is QueryBuilderAst.RowQuery => ast._tag === 'RowQuery'
|
290
302
|
|
291
303
|
export const invalidQueryBuilder = (msg?: string) => {
|
292
|
-
|
304
|
+
return shouldNeverHappen('Invalid query builder' + (msg ? `: ${msg}` : ''))
|
293
305
|
}
|
294
306
|
|
295
307
|
export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<any> => {
|
@@ -312,18 +324,22 @@ export const getResultSchema = (qb: QueryBuilder<any, any, any>): Schema.Schema<
|
|
312
324
|
// For write operations with RETURNING clause, we need to return the appropriate schema
|
313
325
|
if (queryAst.returning && queryAst.returning.length > 0) {
|
314
326
|
// Create a schema for the returned columns
|
315
|
-
return queryAst.tableDef.
|
327
|
+
return queryAst.tableDef.rowSchema.pipe(Schema.pick(...queryAst.returning), Schema.Array)
|
316
328
|
}
|
317
329
|
|
318
330
|
// For write operations without RETURNING, the result is the number of affected rows
|
319
331
|
return Schema.Number
|
320
332
|
}
|
333
|
+
case 'RowQuery': {
|
334
|
+
return queryAst.tableDef.rowSchema.pipe(
|
335
|
+
Schema.pluck('value'),
|
336
|
+
Schema.annotations({ title: `${queryAst.tableDef.sqliteDef.name}.value` }),
|
337
|
+
Schema.Array,
|
338
|
+
Schema.headOrElse(),
|
339
|
+
)
|
340
|
+
}
|
321
341
|
default: {
|
322
|
-
|
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
|
-
}
|
342
|
+
casesHandled(queryAst)
|
327
343
|
}
|
328
344
|
}
|
329
345
|
}
|