@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,10 +1,9 @@
1
+ import { TestSchema } from 'effect/testing'
1
2
  import { describe, expect, it } from 'vitest'
2
3
 
3
- import { Schema } from '@livestore/utils/effect'
4
- import { objectToString } from '@livestore/utils'
4
+ import { Schema, SchemaTransformation } from '@livestore/utils/effect'
5
5
 
6
6
  import { State } from '../../../mod.ts'
7
- import type { QueryBuilder } from './api.ts'
8
7
  import { getResultSchema } from './impl.ts'
9
8
 
10
9
  const todos = State.SQLite.table({
@@ -13,7 +12,7 @@ const todos = State.SQLite.table({
13
12
  id: State.SQLite.text({ primaryKey: true }),
14
13
  text: State.SQLite.text({ default: '', nullable: false }),
15
14
  completed: State.SQLite.boolean({ default: false, nullable: false }),
16
- status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
15
+ status: State.SQLite.text({ schema: Schema.Literals(['active', 'completed']) }),
17
16
  deletedAt: State.SQLite.datetime({ nullable: true }),
18
17
  // TODO consider leaning more into Effect schema
19
18
  // other: Schema.Number.pipe(State.SQLite.asInteger),
@@ -25,7 +24,7 @@ const todosWithIntId = State.SQLite.table({
25
24
  columns: {
26
25
  id: State.SQLite.integer({ primaryKey: true }),
27
26
  text: State.SQLite.text({ default: '', nullable: false }),
28
- status: State.SQLite.text({ schema: Schema.Literal('active', 'completed') }),
27
+ status: State.SQLite.text({ schema: Schema.Literals(['active', 'completed']) }),
29
28
  },
30
29
  })
31
30
 
@@ -41,7 +40,7 @@ const comments = State.SQLite.table({
41
40
  const UiState = State.SQLite.clientDocument({
42
41
  name: 'UiState',
43
42
  schema: Schema.Struct({
44
- filter: Schema.Literal('all', 'active', 'completed'),
43
+ filter: Schema.Literals(['all', 'active', 'completed']),
45
44
  }),
46
45
  default: { value: { filter: 'all' } },
47
46
  })
@@ -49,7 +48,7 @@ const UiState = State.SQLite.clientDocument({
49
48
  const UiStateWithDefaultId = State.SQLite.clientDocument({
50
49
  name: 'UiState',
51
50
  schema: Schema.Struct({
52
- filter: Schema.Literal('all', 'active', 'completed'),
51
+ filter: Schema.Literals(['all', 'active', 'completed']),
53
52
  }),
54
53
  default: {
55
54
  id: 'static',
@@ -63,10 +62,10 @@ const issue = State.SQLite.table({
63
62
  id: State.SQLite.integer({ primaryKey: true }),
64
63
  title: State.SQLite.text({ default: '' }),
65
64
  creator: State.SQLite.text({ default: '' }),
66
- priority: State.SQLite.integer({ schema: Schema.Literal(0, 1, 2, 3, 4), default: 0 }),
67
- created: State.SQLite.integer({ schema: Schema.DateFromNumber }),
68
- deleted: State.SQLite.integer({ nullable: true, schema: Schema.DateFromNumber }),
69
- modified: State.SQLite.integer({ schema: Schema.DateFromNumber }),
65
+ priority: State.SQLite.integer({ schema: Schema.Literals([0, 1, 2, 3, 4]), default: 0 }),
66
+ created: State.SQLite.integer({ schema: Schema.DateFromMillis }),
67
+ deleted: State.SQLite.integer({ nullable: true, schema: Schema.DateFromMillis }),
68
+ modified: State.SQLite.integer({ schema: Schema.DateFromMillis }),
70
69
  kanbanorder: State.SQLite.text({ nullable: false, default: '' }),
71
70
  },
72
71
  indexes: [
@@ -83,7 +82,7 @@ const selections = State.SQLite.table({
83
82
  },
84
83
  })
85
84
 
86
- const Source = Schema.Literal('google', 'linkedin', 'facebook')
85
+ const Source = Schema.Literals(['google', 'linkedin', 'facebook'])
87
86
  const ProfileAttribute = Schema.Struct({ key: Schema.String, value: Schema.String })
88
87
 
89
88
  const personProfiles = State.SQLite.table({
@@ -100,483 +99,352 @@ const personProfiles = State.SQLite.table({
100
99
 
101
100
  const db = { todos, todosWithIntId, comments, issue, selections, UiState, UiStateWithDefaultId, personProfiles }
102
101
 
103
- const dump = (qb: QueryBuilder<any, any, any>) => ({
104
- bindValues: qb.asSql().bindValues,
105
- query: qb.asSql().query,
106
- schema: objectToString(getResultSchema(qb)),
107
- })
108
-
109
102
  describe('query builder', () => {
110
103
  describe('basic queries', () => {
111
104
  it('should handle simple SELECT queries', () => {
112
- expect(dump(db.todos)).toMatchInlineSnapshot(`
113
- {
114
- "bindValues": [],
115
- "query": "SELECT * FROM 'todos'",
116
- "schema": "ReadonlyArray<todos>",
117
- }
118
- `)
105
+ expect(db.todos.asSql()).toEqual({
106
+ bindValues: [],
107
+ query: "SELECT * FROM 'todos'",
108
+ usedTables: new Set(['todos']),
109
+ })
119
110
 
120
- expect(dump(db.todos.select('id'))).toMatchInlineSnapshot(`
121
- {
122
- "bindValues": [],
123
- "query": "SELECT "id" FROM 'todos'",
124
- "schema": "ReadonlyArray<({ readonly id: string } <-> string)>",
125
- }
126
- `)
111
+ expect(db.todos.select('id').asSql()).toEqual({
112
+ bindValues: [],
113
+ query: 'SELECT "id" FROM \'todos\'',
114
+ usedTables: new Set(['todos']),
115
+ })
127
116
 
128
- expect(dump(db.todos.select('id', 'text'))).toMatchInlineSnapshot(`
129
- {
130
- "bindValues": [],
131
- "query": "SELECT "id", "text" FROM 'todos'",
132
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
133
- }
134
- `)
117
+ expect(db.todos.select('id', 'text').asSql()).toEqual({
118
+ bindValues: [],
119
+ query: 'SELECT "id", "text" FROM \'todos\'',
120
+ usedTables: new Set(['todos']),
121
+ })
122
+ })
123
+
124
+ it('derives result schemas for SELECT queries', async () => {
125
+ const selectedRows = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text')))
126
+ await selectedRows.decoding().succeed([{ id: '123', text: 'Buy milk' }])
127
+
128
+ const selectedColumn = new TestSchema.Asserts(getResultSchema(db.todos.select('id')))
129
+ await selectedColumn.decoding().succeed([{ id: '123' }], ['123'])
135
130
  })
136
131
 
137
132
  it('should handle .first()', () => {
138
- expect(dump(db.todos.select('id', 'text').first())).toMatchInlineSnapshot(`
139
- {
140
- "bindValues": [
141
- 1,
142
- ],
143
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
144
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string } | undefined> <-> { readonly id: string; readonly text: string } | undefined)",
145
- }
146
- `)
147
-
148
- expect(dump(db.todos.select('id', 'text').first({ behaviour: 'error' }))).toMatchInlineSnapshot(`
149
- {
150
- "bindValues": [
151
- 1,
152
- ],
153
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
154
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> <-> { readonly id: string; readonly text: string })",
155
- }
156
- `)
133
+ expect(db.todos.select('id', 'text').first().asSql()).toEqual({
134
+ bindValues: [1],
135
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
136
+ usedTables: new Set(['todos']),
137
+ })
138
+
139
+ expect(db.todos.select('id', 'text').first({ behaviour: 'error' }).asSql()).toEqual({
140
+ bindValues: [1],
141
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
142
+ usedTables: new Set(['todos']),
143
+ })
157
144
 
158
145
  expect(
159
- dump(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined })),
160
- ).toMatchInlineSnapshot(`
161
- {
162
- "bindValues": [
163
- 1,
164
- ],
165
- "query": "SELECT "id", "text" FROM 'todos' LIMIT ?",
166
- "schema": "(ReadonlyArray<{ readonly id: string; readonly text: string }> | readonly [undefined] <-> { readonly id: string; readonly text: string } | undefined)",
167
- }
168
- `)
146
+ db.todos
147
+ .select('id', 'text')
148
+ .first({ behaviour: 'fallback', fallback: () => undefined })
149
+ .asSql(),
150
+ ).toEqual({
151
+ bindValues: [1],
152
+ query: 'SELECT "id", "text" FROM \'todos\' LIMIT ?',
153
+ usedTables: new Set(['todos']),
154
+ })
155
+ })
156
+
157
+ it('derives result schemas for .first()', async () => {
158
+ const firstOrUndefined = new TestSchema.Asserts(getResultSchema(db.todos.select('id', 'text').first()))
159
+ await firstOrUndefined.decoding().succeed([], undefined)
160
+ await firstOrUndefined.decoding().succeed([{ id: '123', text: 'Buy milk' }], { id: '123', text: 'Buy milk' })
161
+
162
+ const firstOrError = new TestSchema.Asserts(
163
+ getResultSchema(db.todos.select('id', 'text').first({ behaviour: 'error' })),
164
+ )
165
+ await firstOrError.decoding().succeed([{ id: '123', text: 'Buy milk' }], { id: '123', text: 'Buy milk' })
166
+ await firstOrError.decoding().fail([], 'Unable to retrieve the first element of an empty array')
167
+
168
+ const firstOrFallback = new TestSchema.Asserts(
169
+ getResultSchema(db.todos.select('id', 'text').first({ behaviour: 'fallback', fallback: () => undefined })),
170
+ )
171
+ await firstOrFallback.decoding().succeed([], undefined)
169
172
  })
170
173
 
171
174
  it('should handle WHERE clauses', () => {
172
- expect(dump(db.todos.select('id', 'text').where('completed', true))).toMatchInlineSnapshot(`
173
- {
174
- "bindValues": [
175
- 1,
176
- ],
177
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ?",
178
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
179
- }
180
- `)
181
- expect(dump(db.todos.select('id', 'text').where('completed', '!=', true))).toMatchInlineSnapshot(`
182
- {
183
- "bindValues": [
184
- 1,
185
- ],
186
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" != ?",
187
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
188
- }
189
- `)
190
- expect(dump(db.todos.select('id', 'text').where({ completed: true }))).toMatchInlineSnapshot(`
191
- {
192
- "bindValues": [
193
- 1,
194
- ],
195
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ?",
196
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
197
- }
198
- `)
199
- expect(dump(db.todos.select('id', 'text').where({ completed: undefined }))).toMatchInlineSnapshot(`
200
- {
201
- "bindValues": [],
202
- "query": "SELECT "id", "text" FROM 'todos'",
203
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
204
- }
205
- `)
175
+ expect(db.todos.select('id', 'text').where('completed', true).asSql()).toEqual({
176
+ bindValues: [1],
177
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ?',
178
+ usedTables: new Set(['todos']),
179
+ })
180
+ expect(db.todos.select('id', 'text').where('completed', '!=', true).asSql()).toEqual({
181
+ bindValues: [1],
182
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" != ?',
183
+ usedTables: new Set(['todos']),
184
+ })
185
+ expect(db.todos.select('id', 'text').where({ completed: true }).asSql()).toEqual({
186
+ bindValues: [1],
187
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ?',
188
+ usedTables: new Set(['todos']),
189
+ })
190
+ expect(db.todos.select('id', 'text').where({ completed: undefined }).asSql()).toEqual({
191
+ bindValues: [],
192
+ query: 'SELECT "id", "text" FROM \'todos\'',
193
+ usedTables: new Set(['todos']),
194
+ })
206
195
  expect(
207
- dump(db.todos.select('id', 'text').where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })),
208
- ).toMatchInlineSnapshot(`
209
- {
210
- "bindValues": [
211
- "2024-01-01T00:00:00.000Z",
212
- ],
213
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" <= ?",
214
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
215
- }
216
- `)
196
+ db.todos
197
+ .select('id', 'text')
198
+ .where({ deletedAt: { op: '<=', value: new Date('2024-01-01') } })
199
+ .asSql(),
200
+ ).toEqual({
201
+ bindValues: ['2024-01-01T00:00:00.000Z'],
202
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" <= ?',
203
+ usedTables: new Set(['todos']),
204
+ })
217
205
  expect(
218
- dump(db.todos.select('id', 'text').where({ status: { op: 'IN', value: ['active'] } })),
219
- ).toMatchInlineSnapshot(`
220
- {
221
- "bindValues": [
222
- "active",
223
- ],
224
- "query": "SELECT "id", "text" FROM 'todos' WHERE "status" IN (?)",
225
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
226
- }
227
- `)
206
+ db.todos
207
+ .select('id', 'text')
208
+ .where({ status: { op: 'IN', value: ['active'] } })
209
+ .asSql(),
210
+ ).toEqual({
211
+ bindValues: ['active'],
212
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "status" IN (?)',
213
+ usedTables: new Set(['todos']),
214
+ })
228
215
  expect(
229
- dump(db.todos.select('id', 'text').where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })),
230
- ).toMatchInlineSnapshot(`
231
- {
232
- "bindValues": [
233
- "active",
234
- "completed",
235
- ],
236
- "query": "SELECT "id", "text" FROM 'todos' WHERE "status" NOT IN (?, ?)",
237
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
238
- }
239
- `)
216
+ db.todos
217
+ .select('id', 'text')
218
+ .where({ status: { op: 'NOT IN', value: ['active', 'completed'] } })
219
+ .asSql(),
220
+ ).toEqual({
221
+ bindValues: ['active', 'completed'],
222
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "status" NOT IN (?, ?)',
223
+ usedTables: new Set(['todos']),
224
+ })
240
225
 
241
226
  expect(
242
- dump(
243
- db.todos
244
- .select('id', 'text')
245
- .where({ completed: false })
246
- .where({ status: { op: 'IN', value: ['active'] } })
247
- .where({ deletedAt: undefined }),
248
- ),
249
- ).toMatchInlineSnapshot(`
250
- {
251
- "bindValues": [
252
- 0,
253
- "active",
254
- ],
255
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? AND "status" IN (?)",
256
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
257
- }
258
- `)
227
+ db.todos
228
+ .select('id', 'text')
229
+ .where({ completed: false })
230
+ .where({ status: { op: 'IN', value: ['active'] } })
231
+ .where({ deletedAt: undefined })
232
+ .asSql(),
233
+ ).toEqual({
234
+ bindValues: [0, 'active'],
235
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? AND "status" IN (?)',
236
+ usedTables: new Set(['todos']),
237
+ })
259
238
  })
260
239
 
261
240
  it('should handle OFFSET and LIMIT clauses', () => {
262
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(10).limit(10))).toMatchInlineSnapshot(`
263
- {
264
- "bindValues": [
265
- 1,
266
- 10,
267
- 10,
268
- ],
269
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ? OFFSET ?",
270
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
271
- }
272
- `)
241
+ expect(db.todos.select('id', 'text').where('completed', true).offset(10).limit(10).asSql()).toEqual({
242
+ bindValues: [1, 10, 10],
243
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ? OFFSET ?',
244
+ usedTables: new Set(['todos']),
245
+ })
273
246
  })
274
247
 
275
248
  it('should handle OFFSET and LIMIT clauses correctly', () => {
276
249
  // Test with both offset and limit
277
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(5).limit(10))).toMatchInlineSnapshot(`
278
- {
279
- "bindValues": [
280
- 1,
281
- 10,
282
- 5,
283
- ],
284
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ? OFFSET ?",
285
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
286
- }
287
- `)
250
+ expect(db.todos.select('id', 'text').where('completed', true).offset(5).limit(10).asSql()).toEqual({
251
+ bindValues: [1, 10, 5],
252
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ? OFFSET ?',
253
+ usedTables: new Set(['todos']),
254
+ })
288
255
 
289
256
  // Test with only offset
290
- expect(dump(db.todos.select('id', 'text').where('completed', true).offset(5))).toMatchInlineSnapshot(`
291
- {
292
- "bindValues": [
293
- 1,
294
- 5,
295
- ],
296
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? OFFSET ?",
297
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
298
- }
299
- `)
257
+ expect(db.todos.select('id', 'text').where('completed', true).offset(5).asSql()).toEqual({
258
+ bindValues: [1, 5],
259
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? OFFSET ?',
260
+ usedTables: new Set(['todos']),
261
+ })
300
262
 
301
263
  // Test with only limit
302
- expect(dump(db.todos.select('id', 'text').where('completed', true).limit(10))).toMatchInlineSnapshot(`
303
- {
304
- "bindValues": [
305
- 1,
306
- 10,
307
- ],
308
- "query": "SELECT "id", "text" FROM 'todos' WHERE "completed" = ? LIMIT ?",
309
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
310
- }
311
- `)
264
+ expect(db.todos.select('id', 'text').where('completed', true).limit(10).asSql()).toEqual({
265
+ bindValues: [1, 10],
266
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "completed" = ? LIMIT ?',
267
+ usedTables: new Set(['todos']),
268
+ })
312
269
  })
313
270
 
314
271
  it('should handle COUNT queries', () => {
315
- expect(dump(db.todos.count())).toMatchInlineSnapshot(`
316
- {
317
- "bindValues": [],
318
- "query": "SELECT COUNT(*) as count FROM 'todos'",
319
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
320
- }
321
- `)
322
- expect(dump(db.todos.count().where('completed', true))).toMatchInlineSnapshot(`
323
- {
324
- "bindValues": [
325
- 1,
326
- ],
327
- "query": "SELECT COUNT(*) as count FROM 'todos' WHERE "completed" = ?",
328
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
329
- }
330
- `)
331
- expect(dump(db.todos.where('completed', true).count())).toMatchInlineSnapshot(`
332
- {
333
- "bindValues": [
334
- 1,
335
- ],
336
- "query": "SELECT COUNT(*) as count FROM 'todos' WHERE "completed" = ?",
337
- "schema": "(ReadonlyArray<({ readonly count: number } <-> number)> <-> number)",
338
- }
339
- `)
272
+ expect(db.todos.count().asSql()).toEqual({
273
+ bindValues: [],
274
+ query: "SELECT COUNT(*) as count FROM 'todos'",
275
+ usedTables: new Set(['todos']),
276
+ })
277
+ expect(db.todos.count().where('completed', true).asSql()).toEqual({
278
+ bindValues: [1],
279
+ query: 'SELECT COUNT(*) as count FROM \'todos\' WHERE "completed" = ?',
280
+ usedTables: new Set(['todos']),
281
+ })
282
+ expect(db.todos.where('completed', true).count().asSql()).toEqual({
283
+ bindValues: [1],
284
+ query: 'SELECT COUNT(*) as count FROM \'todos\' WHERE "completed" = ?',
285
+ usedTables: new Set(['todos']),
286
+ })
287
+ })
288
+
289
+ it('derives result schemas for COUNT queries', async () => {
290
+ const countResult = new TestSchema.Asserts(getResultSchema(db.todos.count()))
291
+ await countResult.decoding().succeed([{ count: 3 }], 3)
340
292
  })
341
293
 
342
294
  it('should handle NULL comparisons', () => {
343
- expect(dump(db.todos.select('id', 'text').where('deletedAt', '=', null))).toMatchInlineSnapshot(`
344
- {
345
- "bindValues": [],
346
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" IS NULL",
347
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
348
- }
349
- `)
350
- expect(dump(db.todos.select('id', 'text').where('deletedAt', '!=', null))).toMatchInlineSnapshot(`
351
- {
352
- "bindValues": [],
353
- "query": "SELECT "id", "text" FROM 'todos' WHERE "deletedAt" IS NOT NULL",
354
- "schema": "ReadonlyArray<{ readonly id: string; readonly text: string }>",
355
- }
356
- `)
295
+ expect(db.todos.select('id', 'text').where('deletedAt', '=', null).asSql()).toEqual({
296
+ bindValues: [],
297
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" IS NULL',
298
+ usedTables: new Set(['todos']),
299
+ })
300
+ expect(db.todos.select('id', 'text').where('deletedAt', '!=', null).asSql()).toEqual({
301
+ bindValues: [],
302
+ query: 'SELECT "id", "text" FROM \'todos\' WHERE "deletedAt" IS NOT NULL',
303
+ usedTables: new Set(['todos']),
304
+ })
357
305
  })
358
306
 
359
307
  it('should handle orderBy', () => {
360
- expect(dump(db.todos.orderBy('completed', 'desc'))).toMatchInlineSnapshot(`
361
- {
362
- "bindValues": [],
363
- "query": "SELECT * FROM 'todos' ORDER BY "completed" desc",
364
- "schema": "ReadonlyArray<todos>",
365
- }
366
- `)
308
+ expect(db.todos.orderBy('completed', 'desc').asSql()).toEqual({
309
+ bindValues: [],
310
+ query: 'SELECT * FROM \'todos\' ORDER BY "completed" desc',
311
+ usedTables: new Set(['todos']),
312
+ })
367
313
 
368
- expect(dump(db.todos.orderBy([{ col: 'completed', direction: 'desc' }]))).toMatchInlineSnapshot(`
369
- {
370
- "bindValues": [],
371
- "query": "SELECT * FROM 'todos' ORDER BY "completed" desc",
372
- "schema": "ReadonlyArray<todos>",
373
- }
374
- `)
314
+ expect(db.todos.orderBy([{ col: 'completed', direction: 'desc' }]).asSql()).toEqual({
315
+ bindValues: [],
316
+ query: 'SELECT * FROM \'todos\' ORDER BY "completed" desc',
317
+ usedTables: new Set(['todos']),
318
+ })
375
319
 
376
- expect(dump(db.todos.orderBy([]))).toMatchInlineSnapshot(`
377
- {
378
- "bindValues": [],
379
- "query": "SELECT * FROM 'todos'",
380
- "schema": "ReadonlyArray<todos>",
381
- }
382
- `)
320
+ expect(db.todos.orderBy([]).asSql()).toEqual({
321
+ bindValues: [],
322
+ query: "SELECT * FROM 'todos'",
323
+ usedTables: new Set(['todos']),
324
+ })
383
325
  })
384
326
 
385
327
  it('should handle JSON_CONTAINS operator for JSON array columns', () => {
386
- expect(
387
- dump(db.personProfiles.where({ sources: { op: 'JSON_CONTAINS', value: 'google' } })),
388
- ).toMatchInlineSnapshot(`
389
- {
390
- "bindValues": [
391
- "google",
392
- ],
393
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
394
- "schema": "ReadonlyArray<person_profiles>",
395
- }
396
- `)
328
+ expect(db.personProfiles.where({ sources: { op: 'JSON_CONTAINS', value: 'google' } }).asSql()).toEqual({
329
+ bindValues: ['google'],
330
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
331
+ usedTables: new Set(['person_profiles']),
332
+ })
397
333
 
398
334
  // With select
399
335
  expect(
400
- dump(db.personProfiles.select('personId').where({ sources: { op: 'JSON_CONTAINS', value: 'linkedin' } })),
401
- ).toMatchInlineSnapshot(`
402
- {
403
- "bindValues": [
404
- "linkedin",
405
- ],
406
- "query": "SELECT "personId" FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
407
- "schema": "ReadonlyArray<({ readonly personId: string } <-> string)>",
408
- }
409
- `)
336
+ db.personProfiles
337
+ .select('personId')
338
+ .where({ sources: { op: 'JSON_CONTAINS', value: 'linkedin' } })
339
+ .asSql(),
340
+ ).toEqual({
341
+ bindValues: ['linkedin'],
342
+ query:
343
+ 'SELECT "personId" FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
344
+ usedTables: new Set(['person_profiles']),
345
+ })
410
346
 
411
347
  // With plain string array column
412
- expect(
413
- dump(db.personProfiles.where({ tags: { op: 'JSON_CONTAINS', value: 'important' } })),
414
- ).toMatchInlineSnapshot(`
415
- {
416
- "bindValues": [
417
- "important",
418
- ],
419
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("tags") WHERE value = ?)",
420
- "schema": "ReadonlyArray<person_profiles>",
421
- }
422
- `)
348
+ expect(db.personProfiles.where({ tags: { op: 'JSON_CONTAINS', value: 'important' } }).asSql()).toEqual({
349
+ bindValues: ['important'],
350
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("tags") WHERE value = ?)',
351
+ usedTables: new Set(['person_profiles']),
352
+ })
423
353
  })
424
354
 
425
355
  it('should handle JSON_NOT_CONTAINS operator for JSON array columns', () => {
426
- expect(
427
- dump(db.personProfiles.where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'google' } })),
428
- ).toMatchInlineSnapshot(`
429
- {
430
- "bindValues": [
431
- "google",
432
- ],
433
- "query": "SELECT * FROM 'person_profiles' WHERE NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)",
434
- "schema": "ReadonlyArray<person_profiles>",
435
- }
436
- `)
356
+ expect(db.personProfiles.where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'google' } }).asSql()).toEqual({
357
+ bindValues: ['google'],
358
+ query:
359
+ 'SELECT * FROM \'person_profiles\' WHERE NOT EXISTS (SELECT 1 FROM json_each("sources") WHERE value = ?)',
360
+ usedTables: new Set(['person_profiles']),
361
+ })
437
362
  })
438
363
 
439
364
  it('should JSON-stringify object elements for JSON_CONTAINS', () => {
440
365
  expect(
441
- dump(
442
- db.personProfiles.where({
366
+ db.personProfiles
367
+ .where({
443
368
  attributes: { op: 'JSON_CONTAINS', value: { key: 'language', value: 'typescript' } },
444
- }),
445
- ),
446
- ).toMatchInlineSnapshot(`
447
- {
448
- "bindValues": [
449
- "{"key":"language","value":"typescript"}",
450
- ],
451
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("attributes") WHERE value = ?)",
452
- "schema": "ReadonlyArray<person_profiles>",
453
- }
454
- `)
369
+ })
370
+ .asSql(),
371
+ ).toEqual({
372
+ bindValues: ['{"key":"language","value":"typescript"}'],
373
+ query: 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("attributes") WHERE value = ?)',
374
+ usedTables: new Set(['person_profiles']),
375
+ })
455
376
  })
456
377
 
457
378
  it('should handle combining JSON_CONTAINS with other WHERE clauses', () => {
458
379
  expect(
459
- dump(
460
- db.personProfiles
461
- .where({ sources: { op: 'JSON_CONTAINS', value: 'google' } })
462
- .where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'facebook' } }),
463
- ),
464
- ).toMatchInlineSnapshot(`
465
- {
466
- "bindValues": [
467
- "google",
468
- "facebook",
469
- ],
470
- "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 = ?)",
471
- "schema": "ReadonlyArray<person_profiles>",
472
- }
473
- `)
380
+ db.personProfiles
381
+ .where({ sources: { op: 'JSON_CONTAINS', value: 'google' } })
382
+ .where({ sources: { op: 'JSON_NOT_CONTAINS', value: 'facebook' } })
383
+ .asSql(),
384
+ ).toEqual({
385
+ bindValues: ['google', 'facebook'],
386
+ query:
387
+ '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 = ?)',
388
+ usedTables: new Set(['person_profiles']),
389
+ })
474
390
  })
475
391
 
476
392
  it('should handle JSON_CONTAINS on nullable JSON array columns', () => {
477
- expect(
478
- dump(db.personProfiles.where({ optionalTags: { op: 'JSON_CONTAINS', value: 'important' } })),
479
- ).toMatchInlineSnapshot(`
480
- {
481
- "bindValues": [
482
- "important",
483
- ],
484
- "query": "SELECT * FROM 'person_profiles' WHERE EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)",
485
- "schema": "ReadonlyArray<person_profiles>",
486
- }
487
- `)
393
+ expect(db.personProfiles.where({ optionalTags: { op: 'JSON_CONTAINS', value: 'important' } }).asSql()).toEqual({
394
+ bindValues: ['important'],
395
+ query:
396
+ 'SELECT * FROM \'person_profiles\' WHERE EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)',
397
+ usedTables: new Set(['person_profiles']),
398
+ })
488
399
 
489
400
  // With JSON_NOT_CONTAINS
490
- expect(
491
- dump(db.personProfiles.where({ optionalTags: { op: 'JSON_NOT_CONTAINS', value: 'archived' } })),
492
- ).toMatchInlineSnapshot(`
401
+ expect(db.personProfiles.where({ optionalTags: { op: 'JSON_NOT_CONTAINS', value: 'archived' } }).asSql()).toEqual(
493
402
  {
494
- "bindValues": [
495
- "archived",
496
- ],
497
- "query": "SELECT * FROM 'person_profiles' WHERE NOT EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)",
498
- "schema": "ReadonlyArray<person_profiles>",
499
- }
500
- `)
403
+ bindValues: ['archived'],
404
+ query:
405
+ 'SELECT * FROM \'person_profiles\' WHERE NOT EXISTS (SELECT 1 FROM json_each("optionalTags") WHERE value = ?)',
406
+ usedTables: new Set(['person_profiles']),
407
+ },
408
+ )
501
409
  })
502
410
 
503
411
  it('should throw error when using JSON_CONTAINS on non-JSON array column', () => {
504
412
  expect(() =>
505
413
  // Type system prevents this at compile time for non-array columns, but test runtime check
506
- dump(db.todos.where({ status: { op: 'JSON_CONTAINS', value: 'active' } } as any)),
414
+ db.todos.where({ status: { op: 'JSON_CONTAINS', value: 'active' } } as any).asSql(),
507
415
  ).toThrow('JSON_CONTAINS operator can only be used on JSON array columns')
508
416
  })
509
417
  })
510
418
 
511
- // describe('getOrCreate queries', () => {
512
- // it('should handle getOrCreate queries', () => {
513
- // expect(dump(db.UiState.getOrCreate('sessionid-1'))).toMatchInlineSnapshot(`
514
- // {
515
- // "bindValues": [
516
- // "sessionid-1",
517
- // ],
518
- // "query": "SELECT * FROM 'UiState' WHERE id = ?",
519
- // "schema": "...", // TODO determine schema
520
- // }
521
- // `)
522
- // })
523
-
524
- // it('should handle getOrCreate queries with default id', () => {
525
- // expect(dump(db.UiStateWithDefaultId.getOrCreate())).toMatchInlineSnapshot(`
526
- // {
527
- // "bindValues": [],
528
- // "query": "SELECT * FROM 'UiState' WHERE id = ?",
529
- // "schema": "...", // TODO determine schema
530
- // }
531
- // `)
532
- // })
533
- // // it('should handle row queries with numbers', () => {
534
- // // expect(dump(db.todosWithIntId.getOrCreate(123, { insertValues: { status: 'active' } }))).toMatchInlineSnapshot(`
535
- // // {
536
- // // "bindValues": [
537
- // // 123,
538
- // // ],
539
- // // "query": "SELECT * FROM 'todos_with_int_id' WHERE id = ?",
540
- // // "schema": "...", // TODO determine schema
541
- // // }
542
- // // `)
543
- // // })
544
- // })
545
-
546
419
  describe('write operations', () => {
547
420
  it('should handle INSERT queries', () => {
548
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
549
- {
550
- "bindValues": [
551
- "123",
552
- "Buy milk",
553
- "active",
554
- ],
555
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
556
- "schema": "number",
557
- }
558
- `)
421
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toEqual({
422
+ bindValues: ['123', 'Buy milk', 'active'],
423
+ query: `INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)`,
424
+ usedTables: new Set(['todos']),
425
+ })
426
+ })
427
+
428
+ it('derives result schemas for write queries', async () => {
429
+ const insertResult = new TestSchema.Asserts(
430
+ getResultSchema(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' })),
431
+ )
432
+ await insertResult.decoding().succeed(1)
559
433
  })
560
434
 
561
435
  it('should handle INSERT queries with undefined values', () => {
562
- expect(dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }))).toMatchInlineSnapshot(`
563
- {
564
- "bindValues": [
565
- "123",
566
- "Buy milk",
567
- "active",
568
- ],
569
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
570
- "schema": "number",
571
- }
572
- `)
436
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).asSql()).toEqual({
437
+ bindValues: ['123', 'Buy milk', 'active'],
438
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?)',
439
+ usedTables: new Set(['todos']),
440
+ })
573
441
  })
574
442
 
575
443
  // Test helped to catch a bindValues ordering bug
576
444
  it('should handle INSERT queries (issue)', () => {
577
445
  expect(
578
- dump(
579
- db.issue.insert({
446
+ db.issue
447
+ .insert({
580
448
  id: 1,
581
449
  title: 'Revert the user profile page',
582
450
  priority: 2,
@@ -584,275 +452,180 @@ describe('query builder', () => {
584
452
  modified: new Date('2024-12-29T17:15:20.507Z'),
585
453
  kanbanorder: 'a2',
586
454
  creator: 'John Doe',
587
- }),
588
- ),
589
- ).toMatchInlineSnapshot(`
590
- {
591
- "bindValues": [
592
- 1,
593
- "Revert the user profile page",
594
- 2,
595
- 1722532520507,
596
- 1735492520507,
597
- "a2",
598
- "John Doe",
599
- ],
600
- "query": "INSERT INTO 'issue' ("id", "title", "priority", "created", "modified", "kanbanorder", "creator") VALUES (?, ?, ?, ?, ?, ?, ?)",
601
- "schema": "number",
602
- }
603
- `)
455
+ })
456
+ .asSql(),
457
+ ).toEqual({
458
+ bindValues: [1, 'Revert the user profile page', 2, 1722532520507, 1735492520507, 'a2', 'John Doe'],
459
+ query:
460
+ 'INSERT INTO \'issue\' ("id", "title", "priority", "created", "modified", "kanbanorder", "creator") VALUES (?, ?, ?, ?, ?, ?, ?)',
461
+ usedTables: new Set(['issue']),
462
+ })
604
463
  })
605
464
 
606
465
  it('should handle UPDATE queries', () => {
607
- expect(dump(db.todos.update({ status: 'completed' }).where({ id: '123' }))).toMatchInlineSnapshot(`
608
- {
609
- "bindValues": [
610
- "completed",
611
- "123",
612
- ],
613
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ?",
614
- "schema": "number",
615
- }
616
- `)
466
+ expect(db.todos.update({ status: 'completed' }).where({ id: '123' }).asSql()).toEqual({
467
+ bindValues: ['completed', '123'],
468
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ?',
469
+ usedTables: new Set(['todos']),
470
+ })
617
471
 
618
472
  // empty update set
619
- expect(dump(db.todos.update({}).where({ id: '123' }))).toMatchInlineSnapshot(`
620
- {
621
- "bindValues": [],
622
- "query": "SELECT 1",
623
- "schema": "number",
624
- }
625
- `)
473
+ expect(db.todos.update({}).where({ id: '123' }).asSql()).toEqual({
474
+ bindValues: [],
475
+ query: 'SELECT 1',
476
+ usedTables: new Set(['todos']),
477
+ })
626
478
  })
627
479
 
628
480
  it('should handle UPDATE queries with undefined values', () => {
629
- expect(dump(db.todos.update({ text: 'some text' }).where({ id: '123' }))).toMatchInlineSnapshot(`
630
- {
631
- "bindValues": [
632
- "some text",
633
- "123",
634
- ],
635
- "query": "UPDATE 'todos' SET "text" = ? WHERE "id" = ?",
636
- "schema": "number",
637
- }
638
- `)
481
+ expect(db.todos.update({ text: 'some text' }).where({ id: '123' }).asSql()).toEqual({
482
+ bindValues: ['some text', '123'],
483
+ query: 'UPDATE \'todos\' SET "text" = ? WHERE "id" = ?',
484
+ usedTables: new Set(['todos']),
485
+ })
639
486
  })
640
487
 
641
488
  it('should handle UPDATE queries with undefined values (issue)', () => {
642
- expect(dump(db.issue.update({ priority: 2, creator: 'John Doe' }).where({ id: 1 }))).toMatchInlineSnapshot(`
643
- {
644
- "bindValues": [
645
- 2,
646
- "John Doe",
647
- 1,
648
- ],
649
- "query": "UPDATE 'issue' SET "priority" = ?, "creator" = ? WHERE "id" = ?",
650
- "schema": "number",
651
- }
652
- `)
489
+ expect(db.issue.update({ priority: 2, creator: 'John Doe' }).where({ id: 1 }).asSql()).toEqual({
490
+ bindValues: [2, 'John Doe', 1],
491
+ query: 'UPDATE \'issue\' SET "priority" = ?, "creator" = ? WHERE "id" = ?',
492
+ usedTables: new Set(['issue']),
493
+ })
653
494
  })
654
495
 
655
496
  it('should handle DELETE queries', () => {
656
- expect(dump(db.todos.delete().where({ status: 'completed' }))).toMatchInlineSnapshot(`
657
- {
658
- "bindValues": [
659
- "completed",
660
- ],
661
- "query": "DELETE FROM 'todos' WHERE "status" = ?",
662
- "schema": "number",
663
- }
664
- `)
497
+ expect(db.todos.delete().where({ status: 'completed' }).asSql()).toEqual({
498
+ bindValues: ['completed'],
499
+ query: 'DELETE FROM \'todos\' WHERE "status" = ?',
500
+ usedTables: new Set(['todos']),
501
+ })
665
502
  })
666
503
 
667
504
  it('should handle INSERT with ON CONFLICT', () => {
668
505
  expect(
669
- dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore')),
670
- ).toMatchInlineSnapshot(`
671
- {
672
- "bindValues": [
673
- "123",
674
- "Buy milk",
675
- "active",
676
- ],
677
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO NOTHING",
678
- "schema": "number",
679
- }
680
- `)
506
+ db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'ignore').asSql(),
507
+ ).toEqual({
508
+ bindValues: ['123', 'Buy milk', 'active'],
509
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO NOTHING',
510
+ usedTables: new Set(['todos']),
511
+ })
681
512
 
682
513
  expect(
683
- dump(
684
- db.todos
685
- .insert({ id: '123', text: 'Buy milk', status: 'active' })
686
- .onConflict('id', 'update', { text: 'Buy soy milk', status: 'active' }),
687
- ),
688
- ).toMatchInlineSnapshot(`
689
- {
690
- "bindValues": [
691
- "123",
692
- "Buy milk",
693
- "active",
694
- "Buy soy milk",
695
- "active",
696
- ],
697
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO UPDATE SET "text" = ?, "status" = ?",
698
- "schema": "number",
699
- }
700
- `)
514
+ db.todos
515
+ .insert({ id: '123', text: 'Buy milk', status: 'active' })
516
+ .onConflict('id', 'update', { text: 'Buy soy milk', status: 'active' })
517
+ .asSql(),
518
+ ).toEqual({
519
+ bindValues: ['123', 'Buy milk', 'active', 'Buy soy milk', 'active'],
520
+ query:
521
+ 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id") DO UPDATE SET "text" = ?, "status" = ?',
522
+ usedTables: new Set(['todos']),
523
+ })
701
524
 
702
525
  expect(
703
- dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace')),
704
- ).toMatchInlineSnapshot(`
705
- {
706
- "bindValues": [
707
- "123",
708
- "Buy milk",
709
- "active",
710
- ],
711
- "query": "INSERT OR REPLACE INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?)",
712
- "schema": "number",
713
- }
714
- `)
526
+ db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict('id', 'replace').asSql(),
527
+ ).toEqual({
528
+ bindValues: ['123', 'Buy milk', 'active'],
529
+ query: 'INSERT OR REPLACE INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?)',
530
+ usedTables: new Set(['todos']),
531
+ })
715
532
  })
716
533
 
717
534
  it('should quote reserved column names', () => {
718
- expect(dump(db.selections.insert({ id: 1, group: 'alpha' }).onConflict('id', 'ignore'))).toMatchInlineSnapshot(`
719
- {
720
- "bindValues": [
721
- 1,
722
- "alpha",
723
- ],
724
- "query": "INSERT INTO 'selections' ("id", "group") VALUES (?, ?) ON CONFLICT ("id") DO NOTHING",
725
- "schema": "number",
726
- }
727
- `)
728
-
729
- expect(dump(db.selections.update({ group: 'beta' }).where({ id: 1 }))).toMatchInlineSnapshot(`
730
- {
731
- "bindValues": [
732
- "beta",
733
- 1,
734
- ],
735
- "query": "UPDATE 'selections' SET "group" = ? WHERE "id" = ?",
736
- "schema": "number",
737
- }
738
- `)
535
+ expect(db.selections.insert({ id: 1, group: 'alpha' }).onConflict('id', 'ignore').asSql()).toEqual({
536
+ bindValues: [1, 'alpha'],
537
+ query: 'INSERT INTO \'selections\' ("id", "group") VALUES (?, ?) ON CONFLICT ("id") DO NOTHING',
538
+ usedTables: new Set(['selections']),
539
+ })
540
+
541
+ expect(db.selections.update({ group: 'beta' }).where({ id: 1 }).asSql()).toEqual({
542
+ bindValues: ['beta', 1],
543
+ query: 'UPDATE \'selections\' SET "group" = ? WHERE "id" = ?',
544
+ usedTables: new Set(['selections']),
545
+ })
739
546
  })
740
547
 
741
548
  it('should handle ON CONFLICT with multiple columns', () => {
742
549
  expect(
743
- dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).onConflict(['id', 'status'], 'ignore')),
744
- ).toMatchInlineSnapshot(`
745
- {
746
- "bindValues": [
747
- "123",
748
- "Buy milk",
749
- "active",
750
- ],
751
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id", "status") DO NOTHING",
752
- "schema": "number",
753
- }
754
- `)
550
+ db.todos
551
+ .insert({ id: '123', text: 'Buy milk', status: 'active' })
552
+ .onConflict(['id', 'status'], 'ignore')
553
+ .asSql(),
554
+ ).toEqual({
555
+ bindValues: ['123', 'Buy milk', 'active'],
556
+ query:
557
+ 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) ON CONFLICT ("id", "status") DO NOTHING',
558
+ usedTables: new Set(['todos']),
559
+ })
755
560
  })
756
561
 
757
562
  it('should handle RETURNING clause', () => {
758
- expect(
759
- dump(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')),
760
- ).toMatchInlineSnapshot(`
761
- {
762
- "bindValues": [
763
- "123",
764
- "Buy milk",
765
- "active",
766
- ],
767
- "query": "INSERT INTO 'todos' ("id", "text", "status") VALUES (?, ?, ?) RETURNING "id"",
768
- "schema": "ReadonlyArray<{ readonly id: string }>",
769
- }
770
- `)
563
+ expect(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id').asSql()).toEqual({
564
+ bindValues: ['123', 'Buy milk', 'active'],
565
+ query: 'INSERT INTO \'todos\' ("id", "text", "status") VALUES (?, ?, ?) RETURNING "id"',
566
+ usedTables: new Set(['todos']),
567
+ })
771
568
 
772
- expect(
773
- dump(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id')),
774
- ).toMatchInlineSnapshot(`
775
- {
776
- "bindValues": [
777
- "completed",
778
- "123",
779
- ],
780
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ? RETURNING "id"",
781
- "schema": "ReadonlyArray<{ readonly id: string }>",
782
- }
783
- `)
784
-
785
- expect(dump(db.todos.delete().where({ status: 'completed' }).returning('id'))).toMatchInlineSnapshot(`
786
- {
787
- "bindValues": [
788
- "completed",
789
- ],
790
- "query": "DELETE FROM 'todos' WHERE "status" = ? RETURNING "id"",
791
- "schema": "ReadonlyArray<{ readonly id: string }>",
792
- }
793
- `)
569
+ expect(db.todos.update({ status: 'completed' }).where({ id: '123' }).returning('id').asSql()).toEqual({
570
+ bindValues: ['completed', '123'],
571
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ? RETURNING "id"',
572
+ usedTables: new Set(['todos']),
573
+ })
574
+
575
+ expect(db.todos.delete().where({ status: 'completed' }).returning('id').asSql()).toEqual({
576
+ bindValues: ['completed'],
577
+ query: 'DELETE FROM \'todos\' WHERE "status" = ? RETURNING "id"',
578
+ usedTables: new Set(['todos']),
579
+ })
580
+ })
581
+
582
+ it('derives result schemas for RETURNING clauses', async () => {
583
+ const returningId = new TestSchema.Asserts(
584
+ getResultSchema(db.todos.insert({ id: '123', text: 'Buy milk', status: 'active' }).returning('id')),
585
+ )
586
+ await returningId.decoding().succeed([{ id: '123' }])
794
587
  })
795
588
 
796
589
  it('should handle where().delete() - preserving where clauses', () => {
797
- expect(dump(db.todos.where({ status: 'completed' }).delete())).toMatchInlineSnapshot(`
798
- {
799
- "bindValues": [
800
- "completed",
801
- ],
802
- "query": "DELETE FROM 'todos' WHERE "status" = ?",
803
- "schema": "number",
804
- }
805
- `)
590
+ expect(db.todos.where({ status: 'completed' }).delete().asSql()).toEqual({
591
+ bindValues: ['completed'],
592
+ query: 'DELETE FROM \'todos\' WHERE "status" = ?',
593
+ usedTables: new Set(['todos']),
594
+ })
806
595
 
807
596
  // Multiple where clauses
808
- expect(dump(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete())).toMatchInlineSnapshot(`
809
- {
810
- "bindValues": [
811
- "completed",
812
- ],
813
- "query": "DELETE FROM 'todos' WHERE "status" = ? AND "deletedAt" IS NULL",
814
- "schema": "number",
815
- }
816
- `)
597
+ expect(db.todos.where({ status: 'completed' }).where({ deletedAt: null }).delete().asSql()).toEqual({
598
+ bindValues: ['completed'],
599
+ query: 'DELETE FROM \'todos\' WHERE "status" = ? AND "deletedAt" IS NULL',
600
+ usedTables: new Set(['todos']),
601
+ })
817
602
  })
818
603
 
819
604
  it('should handle where().update() - preserving where clauses', () => {
820
- expect(dump(db.todos.where({ id: '123' }).update({ status: 'completed' }))).toMatchInlineSnapshot(`
821
- {
822
- "bindValues": [
823
- "completed",
824
- "123",
825
- ],
826
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ?",
827
- "schema": "number",
828
- }
829
- `)
605
+ expect(db.todos.where({ id: '123' }).update({ status: 'completed' }).asSql()).toEqual({
606
+ bindValues: ['completed', '123'],
607
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ?',
608
+ usedTables: new Set(['todos']),
609
+ })
830
610
 
831
611
  // Multiple where clauses
832
- expect(
833
- dump(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' })),
834
- ).toMatchInlineSnapshot(`
835
- {
836
- "bindValues": [
837
- "completed",
838
- "123",
839
- ],
840
- "query": "UPDATE 'todos' SET "status" = ? WHERE "id" = ? AND "deletedAt" IS NULL",
841
- "schema": "number",
842
- }
843
- `)
612
+ expect(db.todos.where({ id: '123' }).where({ deletedAt: null }).update({ status: 'completed' }).asSql()).toEqual({
613
+ bindValues: ['completed', '123'],
614
+ query: 'UPDATE \'todos\' SET "status" = ? WHERE "id" = ? AND "deletedAt" IS NULL',
615
+ usedTables: new Set(['todos']),
616
+ })
844
617
  })
845
618
 
846
619
  it('should have equivalent behavior for both delete patterns', () => {
847
- const pattern1 = dump(db.todos.where({ status: 'completed', id: '123' }).delete())
848
- const pattern2 = dump(db.todos.delete().where({ status: 'completed', id: '123' }))
620
+ const pattern1 = db.todos.where({ status: 'completed', id: '123' }).delete().asSql()
621
+ const pattern2 = db.todos.delete().where({ status: 'completed', id: '123' }).asSql()
849
622
 
850
623
  expect(pattern1).toEqual(pattern2)
851
624
  })
852
625
 
853
626
  it('should have equivalent behavior for both update patterns', () => {
854
- const pattern1 = dump(db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }))
855
- const pattern2 = dump(db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }))
627
+ const pattern1 = db.todos.where({ id: '123' }).update({ status: 'completed', text: 'Updated' }).asSql()
628
+ const pattern2 = db.todos.update({ status: 'completed', text: 'Updated' }).where({ id: '123' }).asSql()
856
629
 
857
630
  expect(pattern1).toEqual(pattern2)
858
631
  })
@@ -866,32 +639,33 @@ describe('query builder', () => {
866
639
  contactEmail: Schema.String.pipe(State.SQLite.withUnique),
867
640
  })
868
641
 
869
- const Nested = Schema.transform(
870
- Flat,
871
- Schema.Struct({
872
- id: Schema.String,
873
- contact: Schema.Struct({
874
- firstName: Schema.String,
875
- lastName: Schema.String,
876
- email: Schema.String,
877
- }),
878
- }),
879
- {
880
- decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
881
- id,
882
- contact: {
883
- firstName: contactFirstName,
884
- lastName: contactLastName,
885
- email: contactEmail,
886
- },
642
+ const Nested = Flat.pipe(
643
+ Schema.decodeTo(
644
+ Schema.Struct({
645
+ id: Schema.String,
646
+ contact: Schema.Struct({
647
+ firstName: Schema.String,
648
+ lastName: Schema.String,
649
+ email: Schema.String,
650
+ }),
887
651
  }),
888
- encode: ({ id, contact }) => ({
889
- id,
890
- contactFirstName: contact.firstName,
891
- contactLastName: contact.lastName,
892
- contactEmail: contact.email,
652
+ SchemaTransformation.transform({
653
+ decode: ({ id, contactFirstName, contactLastName, contactEmail }) => ({
654
+ id,
655
+ contact: {
656
+ firstName: contactFirstName,
657
+ lastName: contactLastName,
658
+ email: contactEmail,
659
+ },
660
+ }),
661
+ encode: ({ id, contact }) => ({
662
+ id,
663
+ contactFirstName: contact.firstName,
664
+ contactLastName: contact.lastName,
665
+ contactEmail: contact.email,
666
+ }),
893
667
  }),
894
- },
668
+ ),
895
669
  )
896
670
 
897
671
  const makeContactsTable = () =>
@@ -905,7 +679,7 @@ describe('query builder', () => {
905
679
  const contactsTable = makeContactsTable()
906
680
 
907
681
  type InsertInput = Parameters<(typeof contactsTable)['insert']>[0]
908
- type NestedType = Schema.Schema.Type<typeof Nested>
682
+ type NestedType = (typeof Nested)['Type']
909
683
 
910
684
  type Assert<T extends true> = T
911
685
 
@@ -929,7 +703,7 @@ describe('query builder', () => {
929
703
  void contactsTable
930
704
  })
931
705
 
932
- it('fails to encode nested inserts because flat columns are required', () => {
706
+ it('encodes flat insert values for transformed schemas', () => {
933
707
  const contactsTable = makeContactsTable()
934
708
 
935
709
  expect(
@@ -942,23 +716,15 @@ describe('query builder', () => {
942
716
  contactEmail: 'ada@example.com',
943
717
  })
944
718
  .asSql(),
945
- ).toMatchInlineSnapshot(`
946
- {
947
- "bindValues": [
948
- "person-1",
949
- "Ada",
950
- "Lovelace",
951
- "ada@example.com",
952
- ],
953
- "query": "INSERT INTO 'contacts' ("id", "contactFirstName", "contactLastName", "contactEmail") VALUES (?, ?, ?, ?)",
954
- "usedTables": Set {
955
- "contacts",
956
- },
957
- }
958
- `)
959
- })
960
-
961
- it('fails to encode nested inserts because flat columns are required', () => {
719
+ ).toEqual({
720
+ bindValues: ['person-1', 'Ada', 'Lovelace', 'ada@example.com'],
721
+ query:
722
+ 'INSERT INTO \'contacts\' ("id", "contactFirstName", "contactLastName", "contactEmail") VALUES (?, ?, ?, ?)',
723
+ usedTables: new Set(['contacts']),
724
+ })
725
+ })
726
+
727
+ it('rejects nested insert values because flat columns are required', () => {
962
728
  const contactsTable = makeContactsTable()
963
729
 
964
730
  expect(() =>
@@ -973,15 +739,13 @@ describe('query builder', () => {
973
739
  },
974
740
  })
975
741
  .asSql(),
976
- ).toThrowErrorMatchingInlineSnapshot(`
977
- [ParseError: contacts\n└─ ["contactFirstName"]\n └─ is missing]
978
- `)
742
+ ).toThrowError(/contactFirstName/)
979
743
  })
980
744
  })
981
745
  })
982
746
 
983
747
  // TODO nested queries
984
- // const rawSql = <A, I>(sql: string, params: { [key: string]: any }, schema: Schema.Schema<A, I>) =>
748
+ // const rawSql = <A, I>(sql: string, params: { [key: string]: any }, schema: Schema.Codec<A, I>) =>
985
749
  // ({
986
750
  // sql,
987
751
  // params,