@effect-app/infra 4.0.0-beta.20 → 4.0.0-beta.201

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 (305) hide show
  1. package/CHANGELOG.md +1410 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +15 -7
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +10 -12
  6. package/dist/Emailer/Sendgrid.d.ts +14 -14
  7. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  8. package/dist/Emailer/Sendgrid.js +16 -15
  9. package/dist/Emailer/fake.d.ts +1 -1
  10. package/dist/Emailer/service.d.ts +10 -4
  11. package/dist/Emailer/service.d.ts.map +1 -1
  12. package/dist/Emailer/service.js +3 -3
  13. package/dist/Emailer.d.ts +1 -1
  14. package/dist/MainFiberSet.d.ts +9 -9
  15. package/dist/MainFiberSet.d.ts.map +1 -1
  16. package/dist/MainFiberSet.js +3 -3
  17. package/dist/Model/Repository/Registry.d.ts +20 -0
  18. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  19. package/dist/Model/Repository/Registry.js +17 -0
  20. package/dist/Model/Repository/ext.d.ts +33 -15
  21. package/dist/Model/Repository/ext.d.ts.map +1 -1
  22. package/dist/Model/Repository/ext.js +54 -2
  23. package/dist/Model/Repository/internal/internal.d.ts +6 -6
  24. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  25. package/dist/Model/Repository/internal/internal.js +43 -32
  26. package/dist/Model/Repository/legacy.d.ts +1 -1
  27. package/dist/Model/Repository/makeRepo.d.ts +7 -6
  28. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  29. package/dist/Model/Repository/makeRepo.js +5 -1
  30. package/dist/Model/Repository/service.d.ts +28 -23
  31. package/dist/Model/Repository/service.d.ts.map +1 -1
  32. package/dist/Model/Repository/validation.d.ts +142 -17
  33. package/dist/Model/Repository/validation.d.ts.map +1 -1
  34. package/dist/Model/Repository/validation.js +5 -5
  35. package/dist/Model/Repository.d.ts +2 -1
  36. package/dist/Model/Repository.d.ts.map +1 -1
  37. package/dist/Model/Repository.js +2 -1
  38. package/dist/Model/dsl.d.ts +4 -4
  39. package/dist/Model/dsl.d.ts.map +1 -1
  40. package/dist/Model/filter/filterApi.d.ts +5 -5
  41. package/dist/Model/filter/filterApi.d.ts.map +1 -1
  42. package/dist/Model/filter/types/errors.d.ts +1 -1
  43. package/dist/Model/filter/types/fields.d.ts +1 -1
  44. package/dist/Model/filter/types/path/common.d.ts +1 -1
  45. package/dist/Model/filter/types/path/eager.d.ts +1 -1
  46. package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
  47. package/dist/Model/filter/types/path/eager.js +1 -1
  48. package/dist/Model/filter/types/path/index.d.ts +1 -1
  49. package/dist/Model/filter/types/utils.d.ts +1 -1
  50. package/dist/Model/filter/types/validator.d.ts +1 -1
  51. package/dist/Model/filter/types.d.ts +1 -1
  52. package/dist/Model/query/dsl.d.ts +16 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/new-kid-interpreter.d.ts +6 -6
  55. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  56. package/dist/Model/query/new-kid-interpreter.js +3 -3
  57. package/dist/Model/query.d.ts +1 -1
  58. package/dist/Model.d.ts +2 -1
  59. package/dist/Model.d.ts.map +1 -1
  60. package/dist/Model.js +2 -1
  61. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  62. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  63. package/dist/QueueMaker/SQLQueue.js +105 -114
  64. package/dist/QueueMaker/errors.d.ts +2 -2
  65. package/dist/QueueMaker/errors.d.ts.map +1 -1
  66. package/dist/QueueMaker/memQueue.d.ts +7 -4
  67. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  68. package/dist/QueueMaker/memQueue.js +51 -62
  69. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  70. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  71. package/dist/QueueMaker/sbqueue.js +37 -53
  72. package/dist/QueueMaker/service.d.ts +1 -1
  73. package/dist/RequestContext.d.ts +112 -26
  74. package/dist/RequestContext.d.ts.map +1 -1
  75. package/dist/RequestContext.js +7 -8
  76. package/dist/RequestFiberSet.d.ts +7 -7
  77. package/dist/RequestFiberSet.d.ts.map +1 -1
  78. package/dist/RequestFiberSet.js +5 -5
  79. package/dist/Store/ContextMapContainer.d.ts +19 -3
  80. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  81. package/dist/Store/ContextMapContainer.js +13 -3
  82. package/dist/Store/Cosmos/query.d.ts +1 -1
  83. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  84. package/dist/Store/Cosmos/query.js +10 -12
  85. package/dist/Store/Cosmos.d.ts +1 -1
  86. package/dist/Store/Cosmos.d.ts.map +1 -1
  87. package/dist/Store/Cosmos.js +318 -240
  88. package/dist/Store/Disk.d.ts +2 -2
  89. package/dist/Store/Disk.d.ts.map +1 -1
  90. package/dist/Store/Disk.js +25 -22
  91. package/dist/Store/Memory.d.ts +4 -4
  92. package/dist/Store/Memory.d.ts.map +1 -1
  93. package/dist/Store/Memory.js +27 -22
  94. package/dist/Store/SQL/Pg.d.ts +4 -0
  95. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  96. package/dist/Store/SQL/Pg.js +189 -0
  97. package/dist/Store/SQL/query.d.ts +38 -0
  98. package/dist/Store/SQL/query.d.ts.map +1 -0
  99. package/dist/Store/SQL/query.js +367 -0
  100. package/dist/Store/SQL.d.ts +20 -0
  101. package/dist/Store/SQL.d.ts.map +1 -0
  102. package/dist/Store/SQL.js +381 -0
  103. package/dist/Store/codeFilter.d.ts +1 -1
  104. package/dist/Store/codeFilter.d.ts.map +1 -1
  105. package/dist/Store/codeFilter.js +2 -1
  106. package/dist/Store/index.d.ts +5 -2
  107. package/dist/Store/index.d.ts.map +1 -1
  108. package/dist/Store/index.js +15 -3
  109. package/dist/Store/service.d.ts +18 -7
  110. package/dist/Store/service.d.ts.map +1 -1
  111. package/dist/Store/service.js +24 -6
  112. package/dist/Store/utils.d.ts +1 -1
  113. package/dist/Store/utils.d.ts.map +1 -1
  114. package/dist/Store/utils.js +3 -4
  115. package/dist/Store.d.ts +1 -1
  116. package/dist/adapters/SQL/Model.d.ts +28 -42
  117. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  118. package/dist/adapters/SQL/Model.js +2 -2
  119. package/dist/adapters/SQL.d.ts +1 -1
  120. package/dist/adapters/ServiceBus.d.ts +11 -11
  121. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  122. package/dist/adapters/ServiceBus.js +13 -15
  123. package/dist/adapters/cosmos-client.d.ts +3 -3
  124. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  125. package/dist/adapters/cosmos-client.js +3 -3
  126. package/dist/adapters/index.d.ts +8 -2
  127. package/dist/adapters/index.d.ts.map +1 -1
  128. package/dist/adapters/index.js +8 -2
  129. package/dist/adapters/logger.d.ts +2 -2
  130. package/dist/adapters/logger.d.ts.map +1 -1
  131. package/dist/adapters/memQueue.d.ts +3 -3
  132. package/dist/adapters/memQueue.d.ts.map +1 -1
  133. package/dist/adapters/memQueue.js +3 -3
  134. package/dist/adapters/mongo-client.d.ts +3 -3
  135. package/dist/adapters/mongo-client.d.ts.map +1 -1
  136. package/dist/adapters/mongo-client.js +3 -3
  137. package/dist/adapters/redis-client.d.ts +3 -3
  138. package/dist/adapters/redis-client.d.ts.map +1 -1
  139. package/dist/adapters/redis-client.js +3 -3
  140. package/dist/api/ContextProvider.d.ts +8 -8
  141. package/dist/api/ContextProvider.d.ts.map +1 -1
  142. package/dist/api/ContextProvider.js +6 -6
  143. package/dist/api/codec.d.ts +1 -1
  144. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  145. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  146. package/dist/api/internal/RequestContextMiddleware.js +2 -2
  147. package/dist/api/internal/auth.d.ts +44 -6
  148. package/dist/api/internal/auth.d.ts.map +1 -1
  149. package/dist/api/internal/auth.js +160 -29
  150. package/dist/api/internal/events.d.ts +3 -3
  151. package/dist/api/internal/events.d.ts.map +1 -1
  152. package/dist/api/internal/events.js +9 -7
  153. package/dist/api/internal/health.d.ts +1 -1
  154. package/dist/api/layerUtils.d.ts +6 -6
  155. package/dist/api/layerUtils.d.ts.map +1 -1
  156. package/dist/api/layerUtils.js +5 -5
  157. package/dist/api/middlewares.d.ts +1 -1
  158. package/dist/api/reportError.d.ts +1 -1
  159. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  161. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  162. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  163. package/dist/api/routing/middleware/middleware.js +46 -14
  164. package/dist/api/routing/middleware.d.ts +1 -2
  165. package/dist/api/routing/middleware.d.ts.map +1 -1
  166. package/dist/api/routing/middleware.js +1 -2
  167. package/dist/api/routing/schema/jwt.d.ts +1 -1
  168. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  169. package/dist/api/routing/tsort.d.ts +1 -1
  170. package/dist/api/routing/tsort.d.ts.map +1 -1
  171. package/dist/api/routing/utils.d.ts +3 -3
  172. package/dist/api/routing/utils.d.ts.map +1 -1
  173. package/dist/api/routing.d.ts +25 -26
  174. package/dist/api/routing.d.ts.map +1 -1
  175. package/dist/api/routing.js +99 -35
  176. package/dist/api/setupRequest.d.ts +8 -5
  177. package/dist/api/setupRequest.d.ts.map +1 -1
  178. package/dist/api/setupRequest.js +12 -7
  179. package/dist/api/util.d.ts +1 -1
  180. package/dist/arbs.d.ts +1 -1
  181. package/dist/arbs.d.ts.map +1 -1
  182. package/dist/arbs.js +5 -3
  183. package/dist/errorReporter.d.ts +4 -4
  184. package/dist/errorReporter.d.ts.map +1 -1
  185. package/dist/errorReporter.js +20 -25
  186. package/dist/errors.d.ts +1 -1
  187. package/dist/fileUtil.d.ts +1 -1
  188. package/dist/fileUtil.d.ts.map +1 -1
  189. package/dist/index.d.ts +1 -1
  190. package/dist/logger/jsonLogger.d.ts +1 -1
  191. package/dist/logger/logFmtLogger.d.ts +1 -1
  192. package/dist/logger/shared.d.ts +1 -1
  193. package/dist/logger/shared.js +2 -2
  194. package/dist/logger.d.ts +1 -1
  195. package/dist/logger.d.ts.map +1 -1
  196. package/dist/rateLimit.d.ts +9 -3
  197. package/dist/rateLimit.d.ts.map +1 -1
  198. package/dist/rateLimit.js +5 -11
  199. package/dist/test.d.ts +2 -2
  200. package/dist/test.d.ts.map +1 -1
  201. package/dist/test.js +1 -1
  202. package/dist/vitest.d.ts +1 -1
  203. package/examples/query.ts +39 -35
  204. package/package.json +41 -37
  205. package/src/CUPS.ts +9 -11
  206. package/src/Emailer/Sendgrid.ts +17 -14
  207. package/src/Emailer/service.ts +9 -3
  208. package/src/MainFiberSet.ts +5 -6
  209. package/src/Model/Repository/Registry.ts +33 -0
  210. package/src/Model/Repository/ext.ts +96 -10
  211. package/src/Model/Repository/internal/internal.ts +97 -88
  212. package/src/Model/Repository/makeRepo.ts +12 -10
  213. package/src/Model/Repository/service.ts +31 -22
  214. package/src/Model/Repository/validation.ts +4 -4
  215. package/src/Model/Repository.ts +1 -0
  216. package/src/Model/dsl.ts +3 -3
  217. package/src/Model/filter/types/path/eager.ts +1 -2
  218. package/src/Model/query/dsl.ts +18 -18
  219. package/src/Model/query/new-kid-interpreter.ts +2 -2
  220. package/src/Model.ts +1 -0
  221. package/src/QueueMaker/SQLQueue.ts +121 -151
  222. package/src/QueueMaker/memQueue.ts +82 -103
  223. package/src/QueueMaker/sbqueue.ts +56 -86
  224. package/src/RequestContext.ts +8 -10
  225. package/src/RequestFiberSet.ts +4 -4
  226. package/src/Store/ContextMapContainer.ts +41 -2
  227. package/src/Store/Cosmos/query.ts +16 -20
  228. package/src/Store/Cosmos.ts +452 -342
  229. package/src/Store/Disk.ts +52 -49
  230. package/src/Store/Memory.ts +54 -48
  231. package/src/Store/SQL/Pg.ts +318 -0
  232. package/src/Store/SQL/query.ts +409 -0
  233. package/src/Store/SQL.ts +668 -0
  234. package/src/Store/codeFilter.ts +1 -0
  235. package/src/Store/index.ts +17 -2
  236. package/src/Store/service.ts +32 -8
  237. package/src/Store/utils.ts +23 -22
  238. package/src/adapters/SQL/Model.ts +10 -4
  239. package/src/adapters/ServiceBus.ts +112 -116
  240. package/src/adapters/cosmos-client.ts +2 -2
  241. package/src/adapters/index.ts +7 -0
  242. package/src/adapters/memQueue.ts +2 -2
  243. package/src/adapters/mongo-client.ts +2 -2
  244. package/src/adapters/redis-client.ts +2 -2
  245. package/src/api/ContextProvider.ts +12 -13
  246. package/src/api/internal/RequestContextMiddleware.ts +1 -1
  247. package/src/api/internal/auth.ts +246 -44
  248. package/src/api/internal/events.ts +12 -8
  249. package/src/api/layerUtils.ts +8 -8
  250. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  251. package/src/api/routing/middleware/middleware.ts +53 -12
  252. package/src/api/routing/middleware.ts +0 -2
  253. package/src/api/routing.ts +173 -63
  254. package/src/api/setupRequest.ts +28 -8
  255. package/src/arbs.ts +4 -2
  256. package/src/errorReporter.ts +62 -74
  257. package/src/logger/shared.ts +1 -1
  258. package/src/rateLimit.ts +30 -22
  259. package/src/test.ts +1 -1
  260. package/test/auth.test.ts +101 -0
  261. package/test/contextProvider.test.ts +11 -11
  262. package/test/controller.test.ts +19 -17
  263. package/test/dist/auth.test.d.ts.map +1 -0
  264. package/test/dist/contextProvider.test.d.ts.map +1 -1
  265. package/test/dist/controller.test.d.ts.map +1 -1
  266. package/test/dist/date-query.test.d.ts.map +1 -0
  267. package/test/dist/fixtures.d.ts +26 -12
  268. package/test/dist/fixtures.d.ts.map +1 -1
  269. package/test/dist/fixtures.js +12 -10
  270. package/test/dist/query.test.d.ts.map +1 -1
  271. package/test/dist/rawQuery.test.d.ts.map +1 -1
  272. package/test/dist/repository-ext.test.d.ts.map +1 -0
  273. package/test/dist/requires.test.d.ts.map +1 -1
  274. package/test/dist/router-generator.test.d.ts.map +1 -0
  275. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  276. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  277. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  278. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  279. package/test/dist/sql-store.test.d.ts.map +1 -0
  280. package/test/fixtures.ts +11 -9
  281. package/test/query.test.ts +216 -34
  282. package/test/rawQuery.test.ts +23 -19
  283. package/test/repository-ext.test.ts +60 -0
  284. package/test/requires.test.ts +6 -6
  285. package/test/router-generator.test.ts +183 -0
  286. package/test/routing-interruptibility.test.ts +63 -0
  287. package/test/rpc-e2e-invalidation.test.ts +249 -0
  288. package/test/rpc-multi-middleware.test.ts +78 -9
  289. package/test/rpc-stream-fullstack.test.ts +325 -0
  290. package/test/sql-store.test.ts +1064 -0
  291. package/test/validateSample.test.ts +15 -12
  292. package/tsconfig.examples.json +1 -1
  293. package/tsconfig.json +0 -1
  294. package/tsconfig.json.bak +2 -2
  295. package/tsconfig.src.json +35 -35
  296. package/tsconfig.test.json +2 -2
  297. package/dist/Operations.d.ts +0 -55
  298. package/dist/Operations.d.ts.map +0 -1
  299. package/dist/Operations.js +0 -102
  300. package/dist/OperationsRepo.d.ts +0 -41
  301. package/dist/OperationsRepo.d.ts.map +0 -1
  302. package/dist/OperationsRepo.js +0 -14
  303. package/eslint.config.mjs +0 -24
  304. package/src/Operations.ts +0 -235
  305. package/src/OperationsRepo.ts +0 -16
@@ -0,0 +1,1064 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type Sqlite from "better-sqlite3"
3
+ import BetterSqlite from "better-sqlite3"
4
+ import { describe, expect, it } from "vitest"
5
+ import { parseRow } from "../src/Store/SQL.js"
6
+ import { buildWhereSQLQuery, pgDialect, sqliteDialect } from "../src/Store/SQL/query.js"
7
+ import { makeETag } from "../src/Store/utils.js"
8
+
9
+ const query = (db: Sqlite.Database, sql: string, params: unknown[] = []) =>
10
+ db.prepare(sql).all(...params as any[]) as any[]
11
+
12
+ // --- Query builder unit tests ---
13
+
14
+ describe("SQL query builder (SQLite dialect)", () => {
15
+ it("where eq string", () => {
16
+ const result = buildWhereSQLQuery(
17
+ sqliteDialect,
18
+ "id",
19
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
20
+ "users",
21
+ {}
22
+ )
23
+ expect(result.sql).toContain("json_extract(data, '$.name') = ?")
24
+ expect(result.params).toContain("John")
25
+ })
26
+
27
+ it("where eq number", () => {
28
+ const result = buildWhereSQLQuery(
29
+ sqliteDialect,
30
+ "id",
31
+ [{ t: "where", path: "age", op: "eq", value: 25 as any }],
32
+ "users",
33
+ {}
34
+ )
35
+ expect(result.sql).toContain("json_extract(data, '$.age') = ?")
36
+ expect(result.params).toContain(25)
37
+ })
38
+
39
+ it("where gt", () => {
40
+ const result = buildWhereSQLQuery(
41
+ sqliteDialect,
42
+ "id",
43
+ [{ t: "where", path: "age", op: "gt", value: 18 as any }],
44
+ "users",
45
+ {}
46
+ )
47
+ expect(result.sql).toContain("json_extract(data, '$.age') > ?")
48
+ expect(result.params).toContain(18)
49
+ })
50
+
51
+ it("where or", () => {
52
+ const result = buildWhereSQLQuery(
53
+ sqliteDialect,
54
+ "id",
55
+ [
56
+ { t: "where", path: "name", op: "eq", value: "Alice" },
57
+ { t: "or", path: "name", op: "eq", value: "Bob" }
58
+ ],
59
+ "users",
60
+ {}
61
+ )
62
+ expect(result.sql).toContain("= ?")
63
+ expect(result.sql).toContain("OR")
64
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", "Bob"]))
65
+ })
66
+
67
+ it("where and", () => {
68
+ const result = buildWhereSQLQuery(
69
+ sqliteDialect,
70
+ "id",
71
+ [
72
+ { t: "where", path: "name", op: "eq", value: "Alice" },
73
+ { t: "and", path: "age", op: "gt", value: 18 as any }
74
+ ],
75
+ "users",
76
+ {}
77
+ )
78
+ expect(result.sql).toContain("AND")
79
+ expect(result.params).toEqual(expect.arrayContaining(["Alice", 18]))
80
+ })
81
+
82
+ it("where in", () => {
83
+ const result = buildWhereSQLQuery(
84
+ sqliteDialect,
85
+ "id",
86
+ [{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] as any }],
87
+ "users",
88
+ {}
89
+ )
90
+ expect(result.sql).toContain("id IN (?, ?, ?)")
91
+ expect(result.params).toEqual(expect.arrayContaining(["a", "b", "c"]))
92
+ })
93
+
94
+ it("where null", () => {
95
+ const result = buildWhereSQLQuery(
96
+ sqliteDialect,
97
+ "id",
98
+ [{ t: "where", path: "status", op: "eq", value: null as any }],
99
+ "users",
100
+ {}
101
+ )
102
+ expect(result.sql).toContain("IS NULL")
103
+ })
104
+
105
+ it("where neq null", () => {
106
+ const result = buildWhereSQLQuery(
107
+ sqliteDialect,
108
+ "id",
109
+ [{ t: "where", path: "status", op: "neq", value: null as any }],
110
+ "users",
111
+ {}
112
+ )
113
+ expect(result.sql).toContain("IS NOT NULL")
114
+ })
115
+
116
+ it("where contains", () => {
117
+ const result = buildWhereSQLQuery(
118
+ sqliteDialect,
119
+ "id",
120
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
121
+ "users",
122
+ {}
123
+ )
124
+ expect(result.sql).toContain("LIKE")
125
+ expect(result.sql).toContain("LOWER")
126
+ expect(result.params).toContain("%oh%")
127
+ })
128
+
129
+ it("where startsWith", () => {
130
+ const result = buildWhereSQLQuery(
131
+ sqliteDialect,
132
+ "id",
133
+ [{ t: "where", path: "name", op: "startsWith", value: "Jo" }],
134
+ "users",
135
+ {}
136
+ )
137
+ expect(result.sql).toContain("LIKE")
138
+ expect(result.params).toContain("Jo%")
139
+ })
140
+
141
+ it("where endsWith", () => {
142
+ const result = buildWhereSQLQuery(
143
+ sqliteDialect,
144
+ "id",
145
+ [{ t: "where", path: "name", op: "endsWith", value: "hn" }],
146
+ "users",
147
+ {}
148
+ )
149
+ expect(result.sql).toContain("LIKE")
150
+ expect(result.params).toContain("%hn")
151
+ })
152
+
153
+ it("where includes (array contains)", () => {
154
+ const result = buildWhereSQLQuery(
155
+ sqliteDialect,
156
+ "id",
157
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
158
+ "users",
159
+ {}
160
+ )
161
+ expect(result.sql).toContain("json_each")
162
+ expect(result.sql).toContain("value = ?")
163
+ expect(result.params).toContain("admin")
164
+ })
165
+
166
+ it("where includes-any (array contains any)", () => {
167
+ const result = buildWhereSQLQuery(
168
+ sqliteDialect,
169
+ "id",
170
+ [{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] as any }],
171
+ "users",
172
+ {}
173
+ )
174
+ expect(result.sql).toContain("json_each")
175
+ expect(result.sql).toContain("IN")
176
+ })
177
+
178
+ it("nested scopes", () => {
179
+ const result = buildWhereSQLQuery(
180
+ sqliteDialect,
181
+ "id",
182
+ [
183
+ { t: "where", path: "a", op: "eq", value: "1" },
184
+ {
185
+ t: "or-scope",
186
+ result: [
187
+ { t: "where", path: "b", op: "eq", value: "2" },
188
+ { t: "and", path: "c", op: "eq", value: "3" }
189
+ ],
190
+ relation: "some" as const
191
+ }
192
+ ],
193
+ "test",
194
+ {}
195
+ )
196
+ expect(result.sql).toContain("OR (")
197
+ expect(result.sql).toContain("AND")
198
+ expect(result.params).toEqual(expect.arrayContaining(["1", "2", "3"]))
199
+ })
200
+
201
+ it("id key maps to id column", () => {
202
+ const result = buildWhereSQLQuery(
203
+ sqliteDialect,
204
+ "myId",
205
+ [{ t: "where", path: "myId", op: "eq", value: "123" }],
206
+ "users",
207
+ {}
208
+ )
209
+ expect(result.sql).toContain("id = ?")
210
+ expect(result.sql).not.toContain("json_extract")
211
+ expect(result.params).toContain("123")
212
+ })
213
+
214
+ it("order + limit + skip", () => {
215
+ const result = buildWhereSQLQuery(
216
+ sqliteDialect,
217
+ "id",
218
+ [],
219
+ "users",
220
+ {},
221
+ undefined,
222
+ [{ key: "name", direction: "ASC" }] as any,
223
+ 5,
224
+ 10
225
+ )
226
+ expect(result.sql).toContain("ORDER BY")
227
+ expect(result.sql).toContain("ASC")
228
+ expect(result.sql).toContain("LIMIT")
229
+ expect(result.sql).toContain("OFFSET")
230
+ })
231
+ })
232
+
233
+ describe("SQL query builder (PostgreSQL dialect)", () => {
234
+ it("where eq string uses ->> operator", () => {
235
+ const result = buildWhereSQLQuery(
236
+ pgDialect,
237
+ "id",
238
+ [{ t: "where", path: "name", op: "eq", value: "John" }],
239
+ "users",
240
+ {}
241
+ )
242
+ expect(result.sql).toContain("data->>'name'")
243
+ expect(result.sql).toContain("$1")
244
+ expect(result.params).toContain("John")
245
+ })
246
+
247
+ it("where contains uses ILIKE", () => {
248
+ const result = buildWhereSQLQuery(
249
+ pgDialect,
250
+ "id",
251
+ [{ t: "where", path: "name", op: "contains", value: "oh" }],
252
+ "users",
253
+ {}
254
+ )
255
+ expect(result.sql).toContain("ILIKE")
256
+ expect(result.params).toContain("%oh%")
257
+ })
258
+
259
+ it("where in uses $N placeholders", () => {
260
+ const result = buildWhereSQLQuery(
261
+ pgDialect,
262
+ "id",
263
+ [{ t: "where", path: "status", op: "in", value: ["active", "pending"] as any }],
264
+ "users",
265
+ {}
266
+ )
267
+ expect(result.sql).toContain("$1")
268
+ expect(result.sql).toContain("$2")
269
+ expect(result.params).toEqual(expect.arrayContaining(["active", "pending"]))
270
+ })
271
+
272
+ it("where includes uses @> jsonb operator", () => {
273
+ const result = buildWhereSQLQuery(
274
+ pgDialect,
275
+ "id",
276
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
277
+ "users",
278
+ {}
279
+ )
280
+ expect(result.sql).toContain("@>")
281
+ expect(result.sql).toContain("jsonb")
282
+ })
283
+
284
+ it("nested path uses chained -> operators", () => {
285
+ const result = buildWhereSQLQuery(
286
+ pgDialect,
287
+ "id",
288
+ [{ t: "where", path: "address.city", op: "eq", value: "NYC" }],
289
+ "users",
290
+ {}
291
+ )
292
+ expect(result.sql).toContain("data->'address'->>'city'")
293
+ })
294
+ })
295
+
296
+ // --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
297
+
298
+ describe("SQL Store (SQLite integration)", () => {
299
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
300
+ const db = new BetterSqlite(":memory:")
301
+ db.pragma("journal_mode = WAL")
302
+ try {
303
+ fn(db)
304
+ } finally {
305
+ db.close()
306
+ }
307
+ }
308
+
309
+ it("creates table and seeds data", () =>
310
+ withDb((db) => {
311
+ db.exec(
312
+ `CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
313
+ )
314
+ db
315
+ .prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
316
+ .run("1", "etag1", JSON.stringify({ name: "Alice", age: 30 }))
317
+
318
+ const rows = db.prepare(`SELECT * FROM "test_items"`).all()
319
+ expect(rows.length).toBe(1)
320
+ expect((rows[0] as any).id).toBe("1")
321
+ }))
322
+
323
+ it("data column should not contain _etag or id", () =>
324
+ withDb((db) => {
325
+ db.exec(
326
+ `CREATE TABLE IF NOT EXISTS "test_clean" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
327
+ )
328
+ // Simulate what toRow now produces: data without id or _etag
329
+ const data = { name: "Alice", age: 30, tags: ["admin"] }
330
+ db
331
+ .prepare(`INSERT INTO "test_clean" (id, _etag, data) VALUES (?, ?, ?)`)
332
+ .run("1", "etag1", JSON.stringify(data))
333
+
334
+ const row = db.prepare(`SELECT * FROM "test_clean" WHERE id = ?`).get("1") as any
335
+ const parsed = JSON.parse(row.data) as any
336
+ expect(parsed).not.toHaveProperty("id")
337
+ expect(parsed).not.toHaveProperty("_etag")
338
+ expect(parsed.name).toBe("Alice")
339
+ expect(parsed.age).toBe(30)
340
+ expect(parsed.tags).toEqual(["admin"])
341
+ // id and _etag come from their own columns
342
+ expect(row.id).toBe("1")
343
+ expect(row._etag).toBe("etag1")
344
+ }))
345
+
346
+ it("backward compat: rows with id/_etag in data still work with queries", () =>
347
+ withDb((db) => {
348
+ db.exec(
349
+ `CREATE TABLE IF NOT EXISTS "test_compat" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
350
+ )
351
+ // Old format: id and _etag inside data
352
+ db
353
+ .prepare(`INSERT INTO "test_compat" (id, _etag, data) VALUES (?, ?, ?)`)
354
+ .run("1", "etag1", JSON.stringify({ id: "1", _etag: "old_etag", name: "Alice", age: 30 }))
355
+ // New format: id and _etag stripped from data
356
+ db
357
+ .prepare(`INSERT INTO "test_compat" (id, _etag, data) VALUES (?, ?, ?)`)
358
+ .run("2", "etag2", JSON.stringify({ name: "Bob", age: 25 }))
359
+
360
+ // Both should be queryable by name
361
+ const q1 = buildWhereSQLQuery(
362
+ sqliteDialect,
363
+ "id",
364
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
365
+ "test_compat",
366
+ {}
367
+ )
368
+ const r1 = query(db, q1.sql, q1.params)
369
+ expect(r1.length).toBe(1)
370
+ expect((r1[0] as any).id).toBe("1")
371
+
372
+ const q2 = buildWhereSQLQuery(
373
+ sqliteDialect,
374
+ "id",
375
+ [{ t: "where", path: "name", op: "eq", value: "Bob" }],
376
+ "test_compat",
377
+ {}
378
+ )
379
+ const r2 = query(db, q2.sql, q2.params)
380
+ expect(r2.length).toBe(1)
381
+ expect((r2[0] as any).id).toBe("2")
382
+
383
+ // Both queryable by id column
384
+ const q3 = buildWhereSQLQuery(
385
+ sqliteDialect,
386
+ "id",
387
+ [{ t: "where", path: "id", op: "in", value: ["1", "2"] as any }],
388
+ "test_compat",
389
+ {}
390
+ )
391
+ expect(query(db, q3.sql, q3.params).length).toBe(2)
392
+ }))
393
+
394
+ it("queries work when data does not contain id", () =>
395
+ withDb((db) => {
396
+ db.exec(
397
+ `CREATE TABLE IF NOT EXISTS "test_noid" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
398
+ )
399
+ const people = [
400
+ { name: "Alice", age: 30 },
401
+ { name: "Bob", age: 25 },
402
+ { name: "Charlie", age: 35 }
403
+ ]
404
+ const insert = db.prepare(
405
+ `INSERT INTO "test_noid" (id, _etag, data) VALUES (?, ?, ?)`
406
+ )
407
+ people.forEach((p, i) => insert.run(String(i + 1), `etag_${i + 1}`, JSON.stringify(p)))
408
+
409
+ // Filter by field in data
410
+ const q1 = buildWhereSQLQuery(
411
+ sqliteDialect,
412
+ "id",
413
+ [{ t: "where", path: "age", op: "gt", value: 28 as any }],
414
+ "test_noid",
415
+ {}
416
+ )
417
+ expect(query(db, q1.sql, q1.params).length).toBe(2) // Alice(30), Charlie(35)
418
+
419
+ // Filter by id column
420
+ const q2 = buildWhereSQLQuery(
421
+ sqliteDialect,
422
+ "id",
423
+ [{ t: "where", path: "id", op: "eq", value: "2" }],
424
+ "test_noid",
425
+ {}
426
+ )
427
+ const r2 = query(db, q2.sql, q2.params)
428
+ expect(r2.length).toBe(1)
429
+ expect((r2[0] as any).id).toBe("2")
430
+ expect((JSON.parse((r2[0] as any).data) as any).name).toBe("Bob")
431
+
432
+ // Order + limit still works
433
+ const q3 = buildWhereSQLQuery(
434
+ sqliteDialect,
435
+ "id",
436
+ [],
437
+ "test_noid",
438
+ {},
439
+ undefined,
440
+ [{ key: "age", direction: "ASC" }] as any,
441
+ undefined,
442
+ 2
443
+ )
444
+ const r3 = query(db, q3.sql, q3.params)
445
+ expect(r3.length).toBe(2)
446
+ expect((JSON.parse((r3[0] as any).data) as any).name).toBe("Bob") // youngest first
447
+ }))
448
+
449
+ it("query builder generates valid SQL for SQLite", () =>
450
+ withDb((db) => {
451
+ db.exec(
452
+ `CREATE TABLE IF NOT EXISTS "test_people" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
453
+ )
454
+
455
+ const people = [
456
+ { id: "1", name: "Alice", age: 30, tags: ["admin", "user"] },
457
+ { id: "2", name: "Bob", age: 25, tags: ["user"] },
458
+ { id: "3", name: "Charlie", age: 35, tags: ["admin"] },
459
+ { id: "4", name: "Diana", age: 28, tags: ["user", "editor"] }
460
+ ]
461
+
462
+ const insert = db.prepare(
463
+ `INSERT INTO "test_people" (id, _etag, data) VALUES (?, ?, ?)`
464
+ )
465
+ for (const p of people) {
466
+ const { id, ...data } = p
467
+ insert.run(id, `etag_${id}`, JSON.stringify(data))
468
+ }
469
+
470
+ // Test eq
471
+ const q1 = buildWhereSQLQuery(
472
+ sqliteDialect,
473
+ "id",
474
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
475
+ "test_people",
476
+ {}
477
+ )
478
+ expect(query(db, q1.sql, q1.params).length).toBe(1)
479
+ expect((JSON.parse((query(db, q1.sql, q1.params)[0] as any).data) as any).name).toBe("Alice")
480
+
481
+ // Test gt
482
+ const q2 = buildWhereSQLQuery(
483
+ sqliteDialect,
484
+ "id",
485
+ [{ t: "where", path: "age", op: "gt", value: 28 as any }],
486
+ "test_people",
487
+ {}
488
+ )
489
+ expect(query(db, q2.sql, q2.params).length).toBe(2)
490
+
491
+ // Test OR
492
+ const q3 = buildWhereSQLQuery(
493
+ sqliteDialect,
494
+ "id",
495
+ [
496
+ { t: "where", path: "name", op: "eq", value: "Alice" },
497
+ { t: "or", path: "name", op: "eq", value: "Bob" }
498
+ ],
499
+ "test_people",
500
+ {}
501
+ )
502
+ expect(query(db, q3.sql, q3.params).length).toBe(2)
503
+
504
+ // Test AND
505
+ const q4 = buildWhereSQLQuery(
506
+ sqliteDialect,
507
+ "id",
508
+ [
509
+ { t: "where", path: "name", op: "eq", value: "Alice" },
510
+ { t: "and", path: "age", op: "gt", value: 25 as any }
511
+ ],
512
+ "test_people",
513
+ {}
514
+ )
515
+ const r4 = query(db, q4.sql, q4.params)
516
+ expect(r4.length).toBe(1)
517
+ expect((JSON.parse((r4[0] as any).data) as any).name).toBe("Alice")
518
+
519
+ // Test IN
520
+ const q5 = buildWhereSQLQuery(
521
+ sqliteDialect,
522
+ "id",
523
+ [{ t: "where", path: "id", op: "in", value: ["1", "3"] as any }],
524
+ "test_people",
525
+ {}
526
+ )
527
+ expect(query(db, q5.sql, q5.params).length).toBe(2)
528
+
529
+ // Test contains (string)
530
+ const q6 = buildWhereSQLQuery(
531
+ sqliteDialect,
532
+ "id",
533
+ [{ t: "where", path: "name", op: "contains", value: "li" }],
534
+ "test_people",
535
+ {}
536
+ )
537
+ expect(query(db, q6.sql, q6.params).length).toBe(2) // Alice, Charlie
538
+
539
+ // Test startsWith
540
+ const q7 = buildWhereSQLQuery(
541
+ sqliteDialect,
542
+ "id",
543
+ [{ t: "where", path: "name", op: "startsWith", value: "Al" }],
544
+ "test_people",
545
+ {}
546
+ )
547
+ const r7 = query(db, q7.sql, q7.params)
548
+ expect(r7.length).toBe(1)
549
+ expect((JSON.parse((r7[0] as any).data) as any).name).toBe("Alice")
550
+
551
+ // Test includes (array)
552
+ const q8 = buildWhereSQLQuery(
553
+ sqliteDialect,
554
+ "id",
555
+ [{ t: "where", path: "tags", op: "includes", value: "admin" }],
556
+ "test_people",
557
+ {}
558
+ )
559
+ expect(query(db, q8.sql, q8.params).length).toBe(2) // Alice, Charlie
560
+
561
+ // Test nested scope: where name = Alice OR (age > 30 AND name contains 'ar')
562
+ const q9 = buildWhereSQLQuery(
563
+ sqliteDialect,
564
+ "id",
565
+ [
566
+ { t: "where", path: "name", op: "eq", value: "Alice" },
567
+ {
568
+ t: "or-scope",
569
+ result: [
570
+ { t: "where", path: "age", op: "gt", value: 30 as any },
571
+ { t: "and", path: "name", op: "contains", value: "ar" }
572
+ ],
573
+ relation: "some"
574
+ }
575
+ ],
576
+ "test_people",
577
+ {}
578
+ )
579
+ expect(query(db, q9.sql, q9.params).length).toBe(2) // Alice + Charlie
580
+
581
+ // Test order + limit
582
+ const q10 = buildWhereSQLQuery(
583
+ sqliteDialect,
584
+ "id",
585
+ [],
586
+ "test_people",
587
+ {},
588
+ undefined,
589
+ [{ key: "age", direction: "DESC" }] as any,
590
+ undefined,
591
+ 2
592
+ )
593
+ const r10 = query(db, q10.sql, q10.params)
594
+ expect(r10.length).toBe(2)
595
+ expect((JSON.parse((r10[0] as any).data) as any).name).toBe("Charlie") // oldest first
596
+ }))
597
+
598
+ it("namespace param is in correct position for SQLite positional placeholders", () =>
599
+ withDb((db) => {
600
+ db.exec(
601
+ `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))`
602
+ )
603
+ const insert = db.prepare(
604
+ `INSERT INTO "test_ns" (id, _namespace, _etag, data) VALUES (?, ?, ?, ?)`
605
+ )
606
+ insert.run("1", "primary", "e1", JSON.stringify({ name: "Alice", role: "admin" }))
607
+ insert.run("2", "primary", "e2", JSON.stringify({ name: "Bob", role: "user" }))
608
+ insert.run("3", "other", "e3", JSON.stringify({ name: "Charlie", role: "admin" }))
609
+
610
+ // Build a filter query: role != 'deleted'
611
+ const q = buildWhereSQLQuery(
612
+ sqliteDialect,
613
+ "id",
614
+ [{ t: "where", path: "role", op: "neq", value: "deleted" }],
615
+ "test_ns",
616
+ {}
617
+ )
618
+
619
+ // Simulate what SQL.ts does: prepend _namespace = ? and put ns FIRST in params
620
+ const hasWhere = q.sql.includes("WHERE")
621
+ const nsSql = hasWhere
622
+ ? q.sql.replace("WHERE", `WHERE _namespace = ? AND`)
623
+ : q.sql.replace(`FROM "test_ns"`, `FROM "test_ns" WHERE _namespace = ?`)
624
+ const params = ["primary", ...q.params]
625
+
626
+ const results = query(db, nsSql, params)
627
+ // Should only get Alice and Bob (primary namespace), not Charlie (other namespace)
628
+ expect(results.length).toBe(2)
629
+ const names = results.map((r) => (JSON.parse((r as any).data) as any).name).sort()
630
+ expect(names).toEqual(["Alice", "Bob"])
631
+ }))
632
+ })
633
+
634
+ // --- boolean WHERE clause tests (regression: where("x", true) must work per dialect) ---
635
+
636
+ describe("boolean WHERE clauses — query builder", () => {
637
+ it("sqlite: eq true serializes bool → 1 integer", () => {
638
+ const result = buildWhereSQLQuery(
639
+ sqliteDialect,
640
+ "id",
641
+ [{ t: "where", path: "flag", op: "eq", value: true as any }],
642
+ "t",
643
+ {}
644
+ )
645
+ expect(result.sql).toContain("json_extract(data, '$.flag') = ?")
646
+ expect(result.params).toEqual([1])
647
+ })
648
+
649
+ it("sqlite: eq false serializes bool → 0 integer", () => {
650
+ const result = buildWhereSQLQuery(
651
+ sqliteDialect,
652
+ "id",
653
+ [{ t: "where", path: "flag", op: "eq", value: false as any }],
654
+ "t",
655
+ {}
656
+ )
657
+ expect(result.params).toEqual([0])
658
+ })
659
+
660
+ it("sqlite: neq/in also serialize booleans", () => {
661
+ const r1 = buildWhereSQLQuery(
662
+ sqliteDialect,
663
+ "id",
664
+ [{ t: "where", path: "flag", op: "neq", value: true as any }],
665
+ "t",
666
+ {}
667
+ )
668
+ expect(r1.params).toEqual([1])
669
+
670
+ const r2 = buildWhereSQLQuery(
671
+ sqliteDialect,
672
+ "id",
673
+ [{ t: "where", path: "flag", op: "in", value: [true, false] as any }],
674
+ "t",
675
+ {}
676
+ )
677
+ expect(r2.params).toEqual([1, 0])
678
+ })
679
+
680
+ it("pg: eq true serializes bool → 'true' text (matches ->> output)", () => {
681
+ const result = buildWhereSQLQuery(
682
+ pgDialect,
683
+ "id",
684
+ [{ t: "where", path: "flag", op: "eq", value: true as any }],
685
+ "t",
686
+ {}
687
+ )
688
+ expect(result.sql).toContain("data->>'flag' = $1")
689
+ expect(result.params).toEqual(["true"])
690
+ })
691
+
692
+ it("pg: eq false serializes bool → 'false' text", () => {
693
+ const result = buildWhereSQLQuery(
694
+ pgDialect,
695
+ "id",
696
+ [{ t: "where", path: "flag", op: "eq", value: false as any }],
697
+ "t",
698
+ {}
699
+ )
700
+ expect(result.params).toEqual(["false"])
701
+ })
702
+
703
+ it("pg: in with booleans serializes as text list", () => {
704
+ const result = buildWhereSQLQuery(
705
+ pgDialect,
706
+ "id",
707
+ [{ t: "where", path: "flag", op: "in", value: [true, false] as any }],
708
+ "t",
709
+ {}
710
+ )
711
+ expect(result.params).toEqual(["true", "false"])
712
+ })
713
+
714
+ it("non-boolean scalars pass through unchanged (sqlite)", () => {
715
+ const result = buildWhereSQLQuery(
716
+ sqliteDialect,
717
+ "id",
718
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
719
+ "t",
720
+ {}
721
+ )
722
+ expect(result.params).toEqual(["Alice"])
723
+ })
724
+
725
+ it("non-boolean scalars pass through unchanged (pg)", () => {
726
+ const result = buildWhereSQLQuery(
727
+ pgDialect,
728
+ "id",
729
+ [{ t: "where", path: "age", op: "gt", value: 18 as any }],
730
+ "t",
731
+ {}
732
+ )
733
+ expect(result.params).toEqual([18])
734
+ })
735
+ })
736
+
737
+ describe("boolean WHERE clauses — SQLite integration (end-to-end)", () => {
738
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
739
+ const db = new BetterSqlite(":memory:")
740
+ try {
741
+ fn(db)
742
+ } finally {
743
+ db.close()
744
+ }
745
+ }
746
+
747
+ it("where flag = true matches only true rows", () =>
748
+ withDb((db) => {
749
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
750
+ db
751
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
752
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
753
+ db
754
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
755
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
756
+
757
+ const q = buildWhereSQLQuery(
758
+ sqliteDialect,
759
+ "id",
760
+ [{ t: "where", path: "flag", op: "eq", value: true as any }],
761
+ "t",
762
+ {}
763
+ )
764
+ const rows = query(db, q.sql, q.params)
765
+ expect(rows.length).toBe(1)
766
+ expect((JSON.parse((rows[0] as any).data) as any).name).toBe("Alice")
767
+ }))
768
+
769
+ it("where flag = false matches only false rows", () =>
770
+ withDb((db) => {
771
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
772
+ db
773
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
774
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
775
+ db
776
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
777
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
778
+
779
+ const q = buildWhereSQLQuery(
780
+ sqliteDialect,
781
+ "id",
782
+ [{ t: "where", path: "flag", op: "eq", value: false as any }],
783
+ "t",
784
+ {}
785
+ )
786
+ const rows = query(db, q.sql, q.params)
787
+ expect(rows.length).toBe(1)
788
+ expect((JSON.parse((rows[0] as any).data) as any).name).toBe("Bob")
789
+ }))
790
+
791
+ it("where nested boolean path works", () =>
792
+ withDb((db) => {
793
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
794
+ db
795
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
796
+ .run("1", "e", JSON.stringify({ meta: { active: true }, name: "Alice" }))
797
+ db
798
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
799
+ .run("2", "e", JSON.stringify({ meta: { active: false }, name: "Bob" }))
800
+
801
+ const q = buildWhereSQLQuery(
802
+ sqliteDialect,
803
+ "id",
804
+ [{ t: "where", path: "meta.active", op: "eq", value: true as any }],
805
+ "t",
806
+ {}
807
+ )
808
+ const rows = query(db, q.sql, q.params)
809
+ expect(rows.length).toBe(1)
810
+ expect((JSON.parse((rows[0] as any).data) as any).name).toBe("Alice")
811
+ }))
812
+
813
+ it("where neq boolean works", () =>
814
+ withDb((db) => {
815
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
816
+ db
817
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
818
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
819
+ db
820
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
821
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
822
+
823
+ const q = buildWhereSQLQuery(
824
+ sqliteDialect,
825
+ "id",
826
+ [{ t: "where", path: "flag", op: "neq", value: true as any }],
827
+ "t",
828
+ {}
829
+ )
830
+ const rows = query(db, q.sql, q.params)
831
+ expect(rows.length).toBe(1)
832
+ expect((JSON.parse((rows[0] as any).data) as any).name).toBe("Bob")
833
+ }))
834
+ })
835
+
836
+ // --- jsonExtractJson dialect tests (regression: booleans must survive round-trip) ---
837
+
838
+ describe("sqliteDialect.jsonExtractJson boolean round-trip", () => {
839
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
840
+ const db = new BetterSqlite(":memory:")
841
+ try {
842
+ fn(db)
843
+ } finally {
844
+ db.close()
845
+ }
846
+ }
847
+
848
+ it("SQL uses json_type CASE to preserve booleans", () => {
849
+ const sql = sqliteDialect.jsonExtractJson("flag")
850
+ expect(sql).toContain("json_type(data, '$.flag')")
851
+ expect(sql).toContain("'true'")
852
+ expect(sql).toContain("'false'")
853
+ expect(sql).toContain("json_quote(json_extract(data, '$.flag'))")
854
+ })
855
+
856
+ it("boolean false round-trips as false (not 0)", () =>
857
+ withDb((db) => {
858
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
859
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: false }))
860
+
861
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
862
+ const parsed = JSON.parse(row.v)
863
+ expect(parsed).toBe(false)
864
+ expect(typeof parsed).toBe("boolean")
865
+ }))
866
+
867
+ it("boolean true round-trips as true (not 1)", () =>
868
+ withDb((db) => {
869
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
870
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: true }))
871
+
872
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
873
+ const parsed = JSON.parse(row.v)
874
+ expect(parsed).toBe(true)
875
+ expect(typeof parsed).toBe("boolean")
876
+ }))
877
+
878
+ it("nested boolean round-trips correctly", () =>
879
+ withDb((db) => {
880
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
881
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
882
+ JSON.stringify({ nested: { flag: false } })
883
+ )
884
+
885
+ const row = db
886
+ .prepare(`SELECT ${sqliteDialect.jsonExtractJson("nested.flag")} AS v FROM "t"`)
887
+ .get() as any
888
+ const parsed = JSON.parse(row.v)
889
+ expect(parsed).toBe(false)
890
+ expect(typeof parsed).toBe("boolean")
891
+ }))
892
+
893
+ it("numbers 0 and 1 still parse as numbers (not booleans)", () =>
894
+ withDb((db) => {
895
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
896
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ zero: 0, one: 1 }))
897
+
898
+ const zRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("zero")} AS v FROM "t"`).get() as any
899
+ const oRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("one")} AS v FROM "t"`).get() as any
900
+ expect(JSON.parse(zRow.v)).toBe(0)
901
+ expect(typeof JSON.parse(zRow.v)).toBe("number")
902
+ expect(JSON.parse(oRow.v)).toBe(1)
903
+ expect(typeof JSON.parse(oRow.v)).toBe("number")
904
+ }))
905
+
906
+ it("strings, arrays, and null still round-trip correctly", () =>
907
+ withDb((db) => {
908
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
909
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
910
+ JSON.stringify({ s: "hello", arr: [1, 2, 3], nil: null })
911
+ )
912
+
913
+ const sRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("s")} AS v FROM "t"`).get() as any
914
+ const aRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("arr")} AS v FROM "t"`).get() as any
915
+ const nRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("nil")} AS v FROM "t"`).get() as any
916
+ expect(JSON.parse(sRow.v)).toBe("hello")
917
+ expect(JSON.parse(aRow.v)).toEqual([1, 2, 3])
918
+ expect(JSON.parse(nRow.v)).toBe(null)
919
+ }))
920
+ })
921
+
922
+ // --- toRow stripping and parseRow reconstruction tests ---
923
+
924
+ describe("toRow strips _etag and id from data", () => {
925
+ // Replicate the toRow logic from SQL.ts to test in isolation
926
+ const toRow = <IdKey extends PropertyKey>(e: any, idKey: IdKey) => {
927
+ const newE = makeETag(e)
928
+ const id = newE[idKey] as string
929
+ const { _etag, [idKey]: _id, ...rest } = newE as any
930
+ const data = JSON.stringify(rest)
931
+ return { id, _etag: newE._etag!, data, item: newE }
932
+ }
933
+
934
+ it("data JSON does not contain _etag", () => {
935
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice", age: 30 }, "id")
936
+ const parsed = JSON.parse(row.data) as any
937
+ expect(parsed).not.toHaveProperty("_etag")
938
+ expect(parsed.name).toBe("Alice")
939
+ expect(parsed.age).toBe(30)
940
+ })
941
+
942
+ it("data JSON does not contain id field", () => {
943
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
944
+ const parsed = JSON.parse(row.data) as any
945
+ expect(parsed).not.toHaveProperty("id")
946
+ expect(parsed.name).toBe("Alice")
947
+ })
948
+
949
+ it("data JSON does not contain custom idKey field", () => {
950
+ const row = toRow({ myId: "abc", _etag: undefined, name: "Bob" }, "myId")
951
+ const parsed = JSON.parse(row.data) as any
952
+ expect(parsed).not.toHaveProperty("myId")
953
+ expect(parsed.name).toBe("Bob")
954
+ expect(row.id).toBe("abc")
955
+ })
956
+
957
+ it("id and _etag are returned as separate fields", () => {
958
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
959
+ expect(row.id).toBe("1")
960
+ expect(typeof row._etag).toBe("string")
961
+ expect(row._etag.length).toBeGreaterThan(0)
962
+ })
963
+
964
+ it("item still contains all fields including _etag and id", () => {
965
+ const row = toRow({ id: "1", _etag: undefined, name: "Alice" }, "id")
966
+ expect(row.item.id).toBe("1")
967
+ expect(row.item._etag).toBe(row._etag)
968
+ expect(row.item.name).toBe("Alice")
969
+ })
970
+
971
+ it("preserves nested objects in data", () => {
972
+ const row = toRow({ id: "1", _etag: undefined, address: { city: "NYC", zip: "10001" } }, "id")
973
+ const parsed = JSON.parse(row.data) as any
974
+ expect(parsed.address).toEqual({ city: "NYC", zip: "10001" })
975
+ expect(parsed).not.toHaveProperty("id")
976
+ expect(parsed).not.toHaveProperty("_etag")
977
+ })
978
+ })
979
+
980
+ describe("parseRow reconstructs full object from row", () => {
981
+ it("re-injects id from row column using idKey", () => {
982
+ const result: any = parseRow(
983
+ { id: "42", _etag: "etag1", data: JSON.stringify({ name: "Alice", age: 30 }) },
984
+ "id",
985
+ {}
986
+ )
987
+ expect(result.id).toBe("42")
988
+ expect(result.name).toBe("Alice")
989
+ expect(result.age).toBe(30)
990
+ expect(result._etag).toBe("etag1")
991
+ })
992
+
993
+ it("re-injects custom idKey from row column", () => {
994
+ const result: any = parseRow(
995
+ { id: "abc", _etag: "etag2", data: JSON.stringify({ name: "Bob" }) },
996
+ "myId",
997
+ {}
998
+ )
999
+ expect(result.myId).toBe("abc")
1000
+ expect(result.name).toBe("Bob")
1001
+ expect(result._etag).toBe("etag2")
1002
+ })
1003
+
1004
+ it("uses _etag from row column, not from data", () => {
1005
+ const result: any = parseRow(
1006
+ { id: "1", _etag: "column_etag", data: JSON.stringify({ _etag: "stale_data_etag", name: "Alice" }) },
1007
+ "id",
1008
+ {}
1009
+ )
1010
+ expect(result._etag).toBe("column_etag")
1011
+ })
1012
+
1013
+ it("uses id from row column, not from data", () => {
1014
+ const result: any = parseRow(
1015
+ { id: "correct_id", _etag: "e1", data: JSON.stringify({ id: "wrong_id", name: "Alice" }) },
1016
+ "id",
1017
+ {}
1018
+ )
1019
+ expect(result.id).toBe("correct_id")
1020
+ })
1021
+
1022
+ it("applies defaultValues for missing fields", () => {
1023
+ const result: any = parseRow(
1024
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice" }) },
1025
+ "id",
1026
+ { status: "active", role: "user" }
1027
+ )
1028
+ expect(result.name).toBe("Alice")
1029
+ expect(result.status).toBe("active")
1030
+ expect(result.role).toBe("user")
1031
+ })
1032
+
1033
+ it("data fields override defaultValues", () => {
1034
+ const result: any = parseRow(
1035
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice", status: "inactive" }) },
1036
+ "id",
1037
+ { status: "active" }
1038
+ )
1039
+ expect(result.status).toBe("inactive")
1040
+ })
1041
+
1042
+ it("handles null _etag from row", () => {
1043
+ const result: any = parseRow(
1044
+ { id: "1", _etag: null, data: JSON.stringify({ name: "Alice" }) },
1045
+ "id",
1046
+ {}
1047
+ )
1048
+ expect(result._etag).toBeUndefined()
1049
+ })
1050
+
1051
+ it("round-trip: toRow then parseRow reconstructs the original", () => {
1052
+ const original = { id: "1", _etag: undefined as string | undefined, name: "Alice", age: 30, tags: ["admin"] }
1053
+ const newE = makeETag(original)
1054
+ const { _etag, id: _id, ...rest } = newE as any
1055
+ const row = { id: newE.id, _etag: newE._etag!, data: JSON.stringify(rest) }
1056
+
1057
+ const reconstructed: any = parseRow(row, "id", {})
1058
+ expect(reconstructed.id).toBe("1")
1059
+ expect(reconstructed.name).toBe("Alice")
1060
+ expect(reconstructed.age).toBe(30)
1061
+ expect(reconstructed.tags).toEqual(["admin"])
1062
+ expect(reconstructed._etag).toBe(newE._etag)
1063
+ })
1064
+ })