@livestore/common 0.3.0-dev.28 → 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.
Files changed (277) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/__tests__/fixture.d.ts +83 -221
  3. package/dist/__tests__/fixture.d.ts.map +1 -1
  4. package/dist/__tests__/fixture.js +33 -11
  5. package/dist/__tests__/fixture.js.map +1 -1
  6. package/dist/adapter-types.d.ts +22 -15
  7. package/dist/adapter-types.d.ts.map +1 -1
  8. package/dist/adapter-types.js +15 -2
  9. package/dist/adapter-types.js.map +1 -1
  10. package/dist/bounded-collections.d.ts +1 -1
  11. package/dist/bounded-collections.d.ts.map +1 -1
  12. package/dist/debug-info.d.ts.map +1 -1
  13. package/dist/debug-info.js +1 -0
  14. package/dist/debug-info.js.map +1 -1
  15. package/dist/devtools/devtools-messages-client-session.d.ts +21 -21
  16. package/dist/devtools/devtools-messages-common.d.ts +6 -6
  17. package/dist/devtools/devtools-messages-leader.d.ts +45 -45
  18. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  19. package/dist/devtools/devtools-messages-leader.js +11 -11
  20. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  21. package/dist/index.d.ts +2 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/leader-thread/LeaderSyncProcessor.d.ts +10 -10
  26. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  27. package/dist/leader-thread/LeaderSyncProcessor.js +63 -65
  28. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  29. package/dist/leader-thread/{apply-mutation.d.ts → apply-event.d.ts} +7 -7
  30. package/dist/leader-thread/apply-event.d.ts.map +1 -0
  31. package/dist/leader-thread/apply-event.js +103 -0
  32. package/dist/leader-thread/apply-event.js.map +1 -0
  33. package/dist/leader-thread/eventlog.d.ts +27 -0
  34. package/dist/leader-thread/eventlog.d.ts.map +1 -0
  35. package/dist/leader-thread/eventlog.js +123 -0
  36. package/dist/leader-thread/eventlog.js.map +1 -0
  37. package/dist/leader-thread/leader-worker-devtools.js +18 -18
  38. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  39. package/dist/leader-thread/make-leader-thread-layer.d.ts +2 -2
  40. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  41. package/dist/leader-thread/make-leader-thread-layer.js +16 -16
  42. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  43. package/dist/leader-thread/mod.d.ts +1 -1
  44. package/dist/leader-thread/mod.d.ts.map +1 -1
  45. package/dist/leader-thread/mod.js +1 -1
  46. package/dist/leader-thread/mod.js.map +1 -1
  47. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  48. package/dist/leader-thread/recreate-db.js +6 -8
  49. package/dist/leader-thread/recreate-db.js.map +1 -1
  50. package/dist/leader-thread/types.d.ts +11 -11
  51. package/dist/leader-thread/types.d.ts.map +1 -1
  52. package/dist/materializer-helper.d.ts +23 -0
  53. package/dist/materializer-helper.d.ts.map +1 -0
  54. package/dist/materializer-helper.js +70 -0
  55. package/dist/materializer-helper.js.map +1 -0
  56. package/dist/query-builder/api.d.ts +58 -53
  57. package/dist/query-builder/api.d.ts.map +1 -1
  58. package/dist/query-builder/api.js +3 -5
  59. package/dist/query-builder/api.js.map +1 -1
  60. package/dist/query-builder/astToSql.d.ts.map +1 -1
  61. package/dist/query-builder/astToSql.js +59 -37
  62. package/dist/query-builder/astToSql.js.map +1 -1
  63. package/dist/query-builder/impl.d.ts +2 -3
  64. package/dist/query-builder/impl.d.ts.map +1 -1
  65. package/dist/query-builder/impl.js +48 -46
  66. package/dist/query-builder/impl.js.map +1 -1
  67. package/dist/query-builder/impl.test.d.ts +86 -1
  68. package/dist/query-builder/impl.test.d.ts.map +1 -1
  69. package/dist/query-builder/impl.test.js +244 -36
  70. package/dist/query-builder/impl.test.js.map +1 -1
  71. package/dist/rehydrate-from-eventlog.d.ts +14 -0
  72. package/dist/rehydrate-from-eventlog.d.ts.map +1 -0
  73. package/dist/{rehydrate-from-mutationlog.js → rehydrate-from-eventlog.js} +25 -26
  74. package/dist/rehydrate-from-eventlog.js.map +1 -0
  75. package/dist/schema/EventDef.d.ts +136 -0
  76. package/dist/schema/EventDef.d.ts.map +1 -0
  77. package/dist/schema/EventDef.js +58 -0
  78. package/dist/schema/EventDef.js.map +1 -0
  79. package/dist/schema/EventId.d.ts +2 -2
  80. package/dist/schema/EventId.d.ts.map +1 -1
  81. package/dist/schema/EventId.js +3 -2
  82. package/dist/schema/EventId.js.map +1 -1
  83. package/dist/schema/{MutationEvent.d.ts → LiveStoreEvent.d.ts} +56 -56
  84. package/dist/schema/LiveStoreEvent.d.ts.map +1 -0
  85. package/dist/schema/{MutationEvent.js → LiveStoreEvent.js} +24 -24
  86. package/dist/schema/LiveStoreEvent.js.map +1 -0
  87. package/dist/schema/client-document-def.d.ts +223 -0
  88. package/dist/schema/client-document-def.d.ts.map +1 -0
  89. package/dist/schema/client-document-def.js +170 -0
  90. package/dist/schema/client-document-def.js.map +1 -0
  91. package/dist/schema/client-document-def.test.d.ts +2 -0
  92. package/dist/schema/client-document-def.test.d.ts.map +1 -0
  93. package/dist/schema/client-document-def.test.js +201 -0
  94. package/dist/schema/client-document-def.test.js.map +1 -0
  95. package/dist/schema/db-schema/dsl/mod.d.ts.map +1 -1
  96. package/dist/schema/events.d.ts +2 -0
  97. package/dist/schema/events.d.ts.map +1 -0
  98. package/dist/schema/events.js +2 -0
  99. package/dist/schema/events.js.map +1 -0
  100. package/dist/schema/mod.d.ts +4 -3
  101. package/dist/schema/mod.d.ts.map +1 -1
  102. package/dist/schema/mod.js +4 -3
  103. package/dist/schema/mod.js.map +1 -1
  104. package/dist/schema/schema.d.ts +27 -23
  105. package/dist/schema/schema.d.ts.map +1 -1
  106. package/dist/schema/schema.js +45 -43
  107. package/dist/schema/schema.js.map +1 -1
  108. package/dist/schema/sqlite-state.d.ts +12 -0
  109. package/dist/schema/sqlite-state.d.ts.map +1 -0
  110. package/dist/schema/sqlite-state.js +36 -0
  111. package/dist/schema/sqlite-state.js.map +1 -0
  112. package/dist/schema/system-tables.d.ts +67 -98
  113. package/dist/schema/system-tables.d.ts.map +1 -1
  114. package/dist/schema/system-tables.js +62 -48
  115. package/dist/schema/system-tables.js.map +1 -1
  116. package/dist/schema/table-def.d.ts +26 -96
  117. package/dist/schema/table-def.d.ts.map +1 -1
  118. package/dist/schema/table-def.js +16 -64
  119. package/dist/schema/table-def.js.map +1 -1
  120. package/dist/schema/view.d.ts +3 -0
  121. package/dist/schema/view.d.ts.map +1 -0
  122. package/dist/schema/view.js +3 -0
  123. package/dist/schema/view.js.map +1 -0
  124. package/dist/schema-management/common.d.ts +4 -4
  125. package/dist/schema-management/common.d.ts.map +1 -1
  126. package/dist/schema-management/migrations.d.ts.map +1 -1
  127. package/dist/schema-management/migrations.js +6 -6
  128. package/dist/schema-management/migrations.js.map +1 -1
  129. package/dist/schema-management/validate-mutation-defs.d.ts +3 -3
  130. package/dist/schema-management/validate-mutation-defs.d.ts.map +1 -1
  131. package/dist/schema-management/validate-mutation-defs.js +17 -17
  132. package/dist/schema-management/validate-mutation-defs.js.map +1 -1
  133. package/dist/sync/ClientSessionSyncProcessor.d.ts +7 -7
  134. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  135. package/dist/sync/ClientSessionSyncProcessor.js +31 -30
  136. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  137. package/dist/sync/next/facts.d.ts +19 -19
  138. package/dist/sync/next/facts.d.ts.map +1 -1
  139. package/dist/sync/next/facts.js +2 -2
  140. package/dist/sync/next/facts.js.map +1 -1
  141. package/dist/sync/next/history-dag-common.d.ts +3 -3
  142. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  143. package/dist/sync/next/history-dag-common.js +1 -1
  144. package/dist/sync/next/history-dag-common.js.map +1 -1
  145. package/dist/sync/next/history-dag.js +1 -1
  146. package/dist/sync/next/history-dag.js.map +1 -1
  147. package/dist/sync/next/rebase-events.d.ts +7 -7
  148. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  149. package/dist/sync/next/rebase-events.js +5 -5
  150. package/dist/sync/next/rebase-events.js.map +1 -1
  151. package/dist/sync/next/test/compact-events.calculator.test.js +38 -33
  152. package/dist/sync/next/test/compact-events.calculator.test.js.map +1 -1
  153. package/dist/sync/next/test/compact-events.test.js +71 -71
  154. package/dist/sync/next/test/compact-events.test.js.map +1 -1
  155. package/dist/sync/next/test/{mutation-fixtures.d.ts → event-fixtures.d.ts} +29 -29
  156. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -0
  157. package/dist/sync/next/test/{mutation-fixtures.js → event-fixtures.js} +60 -25
  158. package/dist/sync/next/test/event-fixtures.js.map +1 -0
  159. package/dist/sync/next/test/mod.d.ts +1 -1
  160. package/dist/sync/next/test/mod.d.ts.map +1 -1
  161. package/dist/sync/next/test/mod.js +1 -1
  162. package/dist/sync/next/test/mod.js.map +1 -1
  163. package/dist/sync/sync.d.ts +3 -3
  164. package/dist/sync/sync.d.ts.map +1 -1
  165. package/dist/sync/syncstate.d.ts +32 -32
  166. package/dist/sync/syncstate.d.ts.map +1 -1
  167. package/dist/sync/syncstate.js +10 -10
  168. package/dist/sync/syncstate.js.map +1 -1
  169. package/dist/sync/syncstate.test.js +5 -5
  170. package/dist/sync/syncstate.test.js.map +1 -1
  171. package/dist/sync/validate-push-payload.d.ts +2 -2
  172. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  173. package/dist/sync/validate-push-payload.js.map +1 -1
  174. package/dist/version.d.ts +1 -1
  175. package/dist/version.js +1 -1
  176. package/package.json +3 -3
  177. package/src/__tests__/fixture.ts +36 -15
  178. package/src/adapter-types.ts +23 -16
  179. package/src/debug-info.ts +1 -0
  180. package/src/devtools/devtools-messages-leader.ts +13 -13
  181. package/src/index.ts +2 -5
  182. package/src/leader-thread/LeaderSyncProcessor.ts +81 -91
  183. package/src/leader-thread/{apply-mutation.ts → apply-event.ts} +50 -74
  184. package/src/leader-thread/eventlog.ts +199 -0
  185. package/src/leader-thread/leader-worker-devtools.ts +18 -18
  186. package/src/leader-thread/make-leader-thread-layer.ts +18 -18
  187. package/src/leader-thread/mod.ts +1 -1
  188. package/src/leader-thread/recreate-db.ts +6 -9
  189. package/src/leader-thread/types.ts +12 -12
  190. package/src/materializer-helper.ts +110 -0
  191. package/src/query-builder/api.ts +79 -105
  192. package/src/query-builder/astToSql.ts +68 -39
  193. package/src/query-builder/impl.test.ts +264 -42
  194. package/src/query-builder/impl.ts +72 -56
  195. package/src/{rehydrate-from-mutationlog.ts → rehydrate-from-eventlog.ts} +33 -40
  196. package/src/schema/EventDef.ts +216 -0
  197. package/src/schema/EventId.ts +5 -3
  198. package/src/schema/{MutationEvent.ts → LiveStoreEvent.ts} +67 -68
  199. package/src/schema/client-document-def.test.ts +239 -0
  200. package/src/schema/client-document-def.ts +444 -0
  201. package/src/schema/db-schema/dsl/mod.ts +0 -1
  202. package/src/schema/events.ts +1 -0
  203. package/src/schema/mod.ts +4 -3
  204. package/src/schema/schema.ts +79 -69
  205. package/src/schema/sqlite-state.ts +62 -0
  206. package/src/schema/system-tables.ts +42 -53
  207. package/src/schema/table-def.ts +53 -209
  208. package/src/schema/view.ts +2 -0
  209. package/src/schema-management/common.ts +4 -4
  210. package/src/schema-management/migrations.ts +8 -9
  211. package/src/schema-management/validate-mutation-defs.ts +22 -24
  212. package/src/sync/ClientSessionSyncProcessor.ts +37 -36
  213. package/src/sync/next/facts.ts +31 -32
  214. package/src/sync/next/history-dag-common.ts +4 -4
  215. package/src/sync/next/history-dag.ts +1 -1
  216. package/src/sync/next/rebase-events.ts +13 -13
  217. package/src/sync/next/test/compact-events.calculator.test.ts +45 -45
  218. package/src/sync/next/test/compact-events.test.ts +73 -73
  219. package/src/sync/next/test/event-fixtures.ts +219 -0
  220. package/src/sync/next/test/mod.ts +1 -1
  221. package/src/sync/sync.ts +3 -3
  222. package/src/sync/syncstate.test.ts +8 -8
  223. package/src/sync/syncstate.ts +19 -19
  224. package/src/sync/validate-push-payload.ts +2 -2
  225. package/src/version.ts +1 -1
  226. package/tmp/pack.tgz +0 -0
  227. package/tsconfig.json +1 -0
  228. package/dist/derived-mutations.d.ts +0 -109
  229. package/dist/derived-mutations.d.ts.map +0 -1
  230. package/dist/derived-mutations.js +0 -54
  231. package/dist/derived-mutations.js.map +0 -1
  232. package/dist/derived-mutations.test.d.ts +0 -2
  233. package/dist/derived-mutations.test.d.ts.map +0 -1
  234. package/dist/derived-mutations.test.js +0 -93
  235. package/dist/derived-mutations.test.js.map +0 -1
  236. package/dist/init-singleton-tables.d.ts +0 -4
  237. package/dist/init-singleton-tables.d.ts.map +0 -1
  238. package/dist/init-singleton-tables.js +0 -16
  239. package/dist/init-singleton-tables.js.map +0 -1
  240. package/dist/leader-thread/apply-mutation.d.ts.map +0 -1
  241. package/dist/leader-thread/apply-mutation.js +0 -122
  242. package/dist/leader-thread/apply-mutation.js.map +0 -1
  243. package/dist/leader-thread/mutationlog.d.ts +0 -27
  244. package/dist/leader-thread/mutationlog.d.ts.map +0 -1
  245. package/dist/leader-thread/mutationlog.js +0 -124
  246. package/dist/leader-thread/mutationlog.js.map +0 -1
  247. package/dist/leader-thread/pull-queue-set.d.ts +0 -7
  248. package/dist/leader-thread/pull-queue-set.d.ts.map +0 -1
  249. package/dist/leader-thread/pull-queue-set.js +0 -38
  250. package/dist/leader-thread/pull-queue-set.js.map +0 -1
  251. package/dist/mutation.d.ts +0 -20
  252. package/dist/mutation.d.ts.map +0 -1
  253. package/dist/mutation.js +0 -68
  254. package/dist/mutation.js.map +0 -1
  255. package/dist/query-info.d.ts +0 -41
  256. package/dist/query-info.d.ts.map +0 -1
  257. package/dist/query-info.js +0 -7
  258. package/dist/query-info.js.map +0 -1
  259. package/dist/rehydrate-from-mutationlog.d.ts +0 -15
  260. package/dist/rehydrate-from-mutationlog.d.ts.map +0 -1
  261. package/dist/rehydrate-from-mutationlog.js.map +0 -1
  262. package/dist/schema/MutationEvent.d.ts.map +0 -1
  263. package/dist/schema/MutationEvent.js.map +0 -1
  264. package/dist/schema/mutations.d.ts +0 -115
  265. package/dist/schema/mutations.d.ts.map +0 -1
  266. package/dist/schema/mutations.js +0 -42
  267. package/dist/schema/mutations.js.map +0 -1
  268. package/dist/sync/next/test/mutation-fixtures.d.ts.map +0 -1
  269. package/dist/sync/next/test/mutation-fixtures.js.map +0 -1
  270. package/src/derived-mutations.test.ts +0 -101
  271. package/src/derived-mutations.ts +0 -170
  272. package/src/init-singleton-tables.ts +0 -24
  273. package/src/leader-thread/mutationlog.ts +0 -202
  274. package/src/mutation.ts +0 -108
  275. package/src/query-info.ts +0 -83
  276. package/src/schema/mutations.ts +0 -193
  277. 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 { DbSchema } from '../schema/mod.js'
4
+ import { State } from '../schema/mod.js'
5
5
  import { getResultSchema } from './impl.js'
6
6
 
7
- const todos = DbSchema.table(
8
- 'todos',
9
- {
10
- id: DbSchema.text({ primaryKey: true }),
11
- text: DbSchema.text({ default: '', nullable: false }),
12
- completed: DbSchema.boolean({ default: false, nullable: false }),
13
- status: DbSchema.text({ schema: Schema.Literal('active', 'completed') }),
14
- deletedAt: DbSchema.datetime({ nullable: true }),
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(DbSchema.asInteger),
16
+ // other: Schema.Number.pipe(State.SQLite.asInteger),
17
17
  },
18
- { deriveMutations: true },
19
- )
20
-
21
- const todosWithIntId = DbSchema.table(
22
- 'todos_with_int_id',
23
- {
24
- id: DbSchema.integer({ primaryKey: true }),
25
- text: DbSchema.text({ default: '', nullable: false }),
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
- { deriveMutations: true },
29
- )
27
+ })
30
28
 
31
- const comments = DbSchema.table('comments', {
32
- id: DbSchema.text({ primaryKey: true }),
33
- text: DbSchema.text({ default: '', nullable: false }),
34
- todoId: DbSchema.text({}),
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 db = { todos: todos.query, todosWithIntId: todosWithIntId.query, comments: comments.query }
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('row queries', () => {
207
- it('should handle row queries', () => {
208
- expect(db.todos.row('123', { insertValues: { status: 'completed' } }).asSql()).toMatchInlineSnapshot(`
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": "SELECT * FROM 'todos' WHERE id = ?",
353
+ "query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?)",
214
354
  }
215
355
  `)
216
356
  })
217
357
 
218
- it('should handle row queries with numbers', () => {
219
- expect(db.todosWithIntId.row(123, { insertValues: { status: 'active' } }).asSql()).toMatchInlineSnapshot(`
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": "SELECT * FROM 'todos_with_int_id' WHERE id = ?",
367
+ "query": "INSERT INTO 'todos' (id, text, status) VALUES (?, ?, ?)",
225
368
  }
226
369
  `)
227
370
  })
228
- })
229
371
 
230
- describe('write operations', () => {
231
- it('should handle INSERT queries', () => {
232
- expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toMatchInlineSnapshot(`
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
- "123",
236
- "Buy milk",
237
- "active",
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 'todos' (id, text, status) VALUES (?, ?, ?)",
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 { QueryInfo } from '../query-info.js'
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, TypeId } from './api.js'
6
+ import { QueryBuilderAstSymbol, QueryBuilderTypeId } from './api.js'
8
7
  import { astToSql } from './astToSql.js'
9
8
 
10
- export const makeQueryBuilder = <TResult, TTableDef extends DbSchema.TableDefBase>(
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, QueryInfo.None> => {
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
- if (params.length === 2 && typeof params[0] === 'string' && typeof params[1] === 'object') {
23
- const [col, options] = params as any as [string, { pluck: boolean }]
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: options.pluck ? ast.resultSchemaSingle.pipe(Schema.pluck(col)) : ast.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
- row() {
153
- // eslint-disable-next-line prefer-rest-params
154
- const params = [...arguments]
155
-
156
- let id: string | number
157
-
158
- if (tableDef.options.isSingleton) {
159
- id = tableDef.sqliteDef.columns.id!.default.pipe(Option.getOrThrow)
160
- } else {
161
- id = params[0] as string | number
162
- if (id === undefined) {
163
- invalidQueryBuilder(`Id missing for row query on non-singleton table ${tableDef.sqliteDef.name}`)
164
- }
165
- }
166
-
167
- // TODO validate all required columns are present and values are matching the schema
168
- const insertValues: Record<string, unknown> = params[1]?.insertValues ?? {}
169
-
170
- return makeQueryBuilder(tableDef, {
171
- _tag: 'RowQuery',
172
- id,
173
- tableDef,
174
- insertValues,
175
- }) as any
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: values as any,
184
+ values: filteredValues,
182
185
  onConflict: undefined,
183
186
  returning: undefined,
184
187
  resultSchema: Schema.Void,
185
188
  }) as any
186
189
  },
187
- onConflict: (target: string, action: 'ignore' | 'replace' | 'update', updateValues?: Record<string, unknown>) => {
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', () => ({ target, action: { _tag: 'ignore' } }) satisfies QueryBuilderAst.OnConflict),
192
- Match.when('replace', () => ({ target, action: { _tag: 'replace' } }) satisfies QueryBuilderAst.OnConflict),
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
- () => ({ target, action: { _tag: 'update', update: updateValues! } }) satisfies QueryBuilderAst.OnConflict,
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.schema.pipe(Schema.pick(...columns), Schema.Array),
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: values as any,
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, QueryInfo.None>
247
+ } satisfies QueryBuilder.ApiFull<TResult, TTableDef, never>
237
248
 
238
249
  return {
239
- [TypeId]: TypeId,
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: DbSchema.TableDefBase): QueryBuilderAst.SelectQuery => ({
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.schema,
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
- throw new Error('Expected SelectQuery but got ' + ast._tag)
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
- throw new Error('Expected InsertQuery but got ' + ast._tag)
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
- throw new Error('Expected WriteQuery but got ' + ast._tag)
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
- throw new Error('Invalid query builder' + (msg ? `: ${msg}` : ''))
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.schema.pipe(Schema.pick(...queryAst.returning), Schema.Array)
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
- 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
- }
342
+ casesHandled(queryAst)
327
343
  }
328
344
  }
329
345
  }