@livestore/common 0.4.0 → 0.5.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (421) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts +1 -1
  3. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  4. package/dist/MaterializationJournal.d.ts +44 -0
  5. package/dist/MaterializationJournal.d.ts.map +1 -0
  6. package/dist/MaterializationJournal.js +112 -0
  7. package/dist/MaterializationJournal.js.map +1 -0
  8. package/dist/StateHead.d.ts +26 -0
  9. package/dist/StateHead.d.ts.map +1 -0
  10. package/dist/StateHead.js +50 -0
  11. package/dist/StateHead.js.map +1 -0
  12. package/dist/WorkerTransportError.d.ts +3 -3
  13. package/dist/WorkerTransportError.d.ts.map +1 -1
  14. package/dist/WorkerTransportError.js +3 -3
  15. package/dist/WorkerTransportError.js.map +1 -1
  16. package/dist/__tests__/fixture.d.ts +1 -1
  17. package/dist/__tests__/fixture.d.ts.map +1 -1
  18. package/dist/__tests__/fixture.js +3 -3
  19. package/dist/__tests__/fixture.js.map +1 -1
  20. package/dist/adapter-types.d.ts +29 -57
  21. package/dist/adapter-types.d.ts.map +1 -1
  22. package/dist/adapter-types.js +20 -37
  23. package/dist/adapter-types.js.map +1 -1
  24. package/dist/debug-info.d.ts +56 -56
  25. package/dist/debug-info.d.ts.map +1 -1
  26. package/dist/debug-info.js +28 -41
  27. package/dist/debug-info.js.map +1 -1
  28. package/dist/defs.d.ts +9 -9
  29. package/dist/defs.js +2 -2
  30. package/dist/devtools/devtools-compatibility.test.js +1 -0
  31. package/dist/devtools/devtools-compatibility.test.js.map +1 -1
  32. package/dist/devtools/devtools-messages-client-session.d.ts +520 -332
  33. package/dist/devtools/devtools-messages-client-session.d.ts.map +1 -1
  34. package/dist/devtools/devtools-messages-client-session.js +76 -74
  35. package/dist/devtools/devtools-messages-client-session.js.map +1 -1
  36. package/dist/devtools/devtools-messages-common.d.ts +27 -27
  37. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  38. package/dist/devtools/devtools-messages-common.js +5 -5
  39. package/dist/devtools/devtools-messages-common.js.map +1 -1
  40. package/dist/devtools/devtools-messages-leader.d.ts +405 -343
  41. package/dist/devtools/devtools-messages-leader.d.ts.map +1 -1
  42. package/dist/devtools/devtools-messages-leader.js +101 -89
  43. package/dist/devtools/devtools-messages-leader.js.map +1 -1
  44. package/dist/devtools/devtools-sessioninfo.d.ts +18 -19
  45. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
  46. package/dist/devtools/devtools-sessioninfo.js +8 -5
  47. package/dist/devtools/devtools-sessioninfo.js.map +1 -1
  48. package/dist/devtools/mod.d.ts +4 -5
  49. package/dist/devtools/mod.d.ts.map +1 -1
  50. package/dist/devtools/mod.js +9 -5
  51. package/dist/devtools/mod.js.map +1 -1
  52. package/dist/errors.d.ts +45 -69
  53. package/dist/errors.d.ts.map +1 -1
  54. package/dist/errors.js +18 -16
  55. package/dist/errors.js.map +1 -1
  56. package/dist/index.d.ts +4 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +4 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/leader-thread/LeaderSyncProcessor.d.ts +66 -18
  61. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  62. package/dist/leader-thread/LeaderSyncProcessor.js +519 -461
  63. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  64. package/dist/leader-thread/RejectedPushError.d.ts +67 -36
  65. package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
  66. package/dist/leader-thread/RejectedPushError.js +24 -5
  67. package/dist/leader-thread/RejectedPushError.js.map +1 -1
  68. package/dist/leader-thread/connection.d.ts +1 -0
  69. package/dist/leader-thread/connection.d.ts.map +1 -1
  70. package/dist/leader-thread/connection.js +9 -8
  71. package/dist/leader-thread/connection.js.map +1 -1
  72. package/dist/leader-thread/eventlog.d.ts +4 -4
  73. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  74. package/dist/leader-thread/eventlog.js +6 -6
  75. package/dist/leader-thread/eventlog.js.map +1 -1
  76. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  77. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  78. package/dist/leader-thread/leader-worker-devtools.js +33 -17
  79. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  80. package/dist/leader-thread/make-leader-thread-layer.d.ts +9 -5
  81. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  82. package/dist/leader-thread/make-leader-thread-layer.js +25 -35
  83. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  84. package/dist/leader-thread/make-leader-thread-layer.test.js +7 -7
  85. package/dist/leader-thread/make-leader-thread-layer.test.js.map +1 -1
  86. package/dist/leader-thread/materialize-event.d.ts +2 -1
  87. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  88. package/dist/leader-thread/materialize-event.js +4 -0
  89. package/dist/leader-thread/materialize-event.js.map +1 -1
  90. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  91. package/dist/leader-thread/recreate-db.js +18 -39
  92. package/dist/leader-thread/recreate-db.js.map +1 -1
  93. package/dist/leader-thread/shutdown-channel.d.ts +2 -6
  94. package/dist/leader-thread/shutdown-channel.d.ts.map +1 -1
  95. package/dist/leader-thread/shutdown-channel.js +2 -3
  96. package/dist/leader-thread/shutdown-channel.js.map +1 -1
  97. package/dist/leader-thread/stream-events.d.ts +4 -7
  98. package/dist/leader-thread/stream-events.d.ts.map +1 -1
  99. package/dist/leader-thread/stream-events.js +13 -16
  100. package/dist/leader-thread/stream-events.js.map +1 -1
  101. package/dist/leader-thread/types.d.ts +32 -69
  102. package/dist/leader-thread/types.d.ts.map +1 -1
  103. package/dist/leader-thread/types.js +5 -5
  104. package/dist/leader-thread/types.js.map +1 -1
  105. package/dist/logging.d.ts +5 -30
  106. package/dist/logging.d.ts.map +1 -1
  107. package/dist/logging.js +1 -32
  108. package/dist/logging.js.map +1 -1
  109. package/dist/make-client-session.d.ts +1 -2
  110. package/dist/make-client-session.d.ts.map +1 -1
  111. package/dist/make-client-session.js +3 -3
  112. package/dist/make-client-session.js.map +1 -1
  113. package/dist/materializer-helper.d.ts +0 -1
  114. package/dist/materializer-helper.d.ts.map +1 -1
  115. package/dist/materializer-helper.js +7 -35
  116. package/dist/materializer-helper.js.map +1 -1
  117. package/dist/otel.d.ts +2 -3
  118. package/dist/otel.d.ts.map +1 -1
  119. package/dist/otel.js +2 -7
  120. package/dist/otel.js.map +1 -1
  121. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  122. package/dist/rematerialize-from-eventlog.js +28 -26
  123. package/dist/rematerialize-from-eventlog.js.map +1 -1
  124. package/dist/schema/EventDef/define.d.ts +4 -4
  125. package/dist/schema/EventDef/define.d.ts.map +1 -1
  126. package/dist/schema/EventDef/define.js +5 -5
  127. package/dist/schema/EventDef/define.js.map +1 -1
  128. package/dist/schema/EventDef/define.test.d.ts +2 -0
  129. package/dist/schema/EventDef/define.test.d.ts.map +1 -0
  130. package/dist/schema/EventDef/define.test.js +23 -0
  131. package/dist/schema/EventDef/define.test.js.map +1 -0
  132. package/dist/schema/EventDef/deprecated.d.ts +8 -13
  133. package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
  134. package/dist/schema/EventDef/deprecated.js +12 -13
  135. package/dist/schema/EventDef/deprecated.js.map +1 -1
  136. package/dist/schema/EventDef/deprecated.test.js +1 -1
  137. package/dist/schema/EventDef/deprecated.test.js.map +1 -1
  138. package/dist/schema/EventDef/event-def.d.ts +1 -1
  139. package/dist/schema/EventDef/event-def.d.ts.map +1 -1
  140. package/dist/schema/EventSequenceNumber/client.d.ts +12 -12
  141. package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
  142. package/dist/schema/EventSequenceNumber/client.js +8 -9
  143. package/dist/schema/EventSequenceNumber/client.js.map +1 -1
  144. package/dist/schema/EventSequenceNumber/global.d.ts +2 -2
  145. package/dist/schema/EventSequenceNumber/global.d.ts.map +1 -1
  146. package/dist/schema/EventSequenceNumber/global.js +1 -1
  147. package/dist/schema/EventSequenceNumber/global.js.map +1 -1
  148. package/dist/schema/EventSequenceNumber.test.js +5 -5
  149. package/dist/schema/EventSequenceNumber.test.js.map +1 -1
  150. package/dist/schema/LiveStoreEvent/client.d.ts +50 -179
  151. package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
  152. package/dist/schema/LiveStoreEvent/client.js +32 -29
  153. package/dist/schema/LiveStoreEvent/client.js.map +1 -1
  154. package/dist/schema/LiveStoreEvent/client.test.js +11 -2
  155. package/dist/schema/LiveStoreEvent/client.test.js.map +1 -1
  156. package/dist/schema/LiveStoreEvent/for-event-def.d.ts +7 -7
  157. package/dist/schema/LiveStoreEvent/for-event-def.d.ts.map +1 -1
  158. package/dist/schema/LiveStoreEvent/global.d.ts +6 -6
  159. package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
  160. package/dist/schema/LiveStoreEvent/global.js +1 -1
  161. package/dist/schema/LiveStoreEvent/global.js.map +1 -1
  162. package/dist/schema/LiveStoreEvent/input.d.ts +5 -5
  163. package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
  164. package/dist/schema/LiveStoreEvent/input.js +3 -3
  165. package/dist/schema/LiveStoreEvent/input.js.map +1 -1
  166. package/dist/schema/mod.d.ts +1 -0
  167. package/dist/schema/mod.d.ts.map +1 -1
  168. package/dist/schema/mod.js +1 -0
  169. package/dist/schema/mod.js.map +1 -1
  170. package/dist/schema/schema.d.ts.map +1 -1
  171. package/dist/schema/schema.js +3 -2
  172. package/dist/schema/schema.js.map +1 -1
  173. package/dist/schema/state/sqlite/client-document-def.d.ts +12 -17
  174. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  175. package/dist/schema/state/sqlite/client-document-def.js +23 -20
  176. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  177. package/dist/schema/state/sqlite/client-document-def.test.js +29 -8
  178. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  179. package/dist/schema/state/sqlite/column-annotations.d.ts +13 -13
  180. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  181. package/dist/schema/state/sqlite/column-annotations.js +13 -15
  182. package/dist/schema/state/sqlite/column-annotations.js.map +1 -1
  183. package/dist/schema/state/sqlite/column-annotations.test.js +19 -43
  184. package/dist/schema/state/sqlite/column-annotations.test.js.map +1 -1
  185. package/dist/schema/state/sqlite/column-def.d.ts +1 -1
  186. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  187. package/dist/schema/state/sqlite/column-def.js +57 -31
  188. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  189. package/dist/schema/state/sqlite/column-def.test.js +118 -72
  190. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  191. package/dist/schema/state/sqlite/column-spec.js.map +1 -1
  192. package/dist/schema/state/sqlite/column-spec.test.js +2 -2
  193. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +2 -2
  194. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  195. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +13 -4
  196. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  197. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts +9 -9
  198. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  199. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +8 -13
  200. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  201. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +27 -16
  202. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js.map +1 -1
  203. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts +13 -11
  204. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  205. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +5 -3
  206. package/dist/schema/state/sqlite/db-schema/dsl/mod.js.map +1 -1
  207. package/dist/schema/state/sqlite/mod.d.ts +1 -3
  208. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  209. package/dist/schema/state/sqlite/mod.js +1 -1
  210. package/dist/schema/state/sqlite/mod.js.map +1 -1
  211. package/dist/schema/state/sqlite/persistence.d.ts +11 -0
  212. package/dist/schema/state/sqlite/persistence.d.ts.map +1 -0
  213. package/dist/schema/state/sqlite/persistence.js +10 -0
  214. package/dist/schema/state/sqlite/persistence.js.map +1 -0
  215. package/dist/schema/state/sqlite/query-builder/api.d.ts +7 -7
  216. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  217. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  218. package/dist/schema/state/sqlite/query-builder/astToSql.js +27 -23
  219. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  220. package/dist/schema/state/sqlite/query-builder/impl.d.ts +3 -1
  221. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  222. package/dist/schema/state/sqlite/query-builder/impl.js +16 -14
  223. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  224. package/dist/schema/state/sqlite/query-builder/impl.test.js +362 -596
  225. package/dist/schema/state/sqlite/query-builder/impl.test.js.map +1 -1
  226. package/dist/schema/state/sqlite/schema-helpers.js +1 -1
  227. package/dist/schema/state/sqlite/schema-helpers.js.map +1 -1
  228. package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts +102 -134
  229. package/dist/schema/state/sqlite/system-tables/eventlog-tables.d.ts.map +1 -1
  230. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +2 -2
  231. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js.map +1 -1
  232. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +328 -106
  233. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
  234. package/dist/schema/state/sqlite/system-tables/state-tables.js +33 -1
  235. package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
  236. package/dist/schema/state/sqlite/table-def.d.ts +16 -19
  237. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  238. package/dist/schema/state/sqlite/table-def.js +53 -3
  239. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  240. package/dist/schema/state/sqlite/table-def.test.js +153 -36
  241. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  242. package/dist/schema/unknown-events.d.ts.map +1 -1
  243. package/dist/schema/unknown-events.js +2 -1
  244. package/dist/schema/unknown-events.js.map +1 -1
  245. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +2 -2
  246. package/dist/session-id-symbol.d.ts +29 -0
  247. package/dist/session-id-symbol.d.ts.map +1 -0
  248. package/dist/session-id-symbol.js +44 -0
  249. package/dist/session-id-symbol.js.map +1 -0
  250. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  251. package/dist/sql-queries/sql-queries.js +8 -10
  252. package/dist/sql-queries/sql-queries.js.map +1 -1
  253. package/dist/sql-queries/types.d.ts +6 -7
  254. package/dist/sql-queries/types.d.ts.map +1 -1
  255. package/dist/sql-queries/types.js.map +1 -1
  256. package/dist/sqlite-db-helper.d.ts +21 -1
  257. package/dist/sqlite-db-helper.d.ts.map +1 -1
  258. package/dist/sqlite-db-helper.js +37 -2
  259. package/dist/sqlite-db-helper.js.map +1 -1
  260. package/dist/sqlite-types.d.ts +3 -6
  261. package/dist/sqlite-types.d.ts.map +1 -1
  262. package/dist/sqlite-types.js +3 -3
  263. package/dist/sqlite-types.js.map +1 -1
  264. package/dist/sync/ClientSessionSyncProcessor.d.ts +38 -16
  265. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  266. package/dist/sync/ClientSessionSyncProcessor.js +196 -66
  267. package/dist/sync/ClientSessionSyncProcessor.js.map +1 -1
  268. package/dist/sync/errors.d.ts +12 -18
  269. package/dist/sync/errors.d.ts.map +1 -1
  270. package/dist/sync/errors.js +2 -2
  271. package/dist/sync/errors.js.map +1 -1
  272. package/dist/sync/mock-sync-backend.d.ts +2 -3
  273. package/dist/sync/mock-sync-backend.d.ts.map +1 -1
  274. package/dist/sync/mock-sync-backend.js +54 -33
  275. package/dist/sync/mock-sync-backend.js.map +1 -1
  276. package/dist/sync/mock-sync-backend.test.d.ts +2 -0
  277. package/dist/sync/mock-sync-backend.test.d.ts.map +1 -0
  278. package/dist/sync/mock-sync-backend.test.js +49 -0
  279. package/dist/sync/mock-sync-backend.test.js.map +1 -0
  280. package/dist/sync/next/history-dag-common.d.ts +4 -0
  281. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  282. package/dist/sync/next/history-dag-common.js +52 -31
  283. package/dist/sync/next/history-dag-common.js.map +1 -1
  284. package/dist/sync/next/history-dag.d.ts.map +1 -1
  285. package/dist/sync/next/history-dag.js +2 -1
  286. package/dist/sync/next/history-dag.js.map +1 -1
  287. package/dist/sync/next/test/compact-events.calculator.test.js +2 -2
  288. package/dist/sync/next/test/event-fixtures.d.ts +7 -7
  289. package/dist/sync/next/test/event-fixtures.js +1 -1
  290. package/dist/sync/next/test/event-fixtures.js.map +1 -1
  291. package/dist/sync/next/test/history-dag-common.test.d.ts +2 -0
  292. package/dist/sync/next/test/history-dag-common.test.d.ts.map +1 -0
  293. package/dist/sync/next/test/history-dag-common.test.js +73 -0
  294. package/dist/sync/next/test/history-dag-common.test.js.map +1 -0
  295. package/dist/sync/sync-backend-kv.d.ts.map +1 -1
  296. package/dist/sync/sync-backend-kv.js +4 -2
  297. package/dist/sync/sync-backend-kv.js.map +1 -1
  298. package/dist/sync/sync-backend.d.ts +15 -15
  299. package/dist/sync/sync-backend.d.ts.map +1 -1
  300. package/dist/sync/sync-backend.js +9 -5
  301. package/dist/sync/sync-backend.js.map +1 -1
  302. package/dist/sync/sync.d.ts +1 -1
  303. package/dist/sync/sync.d.ts.map +1 -1
  304. package/dist/sync/syncstate.d.ts +97 -173
  305. package/dist/sync/syncstate.d.ts.map +1 -1
  306. package/dist/sync/syncstate.js +34 -37
  307. package/dist/sync/syncstate.js.map +1 -1
  308. package/dist/sync/syncstate.test.js +100 -61
  309. package/dist/sync/syncstate.test.js.map +1 -1
  310. package/dist/sync/transport-chunking.d.ts +15 -16
  311. package/dist/sync/transport-chunking.d.ts.map +1 -1
  312. package/dist/sync/transport-chunking.js +23 -32
  313. package/dist/sync/transport-chunking.js.map +1 -1
  314. package/dist/util.d.ts +11 -4
  315. package/dist/util.d.ts.map +1 -1
  316. package/dist/util.js +19 -2
  317. package/dist/util.js.map +1 -1
  318. package/dist/version.d.ts.map +1 -1
  319. package/dist/version.js.map +1 -1
  320. package/package.json +19 -37
  321. package/src/ClientSessionLeaderThreadProxy.ts +1 -1
  322. package/src/MaterializationJournal.ts +184 -0
  323. package/src/StateHead.ts +80 -0
  324. package/src/WorkerTransportError.ts +4 -4
  325. package/src/__tests__/fixture.ts +3 -3
  326. package/src/adapter-types.ts +24 -61
  327. package/src/debug-info.ts +50 -70
  328. package/src/defs.ts +2 -2
  329. package/src/devtools/devtools-compatibility.test.ts +1 -0
  330. package/src/devtools/devtools-messages-client-session.ts +56 -65
  331. package/src/devtools/devtools-messages-common.ts +12 -11
  332. package/src/devtools/devtools-messages-leader.ts +71 -69
  333. package/src/devtools/devtools-sessioninfo.ts +13 -11
  334. package/src/devtools/mod.ts +4 -5
  335. package/src/errors.ts +46 -34
  336. package/src/index.ts +8 -1
  337. package/src/leader-thread/LeaderSyncProcessor.ts +794 -691
  338. package/src/leader-thread/RejectedPushError.ts +40 -27
  339. package/src/leader-thread/connection.ts +12 -8
  340. package/src/leader-thread/eventlog.ts +7 -7
  341. package/src/leader-thread/leader-worker-devtools.ts +37 -19
  342. package/src/leader-thread/make-leader-thread-layer.test.ts +7 -7
  343. package/src/leader-thread/make-leader-thread-layer.ts +56 -53
  344. package/src/leader-thread/materialize-event.ts +13 -8
  345. package/src/leader-thread/recreate-db.ts +24 -59
  346. package/src/leader-thread/shutdown-channel.ts +3 -14
  347. package/src/leader-thread/stream-events.ts +104 -101
  348. package/src/leader-thread/types.ts +24 -62
  349. package/src/logging.ts +5 -52
  350. package/src/make-client-session.ts +6 -6
  351. package/src/materializer-helper.ts +8 -37
  352. package/src/otel.ts +5 -14
  353. package/src/rematerialize-from-eventlog.ts +97 -94
  354. package/src/schema/EventDef/define.test.ts +28 -0
  355. package/src/schema/EventDef/define.ts +20 -19
  356. package/src/schema/EventDef/deprecated.test.ts +1 -8
  357. package/src/schema/EventDef/deprecated.ts +17 -20
  358. package/src/schema/EventDef/event-def.ts +2 -4
  359. package/src/schema/EventSequenceNumber/client.ts +8 -9
  360. package/src/schema/EventSequenceNumber/global.ts +1 -1
  361. package/src/schema/EventSequenceNumber.test.ts +5 -5
  362. package/src/schema/LiveStoreEvent/client.test.ts +17 -4
  363. package/src/schema/LiveStoreEvent/client.ts +36 -35
  364. package/src/schema/LiveStoreEvent/for-event-def.ts +7 -7
  365. package/src/schema/LiveStoreEvent/global.ts +1 -1
  366. package/src/schema/LiveStoreEvent/input.ts +6 -6
  367. package/src/schema/mod.ts +1 -0
  368. package/src/schema/schema.ts +3 -2
  369. package/src/schema/state/sqlite/client-document-def.test.ts +32 -13
  370. package/src/schema/state/sqlite/client-document-def.ts +95 -80
  371. package/src/schema/state/sqlite/column-annotations.test.ts +26 -45
  372. package/src/schema/state/sqlite/column-annotations.ts +26 -30
  373. package/src/schema/state/sqlite/column-def.test.ts +132 -77
  374. package/src/schema/state/sqlite/column-def.ts +72 -37
  375. package/src/schema/state/sqlite/column-spec.test.ts +2 -2
  376. package/src/schema/state/sqlite/column-spec.ts +3 -2
  377. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +15 -5
  378. package/src/schema/state/sqlite/db-schema/dsl/__snapshots__/field-defs.test.ts.snap +123 -63
  379. package/src/schema/state/sqlite/db-schema/dsl/field-defs.test.ts +35 -16
  380. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +25 -31
  381. package/src/schema/state/sqlite/db-schema/dsl/mod.ts +35 -21
  382. package/src/schema/state/sqlite/mod.ts +2 -4
  383. package/src/schema/state/sqlite/persistence.ts +11 -0
  384. package/src/schema/state/sqlite/query-builder/api.ts +9 -9
  385. package/src/schema/state/sqlite/query-builder/astToSql.ts +31 -26
  386. package/src/schema/state/sqlite/query-builder/impl.test.ts +433 -669
  387. package/src/schema/state/sqlite/query-builder/impl.ts +19 -15
  388. package/src/schema/state/sqlite/schema-helpers.ts +1 -1
  389. package/src/schema/state/sqlite/system-tables/eventlog-tables.ts +2 -2
  390. package/src/schema/state/sqlite/system-tables/state-tables.ts +41 -1
  391. package/src/schema/state/sqlite/table-def.test.ts +204 -66
  392. package/src/schema/state/sqlite/table-def.ts +96 -45
  393. package/src/schema/unknown-events.ts +3 -2
  394. package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +2 -2
  395. package/src/session-id-symbol.ts +59 -0
  396. package/src/sql-queries/sql-queries.ts +8 -12
  397. package/src/sql-queries/types.ts +6 -7
  398. package/src/sqlite-db-helper.ts +64 -2
  399. package/src/sqlite-types.ts +5 -8
  400. package/src/sync/ClientSessionSyncProcessor.ts +303 -119
  401. package/src/sync/errors.ts +14 -12
  402. package/src/sync/mock-sync-backend.test.ts +78 -0
  403. package/src/sync/mock-sync-backend.ts +93 -47
  404. package/src/sync/next/history-dag-common.ts +60 -34
  405. package/src/sync/next/history-dag.ts +4 -1
  406. package/src/sync/next/test/compact-events.calculator.test.ts +2 -2
  407. package/src/sync/next/test/event-fixtures.ts +1 -1
  408. package/src/sync/next/test/history-dag-common.test.ts +87 -0
  409. package/src/sync/sync-backend-kv.ts +4 -2
  410. package/src/sync/sync-backend.ts +15 -15
  411. package/src/sync/sync.ts +1 -1
  412. package/src/sync/syncstate.test.ts +191 -140
  413. package/src/sync/syncstate.ts +63 -53
  414. package/src/sync/transport-chunking.ts +34 -39
  415. package/src/util.ts +18 -9
  416. package/src/version.ts +1 -2
  417. package/dist/sql-queries/misc.d.ts +0 -2
  418. package/dist/sql-queries/misc.d.ts.map +0 -1
  419. package/dist/sql-queries/misc.js +0 -2
  420. package/dist/sql-queries/misc.js.map +0 -1
  421. package/src/sql-queries/misc.ts +0 -2
@@ -1,6 +1,6 @@
1
+ import { TestSchema } from 'effect/testing';
1
2
  import { describe, expect, it } from 'vitest';
2
- import { Schema } from '@livestore/utils/effect';
3
- import { objectToString } from '@livestore/utils';
3
+ import { Schema, SchemaTransformation } from '@livestore/utils/effect';
4
4
  import { State } from "../../../mod.js";
5
5
  import { getResultSchema } from "./impl.js";
6
6
  const todos = State.SQLite.table({
@@ -9,7 +9,7 @@ const todos = State.SQLite.table({
9
9
  id: State.SQLite.text({ primaryKey: true }),
10
10
  text: State.SQLite.text({ default: '', nullable: false }),
11
11
  completed: State.SQLite.boolean({ default: false, nullable: false }),
12
- status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
12
+ status: State.SQLite.text({ schema: Schema.Literals(['active', 'completed']) }),
13
13
  deletedAt: State.SQLite.datetime({ nullable: true }),
14
14
  // TODO consider leaning more into Effect schema
15
15
  // other: Schema.Number.pipe(State.SQLite.asInteger),
@@ -20,7 +20,7 @@ const todosWithIntId = State.SQLite.table({
20
20
  columns: {
21
21
  id: State.SQLite.integer({ primaryKey: true }),
22
22
  text: State.SQLite.text({ default: '', nullable: false }),
23
- status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
23
+ status: State.SQLite.text({ schema: Schema.Literals(['active', 'completed']) }),
24
24
  },
25
25
  });
26
26
  const comments = State.SQLite.table({
@@ -34,14 +34,14 @@ const comments = State.SQLite.table({
34
34
  const UiState = State.SQLite.clientDocument({
35
35
  name: 'UiState',
36
36
  schema: Schema.Struct({
37
- filter: Schema.Literal('all', 'active', 'completed'),
37
+ filter: Schema.Literals(['all', 'active', 'completed']),
38
38
  }),
39
39
  default: { value: { filter: 'all' } },
40
40
  });
41
41
  const UiStateWithDefaultId = State.SQLite.clientDocument({
42
42
  name: 'UiState',
43
43
  schema: Schema.Struct({
44
- filter: Schema.Literal('all', 'active', 'completed'),
44
+ filter: Schema.Literals(['all', 'active', 'completed']),
45
45
  }),
46
46
  default: {
47
47
  id: 'static',
@@ -54,10 +54,10 @@ const issue = State.SQLite.table({
54
54
  id: State.SQLite.integer({ primaryKey: true }),
55
55
  title: State.SQLite.text({ default: '' }),
56
56
  creator: State.SQLite.text({ default: '' }),
57
- priority: State.SQLite.integer({ schema: Schema.Literal(0, 1, 2, 3, 4), default: 0 }),
58
- created: State.SQLite.integer({ schema: Schema.DateFromNumber }),
59
- deleted: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
60
- modified: State.SQLite.integer({ schema: Schema.DateFromNumber }),
57
+ priority: State.SQLite.integer({ schema: Schema.Literals([0, 1, 2, 3, 4]), default: 0 }),
58
+ created: State.SQLite.integer({ schema: Schema.DateFromMillis }),
59
+ deleted: State.SQLite.integer({ nullable: true, schema: Schema.DateFromMillis }),
60
+ modified: State.SQLite.integer({ schema: Schema.DateFromMillis }),
61
61
  kanbanorder: State.SQLite.text({ nullable: false, default: '' }),
62
62
  },
63
63
  indexes: [
@@ -72,7 +72,7 @@ const selections = State.SQLite.table({
72
72
  group: State.SQLite.text({}),
73
73
  },
74
74
  });
75
- const Source = Schema.Literal('google', 'linkedin', 'facebook');
75
+ const Source = Schema.Literals(['google', 'linkedin', 'facebook']);
76
76
  const ProfileAttribute = Schema.Struct({ key: Schema.String, value: Schema.String });
77
77
  const personProfiles = State.SQLite.table({
78
78
  name: 'person_profiles',
@@ -86,417 +86,286 @@ const personProfiles = State.SQLite.table({
86
86
  },
87
87
  });
88
88
  const db = { todos, todosWithIntId, comments, issue, selections, UiState, UiStateWithDefaultId, personProfiles };
89
- const dump = (qb) => ({
90
- bindValues: qb.asSql().bindValues,
91
- query: qb.asSql().query,
92
- schema: objectToString(getResultSchema(qb)),
93
- });
94
89
  describe('query builder', () => {
95
90
  describe('basic queries', () => {
96
91
  it('should handle simple SELECT queries', () => {
97
- expect(dump(db.todos)).toMatchInlineSnapshot(`
98
- {
99
- "bindValues": [],
100
- "query": "SELECT * FROM 'todos'",
101
- "schema": "ReadonlyArray<todos>",
102
- }
103
- `);
104
- expect(dump(db.todos.select('id'))).toMatchInlineSnapshot(`
105
- {
106
- "bindValues": [],
107
- "query": "SELECT "id" FROM 'todos'",
108
- "schema": "ReadonlyArray<({ readonly id: string } <-> string)>",
109
- }
110
- `);
111
- expect(dump(db.todos.select('id', 'text'))).toMatchInlineSnapshot(`
112
- {
113
- "bindValues": [],
114
- "query": "SELECT "id", "text" FROM 'todos'",
115
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
116
- }
117
- `);
92
+ expect(db.todos.asSql()).toEqual({
93
+ bindValues: [],
94
+ query: "SELECT * FROM 'todos'",
95
+ usedTables: new Set(['todos']),
96
+ });
97
+ expect(db.todos.select('id').asSql()).toEqual({
98
+ bindValues: [],
99
+ query: 'SELECT "id" FROM \'todos\'',
100
+ usedTables: new Set(['todos']),
101
+ });
102
+ expect(db.todos.select('id', 'text').asSql()).toEqual({
103
+ bindValues: [],
104
+ query: 'SELECT "id", "text" FROM \'todos\'',
105
+ usedTables: new Set(['todos']),
106
+ });
107
+ });
108
+ it('derives result schemas for SELECT queries', async () => {
109
+ const selectedRows = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text')));
110
+ await selectedRows.decoding().succeed([{ id: '123', text: 'Buy milk' }]);
111
+ const selectedColumn = new TestSchema.Asserts(getResultSchema(db.todos.select('id')));
112
+ await selectedColumn.decoding().succeed([{ id: '123' }], ['123']);
118
113
  });
119
114
  it('should handle .first()', () => {
120
- expect(dump(db.todos.select('id', 'text').first())).toMatchInlineSnapshot(`
121
- {
122
- "bindValues": [
123
- 1,
124
- ],
125
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
126
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string } | undefined> <-> { readonly id: string; readonly text: string } | undefined)",
127
- }
128
- `);
129
- expect(dump(db.todos.select('id', 'text').first({ behaviour: 'error' }))).toMatchInlineSnapshot(`
130
- {
131
- "bindValues": [
132
- 1,
133
- ],
134
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
135
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> <-> { readonly id: string; readonly text: string })",
136
- }
137
- `);
138
- expect(dump(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined }))).toMatchInlineSnapshot(`
139
- {
140
- "bindValues": [
141
- 1,
142
- ],
143
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
144
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> | readonly [undefined] <-> { readonly id: string; readonly text: string } | undefined)",
145
- }
146
- `);
115
+ expect(db.todos.select('id', 'text').first().asSql()).toEqual({
116
+ bindValues: [1],
117
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
118
+ usedTables: new Set(['todos']),
119
+ });
120
+ expect(db.todos.select('id', 'text').first({ behaviour: 'error' }).asSql()).toEqual({
121
+ bindValues: [1],
122
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
123
+ usedTables: new Set(['todos']),
124
+ });
125
+ expect(db.todos
126
+ .select('id', 'text')
127
+ .first({ behaviour: 'fallback', fallback: () => undefined })
128
+ .asSql()).toEqual({
129
+ bindValues: [1],
130
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
131
+ usedTables: new Set(['todos']),
132
+ });
133
+ });
134
+ it('derives result schemas for .first()', async () => {
135
+ const firstOrUndefined = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text').first()));
136
+ await firstOrUndefined.decoding().succeed([], undefined);
137
+ await firstOrUndefined.decoding().succeed([{ id: '123', text: 'Buy milk' }], { id: '123', text: 'Buy milk' });
138
+ const firstOrError = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text').first({ behaviour: 'error' })));
139
+ await firstOrError.decoding().succeed([{ id: '123', text: 'Buy milk' }], { id: '123', text: 'Buy milk' });
140
+ await firstOrError.decoding().fail([], 'Unable to retrieve the first element of an empty array');
141
+ const firstOrFallback = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined })));
142
+ await firstOrFallback.decoding().succeed([], undefined);
147
143
  });
148
144
  it('should handle WHERE clauses', () => {
149
- expect(dump(db.todos.select('id', 'text').where('completed', true))).toMatchInlineSnapshot(`
150
- {
151
- "bindValues": [
152
- 1,
153
- ],
154
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ?",
155
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
156
- }
157
- `);
158
- expect(dump(db.todos.select('id', 'text').where('completed', '!=', true))).toMatchInlineSnapshot(`
159
- {
160
- "bindValues": [
161
- 1,
162
- ],
163
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" != ?",
164
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
165
- }
166
- `);
167
- expect(dump(db.todos.select('id', 'text').where({ completed: true }))).toMatchInlineSnapshot(`
168
- {
169
- "bindValues": [
170
- 1,
171
- ],
172
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ?",
173
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
174
- }
175
- `);
176
- expect(dump(db.todos.select('id', 'text').where({ completed: undefined }))).toMatchInlineSnapshot(`
177
- {
178
- "bindValues": [],
179
- "query": "SELECT "id", "text" FROM 'todos'",
180
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
181
- }
182
- `);
183
- expect(dump(db.todos.select('id', 'text').where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } }))).toMatchInlineSnapshot(`
184
- {
185
- "bindValues": [
186
- "2024-01-01T00:00:00.000Z",
187
- ],
188
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" <= ?",
189
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
190
- }
191
- `);
192
- expect(dump(db.todos.select('id', 'text').where({ status: { op: 'IN', value: ['active'] } }))).toMatchInlineSnapshot(`
193
- {
194
- "bindValues": [
195
- "active",
196
- ],
197
- "query": "SELECT "id", "text" FROM 'todos' WHERE "status" IN (?)",
198
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
199
- }
200
- `);
201
- expect(dump(db.todos.select('id', 'text').where({ status: { op: 'NOT IN', value: ['active', 'completed'] } }))).toMatchInlineSnapshot(`
202
- {
203
- "bindValues": [
204
- "active",
205
- "completed",
206
- ],
207
- "query": "SELECT "id", "text" FROM 'todos' WHERE "status" NOT IN (?, ?)",
208
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
209
- }
210
- `);
211
- expect(dump(db.todos
145
+ expect(db.todos.select('id', 'text').where('completed', true).asSql()).toEqual({
146
+ bindValues: [1],
147
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ?',
148
+ usedTables: new Set(['todos']),
149
+ });
150
+ expect(db.todos.select('id', 'text').where('completed', '!=', true).asSql()).toEqual({
151
+ bindValues: [1],
152
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" != ?',
153
+ usedTables: new Set(['todos']),
154
+ });
155
+ expect(db.todos.select('id', 'text').where({ completed: true }).asSql()).toEqual({
156
+ bindValues: [1],
157
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ?',
158
+ usedTables: new Set(['todos']),
159
+ });
160
+ expect(db.todos.select('id', 'text').where({ completed: undefined }).asSql()).toEqual({
161
+ bindValues: [],
162
+ query: 'SELECT "id", "text" FROM \'todos\'',
163
+ usedTables: new Set(['todos']),
164
+ });
165
+ expect(db.todos
166
+ .select('id', 'text')
167
+ .where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })
168
+ .asSql()).toEqual({
169
+ bindValues: ['2024-01-01T00:00:00.000Z'],
170
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" <= ?',
171
+ usedTables: new Set(['todos']),
172
+ });
173
+ expect(db.todos
174
+ .select('id', 'text')
175
+ .where({ status: { op: 'IN', value: ['active'] } })
176
+ .asSql()).toEqual({
177
+ bindValues: ['active'],
178
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "status" IN (?)',
179
+ usedTables: new Set(['todos']),
180
+ });
181
+ expect(db.todos
182
+ .select('id', 'text')
183
+ .where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })
184
+ .asSql()).toEqual({
185
+ bindValues: ['active', 'completed'],
186
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "status" NOT IN (?, ?)',
187
+ usedTables: new Set(['todos']),
188
+ });
189
+ expect(db.todos
212
190
  .select('id', 'text')
213
191
  .where({ completed: false })
214
192
  .where({ status: { op: 'IN', value: ['active'] } })
215
- .where({ deletedAt: undefined }))).toMatchInlineSnapshot(`
216
- {
217
- "bindValues": [
218
- 0,
219
- "active",
220
- ],
221
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? AND "status" IN (?)",
222
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
223
- }
224
- `);
193
+ .where({ deletedAt: undefined })
194
+ .asSql()).toEqual({
195
+ bindValues: [0, 'active'],
196
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? AND "status" IN (?)',
197
+ usedTables: new Set(['todos']),
198
+ });
225
199
  });
226
200
  it('should handle OFFSET and LIMIT clauses', () => {
227
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(10).limit(10))).toMatchInlineSnapshot(`
228
- {
229
- "bindValues": [
230
- 1,
231
- 10,
232
- 10,
233
- ],
234
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ? OFFSET ?",
235
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
236
- }
237
- `);
201
+ expect(db.todos.select('id', 'text').where('completed', true).offset(10).limit(10).asSql()).toEqual({
202
+ bindValues: [1, 10, 10],
203
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ? OFFSET ?',
204
+ usedTables: new Set(['todos']),
205
+ });
238
206
  });
239
207
  it('should handle OFFSET and LIMIT clauses correctly', () => {
240
208
  // Test with both offset and limit
241
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(5).limit(10))).toMatchInlineSnapshot(`
242
- {
243
- "bindValues": [
244
- 1,
245
- 10,
246
- 5,
247
- ],
248
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ? OFFSET ?",
249
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
250
- }
251
- `);
209
+ expect(db.todos.select('id', 'text').where('completed', true).offset(5).limit(10).asSql()).toEqual({
210
+ bindValues: [1, 10, 5],
211
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ? OFFSET ?',
212
+ usedTables: new Set(['todos']),
213
+ });
252
214
  // Test with only offset
253
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(5))).toMatchInlineSnapshot(`
254
- {
255
- "bindValues": [
256
- 1,
257
- 5,
258
- ],
259
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? OFFSET ?",
260
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
261
- }
262
- `);
215
+ expect(db.todos.select('id', 'text').where('completed', true).offset(5).asSql()).toEqual({
216
+ bindValues: [1, 5],
217
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? OFFSET ?',
218
+ usedTables: new Set(['todos']),
219
+ });
263
220
  // Test with only limit
264
- expect(dump(db.todos.select('id', 'text').where('completed', true).limit(10))).toMatchInlineSnapshot(`
265
- {
266
- "bindValues": [
267
- 1,
268
- 10,
269
- ],
270
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ?",
271
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
272
- }
273
- `);
221
+ expect(db.todos.select('id', 'text').where('completed', true).limit(10).asSql()).toEqual({
222
+ bindValues: [1, 10],
223
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ?',
224
+ usedTables: new Set(['todos']),
225
+ });
274
226
  });
275
227
  it('should handle COUNT queries', () => {
276
- expect(dump(db.todos.count())).toMatchInlineSnapshot(`
277
- {
278
- "bindValues": [],
279
- "query": "SELECT COUNT(*) as count FROM 'todos'",
280
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
281
- }
282
- `);
283
- expect(dump(db.todos.count().where('completed', true))).toMatchInlineSnapshot(`
284
- {
285
- "bindValues": [
286
- 1,
287
- ],
288
- "query": "SELECT COUNT(*) as count FROM 'todos' WHERE "completed" = ?",
289
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
290
- }
291
- `);
292
- expect(dump(db.todos.where('completed', true).count())).toMatchInlineSnapshot(`
293
- {
294
- "bindValues": [
295
- 1,
296
- ],
297
- "query": "SELECT COUNT(*) as count FROM 'todos' WHERE "completed" = ?",
298
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
299
- }
300
- `);
228
+ expect(db.todos.count().asSql()).toEqual({
229
+ bindValues: [],
230
+ query: "SELECT COUNT(*) as count FROM 'todos'",
231
+ usedTables: new Set(['todos']),
232
+ });
233
+ expect(db.todos.count().where('completed', true).asSql()).toEqual({
234
+ bindValues: [1],
235
+ query: 'SELECT COUNT(*) as count FROM \'todos\' WHERE "completed" = ?',
236
+ usedTables: new Set(['todos']),
237
+ });
238
+ expect(db.todos.where('completed', true).count().asSql()).toEqual({
239
+ bindValues: [1],
240
+ query: 'SELECT COUNT(*) as count FROM \'todos\' WHERE "completed" = ?',
241
+ usedTables: new Set(['todos']),
242
+ });
243
+ });
244
+ it('derives result schemas for COUNT queries', async () => {
245
+ const countResult = new TestSchema.Asserts(getResultSchema(db.todos.count()));
246
+ await countResult.decoding().succeed([{ count: 3 }], 3);
301
247
  });
302
248
  it('should handle NULL comparisons', () => {
303
- expect(dump(db.todos.select('id', 'text').where('deletedAt', '=', null))).toMatchInlineSnapshot(`
304
- {
305
- "bindValues": [],
306
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" IS NULL",
307
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
308
- }
309
- `);
310
- expect(dump(db.todos.select('id', 'text').where('deletedAt', '!=', null))).toMatchInlineSnapshot(`
311
- {
312
- "bindValues": [],
313
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" IS NOT NULL",
314
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
315
- }
316
- `);
249
+ expect(db.todos.select('id', 'text').where('deletedAt', '=', null).asSql()).toEqual({
250
+ bindValues: [],
251
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" IS NULL',
252
+ usedTables: new Set(['todos']),
253
+ });
254
+ expect(db.todos.select('id', 'text').where('deletedAt', '!=', null).asSql()).toEqual({
255
+ bindValues: [],
256
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" IS NOT NULL',
257
+ usedTables: new Set(['todos']),
258
+ });
317
259
  });
318
260
  it('should handle orderBy', () => {
319
- expect(dump(db.todos.orderBy('completed', 'desc'))).toMatchInlineSnapshot(`
320
- {
321
- "bindValues": [],
322
- "query": "SELECT * FROM 'todos' ORDER BY "completed" desc",
323
- "schema": "ReadonlyArray<todos>",
324
- }
325
- `);
326
- expect(dump(db.todos.orderBy([{ col: 'completed', direction: 'desc' }]))).toMatchInlineSnapshot(`
327
- {
328
- "bindValues": [],
329
- "query": "SELECT * FROM 'todos' ORDER BY "completed" desc",
330
- "schema": "ReadonlyArray<todos>",
331
- }
332
- `);
333
- expect(dump(db.todos.orderBy([]))).toMatchInlineSnapshot(`
334
- {
335
- "bindValues": [],
336
- "query": "SELECT * FROM 'todos'",
337
- "schema": "ReadonlyArray<todos>",
338
- }
339
- `);
261
+ expect(db.todos.orderBy('completed', 'desc').asSql()).toEqual({
262
+ bindValues: [],
263
+ query: 'SELECT * FROM \'todos\' ORDER BY "completed" desc',
264
+ usedTables: new Set(['todos']),
265
+ });
266
+ expect(db.todos.orderBy([{ col: 'completed', direction: 'desc' }]).asSql()).toEqual({
267
+ bindValues: [],
268
+ query: 'SELECT * FROM \'todos\' ORDER BY "completed" desc',
269
+ usedTables: new Set(['todos']),
270
+ });
271
+ expect(db.todos.orderBy([]).asSql()).toEqual({
272
+ bindValues: [],
273
+ query: "SELECT * FROM 'todos'",
274
+ usedTables: new Set(['todos']),
275
+ });
340
276
  });
341
277
  it('should handle JSON_CONTAINS operator for JSON array columns', () => {
342
- expect(dump(db.personProfiles.where({ sources: { op: 'JSON_CONTAINS', value: 'google' } }))).toMatchInlineSnapshot(`
343
- {
344
- "bindValues": [
345
- "google",
346
- ],
347
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
348
- "schema": "ReadonlyArray<person_profiles>",
349
- }
350
- `);
278
+ expect(db.personProfiles.where({ sources: { op: 'JSON_CONTAINS', value: 'google' } }).asSql()).toEqual({
279
+ bindValues: ['google'],
280
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
281
+ usedTables: new Set(['person_profiles']),
282
+ });
351
283
  // With select
352
- expect(dump(db.personProfiles.select('personId').where({ sources: { op: 'JSON_CONTAINS', value: 'linkedin' } }))).toMatchInlineSnapshot(`
353
- {
354
- "bindValues": [
355
- "linkedin",
356
- ],
357
- "query": "SELECT "personId" FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
358
- "schema": "ReadonlyArray<({ readonly personId: string } <-> string)>",
359
- }
360
- `);
284
+ expect(db.personProfiles
285
+ .select('personId')
286
+ .where({ sources: { op: 'JSON_CONTAINS', value: 'linkedin' } })
287
+ .asSql()).toEqual({
288
+ bindValues: ['linkedin'],
289
+ query: 'SELECT "personId" FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
290
+ usedTables: new Set(['person_profiles']),
291
+ });
361
292
  // With plain string array column
362
- expect(dump(db.personProfiles.where({ tags: { op: 'JSON_CONTAINS', value: 'important' } }))).toMatchInlineSnapshot(`
363
- {
364
- "bindValues": [
365
- "important",
366
- ],
367
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("tags") WHERE value = ?)",
368
- "schema": "ReadonlyArray<person_profiles>",
369
- }
370
- `);
293
+ expect(db.personProfiles.where({ tags: { op: 'JSON_CONTAINS', value: 'important' } }).asSql()).toEqual({
294
+ bindValues: ['important'],
295
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("tags") WHERE value = ?)',
296
+ usedTables: new Set(['person_profiles']),
297
+ });
371
298
  });
372
299
  it('should handle JSON_NOT_CONTAINS operator for JSON array columns', () => {
373
- expect(dump(db.personProfiles.where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'google' } }))).toMatchInlineSnapshot(`
374
- {
375
- "bindValues": [
376
- "google",
377
- ],
378
- "query": "SELECT * FROM 'person_profiles' WHERE NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
379
- "schema": "ReadonlyArray<person_profiles>",
380
- }
381
- `);
300
+ expect(db.personProfiles.where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'google' } }).asSql()).toEqual({
301
+ bindValues: ['google'],
302
+ query: 'SELECT * FROM \'person_profiles\' WHERE NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
303
+ usedTables: new Set(['person_profiles']),
304
+ });
382
305
  });
383
306
  it('should JSON-stringify object elements for JSON_CONTAINS', () => {
384
- expect(dump(db.personProfiles.where({
307
+ expect(db.personProfiles
308
+ .where({
385
309
  attributes: { op: 'JSON_CONTAINS', value: { key: 'language', value: 'typescript' } },
386
- }))).toMatchInlineSnapshot(`
387
- {
388
- "bindValues": [
389
- "{"key":"language","value":"typescript"}",
390
- ],
391
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("attributes") WHERE value = ?)",
392
- "schema": "ReadonlyArray<person_profiles>",
393
- }
394
- `);
310
+ })
311
+ .asSql()).toEqual({
312
+ bindValues: ['{"key":"language","value":"typescript"}'],
313
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("attributes") WHERE value = ?)',
314
+ usedTables: new Set(['person_profiles']),
315
+ });
395
316
  });
396
317
  it('should handle combining JSON_CONTAINS with other WHERE clauses', () => {
397
- expect(dump(db.personProfiles
318
+ expect(db.personProfiles
398
319
  .where({ sources: { op: 'JSON_CONTAINS', value: 'google' } })
399
- .where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'facebook' } }))).toMatchInlineSnapshot(`
400
- {
401
- "bindValues": [
402
- "google",
403
- "facebook",
404
- ],
405
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?) AND NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
406
- "schema": "ReadonlyArray<person_profiles>",
407
- }
408
- `);
320
+ .where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'facebook' } })
321
+ .asSql()).toEqual({
322
+ bindValues: ['google', 'facebook'],
323
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?) AND NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
324
+ usedTables: new Set(['person_profiles']),
325
+ });
409
326
  });
410
327
  it('should handle JSON_CONTAINS on nullable JSON array columns', () => {
411
- expect(dump(db.personProfiles.where({ optionalTags: { op: 'JSON_CONTAINS', value: 'important' } }))).toMatchInlineSnapshot(`
412
- {
413
- "bindValues": [
414
- "important",
415
- ],
416
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)",
417
- "schema": "ReadonlyArray<person_profiles>",
418
- }
419
- `);
328
+ expect(db.personProfiles.where({ optionalTags: { op: 'JSON_CONTAINS', value: 'important' } }).asSql()).toEqual({
329
+ bindValues: ['important'],
330
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)',
331
+ usedTables: new Set(['person_profiles']),
332
+ });
420
333
  // With JSON_NOT_CONTAINS
421
- expect(dump(db.personProfiles.where({ optionalTags: { op: 'JSON_NOT_CONTAINS', value: 'archived' } }))).toMatchInlineSnapshot(`
422
- {
423
- "bindValues": [
424
- "archived",
425
- ],
426
- "query": "SELECT * FROM 'person_profiles' WHERE NOT EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)",
427
- "schema": "ReadonlyArray<person_profiles>",
428
- }
429
- `);
334
+ expect(db.personProfiles.where({ optionalTags: { op: 'JSON_NOT_CONTAINS', value: 'archived' } }).asSql()).toEqual({
335
+ bindValues: ['archived'],
336
+ query: 'SELECT * FROM \'person_profiles\' WHERE NOT EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)',
337
+ usedTables: new Set(['person_profiles']),
338
+ });
430
339
  });
431
340
  it('should throw error when using JSON_CONTAINS on non-JSON array column', () => {
432
341
  expect(() =>
433
342
  // Type system prevents this at compile time for non-array columns, but test runtime check
434
- dump(db.todos.where({ status: { op: 'JSON_CONTAINS', value: 'active' } }))).toThrow('JSON_CONTAINS operator can only be used on JSON array columns');
343
+ db.todos.where({ status: { op: 'JSON_CONTAINS', value: 'active' } }).asSql()).toThrow('JSON_CONTAINS operator can only be used on JSON array columns');
435
344
  });
436
345
  });
437
- // describe('getOrCreate queries', () => {
438
- // it('should handle getOrCreate queries', () => {
439
- // expect(dump(db.UiState.getOrCreate('sessionid-1'))).toMatchInlineSnapshot(`
440
- // {
441
- // "bindValues": [
442
- // "sessionid-1",
443
- // ],
444
- // "query": "SELECT * FROM 'UiState' WHERE id = ?",
445
- // "schema": "...", // TODO determine schema
446
- // }
447
- // `)
448
- // })
449
- // it('should handle getOrCreate queries with default id', () => {
450
- // expect(dump(db.UiStateWithDefaultId.getOrCreate())).toMatchInlineSnapshot(`
451
- // {
452
- // "bindValues": [],
453
- // "query": "SELECT * FROM 'UiState' WHERE id = ?",
454
- // "schema": "...", // TODO determine schema
455
- // }
456
- // `)
457
- // })
458
- // // it('should handle row queries with numbers', () => {
459
- // // expect(dump(db.todosWithIntId.getOrCreate(123, { insertValues: { status: 'active' } }))).toMatchInlineSnapshot(`
460
- // // {
461
- // // "bindValues": [
462
- // // 123,
463
- // // ],
464
- // // "query": "SELECT * FROM 'todos_with_int_id' WHERE id = ?",
465
- // // "schema": "...", // TODO determine schema
466
- // // }
467
- // // `)
468
- // // })
469
- // })
470
346
  describe('write operations', () => {
471
347
  it('should handle INSERT queries', () => {
472
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
473
- {
474
- "bindValues": [
475
- "123",
476
- "Buy milk",
477
- "active",
478
- ],
479
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
480
- "schema": "number",
481
- }
482
- `);
348
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toEqual({
349
+ bindValues: ['123', 'Buy milk', 'active'],
350
+ query: `INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)`,
351
+ usedTables: new Set(['todos']),
352
+ });
353
+ });
354
+ it('derives result schemas for write queries', async () => {
355
+ const insertResult = new TestSchema.Asserts(getResultSchema(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' })));
356
+ await insertResult.decoding().succeed(1);
483
357
  });
484
358
  it('should handle INSERT queries with undefined values', () => {
485
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
486
- {
487
- "bindValues": [
488
- "123",
489
- "Buy milk",
490
- "active",
491
- ],
492
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
493
- "schema": "number",
494
- }
495
- `);
359
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toEqual({
360
+ bindValues: ['123', 'Buy milk', 'active'],
361
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?)',
362
+ usedTables: new Set(['todos']),
363
+ });
496
364
  });
497
365
  // Test helped to catch a bindValues ordering bug
498
366
  it('should handle INSERT queries (issue)', () => {
499
- expect(dump(db.issue.insert({
367
+ expect(db.issue
368
+ .insert({
500
369
  id: 1,
501
370
  title: 'Revert the user profile page',
502
371
  priority: 2,
@@ -504,236 +373,144 @@ describe('query builder', () => {
504
373
  modified: new Date('2024-12-29T17:15:20.507Z'),
505
374
  kanbanorder: 'a2',
506
375
  creator: 'John Doe',
507
- }))).toMatchInlineSnapshot(`
508
- {
509
- "bindValues": [
510
- 1,
511
- "Revert the user profile page",
512
- 2,
513
- 1722532520507,
514
- 1735492520507,
515
- "a2",
516
- "John Doe",
517
- ],
518
- "query": "INSERT INTO 'issue' ("id", "title", "priority", "created", "modified", "kanbanorder", "creator") VALUES (?, ?, ?, ?, ?, ?, ?)",
519
- "schema": "number",
520
- }
521
- `);
376
+ })
377
+ .asSql()).toEqual({
378
+ bindValues: [1, 'Revert the user profile page', 2, 1722532520507, 1735492520507, 'a2', 'John Doe'],
379
+ query: 'INSERT INTO \'issue\' ("id", "title", "priority", "created", "modified", "kanbanorder", "creator") VALUES (?, ?, ?, ?, ?, ?, ?)',
380
+ usedTables: new Set(['issue']),
381
+ });
522
382
  });
523
383
  it('should handle UPDATE queries', () => {
524
- expect(dump(db.todos.update({ status: 'completed' }).where({ id: '123' }))).toMatchInlineSnapshot(`
525
- {
526
- "bindValues": [
527
- "completed",
528
- "123",
529
- ],
530
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ?",
531
- "schema": "number",
532
- }
533
- `);
384
+ expect(db.todos.update({ status: 'completed' }).where({ id: '123' }).asSql()).toEqual({
385
+ bindValues: ['completed', '123'],
386
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ?',
387
+ usedTables: new Set(['todos']),
388
+ });
534
389
  // empty update set
535
- expect(dump(db.todos.update({}).where({ id: '123' }))).toMatchInlineSnapshot(`
536
- {
537
- "bindValues": [],
538
- "query": "SELECT 1",
539
- "schema": "number",
540
- }
541
- `);
390
+ expect(db.todos.update({}).where({ id: '123' }).asSql()).toEqual({
391
+ bindValues: [],
392
+ query: 'SELECT 1',
393
+ usedTables: new Set(['todos']),
394
+ });
542
395
  });
543
396
  it('should handle UPDATE queries with undefined values', () => {
544
- expect(dump(db.todos.update({ text: 'some text' }).where({ id: '123' }))).toMatchInlineSnapshot(`
545
- {
546
- "bindValues": [
547
- "some text",
548
- "123",
549
- ],
550
- "query": "UPDATE 'todos' SET "text" = ? WHERE "id" = ?",
551
- "schema": "number",
552
- }
553
- `);
397
+ expect(db.todos.update({ text: 'some text' }).where({ id: '123' }).asSql()).toEqual({
398
+ bindValues: ['some text', '123'],
399
+ query: 'UPDATE \'todos\' SET "text" = ? WHERE "id" = ?',
400
+ usedTables: new Set(['todos']),
401
+ });
554
402
  });
555
403
  it('should handle UPDATE queries with undefined values (issue)', () => {
556
- expect(dump(db.issue.update({ priority: 2, creator: 'John Doe' }).where({ id: 1 }))).toMatchInlineSnapshot(`
557
- {
558
- "bindValues": [
559
- 2,
560
- "John Doe",
561
- 1,
562
- ],
563
- "query": "UPDATE 'issue' SET "priority" = ?, "creator" = ? WHERE "id" = ?",
564
- "schema": "number",
565
- }
566
- `);
404
+ expect(db.issue.update({ priority: 2, creator: 'John Doe' }).where({ id: 1 }).asSql()).toEqual({
405
+ bindValues: [2, 'John Doe', 1],
406
+ query: 'UPDATE \'issue\' SET "priority" = ?, "creator" = ? WHERE "id" = ?',
407
+ usedTables: new Set(['issue']),
408
+ });
567
409
  });
568
410
  it('should handle DELETE queries', () => {
569
- expect(dump(db.todos.delete().where({ status: 'completed' }))).toMatchInlineSnapshot(`
570
- {
571
- "bindValues": [
572
- "completed",
573
- ],
574
- "query": "DELETE FROM 'todos' WHERE "status" = ?",
575
- "schema": "number",
576
- }
577
- `);
411
+ expect(db.todos.delete().where({ status: 'completed' }).asSql()).toEqual({
412
+ bindValues: ['completed'],
413
+ query: 'DELETE FROM \'todos\' WHERE "status" = ?',
414
+ usedTables: new Set(['todos']),
415
+ });
578
416
  });
579
417
  it('should handle INSERT with ON CONFLICT', () => {
580
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore'))).toMatchInlineSnapshot(`
581
- {
582
- "bindValues": [
583
- "123",
584
- "Buy milk",
585
- "active",
586
- ],
587
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO NOTHING",
588
- "schema": "number",
589
- }
590
- `);
591
- expect(dump(db.todos
418
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore').asSql()).toEqual({
419
+ bindValues: ['123', 'Buy milk', 'active'],
420
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO NOTHING',
421
+ usedTables: new Set(['todos']),
422
+ });
423
+ expect(db.todos
592
424
  .insert({ id: '123', text: 'Buy milk', status: 'active' })
593
- .onConflict('id', 'update', { text: 'Buy soy milk', status: 'active' }))).toMatchInlineSnapshot(`
594
- {
595
- "bindValues": [
596
- "123",
597
- "Buy milk",
598
- "active",
599
- "Buy soy milk",
600
- "active",
601
- ],
602
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO UPDATE SET "text" = ?, "status" = ?",
603
- "schema": "number",
604
- }
605
- `);
606
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace'))).toMatchInlineSnapshot(`
607
- {
608
- "bindValues": [
609
- "123",
610
- "Buy milk",
611
- "active",
612
- ],
613
- "query": "INSERT OR REPLACE INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
614
- "schema": "number",
615
- }
616
- `);
425
+ .onConflict('id', 'update', { text: 'Buy soy milk', status: 'active' })
426
+ .asSql()).toEqual({
427
+ bindValues: ['123', 'Buy milk', 'active', 'Buy soy milk', 'active'],
428
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO UPDATE SET "text" = ?, "status" = ?',
429
+ usedTables: new Set(['todos']),
430
+ });
431
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace').asSql()).toEqual({
432
+ bindValues: ['123', 'Buy milk', 'active'],
433
+ query: 'INSERT OR REPLACE INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?)',
434
+ usedTables: new Set(['todos']),
435
+ });
617
436
  });
618
437
  it('should quote reserved column names', () => {
619
- expect(dump(db.selections.insert({ id: 1, group: 'alpha' }).onConflict('id', 'ignore'))).toMatchInlineSnapshot(`
620
- {
621
- "bindValues": [
622
- 1,
623
- "alpha",
624
- ],
625
- "query": "INSERT INTO 'selections' ("id", "group") VALUES (?, ?) ON CONFLICT ("id") DO NOTHING",
626
- "schema": "number",
627
- }
628
- `);
629
- expect(dump(db.selections.update({ group: 'beta' }).where({ id: 1 }))).toMatchInlineSnapshot(`
630
- {
631
- "bindValues": [
632
- "beta",
633
- 1,
634
- ],
635
- "query": "UPDATE 'selections' SET "group" = ? WHERE "id" = ?",
636
- "schema": "number",
637
- }
638
- `);
438
+ expect(db.selections.insert({ id: 1, group: 'alpha' }).onConflict('id', 'ignore').asSql()).toEqual({
439
+ bindValues: [1, 'alpha'],
440
+ query: 'INSERT INTO \'selections\' ("id", "group") VALUES (?, ?) ON CONFLICT ("id") DO NOTHING',
441
+ usedTables: new Set(['selections']),
442
+ });
443
+ expect(db.selections.update({ group: 'beta' }).where({ id: 1 }).asSql()).toEqual({
444
+ bindValues: ['beta', 1],
445
+ query: 'UPDATE \'selections\' SET "group" = ? WHERE "id" = ?',
446
+ usedTables: new Set(['selections']),
447
+ });
639
448
  });
640
449
  it('should handle ON CONFLICT with multiple columns', () => {
641
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict(['id', 'status'], 'ignore'))).toMatchInlineSnapshot(`
642
- {
643
- "bindValues": [
644
- "123",
645
- "Buy milk",
646
- "active",
647
- ],
648
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id", "status") DO NOTHING",
649
- "schema": "number",
650
- }
651
- `);
450
+ expect(db.todos
451
+ .insert({ id: '123', text: 'Buy milk', status: 'active' })
452
+ .onConflict(['id', 'status'], 'ignore')
453
+ .asSql()).toEqual({
454
+ bindValues: ['123', 'Buy milk', 'active'],
455
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id", "status") DO NOTHING',
456
+ usedTables: new Set(['todos']),
457
+ });
652
458
  });
653
459
  it('should handle RETURNING clause', () => {
654
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id'))).toMatchInlineSnapshot(`
655
- {
656
- "bindValues": [
657
- "123",
658
- "Buy milk",
659
- "active",
660
- ],
661
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) RETURNING "id"",
662
- "schema": "ReadonlyArray<{ readonly id: string }>",
663
- }
664
- `);
665
- expect(dump(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id'))).toMatchInlineSnapshot(`
666
- {
667
- "bindValues": [
668
- "completed",
669
- "123",
670
- ],
671
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ? RETURNING "id"",
672
- "schema": "ReadonlyArray<{ readonly id: string }>",
673
- }
674
- `);
675
- expect(dump(db.todos.delete().where({ status: 'completed' }).returning('id'))).toMatchInlineSnapshot(`
676
- {
677
- "bindValues": [
678
- "completed",
679
- ],
680
- "query": "DELETE FROM 'todos' WHERE "status" = ? RETURNING "id"",
681
- "schema": "ReadonlyArray<{ readonly id: string }>",
682
- }
683
- `);
460
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id').asSql()).toEqual({
461
+ bindValues: ['123', 'Buy milk', 'active'],
462
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) RETURNING "id"',
463
+ usedTables: new Set(['todos']),
464
+ });
465
+ expect(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id').asSql()).toEqual({
466
+ bindValues: ['completed', '123'],
467
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ? RETURNING "id"',
468
+ usedTables: new Set(['todos']),
469
+ });
470
+ expect(db.todos.delete().where({ status: 'completed' }).returning('id').asSql()).toEqual({
471
+ bindValues: ['completed'],
472
+ query: 'DELETE FROM \'todos\' WHERE "status" = ? RETURNING "id"',
473
+ usedTables: new Set(['todos']),
474
+ });
475
+ });
476
+ it('derives result schemas for RETURNING clauses', async () => {
477
+ const returningId = new TestSchema.Asserts(getResultSchema(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')));
478
+ await returningId.decoding().succeed([{ id: '123' }]);
684
479
  });
685
480
  it('should handle where().delete() - preserving where clauses', () => {
686
- expect(dump(db.todos.where({ status: 'completed' }).delete())).toMatchInlineSnapshot(`
687
- {
688
- "bindValues": [
689
- "completed",
690
- ],
691
- "query": "DELETE FROM 'todos' WHERE "status" = ?",
692
- "schema": "number",
693
- }
694
- `);
481
+ expect(db.todos.where({ status: 'completed' }).delete().asSql()).toEqual({
482
+ bindValues: ['completed'],
483
+ query: 'DELETE FROM \'todos\' WHERE "status" = ?',
484
+ usedTables: new Set(['todos']),
485
+ });
695
486
  // Multiple where clauses
696
- expect(dump(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete())).toMatchInlineSnapshot(`
697
- {
698
- "bindValues": [
699
- "completed",
700
- ],
701
- "query": "DELETE FROM 'todos' WHERE "status" = ? AND "deletedAt" IS NULL",
702
- "schema": "number",
703
- }
704
- `);
487
+ expect(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete().asSql()).toEqual({
488
+ bindValues: ['completed'],
489
+ query: 'DELETE FROM \'todos\' WHERE "status" = ? AND "deletedAt" IS NULL',
490
+ usedTables: new Set(['todos']),
491
+ });
705
492
  });
706
493
  it('should handle where().update() - preserving where clauses', () => {
707
- expect(dump(db.todos.where({ id: '123' }).update({ status: 'completed' }))).toMatchInlineSnapshot(`
708
- {
709
- "bindValues": [
710
- "completed",
711
- "123",
712
- ],
713
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ?",
714
- "schema": "number",
715
- }
716
- `);
494
+ expect(db.todos.where({ id: '123' }).update({ status: 'completed' }).asSql()).toEqual({
495
+ bindValues: ['completed', '123'],
496
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ?',
497
+ usedTables: new Set(['todos']),
498
+ });
717
499
  // Multiple where clauses
718
- expect(dump(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' }))).toMatchInlineSnapshot(`
719
- {
720
- "bindValues": [
721
- "completed",
722
- "123",
723
- ],
724
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ? AND "deletedAt" IS NULL",
725
- "schema": "number",
726
- }
727
- `);
500
+ expect(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' }).asSql()).toEqual({
501
+ bindValues: ['completed', '123'],
502
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ? AND "deletedAt" IS NULL',
503
+ usedTables: new Set(['todos']),
504
+ });
728
505
  });
729
506
  it('should have equivalent behavior for both delete patterns', () => {
730
- const pattern1 = dump(db.todos.where({ status: 'completed', id: '123' }).delete());
731
- const pattern2 = dump(db.todos.delete().where({ status: 'completed', id: '123' }));
507
+ const pattern1 = db.todos.where({ status: 'completed', id: '123' }).delete().asSql();
508
+ const pattern2 = db.todos.delete().where({ status: 'completed', id: '123' }).asSql();
732
509
  expect(pattern1).toEqual(pattern2);
733
510
  });
734
511
  it('should have equivalent behavior for both update patterns', () => {
735
- const pattern1 = dump(db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }));
736
- const pattern2 = dump(db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }));
512
+ const pattern1 = db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }).asSql();
513
+ const pattern2 = db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }).asSql();
737
514
  expect(pattern1).toEqual(pattern2);
738
515
  });
739
516
  });
@@ -744,14 +521,14 @@ describe('query builder', () => {
744
521
  contactLastName: Schema.String,
745
522
  contactEmail: Schema.String.pipe(State.SQLite.withUnique),
746
523
  });
747
- const Nested = Schema.transform(Flat, Schema.Struct({
524
+ const Nested = Flat.pipe(Schema.decodeTo(Schema.Struct({
748
525
  id: Schema.String,
749
526
  contact: Schema.Struct({
750
527
  firstName: Schema.String,
751
528
  lastName: Schema.String,
752
529
  email: Schema.String,
753
530
  }),
754
- }), {
531
+ }), SchemaTransformation.transform({
755
532
  decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
756
533
  id,
757
534
  contact: {
@@ -766,7 +543,7 @@ describe('query builder', () => {
766
543
  contactLastName: contact.lastName,
767
544
  contactEmail: contact.email,
768
545
  }),
769
- });
546
+ })));
770
547
  const makeContactsTable = () => State.SQLite.table({
771
548
  name: 'contacts',
772
549
  schema: Nested,
@@ -776,7 +553,7 @@ describe('query builder', () => {
776
553
  const contactsTable = makeContactsTable();
777
554
  void contactsTable;
778
555
  });
779
- it('fails to encode nested inserts because flat columns are required', () => {
556
+ it('encodes flat insert values for transformed schemas', () => {
780
557
  const contactsTable = makeContactsTable();
781
558
  expect(contactsTable
782
559
  // TODO in the future we should use decoded types here instead of encoded
@@ -786,22 +563,13 @@ describe('query builder', () => {
786
563
  contactLastName: 'Lovelace',
787
564
  contactEmail: 'ada@example.com',
788
565
  })
789
- .asSql()).toMatchInlineSnapshot(`
790
- {
791
- "bindValues": [
792
- "person-1",
793
- "Ada",
794
- "Lovelace",
795
- "ada@example.com",
796
- ],
797
- "query": "INSERT INTO 'contacts' ("id", "contactFirstName", "contactLastName", "contactEmail") VALUES (?, ?, ?, ?)",
798
- "usedTables": Set {
799
- "contacts",
800
- },
801
- }
802
- `);
803
- });
804
- it('fails to encode nested inserts because flat columns are required', () => {
566
+ .asSql()).toEqual({
567
+ bindValues: ['person-1', 'Ada', 'Lovelace', 'ada@example.com'],
568
+ query: 'INSERT INTO \'contacts\' ("id", "contactFirstName", "contactLastName", "contactEmail") VALUES (?, ?, ?, ?)',
569
+ usedTables: new Set(['contacts']),
570
+ });
571
+ });
572
+ it('rejects nested insert values because flat columns are required', () => {
805
573
  const contactsTable = makeContactsTable();
806
574
  expect(() => contactsTable
807
575
  .insert({
@@ -813,14 +581,12 @@ describe('query builder', () => {
813
581
  email: 'ada@example.com',
814
582
  },
815
583
  })
816
- .asSql()).toThrowErrorMatchingInlineSnapshot(`
817
- [ParseError: contacts\n└─ ["contactFirstName"]\n └─ is missing]
818
- `);
584
+ .asSql()).toThrowError(/contactFirstName/);
819
585
  });
820
586
  });
821
587
  });
822
588
  // TODO nested queries
823
- // const rawSql = <A, I>(sql: string, params: { [key: string]: any }, schema: Schema.Schema<A, I>) =>
589
+ // const rawSql = <A, I>(sql: string, params: { [key: string]: any }, schema: Schema.Codec<A, I>) =>
824
590
  // ({
825
591
  // sql,
826
592
  // params,