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