@effect-app/infra 4.0.0-beta.22 → 4.0.0-beta.220

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 (310) hide show
  1. package/CHANGELOG.md +1640 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +7 -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 +103 -51
  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 +46 -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 +139 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/dsl.js +187 -1
  55. package/dist/Model/query/new-kid-interpreter.d.ts +76 -7
  56. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  57. package/dist/Model/query/new-kid-interpreter.js +122 -6
  58. package/dist/Model/query.d.ts +1 -1
  59. package/dist/Model.d.ts +2 -1
  60. package/dist/Model.d.ts.map +1 -1
  61. package/dist/Model.js +2 -1
  62. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  63. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  64. package/dist/QueueMaker/SQLQueue.js +130 -116
  65. package/dist/QueueMaker/errors.d.ts +2 -2
  66. package/dist/QueueMaker/errors.d.ts.map +1 -1
  67. package/dist/QueueMaker/memQueue.d.ts +7 -4
  68. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  69. package/dist/QueueMaker/memQueue.js +75 -63
  70. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  71. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  72. package/dist/QueueMaker/sbqueue.js +52 -53
  73. package/dist/QueueMaker/service.d.ts +1 -1
  74. package/dist/RequestContext.d.ts +74 -35
  75. package/dist/RequestContext.d.ts.map +1 -1
  76. package/dist/RequestContext.js +13 -14
  77. package/dist/RequestFiberSet.d.ts +7 -7
  78. package/dist/RequestFiberSet.d.ts.map +1 -1
  79. package/dist/RequestFiberSet.js +3 -3
  80. package/dist/Store/ContextMapContainer.d.ts +19 -3
  81. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  82. package/dist/Store/ContextMapContainer.js +13 -3
  83. package/dist/Store/Cosmos/query.d.ts +5 -1
  84. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  85. package/dist/Store/Cosmos/query.js +113 -34
  86. package/dist/Store/Cosmos.d.ts +1 -1
  87. package/dist/Store/Cosmos.d.ts.map +1 -1
  88. package/dist/Store/Cosmos.js +335 -243
  89. package/dist/Store/Disk.d.ts +2 -2
  90. package/dist/Store/Disk.d.ts.map +1 -1
  91. package/dist/Store/Disk.js +72 -35
  92. package/dist/Store/Memory.d.ts +6 -4
  93. package/dist/Store/Memory.d.ts.map +1 -1
  94. package/dist/Store/Memory.js +242 -58
  95. package/dist/Store/SQL/Pg.d.ts +4 -0
  96. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  97. package/dist/Store/SQL/Pg.js +231 -0
  98. package/dist/Store/SQL/query.d.ts +42 -0
  99. package/dist/Store/SQL/query.d.ts.map +1 -0
  100. package/dist/Store/SQL/query.js +479 -0
  101. package/dist/Store/SQL.d.ts +20 -0
  102. package/dist/Store/SQL.d.ts.map +1 -0
  103. package/dist/Store/SQL.js +446 -0
  104. package/dist/Store/codeFilter.d.ts +1 -1
  105. package/dist/Store/codeFilter.d.ts.map +1 -1
  106. package/dist/Store/codeFilter.js +4 -2
  107. package/dist/Store/index.d.ts +5 -2
  108. package/dist/Store/index.d.ts.map +1 -1
  109. package/dist/Store/index.js +15 -3
  110. package/dist/Store/service.d.ts +22 -8
  111. package/dist/Store/service.d.ts.map +1 -1
  112. package/dist/Store/service.js +24 -6
  113. package/dist/Store/utils.d.ts +1 -1
  114. package/dist/Store/utils.d.ts.map +1 -1
  115. package/dist/Store/utils.js +3 -4
  116. package/dist/Store.d.ts +1 -1
  117. package/dist/adapters/SQL/Model.d.ts +31 -42
  118. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  119. package/dist/adapters/SQL/Model.js +29 -38
  120. package/dist/adapters/SQL.d.ts +1 -1
  121. package/dist/adapters/ServiceBus.d.ts +11 -11
  122. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  123. package/dist/adapters/ServiceBus.js +25 -21
  124. package/dist/adapters/cosmos-client.d.ts +3 -3
  125. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  126. package/dist/adapters/cosmos-client.js +3 -3
  127. package/dist/adapters/index.d.ts +8 -2
  128. package/dist/adapters/index.d.ts.map +1 -1
  129. package/dist/adapters/index.js +8 -2
  130. package/dist/adapters/logger.d.ts +1 -1
  131. package/dist/adapters/logger.d.ts.map +1 -1
  132. package/dist/adapters/memQueue.d.ts +3 -3
  133. package/dist/adapters/memQueue.d.ts.map +1 -1
  134. package/dist/adapters/memQueue.js +3 -3
  135. package/dist/adapters/mongo-client.d.ts +3 -3
  136. package/dist/adapters/mongo-client.d.ts.map +1 -1
  137. package/dist/adapters/mongo-client.js +3 -3
  138. package/dist/adapters/redis-client.d.ts +3 -3
  139. package/dist/adapters/redis-client.d.ts.map +1 -1
  140. package/dist/adapters/redis-client.js +3 -3
  141. package/dist/api/ContextProvider.d.ts +8 -8
  142. package/dist/api/ContextProvider.d.ts.map +1 -1
  143. package/dist/api/ContextProvider.js +6 -6
  144. package/dist/api/codec.d.ts +1 -1
  145. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  146. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  147. package/dist/api/internal/RequestContextMiddleware.js +9 -6
  148. package/dist/api/internal/auth.d.ts +44 -6
  149. package/dist/api/internal/auth.d.ts.map +1 -1
  150. package/dist/api/internal/auth.js +160 -29
  151. package/dist/api/internal/events.d.ts +3 -3
  152. package/dist/api/internal/events.d.ts.map +1 -1
  153. package/dist/api/internal/events.js +10 -8
  154. package/dist/api/internal/health.d.ts +1 -1
  155. package/dist/api/layerUtils.d.ts +6 -6
  156. package/dist/api/layerUtils.d.ts.map +1 -1
  157. package/dist/api/layerUtils.js +5 -5
  158. package/dist/api/middlewares.d.ts +1 -1
  159. package/dist/api/reportError.d.ts +1 -1
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  161. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  162. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  163. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  164. package/dist/api/routing/middleware/middleware.js +48 -16
  165. package/dist/api/routing/middleware.d.ts +1 -2
  166. package/dist/api/routing/middleware.d.ts.map +1 -1
  167. package/dist/api/routing/middleware.js +1 -2
  168. package/dist/api/routing/schema/jwt.d.ts +1 -1
  169. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  170. package/dist/api/routing/tsort.d.ts +1 -1
  171. package/dist/api/routing/tsort.d.ts.map +1 -1
  172. package/dist/api/routing/utils.d.ts +3 -3
  173. package/dist/api/routing/utils.d.ts.map +1 -1
  174. package/dist/api/routing.d.ts +80 -37
  175. package/dist/api/routing.d.ts.map +1 -1
  176. package/dist/api/routing.js +109 -41
  177. package/dist/api/setupRequest.d.ts +8 -5
  178. package/dist/api/setupRequest.d.ts.map +1 -1
  179. package/dist/api/setupRequest.js +12 -7
  180. package/dist/api/util.d.ts +1 -1
  181. package/dist/arbs.d.ts +1 -1
  182. package/dist/arbs.d.ts.map +1 -1
  183. package/dist/arbs.js +5 -3
  184. package/dist/errorReporter.d.ts +4 -4
  185. package/dist/errorReporter.d.ts.map +1 -1
  186. package/dist/errorReporter.js +20 -25
  187. package/dist/errors.d.ts +1 -1
  188. package/dist/fileUtil.d.ts +1 -1
  189. package/dist/fileUtil.d.ts.map +1 -1
  190. package/dist/index.d.ts +1 -1
  191. package/dist/logger/jsonLogger.d.ts +1 -1
  192. package/dist/logger/logFmtLogger.d.ts +1 -1
  193. package/dist/logger/shared.d.ts +1 -1
  194. package/dist/logger/shared.js +2 -2
  195. package/dist/logger.d.ts +1 -1
  196. package/dist/logger.d.ts.map +1 -1
  197. package/dist/otel.d.ts +75 -0
  198. package/dist/otel.d.ts.map +1 -0
  199. package/dist/otel.js +65 -0
  200. package/dist/rateLimit.d.ts +9 -3
  201. package/dist/rateLimit.d.ts.map +1 -1
  202. package/dist/rateLimit.js +5 -11
  203. package/dist/test.d.ts +2 -2
  204. package/dist/test.d.ts.map +1 -1
  205. package/dist/test.js +1 -1
  206. package/dist/vitest.d.ts +1 -1
  207. package/examples/query.ts +42 -38
  208. package/package.json +46 -37
  209. package/src/CUPS.ts +9 -11
  210. package/src/Emailer/Sendgrid.ts +17 -14
  211. package/src/Emailer/service.ts +9 -3
  212. package/src/MainFiberSet.ts +5 -6
  213. package/src/Model/Repository/Registry.ts +33 -0
  214. package/src/Model/Repository/ext.ts +96 -10
  215. package/src/Model/Repository/internal/internal.ts +218 -149
  216. package/src/Model/Repository/makeRepo.ts +12 -10
  217. package/src/Model/Repository/service.ts +31 -22
  218. package/src/Model/Repository/validation.ts +4 -4
  219. package/src/Model/Repository.ts +1 -0
  220. package/src/Model/dsl.ts +3 -3
  221. package/src/Model/filter/types/path/eager.ts +1 -2
  222. package/src/Model/query/dsl.ts +348 -18
  223. package/src/Model/query/new-kid-interpreter.ts +206 -6
  224. package/src/Model.ts +1 -0
  225. package/src/QueueMaker/SQLQueue.ts +144 -152
  226. package/src/QueueMaker/memQueue.ts +104 -103
  227. package/src/QueueMaker/sbqueue.ts +70 -86
  228. package/src/RequestContext.ts +14 -16
  229. package/src/RequestFiberSet.ts +2 -2
  230. package/src/Store/ContextMapContainer.ts +41 -2
  231. package/src/Store/Cosmos/query.ts +140 -43
  232. package/src/Store/Cosmos.ts +482 -349
  233. package/src/Store/Disk.ts +102 -65
  234. package/src/Store/Memory.ts +275 -87
  235. package/src/Store/SQL/Pg.ts +361 -0
  236. package/src/Store/SQL/query.ts +539 -0
  237. package/src/Store/SQL.ts +731 -0
  238. package/src/Store/codeFilter.ts +3 -1
  239. package/src/Store/index.ts +17 -2
  240. package/src/Store/service.ts +41 -10
  241. package/src/Store/utils.ts +23 -22
  242. package/src/adapters/SQL/Model.ts +41 -40
  243. package/src/adapters/ServiceBus.ts +125 -121
  244. package/src/adapters/cosmos-client.ts +2 -2
  245. package/src/adapters/index.ts +7 -0
  246. package/src/adapters/memQueue.ts +2 -2
  247. package/src/adapters/mongo-client.ts +2 -2
  248. package/src/adapters/redis-client.ts +2 -2
  249. package/src/api/ContextProvider.ts +12 -13
  250. package/src/api/internal/RequestContextMiddleware.ts +15 -5
  251. package/src/api/internal/auth.ts +246 -44
  252. package/src/api/internal/events.ts +13 -9
  253. package/src/api/layerUtils.ts +8 -8
  254. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  255. package/src/api/routing/middleware/middleware.ts +55 -14
  256. package/src/api/routing/middleware.ts +0 -2
  257. package/src/api/routing.ts +296 -131
  258. package/src/api/setupRequest.ts +28 -8
  259. package/src/arbs.ts +4 -2
  260. package/src/errorReporter.ts +62 -74
  261. package/src/logger/shared.ts +1 -1
  262. package/src/otel.ts +152 -0
  263. package/src/rateLimit.ts +30 -22
  264. package/src/test.ts +1 -1
  265. package/test/auth.test.ts +101 -0
  266. package/test/contextProvider.test.ts +11 -11
  267. package/test/controller.test.ts +21 -30
  268. package/test/dist/auth.test.d.ts.map +1 -0
  269. package/test/dist/contextProvider.test.d.ts.map +1 -1
  270. package/test/dist/controller.test.d.ts.map +1 -1
  271. package/test/dist/date-query.test.d.ts.map +1 -0
  272. package/test/dist/fixtures.d.ts +26 -12
  273. package/test/dist/fixtures.d.ts.map +1 -1
  274. package/test/dist/fixtures.js +12 -10
  275. package/test/dist/query.test.d.ts.map +1 -1
  276. package/test/dist/rawQuery.test.d.ts.map +1 -1
  277. package/test/dist/repository-ext.test.d.ts.map +1 -0
  278. package/test/dist/requires.test.d.ts.map +1 -1
  279. package/test/dist/router-generator.test.d.ts.map +1 -0
  280. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  281. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  282. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  283. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  284. package/test/dist/sql-store.test.d.ts.map +1 -0
  285. package/test/fixtures.ts +11 -9
  286. package/test/query.test.ts +813 -38
  287. package/test/rawQuery.test.ts +301 -20
  288. package/test/repository-ext.test.ts +60 -0
  289. package/test/requires.test.ts +6 -6
  290. package/test/router-generator.test.ts +183 -0
  291. package/test/routing-interruptibility.test.ts +63 -0
  292. package/test/rpc-e2e-invalidation.test.ts +251 -0
  293. package/test/rpc-multi-middleware.test.ts +78 -9
  294. package/test/rpc-stream-fullstack.test.ts +300 -0
  295. package/test/sql-store.test.ts +1592 -0
  296. package/test/validateSample.test.ts +15 -12
  297. package/tsconfig.examples.json +1 -1
  298. package/tsconfig.json +0 -1
  299. package/tsconfig.json.bak +2 -2
  300. package/tsconfig.src.json +35 -35
  301. package/tsconfig.test.json +2 -2
  302. package/dist/Operations.d.ts +0 -55
  303. package/dist/Operations.d.ts.map +0 -1
  304. package/dist/Operations.js +0 -102
  305. package/dist/OperationsRepo.d.ts +0 -41
  306. package/dist/OperationsRepo.d.ts.map +0 -1
  307. package/dist/OperationsRepo.js +0 -14
  308. package/eslint.config.mjs +0 -24
  309. package/src/Operations.ts +0 -235
  310. package/src/OperationsRepo.ts +0 -16
@@ -0,0 +1,539 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Effect, type NonEmptyReadonlyArray } from "effect-app"
3
+ import { assertUnreachable } from "effect-app/utils"
4
+ import { InfraLogger } from "../../logger.js"
5
+ import type { FilterR, FilterResult } from "../../Model/filter/filterApi.js"
6
+ import type { ComputedProjectionIrExpression, ComputedProjectionMathIrExpression } from "../../Model/query.js"
7
+ import { isRelationCheck } from "../codeFilter.js"
8
+
9
+ export interface SQLDialect {
10
+ readonly jsonExtract: (path: string) => string
11
+ readonly jsonExtractJson: (path: string) => string
12
+ readonly placeholder: (index: number) => string
13
+ readonly jsonArrayContains: (arrPath: string, valPlaceholder: string) => string
14
+ readonly jsonArrayNotContains: (arrPath: string, valPlaceholder: string) => string
15
+ readonly jsonArrayContainsAny: (arrPath: string, valPlaceholders: readonly string[]) => string
16
+ readonly jsonArrayNotContainsAny: (arrPath: string, valPlaceholders: readonly string[]) => string
17
+ readonly jsonArrayContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
18
+ readonly jsonArrayNotContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
19
+ readonly caseInsensitiveLike: (expr: string, valPlaceholder: string) => string
20
+ readonly caseInsensitiveNotLike: (expr: string, valPlaceholder: string) => string
21
+ readonly jsonColumnType: "JSON" | "JSONB"
22
+ readonly arrayLength: (path: string) => string
23
+ readonly jsonEachFrom: (arrPath: string, alias: string) => string
24
+ readonly jsonExtractElement: (alias: string, subPath: string) => string
25
+ readonly serializeJsonValue: (v: unknown) => unknown
26
+ readonly serializeScalar: (v: unknown) => unknown
27
+ }
28
+
29
+ export const sqliteDialect: SQLDialect = {
30
+ jsonExtract: (path) => `json_extract(data, '$.${path}')`,
31
+ jsonExtractJson: (path) =>
32
+ `CASE json_type(data, '$.${path}') WHEN 'true' THEN 'true' WHEN 'false' THEN 'false' ELSE json_quote(json_extract(data, '$.${path}')) END`,
33
+ placeholder: (_index) => "?",
34
+ jsonArrayContains: (arrPath, val) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${val})`,
35
+ jsonArrayNotContains: (arrPath, val) =>
36
+ `NOT EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${val})`,
37
+ jsonArrayContainsAny: (arrPath, vals) =>
38
+ `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value IN (${vals.join(", ")}))`,
39
+ jsonArrayNotContainsAny: (arrPath, vals) =>
40
+ `NOT EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value IN (${vals.join(", ")}))`,
41
+ jsonArrayContainsAll: (arrPath, vals) =>
42
+ vals.map((v) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${v})`).join(" AND "),
43
+ jsonArrayNotContainsAll: (arrPath, vals) =>
44
+ `NOT (${
45
+ vals.map((v) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${v})`).join(" AND ")
46
+ })`,
47
+ caseInsensitiveLike: (expr, val) => `LOWER(${expr}) LIKE LOWER(${val})`,
48
+ caseInsensitiveNotLike: (expr, val) => `LOWER(${expr}) NOT LIKE LOWER(${val})`,
49
+ jsonColumnType: "JSON",
50
+ arrayLength: (path) => `json_array_length(data, '$.${path}')`,
51
+ jsonEachFrom: (arrPath, alias) => `json_each(data, '$.${arrPath}') AS ${alias}`,
52
+ jsonExtractElement: (alias, subPath) => `json_extract(${alias}.value, '$.${subPath}')`,
53
+ serializeJsonValue: (v) => v,
54
+ // SQLite stores JSON booleans as integers (0/1) and better-sqlite3 refuses
55
+ // to bind JS booleans, so coerce them to integers for WHERE params.
56
+ serializeScalar: (v) => typeof v === "boolean" ? (v ? 1 : 0) : v
57
+ }
58
+
59
+ export const pgDialect: SQLDialect = {
60
+ jsonExtract: (path) => {
61
+ const parts = path.split(".")
62
+ if (parts.length === 1) return `data->>'${parts[0]}'`
63
+ const last = parts.pop()!
64
+ return `data${parts.map((p) => `->'${p}'`).join("")}->>'${last}'`
65
+ },
66
+ jsonExtractJson: (path) => {
67
+ const parts = path.split(".")
68
+ if (parts.length === 1) return `data->'${parts[0]}'`
69
+ return `data${parts.map((p) => `->'${p}'`).join("")}`
70
+ },
71
+ placeholder: (index) => `$${index}`,
72
+ jsonArrayContains: (arrPath, val) => {
73
+ const parts = arrPath.split(".")
74
+ const jsonPath = parts.length === 1
75
+ ? `data->'${parts[0]}'`
76
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
77
+ return `${jsonPath} @> ${val}::jsonb`
78
+ },
79
+ jsonArrayNotContains: (arrPath, val) => {
80
+ const parts = arrPath.split(".")
81
+ const jsonPath = parts.length === 1
82
+ ? `data->'${parts[0]}'`
83
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
84
+ return `NOT (${jsonPath} @> ${val}::jsonb)`
85
+ },
86
+ jsonArrayContainsAny: (arrPath, vals) => {
87
+ const parts = arrPath.split(".")
88
+ const jsonPath = parts.length === 1
89
+ ? `data->'${parts[0]}'`
90
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
91
+ return `(${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" OR ")})`
92
+ },
93
+ jsonArrayNotContainsAny: (arrPath, vals) => {
94
+ const parts = arrPath.split(".")
95
+ const jsonPath = parts.length === 1
96
+ ? `data->'${parts[0]}'`
97
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
98
+ return `NOT (${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" OR ")})`
99
+ },
100
+ jsonArrayContainsAll: (arrPath, vals) => {
101
+ const parts = arrPath.split(".")
102
+ const jsonPath = parts.length === 1
103
+ ? `data->'${parts[0]}'`
104
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
105
+ return vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" AND ")
106
+ },
107
+ jsonArrayNotContainsAll: (arrPath, vals) => {
108
+ const parts = arrPath.split(".")
109
+ const jsonPath = parts.length === 1
110
+ ? `data->'${parts[0]}'`
111
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
112
+ return `NOT (${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" AND ")})`
113
+ },
114
+ caseInsensitiveLike: (expr, val) => `${expr} ILIKE ${val}`,
115
+ caseInsensitiveNotLike: (expr, val) => `${expr} NOT ILIKE ${val}`,
116
+ jsonColumnType: "JSONB",
117
+ arrayLength: (path) => `jsonb_array_length(data->'${path}')`,
118
+ jsonEachFrom: (arrPath, alias) => {
119
+ const parts = arrPath.split(".")
120
+ const jsonPath = parts.length === 1
121
+ ? `data->'${parts[0]}'`
122
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
123
+ return `jsonb_array_elements(${jsonPath}) AS ${alias}`
124
+ },
125
+ jsonExtractElement: (alias, subPath) => {
126
+ const parts = subPath.split(".")
127
+ if (parts.length === 1) return `${alias}->>'${parts[0]}'`
128
+ const last = parts.pop()!
129
+ return `${alias}${parts.map((p) => `->'${p}'`).join("")}->>'${last}'`
130
+ },
131
+ serializeJsonValue: (v) => JSON.stringify(v),
132
+ // PG's ->> operator yields text, so compare booleans as 'true'/'false' text.
133
+ serializeScalar: (v) => typeof v === "boolean" ? (v ? "true" : "false") : v
134
+ }
135
+
136
+ export function logQuery(q: { sql: string; params: unknown[] }) {
137
+ return InfraLogger
138
+ .logDebug("sql query")
139
+ .pipe(Effect.annotateLogs({
140
+ query: q.sql,
141
+ parameters: JSON.stringify(q.params, undefined, 2)
142
+ }))
143
+ }
144
+
145
+ const dottedToJsonPath = (path: string) =>
146
+ path
147
+ .split(".")
148
+ .filter((p) => p !== "-1")
149
+ .join(".")
150
+
151
+ const sqlStringLiteral = (value: string) => `'${value.replaceAll("'", "''")}'`
152
+
153
+ export function buildWhereSQLQuery(
154
+ dialect: SQLDialect,
155
+ idKey: PropertyKey,
156
+ filter: readonly FilterResult[],
157
+ tableName: string,
158
+ defaultValues: Record<string, unknown>,
159
+ select?: NonEmptyReadonlyArray<
160
+ string | {
161
+ key: string
162
+ subKeys: readonly string[]
163
+ } | {
164
+ key: string
165
+ computed: ComputedProjectionIrExpression
166
+ }
167
+ >,
168
+ order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
169
+ skip?: number,
170
+ limit?: number,
171
+ namespace?: string
172
+ ) {
173
+ const params: unknown[] = []
174
+ let paramIndex = 1
175
+
176
+ const addParam = (value: unknown): string => {
177
+ params.push(dialect.serializeScalar(value))
178
+ return dialect.placeholder(paramIndex++)
179
+ }
180
+
181
+ const fieldExpr = (path: string, relation?: string): string => {
182
+ if (path === idKey || path === "id") return "id"
183
+ if (relation && path.includes(".-1.")) {
184
+ const subPath = path.split(".-1.")[1]!
185
+ if (subPath.endsWith(".length")) {
186
+ // TODO: array length inside relation element
187
+ return dialect.jsonExtractElement(`_${relation}`, subPath.slice(0, -".length".length))
188
+ }
189
+ return dialect.jsonExtractElement(`_${relation}`, subPath)
190
+ }
191
+ if (path.endsWith(".length")) {
192
+ const arrPath = dottedToJsonPath(path.slice(0, -".length".length))
193
+ return dialect.arrayLength(arrPath)
194
+ }
195
+ const jsonPath = dottedToJsonPath(path)
196
+ const expr = dialect.jsonExtract(jsonPath)
197
+ const topKey = path.split(".")[0]
198
+ if (topKey in defaultValues) {
199
+ return `COALESCE(${expr}, ${addParam(defaultValues[topKey])})`
200
+ }
201
+ return expr
202
+ }
203
+
204
+ const statement = (x: FilterR, relation?: string): string => {
205
+ const resolvedPath = x.path === idKey ? "id" : x.path
206
+ const k = fieldExpr(resolvedPath, relation)
207
+
208
+ switch (x.op) {
209
+ case "in": {
210
+ const vals = x.value as unknown as readonly unknown[]
211
+ const hasNull = vals.some((v) => v == null)
212
+ const nonNullVals = vals.filter((v) => v != null)
213
+ const parts: string[] = []
214
+ if (nonNullVals.length > 0) {
215
+ const placeholders = nonNullVals.map((v) => addParam(v))
216
+ parts.push(`${k} IN (${placeholders.join(", ")})`)
217
+ }
218
+ if (hasNull) parts.push(`${k} IS NULL`)
219
+ return parts.length > 1 ? `(${parts.join(" OR ")})` : parts[0] ?? "1=0"
220
+ }
221
+ case "notIn": {
222
+ const vals = x.value as unknown as readonly unknown[]
223
+ const hasNull = vals.some((v) => v == null)
224
+ const nonNullVals = vals.filter((v) => v != null)
225
+ const parts: string[] = []
226
+ if (nonNullVals.length > 0) {
227
+ const placeholders = nonNullVals.map((v) => addParam(v))
228
+ parts.push(`${k} NOT IN (${placeholders.join(", ")})`)
229
+ }
230
+ if (hasNull) parts.push(`${k} IS NOT NULL`)
231
+ return parts.length > 1 ? `(${parts.join(" AND ")})` : parts[0] ?? "1=1"
232
+ }
233
+
234
+ case "includes": {
235
+ const arrPath = dottedToJsonPath(resolvedPath)
236
+ const v = addParam(x.value)
237
+ return dialect.jsonArrayContains(arrPath, v)
238
+ }
239
+ case "notIncludes": {
240
+ const arrPath = dottedToJsonPath(resolvedPath)
241
+ const v = addParam(x.value)
242
+ return dialect.jsonArrayNotContains(arrPath, v)
243
+ }
244
+
245
+ case "includes-any": {
246
+ const arrPath = dottedToJsonPath(resolvedPath)
247
+ const vals = x.value as unknown as readonly unknown[]
248
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
249
+ return dialect.jsonArrayContainsAny(arrPath, placeholders)
250
+ }
251
+ case "notIncludes-any": {
252
+ const arrPath = dottedToJsonPath(resolvedPath)
253
+ const vals = x.value as unknown as readonly unknown[]
254
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
255
+ return dialect.jsonArrayNotContainsAny(arrPath, placeholders)
256
+ }
257
+
258
+ case "includes-all": {
259
+ const arrPath = dottedToJsonPath(resolvedPath)
260
+ const vals = x.value as unknown as readonly unknown[]
261
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
262
+ return dialect.jsonArrayContainsAll(arrPath, placeholders)
263
+ }
264
+ case "notIncludes-all": {
265
+ const arrPath = dottedToJsonPath(resolvedPath)
266
+ const vals = x.value as unknown as readonly unknown[]
267
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
268
+ return dialect.jsonArrayNotContainsAll(arrPath, placeholders)
269
+ }
270
+
271
+ case "contains": {
272
+ const v = addParam(`%${x.value}%`)
273
+ return dialect.caseInsensitiveLike(k, v)
274
+ }
275
+ case "notContains": {
276
+ const v = addParam(`%${x.value}%`)
277
+ return dialect.caseInsensitiveNotLike(k, v)
278
+ }
279
+ case "startsWith": {
280
+ const v = addParam(`${x.value}%`)
281
+ return dialect.caseInsensitiveLike(k, v)
282
+ }
283
+ case "notStartsWith": {
284
+ const v = addParam(`${x.value}%`)
285
+ return dialect.caseInsensitiveNotLike(k, v)
286
+ }
287
+ case "endsWith": {
288
+ const v = addParam(`%${x.value}`)
289
+ return dialect.caseInsensitiveLike(k, v)
290
+ }
291
+ case "notEndsWith": {
292
+ const v = addParam(`%${x.value}`)
293
+ return dialect.caseInsensitiveNotLike(k, v)
294
+ }
295
+
296
+ case "lt": {
297
+ const v = addParam(x.value)
298
+ return `${k} < ${v}`
299
+ }
300
+ case "lte": {
301
+ const v = addParam(x.value)
302
+ return `${k} <= ${v}`
303
+ }
304
+ case "gt": {
305
+ const v = addParam(x.value)
306
+ return `${k} > ${v}`
307
+ }
308
+ case "gte": {
309
+ const v = addParam(x.value)
310
+ return `${k} >= ${v}`
311
+ }
312
+ case "neq": {
313
+ if (x.value === null) return `${k} IS NOT NULL`
314
+ const v = addParam(x.value)
315
+ return `${k} <> ${v}`
316
+ }
317
+ case undefined:
318
+ case "eq": {
319
+ if (x.value === null) return `${k} IS NULL`
320
+ const v = addParam(x.value)
321
+ return `${k} = ${v}`
322
+ }
323
+ default:
324
+ return assertUnreachable(x.op)
325
+ }
326
+ }
327
+
328
+ const wrapRelation = (rel: string, inner: string, every: boolean): string => {
329
+ // Optimize tautological/contradictory conditions
330
+ if (every && inner === "1=1") return "1=1"
331
+ if (!every && inner === "1=0") return "1=0"
332
+ const from = dialect.jsonEachFrom(rel, `_${rel}`)
333
+ // ∀x.P(x) ≡ ¬∃x.¬P(x), i.e. NOT EXISTS(... WHERE NOT P)
334
+ return every
335
+ ? `NOT EXISTS(SELECT 1 FROM ${from} WHERE NOT (${inner}))`
336
+ : `EXISTS(SELECT 1 FROM ${from} WHERE ${inner})`
337
+ }
338
+
339
+ const print = (state: readonly FilterResult[], isRelation: string | null, every: boolean): string => {
340
+ let s = ""
341
+ for (const e of state) {
342
+ switch (e.t) {
343
+ case "where":
344
+ s += statement(e, isRelation ?? undefined)
345
+ break
346
+ case "or":
347
+ s += ` OR ${statement(e, isRelation ?? undefined)}`
348
+ break
349
+ case "and":
350
+ s += ` AND ${statement(e, isRelation ?? undefined)}`
351
+ break
352
+ case "or-scope": {
353
+ if (!every) every = e.relation === "every"
354
+ const rel = isRelationCheck(e.result, isRelation)
355
+ if (rel) {
356
+ s += isRelation
357
+ ? ` OR (${print(e.result, rel, every)})`
358
+ : ` OR ${wrapRelation(rel, print(e.result, rel, every), every)}`
359
+ } else {
360
+ s += ` OR (${print(e.result, null, every)})`
361
+ }
362
+ break
363
+ }
364
+ case "and-scope": {
365
+ if (!every) every = e.relation === "every"
366
+ const rel = isRelationCheck(e.result, isRelation)
367
+ if (rel) {
368
+ s += isRelation
369
+ ? ` AND (${print(e.result, rel, every)})`
370
+ : ` AND ${wrapRelation(rel, print(e.result, rel, every), every)}`
371
+ } else {
372
+ s += ` AND (${print(e.result, null, every)})`
373
+ }
374
+ break
375
+ }
376
+ case "where-scope": {
377
+ if (!every) every = e.relation === "every"
378
+ const rel = isRelationCheck(e.result, isRelation)
379
+ if (rel) {
380
+ s += isRelation
381
+ ? `(${print(e.result, rel, every)})`
382
+ : wrapRelation(rel, print(e.result, rel, every), every)
383
+ } else {
384
+ s += `(${print(e.result, null, every)})`
385
+ }
386
+ break
387
+ }
388
+ }
389
+ }
390
+ return s
391
+ }
392
+
393
+ const computedSelectExpr = (key: string, computed: ComputedProjectionIrExpression): string => {
394
+ const relationPath = dottedToJsonPath(computed.path)
395
+ const relationAlias = `_${computed.path}`
396
+ const relationFrom = dialect.jsonEachFrom(relationPath, relationAlias)
397
+ const toNumber = (expr: string) =>
398
+ dialect.jsonColumnType === "JSON" ? `CAST(${expr} AS REAL)` : `(${expr})::numeric`
399
+ const compileExpr = (expression: ComputedProjectionMathIrExpression): string => {
400
+ switch (expression._tag) {
401
+ case "field":
402
+ return toNumber(dialect.jsonExtractElement(relationAlias, expression.field))
403
+ case "mul":
404
+ return `(${compileExpr(expression.left)} * ${compileExpr(expression.right)})`
405
+ default:
406
+ return assertUnreachable(expression)
407
+ }
408
+ }
409
+ const factorCaseExpr = (unitExpr: string, toBase: string, factors: Readonly<Record<string, number>>) => {
410
+ const entries = Object.entries(factors).filter(([, factor]) => Number.isFinite(factor))
411
+ const cases = entries.map(([unit, factor]) => ` WHEN ${sqlStringLiteral(unit)} THEN ${factor}`).join("")
412
+ return `CASE ${unitExpr} WHEN ${sqlStringLiteral(toBase)} THEN 1${cases} ELSE NULL END`
413
+ }
414
+ const whereClause = () =>
415
+ computed.filter.length > 0
416
+ ? ` WHERE ${print(computed.filter, computed.path, false)}`
417
+ : ""
418
+ const boolExpr = (sqlExpr: string) =>
419
+ dialect.jsonColumnType === "JSON"
420
+ ? `CASE WHEN ${sqlExpr} THEN 'true' ELSE 'false' END AS "${key}"`
421
+ : `${sqlExpr} AS "${key}"`
422
+ switch (computed._tag) {
423
+ case "relation-count":
424
+ return `(SELECT COUNT(1) FROM ${relationFrom}${whereClause()}) AS "${key}"`
425
+ case "relation-any":
426
+ return boolExpr(`EXISTS(SELECT 1 FROM ${relationFrom}${whereClause()})`)
427
+ case "relation-every":
428
+ // ∀x.P(x) ≡ ¬∃x.¬P(x). When no filter, no element exists that violates ⊤ → true.
429
+ return boolExpr(
430
+ computed.filter.length === 0
431
+ ? `1=1`
432
+ : `NOT EXISTS(SELECT 1 FROM ${relationFrom} WHERE NOT (${print(computed.filter, computed.path, false)}))`
433
+ )
434
+ case "relation-distinct-count": {
435
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
436
+ return `(SELECT COUNT(DISTINCT ${fieldExtract}) FROM ${relationFrom}${whereClause()}) AS "${key}"`
437
+ }
438
+ case "relation-sum": {
439
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
440
+ return `(SELECT COALESCE(SUM(${toNumber(fieldExtract)}), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
441
+ }
442
+ case "relation-sum-expr": {
443
+ const expression = compileExpr(computed.expression)
444
+ return `(SELECT COALESCE(SUM(${expression}), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
445
+ }
446
+ case "relation-sum-expr-by": {
447
+ const expression = compileExpr(computed.expression)
448
+ const unitExpr = dialect.jsonExtractElement(relationAlias, computed.unit)
449
+ if (dialect.jsonColumnType === "JSON") {
450
+ return `(SELECT COALESCE(json_group_array(json_object('unit', __unit, 'total', __total)), json_array()) FROM (SELECT ${unitExpr} AS __unit, COALESCE(SUM(${expression}), 0) AS __total FROM ${relationFrom}${whereClause()} GROUP BY ${unitExpr})) AS "${key}"`
451
+ }
452
+ return `(SELECT COALESCE(jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total)), '[]'::jsonb) FROM (SELECT ${unitExpr} AS __unit, COALESCE(SUM(${expression}), 0) AS __total FROM ${relationFrom}${whereClause()} GROUP BY ${unitExpr}) __grouped) AS "${key}"`
453
+ }
454
+ case "relation-sum-expr-normalized": {
455
+ const expression = compileExpr(computed.expression)
456
+ const unitExpr = dialect.jsonExtractElement(relationAlias, computed.unit)
457
+ const factorExpr = factorCaseExpr(unitExpr, computed.toBase, computed.factors)
458
+ return `(SELECT COALESCE(SUM((${expression}) * (${factorExpr})), 0) FROM ${relationFrom}${whereClause()}) AS "${key}"`
459
+ }
460
+ case "relation-collect": {
461
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
462
+ if (dialect.jsonColumnType === "JSON") {
463
+ // sqlite: json_group_array does not accept DISTINCT; emulate via inner DISTINCT subquery
464
+ if (computed.distinct) {
465
+ return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (SELECT DISTINCT ${fieldExtract} AS __v FROM ${relationFrom}${whereClause()})) AS "${key}"`
466
+ }
467
+ return `(SELECT COALESCE(json_group_array(${fieldExtract}), json_array()) FROM ${relationFrom}${whereClause()}) AS "${key}"`
468
+ }
469
+ const aggArg = computed.distinct ? `DISTINCT ${fieldExtract}` : fieldExtract
470
+ return `(SELECT COALESCE(jsonb_agg(${aggArg}), '[]'::jsonb) FROM ${relationFrom}${whereClause()}) AS "${key}"`
471
+ }
472
+ case "relation-collect-fields": {
473
+ const branches = computed.fields.map((field) => {
474
+ const fieldExtract = dialect.jsonExtractElement(relationAlias, field)
475
+ return `SELECT ${fieldExtract} AS __v FROM ${relationFrom}${whereClause()}`
476
+ })
477
+ const unionQuery = branches.join(" UNION ALL ")
478
+ if (dialect.jsonColumnType === "JSON") {
479
+ if (computed.distinct) {
480
+ return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (SELECT DISTINCT __v FROM (${unionQuery}))) AS "${key}"`
481
+ }
482
+ return `(SELECT COALESCE(json_group_array(__v), json_array()) FROM (${unionQuery})) AS "${key}"`
483
+ }
484
+ if (computed.distinct) {
485
+ return `(SELECT COALESCE(jsonb_agg(__v), '[]'::jsonb) FROM (SELECT DISTINCT __v FROM (${unionQuery}) inner_q) outer_q) AS "${key}"`
486
+ }
487
+ return `(SELECT COALESCE(jsonb_agg(__v), '[]'::jsonb) FROM (${unionQuery}) t) AS "${key}"`
488
+ }
489
+ default:
490
+ return assertUnreachable(computed)
491
+ }
492
+ }
493
+
494
+ const getSelectExpr = (): string => {
495
+ if (!select) return "id, _etag, data"
496
+ const fields = select.map((s) => {
497
+ if (typeof s === "string") {
498
+ if (s === idKey || s === "id") return `id`
499
+ if (s === "_etag") return `_etag`
500
+ return `${dialect.jsonExtractJson(s)} AS "${s}"`
501
+ }
502
+ if ("computed" in s) {
503
+ return computedSelectExpr(s.key, s.computed)
504
+ }
505
+ return `${dialect.jsonExtractJson(s.key)} AS "${s.key}"`
506
+ })
507
+ return fields.join(", ")
508
+ }
509
+
510
+ // Order matters: projection params must be emitted BEFORE user-filter
511
+ // params so positional `?` placeholders in SQLite match `params[]` order.
512
+ const selectExpr = getSelectExpr()
513
+
514
+ const namespaceClause = namespace !== undefined
515
+ ? `_namespace = ${addParam(namespace)}`
516
+ : ""
517
+ const userWhere = filter.length
518
+ ? print([{ t: "where-scope", result: filter, relation: "some" }], null, false)
519
+ : ""
520
+ const whereClause = namespaceClause && userWhere
521
+ ? `WHERE ${namespaceClause} AND ${userWhere}`
522
+ : namespaceClause
523
+ ? `WHERE ${namespaceClause}`
524
+ : userWhere
525
+ ? `WHERE ${userWhere}`
526
+ : ""
527
+
528
+ const orderClause = order
529
+ ? `ORDER BY ${order.map((_) => `${fieldExpr(_.key)} ${_.direction}`).join(", ")}`
530
+ : ""
531
+
532
+ const limitClause = limit !== undefined || skip !== undefined
533
+ ? `LIMIT ${addParam(limit ?? 999999)} OFFSET ${addParam(skip ?? 0)}`
534
+ : ""
535
+
536
+ const sql = `SELECT ${selectExpr} FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim()
537
+
538
+ return { sql, params }
539
+ }