@effect-app/infra 4.0.0-beta.33 → 4.0.0-beta.331

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 (490) hide show
  1. package/CHANGELOG.md +2562 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +29 -10
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +36 -15
  6. package/dist/ClusterCosmos.d.ts +64 -0
  7. package/dist/ClusterCosmos.d.ts.map +1 -0
  8. package/dist/ClusterCosmos.js +576 -0
  9. package/dist/ClusterServiceBus.d.ts +67 -0
  10. package/dist/ClusterServiceBus.d.ts.map +1 -0
  11. package/dist/ClusterServiceBus.js +84 -0
  12. package/dist/ContextProvider.d.ts +34 -0
  13. package/dist/ContextProvider.d.ts.map +1 -0
  14. package/dist/ContextProvider.js +40 -0
  15. package/dist/Emailer/Sendgrid.d.ts +110 -146
  16. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  17. package/dist/Emailer/Sendgrid.js +24 -19
  18. package/dist/Emailer/fake.d.ts +1 -1
  19. package/dist/Emailer/fake.d.ts.map +1 -1
  20. package/dist/Emailer/fake.js +4 -4
  21. package/dist/MainFiberSet.d.ts +12 -8
  22. package/dist/MainFiberSet.d.ts.map +1 -1
  23. package/dist/MainFiberSet.js +23 -7
  24. package/dist/QueueMaker/errors.d.ts +4 -2
  25. package/dist/QueueMaker/errors.d.ts.map +1 -1
  26. package/dist/QueueMaker/errors.js +4 -2
  27. package/dist/{adapters/SQL → SQL}/Model.d.ts +31 -42
  28. package/dist/SQL/Model.d.ts.map +1 -0
  29. package/dist/SQL/Model.js +469 -0
  30. package/dist/SQL.d.ts.map +1 -0
  31. package/dist/{adapters/SQL.js → SQL.js} +1 -1
  32. package/dist/Store/Cosmos/query.d.ts +16 -4
  33. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  34. package/dist/Store/Cosmos/query.js +238 -42
  35. package/dist/Store/Cosmos.d.ts +2 -2
  36. package/dist/Store/Cosmos.d.ts.map +1 -1
  37. package/dist/Store/Cosmos.js +395 -256
  38. package/dist/Store/Disk.d.ts +4 -4
  39. package/dist/Store/Disk.d.ts.map +1 -1
  40. package/dist/Store/Disk.js +93 -42
  41. package/dist/Store/Memory.d.ts +7 -10
  42. package/dist/Store/Memory.d.ts.map +1 -1
  43. package/dist/Store/Memory.js +349 -79
  44. package/dist/Store/SQL/Pg.d.ts +4 -0
  45. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  46. package/dist/Store/SQL/Pg.js +261 -0
  47. package/dist/Store/SQL/query.d.ts +53 -0
  48. package/dist/Store/SQL/query.d.ts.map +1 -0
  49. package/dist/Store/SQL/query.js +656 -0
  50. package/dist/Store/SQL.d.ts +28 -0
  51. package/dist/Store/SQL.d.ts.map +1 -0
  52. package/dist/Store/SQL.js +483 -0
  53. package/dist/Store/codeFilter.d.ts +4 -4
  54. package/dist/Store/codeFilter.d.ts.map +1 -1
  55. package/dist/Store/codeFilter.js +71 -22
  56. package/dist/Store/index.d.ts +6 -4
  57. package/dist/Store/index.d.ts.map +1 -1
  58. package/dist/Store/index.js +18 -5
  59. package/dist/Store/jsonDocument.d.ts +67 -0
  60. package/dist/Store/jsonDocument.d.ts.map +1 -0
  61. package/dist/Store/jsonDocument.js +107 -0
  62. package/dist/Store/utils.d.ts +21 -2
  63. package/dist/Store/utils.d.ts.map +1 -1
  64. package/dist/Store/utils.js +253 -10
  65. package/dist/WorkflowEngineCosmos.d.ts +30 -0
  66. package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
  67. package/dist/WorkflowEngineCosmos.js +542 -0
  68. package/dist/WorkflowEngineSqlite.d.ts +24 -0
  69. package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
  70. package/dist/WorkflowEngineSqlite.js +550 -0
  71. package/dist/arbs.d.ts +6 -3
  72. package/dist/arbs.d.ts.map +1 -1
  73. package/dist/arbs.js +20 -4
  74. package/dist/codec.d.ts.map +1 -0
  75. package/dist/codec.js +5 -0
  76. package/dist/cosmos-client.d.ts +40 -0
  77. package/dist/cosmos-client.d.ts.map +1 -0
  78. package/dist/cosmos-client.js +41 -0
  79. package/dist/errorReporter.d.ts +6 -4
  80. package/dist/errorReporter.d.ts.map +1 -1
  81. package/dist/errorReporter.js +23 -27
  82. package/dist/fileUtil.d.ts +1 -1
  83. package/dist/fileUtil.d.ts.map +1 -1
  84. package/dist/fileUtil.js +2 -2
  85. package/dist/index.d.ts +2 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3 -2
  88. package/dist/internal/RequestContextMiddleware.d.ts +5 -0
  89. package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
  90. package/dist/internal/RequestContextMiddleware.js +45 -0
  91. package/dist/internal/auth.d.ts +53 -0
  92. package/dist/internal/auth.d.ts.map +1 -0
  93. package/dist/internal/auth.js +180 -0
  94. package/dist/internal/events.d.ts +11 -0
  95. package/dist/internal/events.d.ts.map +1 -0
  96. package/dist/internal/events.js +49 -0
  97. package/dist/internal/health.d.ts.map +1 -0
  98. package/dist/internal/health.js +5 -0
  99. package/dist/{api/layerUtils.d.ts → layerUtils.d.ts} +14 -6
  100. package/dist/layerUtils.d.ts.map +1 -0
  101. package/dist/layerUtils.js +17 -0
  102. package/dist/logger/jsonLogger.d.ts +1 -1
  103. package/dist/logger/jsonLogger.d.ts.map +1 -1
  104. package/dist/logger/jsonLogger.js +5 -3
  105. package/dist/logger/logFmtLogger.d.ts +1 -1
  106. package/dist/logger/logFmtLogger.d.ts.map +1 -1
  107. package/dist/logger/logFmtLogger.js +3 -3
  108. package/dist/logger/shared.d.ts +2 -2
  109. package/dist/logger/shared.d.ts.map +1 -1
  110. package/dist/logger/shared.js +6 -6
  111. package/dist/logger.d.ts +4 -4
  112. package/dist/logger.d.ts.map +1 -1
  113. package/dist/middlewares.d.ts.map +1 -0
  114. package/dist/{api/middlewares.js → middlewares.js} +1 -1
  115. package/dist/{adapters/mongo-client.d.ts → mongo-client.d.ts} +3 -2
  116. package/dist/mongo-client.d.ts.map +1 -0
  117. package/dist/mongo-client.js +15 -0
  118. package/dist/otel.d.ts +75 -0
  119. package/dist/otel.d.ts.map +1 -0
  120. package/dist/otel.js +84 -0
  121. package/dist/rateLimit.d.ts +11 -3
  122. package/dist/rateLimit.d.ts.map +1 -1
  123. package/dist/rateLimit.js +7 -12
  124. package/dist/{adapters/redis-client.d.ts → redis-client.d.ts} +5 -2
  125. package/dist/redis-client.d.ts.map +1 -0
  126. package/dist/redis-client.js +98 -0
  127. package/dist/{api/reportError.d.ts → reportError.d.ts} +1 -1
  128. package/dist/reportError.d.ts.map +1 -0
  129. package/dist/reportError.js +28 -0
  130. package/dist/{api/routing → routing}/middleware/RouterMiddleware.d.ts +4 -3
  131. package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
  132. package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
  133. package/dist/routing/middleware/middleware.d.ts +48 -0
  134. package/dist/routing/middleware/middleware.d.ts.map +1 -0
  135. package/dist/routing/middleware/middleware.js +152 -0
  136. package/dist/{api/routing → routing}/middleware.d.ts +0 -1
  137. package/dist/routing/middleware.d.ts.map +1 -0
  138. package/dist/{api/routing → routing}/middleware.js +1 -2
  139. package/dist/routing/schema/jwt.d.ts.map +1 -0
  140. package/dist/routing/schema/jwt.js +12 -0
  141. package/dist/routing/tsort.d.ts.map +1 -0
  142. package/dist/routing/tsort.js +51 -0
  143. package/dist/{api/routing → routing}/utils.d.ts +3 -3
  144. package/dist/routing/utils.d.ts.map +1 -0
  145. package/dist/routing/utils.js +45 -0
  146. package/dist/routing.d.ts +182 -0
  147. package/dist/routing.d.ts.map +1 -0
  148. package/dist/routing.js +256 -0
  149. package/dist/test.d.ts +2 -2
  150. package/dist/test.d.ts.map +1 -1
  151. package/dist/test.js +5 -5
  152. package/dist/util.d.ts.map +1 -0
  153. package/dist/util.js +14 -0
  154. package/docs/cluster-storage.md +26 -0
  155. package/docs/workflow-engine.md +262 -0
  156. package/examples/query.ts +25 -21
  157. package/package.json +38 -352
  158. package/src/CUPS.ts +54 -15
  159. package/src/ClusterCosmos.ts +1150 -0
  160. package/src/ClusterServiceBus.ts +244 -0
  161. package/src/{api/ContextProvider.ts → ContextProvider.ts} +20 -17
  162. package/src/Emailer/Sendgrid.ts +83 -60
  163. package/src/Emailer/fake.ts +4 -4
  164. package/src/MainFiberSet.ts +32 -13
  165. package/src/QueueMaker/errors.ts +3 -1
  166. package/src/{adapters/SQL → SQL}/Model.ts +42 -41
  167. package/src/SQL.ts +1 -0
  168. package/src/Store/Cosmos/query.ts +275 -55
  169. package/src/Store/Cosmos.ts +547 -366
  170. package/src/Store/Disk.ts +146 -73
  171. package/src/Store/Memory.ts +420 -115
  172. package/src/Store/SQL/Pg.ts +412 -0
  173. package/src/Store/SQL/query.ts +748 -0
  174. package/src/Store/SQL.ts +806 -0
  175. package/src/Store/codeFilter.ts +78 -28
  176. package/src/Store/index.ts +25 -10
  177. package/src/Store/jsonDocument.ts +143 -0
  178. package/src/Store/utils.ts +307 -30
  179. package/src/WorkflowEngineCosmos.ts +749 -0
  180. package/src/WorkflowEngineSqlite.ts +813 -0
  181. package/src/arbs.ts +25 -5
  182. package/src/cosmos-client.ts +72 -0
  183. package/src/errorReporter.ts +68 -78
  184. package/src/fileUtil.ts +1 -1
  185. package/src/index.ts +2 -1
  186. package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
  187. package/src/internal/auth.ts +272 -0
  188. package/src/{api/internal → internal}/events.ts +22 -13
  189. package/src/{api/layerUtils.ts → layerUtils.ts} +14 -10
  190. package/src/logger/jsonLogger.ts +5 -3
  191. package/src/logger/logFmtLogger.ts +3 -3
  192. package/src/logger/shared.ts +6 -5
  193. package/src/middlewares.ts +10 -0
  194. package/src/{adapters/mongo-client.ts → mongo-client.ts} +4 -2
  195. package/src/otel.ts +177 -0
  196. package/src/rateLimit.ts +35 -24
  197. package/src/{adapters/redis-client.ts → redis-client.ts} +7 -3
  198. package/src/{api/reportError.ts → reportError.ts} +3 -2
  199. package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +5 -4
  200. package/src/routing/middleware/middleware.ts +196 -0
  201. package/src/routing/middleware.ts +4 -0
  202. package/src/{api/routing → routing}/schema/jwt.ts +2 -2
  203. package/src/{api/routing → routing}/utils.ts +2 -1
  204. package/src/routing.ts +823 -0
  205. package/src/test.ts +6 -6
  206. package/src/vitest.ts +1 -1
  207. package/test/arbs-magnitude.test.ts +33 -0
  208. package/test/arbs.test.ts +41 -0
  209. package/test/auth.test.ts +101 -0
  210. package/test/cluster-cosmos.test.ts +612 -0
  211. package/test/cluster-servicebus.test.ts +180 -0
  212. package/test/cluster-sqlite.test.ts +207 -0
  213. package/test/contextProvider.test.ts +15 -12
  214. package/test/controller.test.ts +27 -31
  215. package/test/cosmos-container-throughput.test.ts +80 -0
  216. package/test/cosmos-query.test.ts +368 -0
  217. package/test/dist/arbs-magnitude.test.d.ts.map +1 -0
  218. package/test/dist/arbs.test.d.ts.map +1 -0
  219. package/test/dist/auth.test.d.ts.map +1 -0
  220. package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
  221. package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
  222. package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
  223. package/test/dist/contextProvider.test.d.ts.map +1 -1
  224. package/test/dist/controller.test.d.ts.map +1 -1
  225. package/test/dist/cosmos-container-throughput.test.d.ts.map +1 -0
  226. package/test/dist/cosmos-query.test.d.ts.map +1 -0
  227. package/test/dist/fixtures.d.ts +29 -11
  228. package/test/dist/fixtures.d.ts.map +1 -1
  229. package/test/dist/fixtures.js +17 -10
  230. package/test/dist/json-lower.test.d.ts.map +1 -0
  231. package/test/dist/query.test.d.ts.map +1 -1
  232. package/test/dist/rawQuery.test.d.ts.map +1 -1
  233. package/test/dist/repository-ext.test.d.ts.map +1 -0
  234. package/test/dist/repository-legacy-document.test.d.ts.map +1 -0
  235. package/test/dist/requires.test.d.ts.map +1 -1
  236. package/test/dist/router-generator.test.d.ts.map +1 -0
  237. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  238. package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
  239. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  240. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  241. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  242. package/test/dist/sql-store.test.d.ts.map +1 -0
  243. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  244. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  245. package/test/fixtures.ts +16 -9
  246. package/test/json-lower.test.ts +129 -0
  247. package/test/layerUtils.test.ts +2 -2
  248. package/test/query.test.ts +1382 -44
  249. package/test/rawQuery.test.ts +433 -8
  250. package/test/repository-ext.test.ts +456 -0
  251. package/test/repository-legacy-document.test.ts +394 -0
  252. package/test/requires.test.ts +10 -5
  253. package/test/router-generator.test.ts +187 -0
  254. package/test/routing-interruptibility.test.ts +66 -0
  255. package/test/rpc-context-map-streaming.test.ts +265 -0
  256. package/test/rpc-e2e-invalidation.test.ts +421 -0
  257. package/test/rpc-multi-middleware.test.ts +85 -10
  258. package/test/rpc-stream-fullstack.test.ts +307 -0
  259. package/test/sql-store.test.ts +1931 -0
  260. package/test/validateSample.test.ts +71 -43
  261. package/test/workflow-engine-cosmos.test.ts +345 -0
  262. package/test/workflow-engine-sqlite.test.ts +297 -0
  263. package/tsconfig.examples.json +1 -1
  264. package/tsconfig.json +9 -2
  265. package/tsconfig.src.json +35 -35
  266. package/tsconfig.test.json +2 -2
  267. package/dist/Emailer/service.d.ts +0 -55
  268. package/dist/Emailer/service.d.ts.map +0 -1
  269. package/dist/Emailer/service.js +0 -6
  270. package/dist/Emailer.d.ts +0 -2
  271. package/dist/Emailer.d.ts.map +0 -1
  272. package/dist/Emailer.js +0 -2
  273. package/dist/Model/Repository/ext.d.ts +0 -41
  274. package/dist/Model/Repository/ext.d.ts.map +0 -1
  275. package/dist/Model/Repository/ext.js +0 -65
  276. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  277. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  278. package/dist/Model/Repository/internal/internal.js +0 -316
  279. package/dist/Model/Repository/legacy.d.ts +0 -19
  280. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  281. package/dist/Model/Repository/legacy.js +0 -2
  282. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  283. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  284. package/dist/Model/Repository/makeRepo.js +0 -24
  285. package/dist/Model/Repository/service.d.ts +0 -89
  286. package/dist/Model/Repository/service.d.ts.map +0 -1
  287. package/dist/Model/Repository/service.js +0 -2
  288. package/dist/Model/Repository/validation.d.ts +0 -42
  289. package/dist/Model/Repository/validation.d.ts.map +0 -1
  290. package/dist/Model/Repository/validation.js +0 -32
  291. package/dist/Model/Repository.d.ts +0 -6
  292. package/dist/Model/Repository.d.ts.map +0 -1
  293. package/dist/Model/Repository.js +0 -6
  294. package/dist/Model/dsl.d.ts +0 -32
  295. package/dist/Model/dsl.d.ts.map +0 -1
  296. package/dist/Model/dsl.js +0 -44
  297. package/dist/Model/filter/filterApi.d.ts +0 -30
  298. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  299. package/dist/Model/filter/filterApi.js +0 -2
  300. package/dist/Model/filter/types/errors.d.ts +0 -29
  301. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  302. package/dist/Model/filter/types/errors.js +0 -2
  303. package/dist/Model/filter/types/fields.d.ts +0 -15
  304. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  305. package/dist/Model/filter/types/fields.js +0 -2
  306. package/dist/Model/filter/types/path/common.d.ts +0 -316
  307. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  308. package/dist/Model/filter/types/path/common.js +0 -2
  309. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  310. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  311. package/dist/Model/filter/types/path/eager.js +0 -31
  312. package/dist/Model/filter/types/path/index.d.ts +0 -4
  313. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  314. package/dist/Model/filter/types/path/index.js +0 -3
  315. package/dist/Model/filter/types/utils.d.ts +0 -79
  316. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  317. package/dist/Model/filter/types/utils.js +0 -2
  318. package/dist/Model/filter/types/validator.d.ts +0 -30
  319. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  320. package/dist/Model/filter/types/validator.js +0 -2
  321. package/dist/Model/filter/types.d.ts +0 -5
  322. package/dist/Model/filter/types.d.ts.map +0 -1
  323. package/dist/Model/filter/types.js +0 -7
  324. package/dist/Model/query/dsl.d.ts +0 -248
  325. package/dist/Model/query/dsl.d.ts.map +0 -1
  326. package/dist/Model/query/dsl.js +0 -104
  327. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  328. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  329. package/dist/Model/query/new-kid-interpreter.js +0 -165
  330. package/dist/Model/query.d.ts +0 -15
  331. package/dist/Model/query.d.ts.map +0 -1
  332. package/dist/Model/query.js +0 -3
  333. package/dist/Model.d.ts +0 -4
  334. package/dist/Model.d.ts.map +0 -1
  335. package/dist/Model.js +0 -4
  336. package/dist/Operations.d.ts +0 -55
  337. package/dist/Operations.d.ts.map +0 -1
  338. package/dist/Operations.js +0 -102
  339. package/dist/OperationsRepo.d.ts +0 -41
  340. package/dist/OperationsRepo.d.ts.map +0 -1
  341. package/dist/OperationsRepo.js +0 -14
  342. package/dist/QueueMaker/SQLQueue.d.ts +0 -16
  343. package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
  344. package/dist/QueueMaker/SQLQueue.js +0 -130
  345. package/dist/QueueMaker/memQueue.d.ts +0 -11
  346. package/dist/QueueMaker/memQueue.d.ts.map +0 -1
  347. package/dist/QueueMaker/memQueue.js +0 -76
  348. package/dist/QueueMaker/sbqueue.d.ts +0 -12
  349. package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
  350. package/dist/QueueMaker/sbqueue.js +0 -91
  351. package/dist/QueueMaker/service.d.ts +0 -11
  352. package/dist/QueueMaker/service.d.ts.map +0 -1
  353. package/dist/QueueMaker/service.js +0 -4
  354. package/dist/RequestContext.d.ts +0 -63
  355. package/dist/RequestContext.d.ts.map +0 -1
  356. package/dist/RequestContext.js +0 -49
  357. package/dist/RequestFiberSet.d.ts +0 -36
  358. package/dist/RequestFiberSet.d.ts.map +0 -1
  359. package/dist/RequestFiberSet.js +0 -78
  360. package/dist/Store/ContextMapContainer.d.ts +0 -14
  361. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  362. package/dist/Store/ContextMapContainer.js +0 -16
  363. package/dist/Store/service.d.ts +0 -108
  364. package/dist/Store/service.d.ts.map +0 -1
  365. package/dist/Store/service.js +0 -71
  366. package/dist/Store.d.ts +0 -2
  367. package/dist/Store.d.ts.map +0 -1
  368. package/dist/Store.js +0 -2
  369. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  370. package/dist/adapters/SQL/Model.js +0 -478
  371. package/dist/adapters/SQL.d.ts.map +0 -1
  372. package/dist/adapters/ServiceBus.d.ts +0 -58
  373. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  374. package/dist/adapters/ServiceBus.js +0 -99
  375. package/dist/adapters/cosmos-client.d.ts +0 -14
  376. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  377. package/dist/adapters/cosmos-client.js +0 -9
  378. package/dist/adapters/index.d.ts +0 -2
  379. package/dist/adapters/index.d.ts.map +0 -1
  380. package/dist/adapters/index.js +0 -2
  381. package/dist/adapters/logger.d.ts +0 -9
  382. package/dist/adapters/logger.d.ts.map +0 -1
  383. package/dist/adapters/logger.js +0 -3
  384. package/dist/adapters/memQueue.d.ts +0 -13
  385. package/dist/adapters/memQueue.d.ts.map +0 -1
  386. package/dist/adapters/memQueue.js +0 -20
  387. package/dist/adapters/mongo-client.d.ts.map +0 -1
  388. package/dist/adapters/mongo-client.js +0 -13
  389. package/dist/adapters/redis-client.d.ts.map +0 -1
  390. package/dist/adapters/redis-client.js +0 -94
  391. package/dist/api/ContextProvider.d.ts +0 -31
  392. package/dist/api/ContextProvider.d.ts.map +0 -1
  393. package/dist/api/ContextProvider.js +0 -38
  394. package/dist/api/codec.d.ts.map +0 -1
  395. package/dist/api/codec.js +0 -5
  396. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  397. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  398. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  399. package/dist/api/internal/auth.d.ts +0 -15
  400. package/dist/api/internal/auth.d.ts.map +0 -1
  401. package/dist/api/internal/auth.js +0 -47
  402. package/dist/api/internal/events.d.ts +0 -9
  403. package/dist/api/internal/events.d.ts.map +0 -1
  404. package/dist/api/internal/events.js +0 -42
  405. package/dist/api/internal/health.d.ts.map +0 -1
  406. package/dist/api/internal/health.js +0 -5
  407. package/dist/api/layerUtils.d.ts.map +0 -1
  408. package/dist/api/layerUtils.js +0 -16
  409. package/dist/api/middlewares.d.ts.map +0 -1
  410. package/dist/api/reportError.d.ts.map +0 -1
  411. package/dist/api/reportError.js +0 -27
  412. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  413. package/dist/api/routing/middleware/middleware.d.ts +0 -9
  414. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  415. package/dist/api/routing/middleware/middleware.js +0 -92
  416. package/dist/api/routing/middleware.d.ts.map +0 -1
  417. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  418. package/dist/api/routing/schema/jwt.js +0 -12
  419. package/dist/api/routing/tsort.d.ts.map +0 -1
  420. package/dist/api/routing/tsort.js +0 -51
  421. package/dist/api/routing/utils.d.ts.map +0 -1
  422. package/dist/api/routing/utils.js +0 -44
  423. package/dist/api/routing.d.ts +0 -138
  424. package/dist/api/routing.d.ts.map +0 -1
  425. package/dist/api/routing.js +0 -166
  426. package/dist/api/setupRequest.d.ts +0 -12
  427. package/dist/api/setupRequest.d.ts.map +0 -1
  428. package/dist/api/setupRequest.js +0 -44
  429. package/dist/api/util.d.ts.map +0 -1
  430. package/dist/api/util.js +0 -14
  431. package/eslint.config.mjs +0 -24
  432. package/src/Emailer/service.ts +0 -52
  433. package/src/Emailer.ts +0 -1
  434. package/src/Model/Repository/ext.ts +0 -283
  435. package/src/Model/Repository/internal/internal.ts +0 -577
  436. package/src/Model/Repository/legacy.ts +0 -27
  437. package/src/Model/Repository/makeRepo.ts +0 -139
  438. package/src/Model/Repository/service.ts +0 -627
  439. package/src/Model/Repository/validation.ts +0 -31
  440. package/src/Model/Repository.ts +0 -5
  441. package/src/Model/dsl.ts +0 -128
  442. package/src/Model/filter/filterApi.ts +0 -60
  443. package/src/Model/filter/types/errors.ts +0 -47
  444. package/src/Model/filter/types/fields.ts +0 -50
  445. package/src/Model/filter/types/path/common.ts +0 -404
  446. package/src/Model/filter/types/path/eager.ts +0 -298
  447. package/src/Model/filter/types/path/index.ts +0 -4
  448. package/src/Model/filter/types/utils.ts +0 -128
  449. package/src/Model/filter/types/validator.ts +0 -46
  450. package/src/Model/filter/types.ts +0 -6
  451. package/src/Model/query/dsl.ts +0 -2110
  452. package/src/Model/query/new-kid-interpreter.ts +0 -210
  453. package/src/Model/query.ts +0 -13
  454. package/src/Model.ts +0 -3
  455. package/src/Operations.ts +0 -235
  456. package/src/OperationsRepo.ts +0 -16
  457. package/src/QueueMaker/SQLQueue.ts +0 -189
  458. package/src/QueueMaker/memQueue.ts +0 -130
  459. package/src/QueueMaker/sbqueue.ts +0 -137
  460. package/src/QueueMaker/service.ts +0 -17
  461. package/src/RequestContext.ts +0 -63
  462. package/src/RequestFiberSet.ts +0 -105
  463. package/src/Store/ContextMapContainer.ts +0 -20
  464. package/src/Store/index.test.ts.bak +0 -112
  465. package/src/Store/service.ts +0 -184
  466. package/src/Store.ts +0 -1
  467. package/src/adapters/SQL.ts +0 -1
  468. package/src/adapters/ServiceBus.ts +0 -209
  469. package/src/adapters/cosmos-client.ts +0 -16
  470. package/src/adapters/index.ts +0 -0
  471. package/src/adapters/logger.ts +0 -3
  472. package/src/adapters/memQueue.ts +0 -21
  473. package/src/api/internal/auth.ts +0 -68
  474. package/src/api/middlewares.ts +0 -10
  475. package/src/api/routing/middleware/middleware.ts +0 -128
  476. package/src/api/routing/middleware.ts +0 -6
  477. package/src/api/routing.ts +0 -598
  478. package/src/api/setupRequest.ts +0 -84
  479. package/tsconfig.json.bak +0 -12
  480. /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
  481. /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
  482. /package/dist/{api/internal → internal}/health.d.ts +0 -0
  483. /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
  484. /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
  485. /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
  486. /package/dist/{api/util.d.ts → util.d.ts} +0 -0
  487. /package/src/{api/codec.ts → codec.ts} +0 -0
  488. /package/src/{api/internal → internal}/health.ts +0 -0
  489. /package/src/{api/routing → routing}/tsort.ts +0 -0
  490. /package/src/{api/util.ts → util.ts} +0 -0
@@ -0,0 +1,1931 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type Sqlite from "better-sqlite3"
3
+ import BetterSqlite from "better-sqlite3"
4
+ import * as S from "effect-app/Schema"
5
+ import { describe, expect, it } from "vitest"
6
+ import { parseRow } from "../src/Store/SQL.js"
7
+ import { buildWhereSQLQuery, pgDialect, sqliteDialect } from "../src/Store/SQL/query.js"
8
+ import { makeETag, makeJsonLower } from "../src/Store/utils.js"
9
+
10
+ const query = (db: Sqlite.Database, sql: string, params: unknown[] = []) =>
11
+ db.prepare(sql).all(...params as any[]) as any[]
12
+
13
+ // --- Query builder unit tests ---
14
+
15
+ describe("SQL query builder (SQLite dialect)", () => {
16
+ it("where eq string", () => {
17
+ const result = buildWhereSQLQuery(
18
+ sqliteDialect,
19
+ "id",
20
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
21
+ "users",
22
+ {}
23
+ )
24
+ expect(result.sql).toContain("json_extract(data, '$.name') = ?")
25
+ expect(result.params).toContain("John")
26
+ })
27
+
28
+ it("where eq Date binds ISO string", () => {
29
+ const result = buildWhereSQLQuery(
30
+ sqliteDialect,
31
+ "id",
32
+ [{ t: "where", path: "n", op: "eq", value: new Date("2024-01-01T00:00:00.000Z") }],
33
+ "users",
34
+ {}
35
+ )
36
+ expect(result.params).toContain("2024-01-01T00:00:00.000Z")
37
+ })
38
+
39
+ it("where eq app native Encoded binds via JsonLower", () => {
40
+ class Day {
41
+ readonly ymd: string
42
+ constructor(ymd: string) {
43
+ this.ymd = ymd
44
+ }
45
+ }
46
+ const day = new Day("2024-06-01")
47
+ const json = {
48
+ toJson: (v: unknown) => v instanceof Day ? v.ymd : v,
49
+ jsonifyFilter: (filter: readonly { t: string; path?: string; op?: string; value?: unknown }[]) =>
50
+ filter.map((r) => r.t === "where" ? { ...r, value: r.value instanceof Day ? r.value.ymd : r.value } : r)
51
+ }
52
+ const result = buildWhereSQLQuery(
53
+ sqliteDialect,
54
+ "id",
55
+ [{ t: "where", path: "day", op: "eq", value: day }],
56
+ "users",
57
+ {},
58
+ undefined,
59
+ undefined,
60
+ undefined,
61
+ undefined,
62
+ undefined,
63
+ json as never
64
+ )
65
+ expect(result.params).toContain("2024-06-01")
66
+ })
67
+
68
+ it("does not re-encode store-lowered Date defaultValues", () => {
69
+ class Item extends S.Class<Item>("SqlDefaultValuesItem")({
70
+ id: S.String,
71
+ at: S.Date
72
+ }) {}
73
+ const json = makeJsonLower({ schema: Item })
74
+ const at = new Date("2024-06-01T00:00:00.000Z")
75
+ const lowered = json.toJson({ at }) as Record<string, unknown>
76
+ const result = buildWhereSQLQuery(
77
+ sqliteDialect,
78
+ "id",
79
+ [{ t: "where", path: "at", op: "eq", value: at }],
80
+ "items",
81
+ lowered,
82
+ undefined,
83
+ undefined,
84
+ undefined,
85
+ undefined,
86
+ undefined,
87
+ json
88
+ )
89
+ expect(result.sql).toContain("COALESCE(")
90
+ expect(result.params).toContain("2024-06-01T00:00:00.000Z")
91
+ })
92
+
93
+ it("where in Set binds array values", () => {
94
+ const result = buildWhereSQLQuery(
95
+ sqliteDialect,
96
+ "id",
97
+ [{ t: "where", path: "tags", op: "in", value: new Set(["a", "b"]) }],
98
+ "users",
99
+ {}
100
+ )
101
+ expect(result.params).toEqual(expect.arrayContaining(["a", "b"]))
102
+ })
103
+
104
+ it("where includes Date binds ISO string", () => {
105
+ const result = buildWhereSQLQuery(
106
+ sqliteDialect,
107
+ "id",
108
+ [{ t: "where", path: "dates", op: "includes", value: new Date("2024-01-01T00:00:00.000Z") }],
109
+ "users",
110
+ {}
111
+ )
112
+ expect(result.params).toContain("2024-01-01T00:00:00.000Z")
113
+ })
114
+
115
+ it("where hasKey / hasValue / hasKeyValue on Map JSON tuples", () => {
116
+ const key = buildWhereSQLQuery(
117
+ sqliteDialect,
118
+ "id",
119
+ [{ t: "where", path: "meta", op: "hasKey", value: "n" }],
120
+ "users",
121
+ {}
122
+ )
123
+ expect(key.sql).toContain("json_extract(value, '$[0]')")
124
+ expect(key.params).toContain("n")
125
+
126
+ const value = buildWhereSQLQuery(
127
+ sqliteDialect,
128
+ "id",
129
+ [{ t: "where", path: "meta", op: "hasValue", value: 1 }],
130
+ "users",
131
+ {}
132
+ )
133
+ expect(value.sql).toContain("json_extract(value, '$[1]')")
134
+ expect(value.params).toContain(1)
135
+
136
+ const pair = buildWhereSQLQuery(
137
+ sqliteDialect,
138
+ "id",
139
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
140
+ "users",
141
+ {}
142
+ )
143
+ expect(pair.sql).toContain("json_each")
144
+ expect(pair.params).toContain(JSON.stringify(["n", 1]))
145
+
146
+ const anyKeys = buildWhereSQLQuery(
147
+ sqliteDialect,
148
+ "id",
149
+ [{ t: "where", path: "meta", op: "hasKey-any", value: ["n", "x"] }],
150
+ "users",
151
+ {}
152
+ )
153
+ expect(anyKeys.sql).toContain(" OR ")
154
+ expect(anyKeys.params).toEqual(expect.arrayContaining(["n", "x"]))
155
+ })
156
+
157
+ it("pg where hasKey / hasKeyValue uses jsonb tuple elements", () => {
158
+ const key = buildWhereSQLQuery(
159
+ pgDialect,
160
+ "id",
161
+ [{ t: "where", path: "meta", op: "hasKey", value: "n" }],
162
+ "users",
163
+ {}
164
+ )
165
+ expect(key.sql).toContain("jsonb_array_elements")
166
+ expect(key.sql).toContain("e->0")
167
+ expect(key.params).toContain(JSON.stringify("n"))
168
+
169
+ const pair = buildWhereSQLQuery(
170
+ pgDialect,
171
+ "id",
172
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
173
+ "users",
174
+ {}
175
+ )
176
+ expect(pair.sql).toContain("@>")
177
+ expect(pair.sql).toContain("jsonb_build_array")
178
+ expect(pair.params).toContain(JSON.stringify(["n", 1]))
179
+ })
180
+
181
+ it("where includes-any Date[] binds ISO strings", () => {
182
+ const result = buildWhereSQLQuery(
183
+ sqliteDialect,
184
+ "id",
185
+ [{
186
+ t: "where",
187
+ path: "dates",
188
+ op: "includes-any",
189
+ value: [new Date("2024-01-01T00:00:00.000Z"), new Date("2024-06-01T00:00:00.000Z")]
190
+ }],
191
+ "users",
192
+ {}
193
+ )
194
+ expect(result.params).toEqual(
195
+ expect.arrayContaining(["2024-01-01T00:00:00.000Z", "2024-06-01T00:00:00.000Z"])
196
+ )
197
+ })
198
+
199
+ it("where eq number", () => {
200
+ const result = buildWhereSQLQuery(
201
+ sqliteDialect,
202
+ "id",
203
+ [{ t: "where", path: "age", op: "eq", value: 25 }],
204
+ "users",
205
+ {}
206
+ )
207
+ expect(result.sql).toContain("json_extract(data, '$.age') = ?")
208
+ expect(result.params).toContain(25)
209
+ })
210
+
211
+ it("where gt", () => {
212
+ const result = buildWhereSQLQuery(
213
+ sqliteDialect,
214
+ "id",
215
+ [{ t: "where", path: "age", op: "gt", value: 18 }],
216
+ "users",
217
+ {}
218
+ )
219
+ expect(result.sql).toContain("json_extract(data, '$.age') > ?")
220
+ expect(result.params).toContain(18)
221
+ })
222
+
223
+ it("where or", () => {
224
+ const result = buildWhereSQLQuery(
225
+ sqliteDialect,
226
+ "id",
227
+ [
228
+ { t: "where", path: "name", op: "eq", value: "Alice" },
229
+ { t: "or", path: "name", op: "eq", value: "Bob" }
230
+ ],
231
+ "users",
232
+ {}
233
+ )
234
+ expect(result.sql).toContain("= ?")
235
+ expect(result.sql).toContain("OR")
236
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", "Bob"]))
237
+ })
238
+
239
+ it("where and", () => {
240
+ const result = buildWhereSQLQuery(
241
+ sqliteDialect,
242
+ "id",
243
+ [
244
+ { t: "where", path: "name", op: "eq", value: "Alice" },
245
+ { t: "and", path: "age", op: "gt", value: 18 }
246
+ ],
247
+ "users",
248
+ {}
249
+ )
250
+ expect(result.sql).toContain("AND")
251
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", 18]))
252
+ })
253
+
254
+ it("where in", () => {
255
+ const result = buildWhereSQLQuery(
256
+ sqliteDialect,
257
+ "id",
258
+ [{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] }],
259
+ "users",
260
+ {}
261
+ )
262
+ expect(result.sql).toContain("id IN (?, ?, ?)")
263
+ expect(result.params).toEqual(expect.arrayContaining(["a", "b", "c"]))
264
+ })
265
+
266
+ it("where null", () => {
267
+ const result = buildWhereSQLQuery(
268
+ sqliteDialect,
269
+ "id",
270
+ [{ t: "where", path: "status", op: "eq", value: null as any }],
271
+ "users",
272
+ {}
273
+ )
274
+ expect(result.sql).toContain("IS NULL")
275
+ })
276
+
277
+ it("where neq null", () => {
278
+ const result = buildWhereSQLQuery(
279
+ sqliteDialect,
280
+ "id",
281
+ [{ t: "where", path: "status", op: "neq", value: null as any }],
282
+ "users",
283
+ {}
284
+ )
285
+ expect(result.sql).toContain("IS NOT NULL")
286
+ })
287
+
288
+ it("where contains", () => {
289
+ const result = buildWhereSQLQuery(
290
+ sqliteDialect,
291
+ "id",
292
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
293
+ "users",
294
+ {}
295
+ )
296
+ expect(result.sql).toContain("LIKE")
297
+ expect(result.sql).toContain("LOWER")
298
+ expect(result.params).toContain("%oh%")
299
+ })
300
+
301
+ it("where startsWith", () => {
302
+ const result = buildWhereSQLQuery(
303
+ sqliteDialect,
304
+ "id",
305
+ [{ t: "where", path: "name", op: "startsWith", value: "Jo" }],
306
+ "users",
307
+ {}
308
+ )
309
+ expect(result.sql).toContain("LIKE")
310
+ expect(result.params).toContain("Jo%")
311
+ })
312
+
313
+ it("where endsWith", () => {
314
+ const result = buildWhereSQLQuery(
315
+ sqliteDialect,
316
+ "id",
317
+ [{ t: "where", path: "name", op: "endsWith", value: "hn" }],
318
+ "users",
319
+ {}
320
+ )
321
+ expect(result.sql).toContain("LIKE")
322
+ expect(result.params).toContain("%hn")
323
+ })
324
+
325
+ it("where includes (array contains)", () => {
326
+ const result = buildWhereSQLQuery(
327
+ sqliteDialect,
328
+ "id",
329
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
330
+ "users",
331
+ {}
332
+ )
333
+ expect(result.sql).toContain("json_each")
334
+ expect(result.sql).toContain("value = ?")
335
+ expect(result.params).toContain("admin")
336
+ })
337
+
338
+ it("where includes-any (array contains any)", () => {
339
+ const result = buildWhereSQLQuery(
340
+ sqliteDialect,
341
+ "id",
342
+ [{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] }],
343
+ "users",
344
+ {}
345
+ )
346
+ expect(result.sql).toContain("json_each")
347
+ expect(result.sql).toContain("IN")
348
+ })
349
+
350
+ it("nested scopes", () => {
351
+ const result = buildWhereSQLQuery(
352
+ sqliteDialect,
353
+ "id",
354
+ [
355
+ { t: "where", path: "a", op: "eq", value: "1" },
356
+ {
357
+ t: "or-scope",
358
+ result: [
359
+ { t: "where", path: "b", op: "eq", value: "2" },
360
+ { t: "and", path: "c", op: "eq", value: "3" }
361
+ ],
362
+ relation: "some" as const
363
+ }
364
+ ],
365
+ "test",
366
+ {}
367
+ )
368
+ expect(result.sql).toContain("OR (")
369
+ expect(result.sql).toContain("AND")
370
+ expect(result.params).toEqual(expect.arrayContaining(["1", "2", "3"]))
371
+ })
372
+
373
+ it("id key maps to id column", () => {
374
+ const result = buildWhereSQLQuery(
375
+ sqliteDialect,
376
+ "myId",
377
+ [{ t: "where", path: "myId", op: "eq", value: "123" }],
378
+ "users",
379
+ {}
380
+ )
381
+ expect(result.sql).toContain("id = ?")
382
+ expect(result.sql).not.toContain("json_extract")
383
+ expect(result.params).toContain("123")
384
+ })
385
+
386
+ it("order + limit + skip", () => {
387
+ const result = buildWhereSQLQuery(
388
+ sqliteDialect,
389
+ "id",
390
+ [],
391
+ "users",
392
+ {},
393
+ undefined,
394
+ [{ key: "name", direction: "ASC" }] as any,
395
+ 5,
396
+ 10
397
+ )
398
+ expect(result.sql).toContain("ORDER BY")
399
+ expect(result.sql).toContain("ASC")
400
+ expect(result.sql).toContain("LIMIT")
401
+ expect(result.sql).toContain("OFFSET")
402
+ })
403
+
404
+ it("computed relation count projection", () => {
405
+ const result = buildWhereSQLQuery(
406
+ sqliteDialect,
407
+ "id",
408
+ [],
409
+ "users",
410
+ {},
411
+ [{
412
+ key: "pickedCount",
413
+ computed: {
414
+ _tag: "relation-count",
415
+ path: "items",
416
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
417
+ }
418
+ }]
419
+ )
420
+ expect(result.sql).toContain(`SELECT COUNT(1) FROM json_each(data, '$.items') AS _items`)
421
+ expect(result.sql).toContain(`AS "pickedCount"`)
422
+ expect(result.params).toContain("%picked%")
423
+ })
424
+
425
+ it("computed relation any projection (sqlite bool encoding)", () => {
426
+ const result = buildWhereSQLQuery(
427
+ sqliteDialect,
428
+ "id",
429
+ [],
430
+ "users",
431
+ {},
432
+ [{
433
+ key: "hasPicked",
434
+ computed: {
435
+ _tag: "relation-any",
436
+ path: "items",
437
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
438
+ }
439
+ }]
440
+ )
441
+ expect(result.sql).toContain("CASE WHEN EXISTS(")
442
+ expect(result.sql).toContain(`AS "hasPicked"`)
443
+ })
444
+
445
+ it("computed relation every projection (sqlite emits NOT EXISTS NOT)", () => {
446
+ const result = buildWhereSQLQuery(
447
+ sqliteDialect,
448
+ "id",
449
+ [],
450
+ "users",
451
+ {},
452
+ [{
453
+ key: "allPicked",
454
+ computed: {
455
+ _tag: "relation-every",
456
+ path: "items",
457
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
458
+ }
459
+ }]
460
+ )
461
+ expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM json_each(data, '$.items') AS _items WHERE NOT (`)
462
+ expect(result.sql).toContain(`AS "allPicked"`)
463
+ expect(result.params).toContain("picked")
464
+ })
465
+
466
+ it("computed relation-every with no filter degenerates to true", () => {
467
+ const result = buildWhereSQLQuery(
468
+ sqliteDialect,
469
+ "id",
470
+ [],
471
+ "users",
472
+ {},
473
+ [{
474
+ key: "allPicked",
475
+ computed: { _tag: "relation-every", path: "items", filter: [] }
476
+ }]
477
+ )
478
+ expect(result.sql).toContain("CASE WHEN 1=1")
479
+ expect(result.sql).toContain(`AS "allPicked"`)
480
+ })
481
+
482
+ it("computed relation-distinct-count projection (sqlite)", () => {
483
+ const result = buildWhereSQLQuery(
484
+ sqliteDialect,
485
+ "id",
486
+ [],
487
+ "users",
488
+ {},
489
+ [{
490
+ key: "positionCount",
491
+ computed: {
492
+ _tag: "relation-distinct-count",
493
+ path: "items",
494
+ field: "rowId",
495
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "neq", value: "cancelled" }]
496
+ }
497
+ }]
498
+ )
499
+ expect(result.sql).toContain(`SELECT COUNT(DISTINCT json_extract(_items.value, '$.rowId'))`)
500
+ expect(result.sql).toContain(`AS "positionCount"`)
501
+ expect(result.params).toContain("cancelled")
502
+ })
503
+
504
+ it("computed relation-sum projection (sqlite casts to REAL)", () => {
505
+ const result = buildWhereSQLQuery(
506
+ sqliteDialect,
507
+ "id",
508
+ [],
509
+ "users",
510
+ {},
511
+ [{
512
+ key: "totalWeight",
513
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
514
+ }]
515
+ )
516
+ expect(result.sql).toContain(`SELECT COALESCE(SUM(CAST(json_extract(_items.value, '$.weight') AS REAL)), 0)`)
517
+ expect(result.sql).toContain(`AS "totalWeight"`)
518
+ })
519
+
520
+ it("computed relation-sum-expr projection (sqlite)", () => {
521
+ const result = buildWhereSQLQuery(
522
+ sqliteDialect,
523
+ "id",
524
+ [],
525
+ "users",
526
+ {},
527
+ [{
528
+ key: "totalWeighted",
529
+ computed: {
530
+ _tag: "relation-sum-expr",
531
+ path: "items",
532
+ expression: {
533
+ _tag: "mul",
534
+ left: { _tag: "field", field: "weight" },
535
+ right: { _tag: "field", field: "tradeUnit.amount" }
536
+ },
537
+ filter: []
538
+ }
539
+ }]
540
+ )
541
+ expect(result.sql).toContain(
542
+ `COALESCE(SUM((CAST(json_extract(_items.value, '$.weight') AS REAL) * CAST(json_extract(_items.value, '$.tradeUnit.amount') AS REAL))), 0)`
543
+ )
544
+ expect(result.sql).toContain(`AS "totalWeighted"`)
545
+ })
546
+
547
+ it("computed relation-sum-expr-by projection (sqlite)", () => {
548
+ const result = buildWhereSQLQuery(
549
+ sqliteDialect,
550
+ "id",
551
+ [],
552
+ "users",
553
+ {},
554
+ [{
555
+ key: "totalsByUnit",
556
+ computed: {
557
+ _tag: "relation-sum-expr-by",
558
+ path: "items",
559
+ expression: {
560
+ _tag: "mul",
561
+ left: { _tag: "field", field: "weight" },
562
+ right: { _tag: "field", field: "tradeUnit.amount" }
563
+ },
564
+ unit: "tradeUnit.unit",
565
+ filter: []
566
+ }
567
+ }]
568
+ )
569
+ expect(result.sql).toContain(`json_group_array(json_object('unit', __unit, 'total', __total))`)
570
+ expect(result.sql).toContain(`GROUP BY json_extract(_items.value, '$.tradeUnit.unit')`)
571
+ expect(result.sql).toContain(`AS "totalsByUnit"`)
572
+ })
573
+
574
+ it("computed relation-sum-expr-normalized projection (sqlite)", () => {
575
+ const result = buildWhereSQLQuery(
576
+ sqliteDialect,
577
+ "id",
578
+ [],
579
+ "users",
580
+ {},
581
+ [{
582
+ key: "totalKg",
583
+ computed: {
584
+ _tag: "relation-sum-expr-normalized",
585
+ path: "items",
586
+ expression: {
587
+ _tag: "mul",
588
+ left: { _tag: "field", field: "weight" },
589
+ right: { _tag: "field", field: "tradeUnit.amount" }
590
+ },
591
+ unit: "tradeUnit.unit",
592
+ toBase: "kg",
593
+ factors: { g: 0.001 },
594
+ filter: []
595
+ }
596
+ }]
597
+ )
598
+ expect(result.sql).toContain(
599
+ `CASE json_extract(_items.value, '$.tradeUnit.unit') WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`
600
+ )
601
+ expect(result.sql).toContain(`AS "totalKg"`)
602
+ })
603
+
604
+ it("computed relation-collect (non-distinct) projection (sqlite)", () => {
605
+ const result = buildWhereSQLQuery(
606
+ sqliteDialect,
607
+ "id",
608
+ [],
609
+ "users",
610
+ {},
611
+ [{
612
+ key: "tags",
613
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
614
+ }]
615
+ )
616
+ expect(result.sql).toContain(
617
+ `SELECT COALESCE(json_group_array(json_extract(_items.value, '$.articleId')), json_array())`
618
+ )
619
+ expect(result.sql).toContain(`AS "tags"`)
620
+ })
621
+
622
+ it("computed relation-collect (distinct) emits inner DISTINCT subquery (sqlite)", () => {
623
+ const result = buildWhereSQLQuery(
624
+ sqliteDialect,
625
+ "id",
626
+ [],
627
+ "users",
628
+ {},
629
+ [{
630
+ key: "tags",
631
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
632
+ }]
633
+ )
634
+ expect(result.sql).toContain(`json_group_array(__v)`)
635
+ expect(result.sql).toContain(`SELECT DISTINCT json_extract(_items.value, '$.articleId') AS __v`)
636
+ expect(result.sql).toContain(`AS "tags"`)
637
+ })
638
+ })
639
+
640
+ describe("SQL query builder (PostgreSQL dialect)", () => {
641
+ it("where eq string uses ->> operator", () => {
642
+ const result = buildWhereSQLQuery(
643
+ pgDialect,
644
+ "id",
645
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
646
+ "users",
647
+ {}
648
+ )
649
+ expect(result.sql).toContain("data->>'name'")
650
+ expect(result.sql).toContain("$1")
651
+ expect(result.params).toContain("John")
652
+ })
653
+
654
+ it("where contains uses ILIKE", () => {
655
+ const result = buildWhereSQLQuery(
656
+ pgDialect,
657
+ "id",
658
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
659
+ "users",
660
+ {}
661
+ )
662
+ expect(result.sql).toContain("ILIKE")
663
+ expect(result.params).toContain("%oh%")
664
+ })
665
+
666
+ it("where in uses $N placeholders", () => {
667
+ const result = buildWhereSQLQuery(
668
+ pgDialect,
669
+ "id",
670
+ [{ t: "where", path: "status", op: "in", value: ["active", "pending"] }],
671
+ "users",
672
+ {}
673
+ )
674
+ expect(result.sql).toContain("$1")
675
+ expect(result.sql).toContain("$2")
676
+ expect(result.params).toEqual(expect.arrayContaining(["active", "pending"]))
677
+ })
678
+
679
+ it("where includes uses @> jsonb operator", () => {
680
+ const result = buildWhereSQLQuery(
681
+ pgDialect,
682
+ "id",
683
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
684
+ "users",
685
+ {}
686
+ )
687
+ expect(result.sql).toContain("@>")
688
+ expect(result.sql).toContain("jsonb")
689
+ })
690
+
691
+ it("nested path uses chained -> operators", () => {
692
+ const result = buildWhereSQLQuery(
693
+ pgDialect,
694
+ "id",
695
+ [{ t: "where", path: "address.city", op: "eq", value: "NYC" }],
696
+ "users",
697
+ {}
698
+ )
699
+ expect(result.sql).toContain("data->'address'->>'city'")
700
+ })
701
+
702
+ it("computed relation any projection", () => {
703
+ const result = buildWhereSQLQuery(
704
+ pgDialect,
705
+ "id",
706
+ [],
707
+ "users",
708
+ {},
709
+ [{
710
+ key: "hasPicked",
711
+ computed: {
712
+ _tag: "relation-any",
713
+ path: "items",
714
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
715
+ }
716
+ }]
717
+ )
718
+ expect(result.sql).toContain("EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items")
719
+ expect(result.sql).toContain(`AS "hasPicked"`)
720
+ })
721
+
722
+ it("computed relation-every (pg)", () => {
723
+ const result = buildWhereSQLQuery(
724
+ pgDialect,
725
+ "id",
726
+ [],
727
+ "users",
728
+ {},
729
+ [{
730
+ key: "allPicked",
731
+ computed: {
732
+ _tag: "relation-every",
733
+ path: "items",
734
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
735
+ }
736
+ }]
737
+ )
738
+ expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items WHERE NOT (`)
739
+ expect(result.sql).toContain(`AS "allPicked"`)
740
+ })
741
+
742
+ it("computed relation-distinct-count (pg)", () => {
743
+ const result = buildWhereSQLQuery(
744
+ pgDialect,
745
+ "id",
746
+ [],
747
+ "users",
748
+ {},
749
+ [{
750
+ key: "positions",
751
+ computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
752
+ }]
753
+ )
754
+ expect(result.sql).toContain(`COUNT(DISTINCT _items->>'rowId')`)
755
+ expect(result.sql).toContain(`AS "positions"`)
756
+ })
757
+
758
+ it("computed relation-sum (pg casts via ::numeric)", () => {
759
+ const result = buildWhereSQLQuery(
760
+ pgDialect,
761
+ "id",
762
+ [],
763
+ "users",
764
+ {},
765
+ [{
766
+ key: "totalWeight",
767
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
768
+ }]
769
+ )
770
+ expect(result.sql).toContain(`COALESCE(SUM((_items->>'weight')::numeric), 0)`)
771
+ expect(result.sql).toContain(`AS "totalWeight"`)
772
+ })
773
+
774
+ it("computed relation-sum-expr (pg)", () => {
775
+ const result = buildWhereSQLQuery(
776
+ pgDialect,
777
+ "id",
778
+ [],
779
+ "users",
780
+ {},
781
+ [{
782
+ key: "totalWeighted",
783
+ computed: {
784
+ _tag: "relation-sum-expr",
785
+ path: "items",
786
+ expression: {
787
+ _tag: "mul",
788
+ left: { _tag: "field", field: "weight" },
789
+ right: { _tag: "field", field: "tradeUnit.amount" }
790
+ },
791
+ filter: []
792
+ }
793
+ }]
794
+ )
795
+ expect(result.sql).toContain(
796
+ `COALESCE(SUM(((_items->>'weight')::numeric * (_items->'tradeUnit'->>'amount')::numeric)), 0)`
797
+ )
798
+ expect(result.sql).toContain(`AS "totalWeighted"`)
799
+ })
800
+
801
+ it("computed relation-sum-expr-by (pg)", () => {
802
+ const result = buildWhereSQLQuery(
803
+ pgDialect,
804
+ "id",
805
+ [],
806
+ "users",
807
+ {},
808
+ [{
809
+ key: "totalsByUnit",
810
+ computed: {
811
+ _tag: "relation-sum-expr-by",
812
+ path: "items",
813
+ expression: {
814
+ _tag: "mul",
815
+ left: { _tag: "field", field: "weight" },
816
+ right: { _tag: "field", field: "tradeUnit.amount" }
817
+ },
818
+ unit: "tradeUnit.unit",
819
+ filter: []
820
+ }
821
+ }]
822
+ )
823
+ expect(result.sql).toContain(`jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total))`)
824
+ expect(result.sql).toContain(`GROUP BY _items->'tradeUnit'->>'unit'`)
825
+ expect(result.sql).toContain(`AS "totalsByUnit"`)
826
+ })
827
+
828
+ it("computed relation-sum-expr-normalized (pg)", () => {
829
+ const result = buildWhereSQLQuery(
830
+ pgDialect,
831
+ "id",
832
+ [],
833
+ "users",
834
+ {},
835
+ [{
836
+ key: "totalKg",
837
+ computed: {
838
+ _tag: "relation-sum-expr-normalized",
839
+ path: "items",
840
+ expression: {
841
+ _tag: "mul",
842
+ left: { _tag: "field", field: "weight" },
843
+ right: { _tag: "field", field: "tradeUnit.amount" }
844
+ },
845
+ unit: "tradeUnit.unit",
846
+ toBase: "kg",
847
+ factors: { g: 0.001 },
848
+ filter: []
849
+ }
850
+ }]
851
+ )
852
+ expect(result.sql).toContain(`CASE _items->'tradeUnit'->>'unit' WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`)
853
+ expect(result.sql).toContain(`AS "totalKg"`)
854
+ })
855
+
856
+ it("computed relation-collect (pg jsonb_agg)", () => {
857
+ const result = buildWhereSQLQuery(
858
+ pgDialect,
859
+ "id",
860
+ [],
861
+ "users",
862
+ {},
863
+ [{
864
+ key: "ids",
865
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
866
+ }]
867
+ )
868
+ expect(result.sql).toContain(`COALESCE(jsonb_agg(_items->>'articleId'), '[]'::jsonb)`)
869
+ expect(result.sql).toContain(`AS "ids"`)
870
+ })
871
+
872
+ it("computed relation-collect distinct (pg jsonb_agg DISTINCT)", () => {
873
+ const result = buildWhereSQLQuery(
874
+ pgDialect,
875
+ "id",
876
+ [],
877
+ "users",
878
+ {},
879
+ [{
880
+ key: "ids",
881
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
882
+ }]
883
+ )
884
+ expect(result.sql).toContain(`COALESCE(jsonb_agg(DISTINCT _items->>'articleId'), '[]'::jsonb)`)
885
+ })
886
+ })
887
+
888
+ // --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
889
+
890
+ describe("SQL Store (SQLite integration)", () => {
891
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
892
+ const db = new BetterSqlite(":memory:")
893
+ db.pragma("journal_mode = WAL")
894
+ try {
895
+ fn(db)
896
+ } finally {
897
+ db.close()
898
+ }
899
+ }
900
+
901
+ it("creates table and seeds data", () =>
902
+ withDb((db) => {
903
+ db.exec(
904
+ `CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
905
+ )
906
+ db
907
+ .prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
908
+ .run("1", "etag1", JSON.stringify({ name: "Alice", age: 30 }))
909
+
910
+ const rows = db.prepare(`SELECT * FROM "test_items"`).all()
911
+ expect(rows.length).toBe(1)
912
+ expect((rows[0] as any).id).toBe("1")
913
+ }))
914
+
915
+ it("data column should not contain _etag or id", () =>
916
+ withDb((db) => {
917
+ db.exec(
918
+ `CREATE TABLE IF NOT EXISTS "test_clean" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
919
+ )
920
+ // Simulate what toRow now produces: data without id or _etag
921
+ const data = { name: "Alice", age: 30, tags: ["admin"] }
922
+ db
923
+ .prepare(`INSERT INTO "test_clean" (id, _etag, data) VALUES (?, ?, ?)`)
924
+ .run("1", "etag1", JSON.stringify(data))
925
+
926
+ const row = db.prepare(`SELECT * FROM "test_clean" WHERE id = ?`).get("1") as any
927
+ const parsed = JSON.parse(row.data) as any
928
+ expect(parsed).not.toHaveProperty("id")
929
+ expect(parsed).not.toHaveProperty("_etag")
930
+ expect(parsed.name).toBe("Alice")
931
+ expect(parsed.age).toBe(30)
932
+ expect(parsed.tags).toEqual(["admin"])
933
+ // id and _etag come from their own columns
934
+ expect(row.id).toBe("1")
935
+ expect(row._etag).toBe("etag1")
936
+ }))
937
+
938
+ it("backward compat: rows with id/_etag in data still work with queries", () =>
939
+ withDb((db) => {
940
+ db.exec(
941
+ `CREATE TABLE IF NOT EXISTS "test_compat" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
942
+ )
943
+ // Old format: id and _etag inside data
944
+ db
945
+ .prepare(`INSERT INTO "test_compat" (id, _etag, data) VALUES (?, ?, ?)`)
946
+ .run("1", "etag1", JSON.stringify({ id: "1", _etag: "old_etag", name: "Alice", age: 30 }))
947
+ // New format: id and _etag stripped from data
948
+ db
949
+ .prepare(`INSERT INTO "test_compat" (id, _etag, data) VALUES (?, ?, ?)`)
950
+ .run("2", "etag2", JSON.stringify({ name: "Bob", age: 25 }))
951
+
952
+ // Both should be queryable by name
953
+ const q1 = buildWhereSQLQuery(
954
+ sqliteDialect,
955
+ "id",
956
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
957
+ "test_compat",
958
+ {}
959
+ )
960
+ const r1 = query(db, q1.sql, q1.params)
961
+ expect(r1.length).toBe(1)
962
+ expect(r1[0].id).toBe("1")
963
+
964
+ const q2 = buildWhereSQLQuery(
965
+ sqliteDialect,
966
+ "id",
967
+ [{ t: "where", path: "name", op: "eq", value: "Bob" }],
968
+ "test_compat",
969
+ {}
970
+ )
971
+ const r2 = query(db, q2.sql, q2.params)
972
+ expect(r2.length).toBe(1)
973
+ expect(r2[0].id).toBe("2")
974
+
975
+ // Both queryable by id column
976
+ const q3 = buildWhereSQLQuery(
977
+ sqliteDialect,
978
+ "id",
979
+ [{ t: "where", path: "id", op: "in", value: ["1", "2"] }],
980
+ "test_compat",
981
+ {}
982
+ )
983
+ expect(query(db, q3.sql, q3.params).length).toBe(2)
984
+ }))
985
+
986
+ it("queries work when data does not contain id", () =>
987
+ withDb((db) => {
988
+ db.exec(
989
+ `CREATE TABLE IF NOT EXISTS "test_noid" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
990
+ )
991
+ const people = [
992
+ { name: "Alice", age: 30 },
993
+ { name: "Bob", age: 25 },
994
+ { name: "Charlie", age: 35 }
995
+ ]
996
+ const insert = db.prepare(
997
+ `INSERT INTO "test_noid" (id, _etag, data) VALUES (?, ?, ?)`
998
+ )
999
+ people.forEach((p, i) => insert.run(String(i + 1), `etag_${i + 1}`, JSON.stringify(p)))
1000
+
1001
+ // Filter by field in data
1002
+ const q1 = buildWhereSQLQuery(
1003
+ sqliteDialect,
1004
+ "id",
1005
+ [{ t: "where", path: "age", op: "gt", value: 28 }],
1006
+ "test_noid",
1007
+ {}
1008
+ )
1009
+ expect(query(db, q1.sql, q1.params).length).toBe(2) // Alice(30), Charlie(35)
1010
+
1011
+ // Filter by id column
1012
+ const q2 = buildWhereSQLQuery(
1013
+ sqliteDialect,
1014
+ "id",
1015
+ [{ t: "where", path: "id", op: "eq", value: "2" }],
1016
+ "test_noid",
1017
+ {}
1018
+ )
1019
+ const r2 = query(db, q2.sql, q2.params)
1020
+ expect(r2.length).toBe(1)
1021
+ expect(r2[0].id).toBe("2")
1022
+ expect((JSON.parse(r2[0].data) as any).name).toBe("Bob")
1023
+
1024
+ // Order + limit still works
1025
+ const q3 = buildWhereSQLQuery(
1026
+ sqliteDialect,
1027
+ "id",
1028
+ [],
1029
+ "test_noid",
1030
+ {},
1031
+ undefined,
1032
+ [{ key: "age", direction: "ASC" }] as any,
1033
+ undefined,
1034
+ 2
1035
+ )
1036
+ const r3 = query(db, q3.sql, q3.params)
1037
+ expect(r3.length).toBe(2)
1038
+ expect((JSON.parse(r3[0].data) as any).name).toBe("Bob") // youngest first
1039
+ }))
1040
+
1041
+ it("query builder generates valid SQL for SQLite", () =>
1042
+ withDb((db) => {
1043
+ db.exec(
1044
+ `CREATE TABLE IF NOT EXISTS "test_people" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
1045
+ )
1046
+
1047
+ const people = [
1048
+ { id: "1", name: "Alice", age: 30, tags: ["admin", "user"] },
1049
+ { id: "2", name: "Bob", age: 25, tags: ["user"] },
1050
+ { id: "3", name: "Charlie", age: 35, tags: ["admin"] },
1051
+ { id: "4", name: "Diana", age: 28, tags: ["user", "editor"] }
1052
+ ]
1053
+
1054
+ const insert = db.prepare(
1055
+ `INSERT INTO "test_people" (id, _etag, data) VALUES (?, ?, ?)`
1056
+ )
1057
+ for (const p of people) {
1058
+ const { id, ...data } = p
1059
+ insert.run(id, `etag_${id}`, JSON.stringify(data))
1060
+ }
1061
+
1062
+ // Test eq
1063
+ const q1 = buildWhereSQLQuery(
1064
+ sqliteDialect,
1065
+ "id",
1066
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
1067
+ "test_people",
1068
+ {}
1069
+ )
1070
+ expect(query(db, q1.sql, q1.params).length).toBe(1)
1071
+ expect((JSON.parse(query(db, q1.sql, q1.params)[0].data) as any).name).toBe("Alice")
1072
+
1073
+ // Test gt
1074
+ const q2 = buildWhereSQLQuery(
1075
+ sqliteDialect,
1076
+ "id",
1077
+ [{ t: "where", path: "age", op: "gt", value: 28 }],
1078
+ "test_people",
1079
+ {}
1080
+ )
1081
+ expect(query(db, q2.sql, q2.params).length).toBe(2)
1082
+
1083
+ // Test OR
1084
+ const q3 = buildWhereSQLQuery(
1085
+ sqliteDialect,
1086
+ "id",
1087
+ [
1088
+ { t: "where", path: "name", op: "eq", value: "Alice" },
1089
+ { t: "or", path: "name", op: "eq", value: "Bob" }
1090
+ ],
1091
+ "test_people",
1092
+ {}
1093
+ )
1094
+ expect(query(db, q3.sql, q3.params).length).toBe(2)
1095
+
1096
+ // Test AND
1097
+ const q4 = buildWhereSQLQuery(
1098
+ sqliteDialect,
1099
+ "id",
1100
+ [
1101
+ { t: "where", path: "name", op: "eq", value: "Alice" },
1102
+ { t: "and", path: "age", op: "gt", value: 25 }
1103
+ ],
1104
+ "test_people",
1105
+ {}
1106
+ )
1107
+ const r4 = query(db, q4.sql, q4.params)
1108
+ expect(r4.length).toBe(1)
1109
+ expect((JSON.parse(r4[0].data) as any).name).toBe("Alice")
1110
+
1111
+ // Test IN
1112
+ const q5 = buildWhereSQLQuery(
1113
+ sqliteDialect,
1114
+ "id",
1115
+ [{ t: "where", path: "id", op: "in", value: ["1", "3"] }],
1116
+ "test_people",
1117
+ {}
1118
+ )
1119
+ expect(query(db, q5.sql, q5.params).length).toBe(2)
1120
+
1121
+ // Test contains (string)
1122
+ const q6 = buildWhereSQLQuery(
1123
+ sqliteDialect,
1124
+ "id",
1125
+ [{ t: "where", path: "name", op: "contains", value: "li" }],
1126
+ "test_people",
1127
+ {}
1128
+ )
1129
+ expect(query(db, q6.sql, q6.params).length).toBe(2) // Alice, Charlie
1130
+
1131
+ // Test startsWith
1132
+ const q7 = buildWhereSQLQuery(
1133
+ sqliteDialect,
1134
+ "id",
1135
+ [{ t: "where", path: "name", op: "startsWith", value: "Al" }],
1136
+ "test_people",
1137
+ {}
1138
+ )
1139
+ const r7 = query(db, q7.sql, q7.params)
1140
+ expect(r7.length).toBe(1)
1141
+ expect((JSON.parse(r7[0].data) as any).name).toBe("Alice")
1142
+
1143
+ // Test includes (array)
1144
+ const q8 = buildWhereSQLQuery(
1145
+ sqliteDialect,
1146
+ "id",
1147
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
1148
+ "test_people",
1149
+ {}
1150
+ )
1151
+ expect(query(db, q8.sql, q8.params).length).toBe(2) // Alice, Charlie
1152
+
1153
+ // Test nested scope: where name = Alice OR (age > 30 AND name contains 'ar')
1154
+ const q9 = buildWhereSQLQuery(
1155
+ sqliteDialect,
1156
+ "id",
1157
+ [
1158
+ { t: "where", path: "name", op: "eq", value: "Alice" },
1159
+ {
1160
+ t: "or-scope",
1161
+ result: [
1162
+ { t: "where", path: "age", op: "gt", value: 30 },
1163
+ { t: "and", path: "name", op: "contains", value: "ar" }
1164
+ ],
1165
+ relation: "some"
1166
+ }
1167
+ ],
1168
+ "test_people",
1169
+ {}
1170
+ )
1171
+ expect(query(db, q9.sql, q9.params).length).toBe(2) // Alice + Charlie
1172
+
1173
+ // Test order + limit
1174
+ const q10 = buildWhereSQLQuery(
1175
+ sqliteDialect,
1176
+ "id",
1177
+ [],
1178
+ "test_people",
1179
+ {},
1180
+ undefined,
1181
+ [{ key: "age", direction: "DESC" }] as any,
1182
+ undefined,
1183
+ 2
1184
+ )
1185
+ const r10 = query(db, q10.sql, q10.params)
1186
+ expect(r10.length).toBe(2)
1187
+ expect((JSON.parse(r10[0].data) as any).name).toBe("Charlie") // oldest first
1188
+ }))
1189
+
1190
+ it("computed relation-every / distinct-count / sum / collect run on SQLite", () =>
1191
+ withDb((db) => {
1192
+ db.exec(`CREATE TABLE "test_orders" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1193
+ const orders = [
1194
+ {
1195
+ id: "o1",
1196
+ items: [
1197
+ { rowId: "r1", articleId: "A", weight: 1.5, state: { _tag: "picked" } },
1198
+ { rowId: "r2", articleId: "A", weight: 2.5, state: { _tag: "picked" } },
1199
+ { rowId: "r2", articleId: "B", weight: 0.25, state: { _tag: "picking" } }
1200
+ ]
1201
+ },
1202
+ {
1203
+ id: "o2",
1204
+ items: [
1205
+ { rowId: "r9", articleId: "Z", weight: 10, state: { _tag: "packed" } }
1206
+ ]
1207
+ }
1208
+ ]
1209
+ const insert = db.prepare(`INSERT INTO "test_orders" (id, _etag, data) VALUES (?, ?, ?)`)
1210
+ for (const o of orders) {
1211
+ const { id, ...data } = o
1212
+ insert.run(id, `etag_${id}`, JSON.stringify(data))
1213
+ }
1214
+
1215
+ const q = buildWhereSQLQuery(
1216
+ sqliteDialect,
1217
+ "id",
1218
+ [],
1219
+ "test_orders",
1220
+ {},
1221
+ [
1222
+ {
1223
+ key: "allPicked",
1224
+ computed: {
1225
+ _tag: "relation-every",
1226
+ path: "items",
1227
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
1228
+ }
1229
+ },
1230
+ {
1231
+ key: "positionCount",
1232
+ computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
1233
+ },
1234
+ {
1235
+ key: "totalWeight",
1236
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
1237
+ },
1238
+ {
1239
+ key: "articleIds",
1240
+ computed: {
1241
+ _tag: "relation-collect",
1242
+ path: "items",
1243
+ field: "articleId",
1244
+ distinct: true,
1245
+ filter: []
1246
+ }
1247
+ }
1248
+ ] as any,
1249
+ [{ key: "id", direction: "ASC" }] as any
1250
+ )
1251
+ const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1252
+ expect(rows.length).toBe(2)
1253
+ // o1: not all picked (one is "picking")
1254
+ expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(false)
1255
+ expect(rows[0]!["positionCount"]).toBe(2)
1256
+ expect(rows[0]!["totalWeight"]).toBeCloseTo(4.25)
1257
+ expect((JSON.parse(rows[0]!["articleIds"] as string) as string[]).sort()).toEqual(["A", "B"])
1258
+ // o2: all packed (so not "picked"), allPicked = !exists(NOT picked) = false
1259
+ expect(JSON.parse(rows[1]!["allPicked"] as string)).toBe(false)
1260
+ expect(rows[1]!["positionCount"]).toBe(1)
1261
+ expect(rows[1]!["totalWeight"]).toBeCloseTo(10)
1262
+ expect(JSON.parse(rows[1]!["articleIds"] as string)).toEqual(["Z"])
1263
+ }))
1264
+
1265
+ it("computed relation-every is true when all items match filter", () =>
1266
+ withDb((db) => {
1267
+ db.exec(`CREATE TABLE "test_every" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1268
+ db.prepare(`INSERT INTO "test_every" (id, _etag, data) VALUES (?, ?, ?)`).run(
1269
+ "1",
1270
+ "e",
1271
+ JSON.stringify({
1272
+ items: [
1273
+ { state: { _tag: "picked" } },
1274
+ { state: { _tag: "picked" } }
1275
+ ]
1276
+ })
1277
+ )
1278
+
1279
+ const q = buildWhereSQLQuery(
1280
+ sqliteDialect,
1281
+ "id",
1282
+ [],
1283
+ "test_every",
1284
+ {},
1285
+ [{
1286
+ key: "allPicked",
1287
+ computed: {
1288
+ _tag: "relation-every",
1289
+ path: "items",
1290
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
1291
+ }
1292
+ }]
1293
+ )
1294
+ const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1295
+ expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(true)
1296
+ }))
1297
+
1298
+ it("namespace param is in correct position for SQLite positional placeholders", () =>
1299
+ withDb((db) => {
1300
+ db.exec(
1301
+ `CREATE TABLE IF NOT EXISTS "test_ns" (id TEXT NOT NULL, _namespace TEXT NOT NULL DEFAULT 'primary', _etag TEXT, data JSON NOT NULL, PRIMARY KEY (id, _namespace))`
1302
+ )
1303
+ const insert = db.prepare(
1304
+ `INSERT INTO "test_ns" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`
1305
+ )
1306
+ insert.run("1", "primary", "e1", JSON.stringify({ name: "Alice", role: "admin" }))
1307
+ insert.run("2", "primary", "e2", JSON.stringify({ name: "Bob", role: "user" }))
1308
+ insert.run("3", "other", "e3", JSON.stringify({ name: "Charlie", role: "admin" }))
1309
+
1310
+ // Build a filter query: role != 'deleted'
1311
+ const q = buildWhereSQLQuery(
1312
+ sqliteDialect,
1313
+ "id",
1314
+ [{ t: "where", path: "role", op: "neq", value: "deleted" }],
1315
+ "test_ns",
1316
+ {}
1317
+ )
1318
+
1319
+ // Simulate what SQL.ts does: prepend _namespace = ? and put ns FIRST in params
1320
+ const hasWhere = q.sql.includes("WHERE")
1321
+ const nsSql = hasWhere
1322
+ ? q.sql.replace("WHERE", `WHERE _namespace = ? AND`)
1323
+ : q.sql.replace(`FROM "test_ns"`, `FROM "test_ns" WHERE _namespace = ?`)
1324
+ const params = ["primary", ...q.params]
1325
+
1326
+ const results = query(db, nsSql, params)
1327
+ // Should only get Alice and Bob (primary namespace), not Charlie (other namespace)
1328
+ expect(results.length).toBe(2)
1329
+ const names = results.map((r) => (JSON.parse(r.data) as any).name).sort()
1330
+ expect(names).toEqual(["Alice", "Bob"])
1331
+ }))
1332
+
1333
+ it("aggregate: agg-count-when with GROUP BY on aliased path field (SQLite)", () =>
1334
+ withDb((db) => {
1335
+ db.exec(`CREATE TABLE "test_agg" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1336
+ const rows = [
1337
+ { city: "NYC", state: "active" },
1338
+ { city: "NYC", state: "inactive" },
1339
+ { city: "NYC", state: "active" },
1340
+ { city: "LA", state: "active" }
1341
+ ]
1342
+ const insert = db.prepare(`INSERT INTO "test_agg" (id, _etag, data) VALUES (?, ?, ?)`)
1343
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1344
+
1345
+ const q = buildWhereSQLQuery(
1346
+ sqliteDialect,
1347
+ "id",
1348
+ [],
1349
+ "test_agg",
1350
+ {},
1351
+ [
1352
+ { key: "city", path: "city" },
1353
+ {
1354
+ key: "activeCount",
1355
+ aggregate: {
1356
+ _tag: "agg-count-when",
1357
+ filter: [{ t: "where", path: "state", op: "eq", value: "active" }]
1358
+ }
1359
+ },
1360
+ { key: "total", aggregate: { _tag: "agg-count" } }
1361
+ ] as any,
1362
+ [{ key: "city", direction: "ASC" }] as any
1363
+ )
1364
+
1365
+ expect(q.sql).toContain("GROUP BY")
1366
+ expect(q.sql).toContain("COUNT(CASE WHEN")
1367
+
1368
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1369
+ expect(result.length).toBe(2)
1370
+
1371
+ const la = result.find((r) => r["city"] === "LA")!
1372
+ expect(la["activeCount"]).toBe(1)
1373
+ expect(la["total"]).toBe(1)
1374
+
1375
+ const nyc = result.find((r) => r["city"] === "NYC")!
1376
+ expect(nyc["activeCount"]).toBe(2)
1377
+ expect(nyc["total"]).toBe(3)
1378
+ }))
1379
+
1380
+ it("aggregate: agg-sum and agg-min / agg-max (SQLite)", () =>
1381
+ withDb((db) => {
1382
+ db.exec(`CREATE TABLE "test_agg2" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1383
+ const rows = [
1384
+ { dept: "eng", salary: 100 },
1385
+ { dept: "eng", salary: 200 },
1386
+ { dept: "hr", salary: 50 }
1387
+ ]
1388
+ const insert = db.prepare(`INSERT INTO "test_agg2" (id, _etag, data) VALUES (?, ?, ?)`)
1389
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1390
+
1391
+ const q = buildWhereSQLQuery(
1392
+ sqliteDialect,
1393
+ "id",
1394
+ [],
1395
+ "test_agg2",
1396
+ {},
1397
+ [
1398
+ { key: "dept", path: "dept" },
1399
+ { key: "totalSalary", aggregate: { _tag: "agg-sum", field: "salary" } },
1400
+ { key: "minSalary", aggregate: { _tag: "agg-min", field: "salary" } },
1401
+ { key: "maxSalary", aggregate: { _tag: "agg-max", field: "salary" } }
1402
+ ] as any,
1403
+ [{ key: "dept", direction: "ASC" }] as any
1404
+ )
1405
+
1406
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1407
+ expect(result.length).toBe(2)
1408
+
1409
+ const eng = result.find((r) => r["dept"] === "eng")!
1410
+ expect(eng["totalSalary"]).toBeCloseTo(300)
1411
+ expect(eng["minSalary"]).toBeCloseTo(100)
1412
+ expect(eng["maxSalary"]).toBeCloseTo(200)
1413
+
1414
+ const hr = result.find((r) => r["dept"] === "hr")!
1415
+ expect(hr["totalSalary"]).toBeCloseTo(50)
1416
+ }))
1417
+
1418
+ it("aggregate: nested path field grouping (SQLite)", () =>
1419
+ withDb((db) => {
1420
+ db.exec(`CREATE TABLE "test_agg3" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1421
+ const rows = [
1422
+ { address: { city: "NYC" }, status: "open" },
1423
+ { address: { city: "NYC" }, status: "closed" },
1424
+ { address: { city: "LA" }, status: "open" }
1425
+ ]
1426
+ const insert = db.prepare(`INSERT INTO "test_agg3" (id, _etag, data) VALUES (?, ?, ?)`)
1427
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1428
+
1429
+ const q = buildWhereSQLQuery(
1430
+ sqliteDialect,
1431
+ "id",
1432
+ [],
1433
+ "test_agg3",
1434
+ {},
1435
+ [
1436
+ { key: "city", path: "address.city" },
1437
+ { key: "total", aggregate: { _tag: "agg-count" } }
1438
+ ] as any,
1439
+ [{ key: "city", direction: "ASC" }] as any
1440
+ )
1441
+
1442
+ expect(q.sql).toContain("GROUP BY")
1443
+
1444
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1445
+ expect(result.length).toBe(2)
1446
+ const la = result.find((r) => r["city"] === "LA")!
1447
+ expect(la["total"]).toBe(1)
1448
+ const nyc = result.find((r) => r["city"] === "NYC")!
1449
+ expect(nyc["total"]).toBe(2)
1450
+ }))
1451
+ })
1452
+
1453
+ // --- boolean WHERE clause tests (regression: where("x", true) must work per dialect) ---
1454
+
1455
+ describe("boolean WHERE clauses — query builder", () => {
1456
+ it("sqlite: eq true serializes bool → 1 integer", () => {
1457
+ const result = buildWhereSQLQuery(
1458
+ sqliteDialect,
1459
+ "id",
1460
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1461
+ "t",
1462
+ {}
1463
+ )
1464
+ expect(result.sql).toContain("json_extract(data, '$.flag') = ?")
1465
+ expect(result.params).toEqual([1])
1466
+ })
1467
+
1468
+ it("sqlite: eq false serializes bool → 0 integer", () => {
1469
+ const result = buildWhereSQLQuery(
1470
+ sqliteDialect,
1471
+ "id",
1472
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1473
+ "t",
1474
+ {}
1475
+ )
1476
+ expect(result.params).toEqual([0])
1477
+ })
1478
+
1479
+ it("sqlite: neq/in also serialize booleans", () => {
1480
+ const r1 = buildWhereSQLQuery(
1481
+ sqliteDialect,
1482
+ "id",
1483
+ [{ t: "where", path: "flag", op: "neq", value: true }],
1484
+ "t",
1485
+ {}
1486
+ )
1487
+ expect(r1.params).toEqual([1])
1488
+
1489
+ const r2 = buildWhereSQLQuery(
1490
+ sqliteDialect,
1491
+ "id",
1492
+ [{ t: "where", path: "flag", op: "in", value: [true, false] }],
1493
+ "t",
1494
+ {}
1495
+ )
1496
+ expect(r2.params).toEqual([1, 0])
1497
+ })
1498
+
1499
+ it("pg: eq true serializes bool → 'true' text (matches ->> output)", () => {
1500
+ const result = buildWhereSQLQuery(
1501
+ pgDialect,
1502
+ "id",
1503
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1504
+ "t",
1505
+ {}
1506
+ )
1507
+ expect(result.sql).toContain("data->>'flag' = $1")
1508
+ expect(result.params).toEqual(["true"])
1509
+ })
1510
+
1511
+ it("pg: eq false serializes bool → 'false' text", () => {
1512
+ const result = buildWhereSQLQuery(
1513
+ pgDialect,
1514
+ "id",
1515
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1516
+ "t",
1517
+ {}
1518
+ )
1519
+ expect(result.params).toEqual(["false"])
1520
+ })
1521
+
1522
+ it("pg: in with booleans serializes as text list", () => {
1523
+ const result = buildWhereSQLQuery(
1524
+ pgDialect,
1525
+ "id",
1526
+ [{ t: "where", path: "flag", op: "in", value: [true, false] }],
1527
+ "t",
1528
+ {}
1529
+ )
1530
+ expect(result.params).toEqual(["true", "false"])
1531
+ })
1532
+
1533
+ it("non-boolean scalars pass through unchanged (sqlite)", () => {
1534
+ const result = buildWhereSQLQuery(
1535
+ sqliteDialect,
1536
+ "id",
1537
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
1538
+ "t",
1539
+ {}
1540
+ )
1541
+ expect(result.params).toEqual(["Alice"])
1542
+ })
1543
+
1544
+ it("non-boolean scalars pass through unchanged (pg)", () => {
1545
+ const result = buildWhereSQLQuery(
1546
+ pgDialect,
1547
+ "id",
1548
+ [{ t: "where", path: "age", op: "gt", value: 18 }],
1549
+ "t",
1550
+ {}
1551
+ )
1552
+ expect(result.params).toEqual([18])
1553
+ })
1554
+ })
1555
+
1556
+ describe("boolean WHERE clauses — SQLite integration (end-to-end)", () => {
1557
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
1558
+ const db = new BetterSqlite(":memory:")
1559
+ try {
1560
+ fn(db)
1561
+ } finally {
1562
+ db.close()
1563
+ }
1564
+ }
1565
+
1566
+ it("where flag = true matches only true rows", () =>
1567
+ withDb((db) => {
1568
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1569
+ db
1570
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1571
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1572
+ db
1573
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1574
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1575
+
1576
+ const q = buildWhereSQLQuery(
1577
+ sqliteDialect,
1578
+ "id",
1579
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1580
+ "t",
1581
+ {}
1582
+ )
1583
+ const rows = query(db, q.sql, q.params)
1584
+ expect(rows.length).toBe(1)
1585
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
1586
+ }))
1587
+
1588
+ it("where flag = false matches only false rows", () =>
1589
+ withDb((db) => {
1590
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1591
+ db
1592
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1593
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1594
+ db
1595
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1596
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1597
+
1598
+ const q = buildWhereSQLQuery(
1599
+ sqliteDialect,
1600
+ "id",
1601
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1602
+ "t",
1603
+ {}
1604
+ )
1605
+ const rows = query(db, q.sql, q.params)
1606
+ expect(rows.length).toBe(1)
1607
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
1608
+ }))
1609
+
1610
+ it("where nested boolean path works", () =>
1611
+ withDb((db) => {
1612
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1613
+ db
1614
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1615
+ .run("1", "e", JSON.stringify({ meta: { active: true }, name: "Alice" }))
1616
+ db
1617
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1618
+ .run("2", "e", JSON.stringify({ meta: { active: false }, name: "Bob" }))
1619
+
1620
+ const q = buildWhereSQLQuery(
1621
+ sqliteDialect,
1622
+ "id",
1623
+ [{ t: "where", path: "meta.active", op: "eq", value: true }],
1624
+ "t",
1625
+ {}
1626
+ )
1627
+ const rows = query(db, q.sql, q.params)
1628
+ expect(rows.length).toBe(1)
1629
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
1630
+ }))
1631
+
1632
+ it("where hasKey / hasValue / hasKeyValue match Map tuple JSON", () =>
1633
+ withDb((db) => {
1634
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1635
+ db
1636
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1637
+ .run("1", "e", JSON.stringify({ meta: [["n", 1], ["x", 2]] }))
1638
+ db
1639
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1640
+ .run("2", "e", JSON.stringify({ meta: [["n", 9]] }))
1641
+
1642
+ const byKey = buildWhereSQLQuery(
1643
+ sqliteDialect,
1644
+ "id",
1645
+ [{ t: "where", path: "meta", op: "hasKey", value: "x" }],
1646
+ "t",
1647
+ {}
1648
+ )
1649
+ expect(query(db, byKey.sql, byKey.params).map((r) => r.id)).toEqual(["1"])
1650
+
1651
+ const byValue = buildWhereSQLQuery(
1652
+ sqliteDialect,
1653
+ "id",
1654
+ [{ t: "where", path: "meta", op: "hasValue", value: 9 }],
1655
+ "t",
1656
+ {}
1657
+ )
1658
+ expect(query(db, byValue.sql, byValue.params).map((r) => r.id)).toEqual(["2"])
1659
+
1660
+ const byPair = buildWhereSQLQuery(
1661
+ sqliteDialect,
1662
+ "id",
1663
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
1664
+ "t",
1665
+ {}
1666
+ )
1667
+ expect(query(db, byPair.sql, byPair.params).map((r) => r.id)).toEqual(["1"])
1668
+ }))
1669
+
1670
+ it("where neq boolean works", () =>
1671
+ withDb((db) => {
1672
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1673
+ db
1674
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1675
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1676
+ db
1677
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1678
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1679
+
1680
+ const q = buildWhereSQLQuery(
1681
+ sqliteDialect,
1682
+ "id",
1683
+ [{ t: "where", path: "flag", op: "neq", value: true }],
1684
+ "t",
1685
+ {}
1686
+ )
1687
+ const rows = query(db, q.sql, q.params)
1688
+ expect(rows.length).toBe(1)
1689
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
1690
+ }))
1691
+ })
1692
+
1693
+ // --- jsonExtractJson dialect tests (regression: booleans must survive round-trip) ---
1694
+
1695
+ describe("sqliteDialect.jsonExtractJson boolean round-trip", () => {
1696
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
1697
+ const db = new BetterSqlite(":memory:")
1698
+ try {
1699
+ fn(db)
1700
+ } finally {
1701
+ db.close()
1702
+ }
1703
+ }
1704
+
1705
+ it("SQL uses json_type CASE to preserve booleans", () => {
1706
+ const sql = sqliteDialect.jsonExtractJson("flag")
1707
+ expect(sql).toContain("json_type(data, '$.flag')")
1708
+ expect(sql).toContain("'true'")
1709
+ expect(sql).toContain("'false'")
1710
+ expect(sql).toContain("json_quote(json_extract(data, '$.flag'))")
1711
+ })
1712
+
1713
+ it("boolean false round-trips as false (not 0)", () =>
1714
+ withDb((db) => {
1715
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1716
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: false }))
1717
+
1718
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
1719
+ const parsed = JSON.parse(row.v)
1720
+ expect(parsed).toBe(false)
1721
+ expect(typeof parsed).toBe("boolean")
1722
+ }))
1723
+
1724
+ it("boolean true round-trips as true (not 1)", () =>
1725
+ withDb((db) => {
1726
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1727
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: true }))
1728
+
1729
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
1730
+ const parsed = JSON.parse(row.v)
1731
+ expect(parsed).toBe(true)
1732
+ expect(typeof parsed).toBe("boolean")
1733
+ }))
1734
+
1735
+ it("nested boolean round-trips correctly", () =>
1736
+ withDb((db) => {
1737
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1738
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
1739
+ JSON.stringify({ nested: { flag: false } })
1740
+ )
1741
+
1742
+ const row = db
1743
+ .prepare(`SELECT ${sqliteDialect.jsonExtractJson("nested.flag")} AS v FROM "t"`)
1744
+ .get() as any
1745
+ const parsed = JSON.parse(row.v)
1746
+ expect(parsed).toBe(false)
1747
+ expect(typeof parsed).toBe("boolean")
1748
+ }))
1749
+
1750
+ it("numbers 0 and 1 still parse as numbers (not booleans)", () =>
1751
+ withDb((db) => {
1752
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1753
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ zero: 0, one: 1 }))
1754
+
1755
+ const zRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("zero")} AS v FROM "t"`).get() as any
1756
+ const oRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("one")} AS v FROM "t"`).get() as any
1757
+ expect(JSON.parse(zRow.v)).toBe(0)
1758
+ expect(typeof JSON.parse(zRow.v)).toBe("number")
1759
+ expect(JSON.parse(oRow.v)).toBe(1)
1760
+ expect(typeof JSON.parse(oRow.v)).toBe("number")
1761
+ }))
1762
+
1763
+ it("strings, arrays, and null still round-trip correctly", () =>
1764
+ withDb((db) => {
1765
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1766
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
1767
+ JSON.stringify({ s: "hello", arr: [1, 2, 3], nil: null })
1768
+ )
1769
+
1770
+ const sRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("s")} AS v FROM "t"`).get() as any
1771
+ const aRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("arr")} AS v FROM "t"`).get() as any
1772
+ const nRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("nil")} AS v FROM "t"`).get() as any
1773
+ expect(JSON.parse(sRow.v)).toBe("hello")
1774
+ expect(JSON.parse(aRow.v)).toEqual([1, 2, 3])
1775
+ expect(JSON.parse(nRow.v)).toBe(null)
1776
+ }))
1777
+ })
1778
+
1779
+ // --- toRow stripping and parseRow reconstruction tests ---
1780
+
1781
+ describe("toRow strips _etag and id from data", () => {
1782
+ // Replicate the toRow logic from SQL.ts to test in isolation
1783
+ const toRow = <IdKey extends PropertyKey>(e: any, idKey: IdKey) => {
1784
+ const newE = makeETag(e)
1785
+ const id = newE[idKey] as string
1786
+ const { _etag, [idKey]: _id, ...rest } = newE
1787
+ const data = JSON.stringify(rest)
1788
+ return { id, _etag: newE._etag!, data, item: newE }
1789
+ }
1790
+
1791
+ it("data JSON does not contain _etag", () => {
1792
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice", age: 30 }, "id")
1793
+ const parsed = JSON.parse(row.data) as any
1794
+ expect(parsed).not.toHaveProperty("_etag")
1795
+ expect(parsed.name).toBe("Alice")
1796
+ expect(parsed.age).toBe(30)
1797
+ })
1798
+
1799
+ it("data JSON does not contain id field", () => {
1800
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
1801
+ const parsed = JSON.parse(row.data) as any
1802
+ expect(parsed).not.toHaveProperty("id")
1803
+ expect(parsed.name).toBe("Alice")
1804
+ })
1805
+
1806
+ it("data JSON does not contain custom idKey field", () => {
1807
+ const row = toRow({ myId: "abc", _etag: undefined, name: "Bob" }, "myId")
1808
+ const parsed = JSON.parse(row.data) as any
1809
+ expect(parsed).not.toHaveProperty("myId")
1810
+ expect(parsed.name).toBe("Bob")
1811
+ expect(row.id).toBe("abc")
1812
+ })
1813
+
1814
+ it("id and _etag are returned as separate fields", () => {
1815
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
1816
+ expect(row.id).toBe("1")
1817
+ expect(typeof row._etag).toBe("string")
1818
+ expect(row._etag.length).toBeGreaterThan(0)
1819
+ })
1820
+
1821
+ it("item still contains all fields including _etag and id", () => {
1822
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
1823
+ expect(row.item.id).toBe("1")
1824
+ expect(row.item._etag).toBe(row._etag)
1825
+ expect(row.item.name).toBe("Alice")
1826
+ })
1827
+
1828
+ it("preserves nested objects in data", () => {
1829
+ const row = toRow({ id: "1", _etag: undefined, address: { city: "NYC", zip: "10001" } }, "id")
1830
+ const parsed = JSON.parse(row.data) as any
1831
+ expect(parsed.address).toEqual({ city: "NYC", zip: "10001" })
1832
+ expect(parsed).not.toHaveProperty("id")
1833
+ expect(parsed).not.toHaveProperty("_etag")
1834
+ })
1835
+ })
1836
+
1837
+ describe("parseRow reconstructs full object from row", () => {
1838
+ it("re-injects id from row column using idKey", () => {
1839
+ const result: any = parseRow(
1840
+ { id: "42", _etag: "etag1", data: JSON.stringify({ name: "Alice", age: 30 }) },
1841
+ "id",
1842
+ {}
1843
+ )
1844
+ expect(result.id).toBe("42")
1845
+ expect(result.name).toBe("Alice")
1846
+ expect(result.age).toBe(30)
1847
+ expect(result._etag).toBe("etag1")
1848
+ })
1849
+
1850
+ it("re-injects custom idKey from row column", () => {
1851
+ const result: any = parseRow(
1852
+ { id: "abc", _etag: "etag2", data: JSON.stringify({ name: "Bob" }) },
1853
+ "myId",
1854
+ {}
1855
+ )
1856
+ expect(result.myId).toBe("abc")
1857
+ expect(result.name).toBe("Bob")
1858
+ expect(result._etag).toBe("etag2")
1859
+ })
1860
+
1861
+ it("uses _etag from row column, not from data", () => {
1862
+ const result: any = parseRow(
1863
+ { id: "1", _etag: "column_etag", data: JSON.stringify({ _etag: "stale_data_etag", name: "Alice" }) },
1864
+ "id",
1865
+ {}
1866
+ )
1867
+ expect(result._etag).toBe("column_etag")
1868
+ })
1869
+
1870
+ it("uses id from row column, not from data", () => {
1871
+ const result: any = parseRow(
1872
+ { id: "correct_id", _etag: "e1", data: JSON.stringify({ id: "wrong_id", name: "Alice" }) },
1873
+ "id",
1874
+ {}
1875
+ )
1876
+ expect(result.id).toBe("correct_id")
1877
+ })
1878
+
1879
+ it("applies defaultValues for missing fields", () => {
1880
+ const result: any = parseRow(
1881
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice" }) },
1882
+ "id",
1883
+ { status: "active", role: "user" }
1884
+ )
1885
+ expect(result.name).toBe("Alice")
1886
+ expect(result.status).toBe("active")
1887
+ expect(result.role).toBe("user")
1888
+ })
1889
+
1890
+ it("data fields override defaultValues", () => {
1891
+ const result: any = parseRow(
1892
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice", status: "inactive" }) },
1893
+ "id",
1894
+ { status: "active" }
1895
+ )
1896
+ expect(result.status).toBe("inactive")
1897
+ })
1898
+
1899
+ it("handles null _etag from row", () => {
1900
+ const result: any = parseRow(
1901
+ { id: "1", _etag: null, data: JSON.stringify({ name: "Alice" }) },
1902
+ "id",
1903
+ {}
1904
+ )
1905
+ expect(result._etag).toBeUndefined()
1906
+ })
1907
+
1908
+ it("round-trip: toRow then parseRow reconstructs the original", () => {
1909
+ const original = { id: "1", _etag: undefined as string | undefined, name: "Alice", age: 30, tags: ["admin"] }
1910
+ const newE = makeETag(original)
1911
+ const { _etag, id: _id, ...rest } = newE as any
1912
+ const row = { id: newE.id, _etag: newE._etag!, data: JSON.stringify(rest) }
1913
+
1914
+ const reconstructed: any = parseRow(row, "id", {})
1915
+ expect(reconstructed.id).toBe("1")
1916
+ expect(reconstructed.name).toBe("Alice")
1917
+ expect(reconstructed.age).toBe(30)
1918
+ expect(reconstructed.tags).toEqual(["admin"])
1919
+ expect(reconstructed._etag).toBe(newE._etag)
1920
+ })
1921
+
1922
+ it("lowers Date defaultValues to ISO before merge", () => {
1923
+ const result: any = parseRow(
1924
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice" }) },
1925
+ "id",
1926
+ { at: new Date("2024-06-01T00:00:00.000Z") }
1927
+ )
1928
+ expect(result.at).toBe("2024-06-01T00:00:00.000Z")
1929
+ expect(result.name).toBe("Alice")
1930
+ })
1931
+ })