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