turbine-orm 0.81.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/cjs/powql.js
CHANGED
|
@@ -69,23 +69,35 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
69
69
|
};
|
|
70
70
|
})();
|
|
71
71
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
72
|
-
exports.PowqlInterface = exports.MAX_POWQL_DATETIME_TERMS = void 0;
|
|
72
|
+
exports.PowqlInterface = exports.MAX_RELATION_FILTER_KEYS = exports.MAX_POWQL_DATETIME_TERMS = void 0;
|
|
73
73
|
const node_crypto_1 = require("node:crypto");
|
|
74
74
|
const connection_guard_js_1 = require("./connection-guard.js");
|
|
75
|
+
const diagnostics_js_1 = require("./diagnostics.js");
|
|
75
76
|
const errors_js_1 = require("./errors.js");
|
|
77
|
+
const logger_js_1 = require("./logger.js");
|
|
76
78
|
const nested_write_js_1 = require("./nested-write.js");
|
|
77
79
|
const powdb_shared_js_1 = require("./powdb-shared.js");
|
|
78
80
|
const aggregates_js_1 = require("./query/aggregates.js");
|
|
81
|
+
const batched_loader_js_1 = require("./query/batched-loader.js");
|
|
82
|
+
const builder_js_1 = require("./query/builder.js");
|
|
83
|
+
const column_ref_js_1 = require("./query/column-ref.js");
|
|
79
84
|
const compound_unique_js_1 = require("./query/compound-unique.js");
|
|
85
|
+
const expr_js_1 = require("./query/expr.js");
|
|
80
86
|
const filters_js_1 = require("./query/filters.js");
|
|
87
|
+
const json_value_js_1 = require("./query/json-value.js");
|
|
88
|
+
const normalize_args_js_1 = require("./query/normalize-args.js");
|
|
81
89
|
const option_surface_js_1 = require("./query/option-surface.js");
|
|
82
90
|
const relation_names_js_1 = require("./query/relation-names.js");
|
|
91
|
+
const relations_js_1 = require("./query/relations.js");
|
|
83
92
|
// The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
|
|
84
93
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
85
94
|
// engines, so a spread request body cannot turn either on here either.
|
|
86
95
|
const types_js_1 = require("./query/types.js");
|
|
87
96
|
const utils_js_1 = require("./query/utils.js");
|
|
97
|
+
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
88
98
|
const where_js_1 = require("./query/where.js");
|
|
99
|
+
const where_compile_js_1 = require("./query/where-compile.js");
|
|
100
|
+
const writes_js_1 = require("./query/writes.js");
|
|
89
101
|
const schema_js_1 = require("./schema.js");
|
|
90
102
|
/**
|
|
91
103
|
* Max parent keys per relation-loader `in (…)` query. A `with` over a large
|
|
@@ -94,6 +106,8 @@ const schema_js_1 = require("./schema.js");
|
|
|
94
106
|
* before grouping. Mirrors the chunking the parity matrix documents.
|
|
95
107
|
*/
|
|
96
108
|
const MAX_RELATION_KEYS = 1000;
|
|
109
|
+
/** A predicate that holds for every row (an empty `notIn`, an `OR` with an empty branch). */
|
|
110
|
+
const ALWAYS_TRUE = '(1 = 1)';
|
|
97
111
|
/**
|
|
98
112
|
* Max values one `in` / `notIn` on a PowDB-native `datetime` column may carry.
|
|
99
113
|
*
|
|
@@ -178,6 +192,71 @@ const POWQL_HAVING_AGG_FNS = new Map([
|
|
|
178
192
|
['_max', 'max'],
|
|
179
193
|
['_count', 'count'],
|
|
180
194
|
]);
|
|
195
|
+
/**
|
|
196
|
+
* Max distinct keys one client-side relation filter may resolve to.
|
|
197
|
+
*
|
|
198
|
+
* A relation filter becomes a literal `in (…)` list on the local key, and the
|
|
199
|
+
* engine evaluates that list per row, so the cost grows with rows times keys
|
|
200
|
+
* (measured on the 0.20 addon over 100k rows: about 0.3 s per 1,000 keys).
|
|
201
|
+
* Past this bound the filter is refused with a typed error naming the bound,
|
|
202
|
+
* instead of building a statement whose cost has no ceiling.
|
|
203
|
+
*/
|
|
204
|
+
exports.MAX_RELATION_FILTER_KEYS = 10_000;
|
|
205
|
+
/** The relation-filter keys the SQL engines accept, in their compile order. */
|
|
206
|
+
const RELATION_FILTER_KEYS = ['some', 'none', 'every', 'is', 'isNot'];
|
|
207
|
+
/**
|
|
208
|
+
* True for a branded column reference (`col('x')`). PowQL has no column-to-column
|
|
209
|
+
* operand form in a filter, so a reference is refused rather than bound as data.
|
|
210
|
+
* The brand is read by its registry key so this needs no import of the module
|
|
211
|
+
* that creates it.
|
|
212
|
+
*/
|
|
213
|
+
function isColumnReference(value) {
|
|
214
|
+
return typeof value === 'object' && value !== null && Symbol.for('turbine-orm.columnRef') in value;
|
|
215
|
+
}
|
|
216
|
+
/** A value a non-JSON column can be compared against. */
|
|
217
|
+
function isScalarOperand(value) {
|
|
218
|
+
const t = typeof value;
|
|
219
|
+
return (t === 'string' ||
|
|
220
|
+
t === 'number' ||
|
|
221
|
+
t === 'bigint' ||
|
|
222
|
+
t === 'boolean' ||
|
|
223
|
+
value instanceof Date ||
|
|
224
|
+
value instanceof Uint8Array);
|
|
225
|
+
}
|
|
226
|
+
/** The largest delay `setTimeout` honours; Node fires anything larger after 1 ms. */
|
|
227
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
228
|
+
/** The PowQL operator each arithmetic update operator compiles to. */
|
|
229
|
+
const POWQL_ARITHMETIC = {
|
|
230
|
+
increment: '+',
|
|
231
|
+
decrement: '-',
|
|
232
|
+
multiply: '*',
|
|
233
|
+
divide: '/',
|
|
234
|
+
};
|
|
235
|
+
/** The accepted `relationLoadStrategy` values, the SQL engines' set. */
|
|
236
|
+
const RELATION_LOAD_STRATEGIES = Object.freeze(['join', 'batched', 'auto', 'flatten']);
|
|
237
|
+
/** A short, value-safe rendering of a rejected option value. */
|
|
238
|
+
function describeOptionValue(value) {
|
|
239
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
240
|
+
return String(value);
|
|
241
|
+
if (typeof value === 'string')
|
|
242
|
+
return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}...` : value);
|
|
243
|
+
return value === null ? 'null' : `a value of type ${typeof value}`;
|
|
244
|
+
}
|
|
245
|
+
/** `{ equals: v }` and nothing else. */
|
|
246
|
+
/** `AND` / `OR` / `NOT`, the keys {@link combinatorBranches} reads. */
|
|
247
|
+
function isCombinatorKey(key) {
|
|
248
|
+
return key === 'AND' || key === 'OR' || key === 'NOT';
|
|
249
|
+
}
|
|
250
|
+
/** Short, data-free description of an operand for an error message. */
|
|
251
|
+
function describeOperand(value) {
|
|
252
|
+
if (value === null)
|
|
253
|
+
return 'null';
|
|
254
|
+
if (Array.isArray(value))
|
|
255
|
+
return 'an array';
|
|
256
|
+
if (typeof value === 'object')
|
|
257
|
+
return 'an object';
|
|
258
|
+
return typeof value;
|
|
259
|
+
}
|
|
181
260
|
/** Filters that have no PowDB representation and must throw E017. */
|
|
182
261
|
function rejectUnsupportedFilter(value, field) {
|
|
183
262
|
if ('distance' in value || 'metric' in value) {
|
|
@@ -204,8 +283,12 @@ function rejectUnsupportedFilter(value, field) {
|
|
|
204
283
|
}
|
|
205
284
|
/**
|
|
206
285
|
* The PowQL query interface. Constructed by `turbinePowDB` via the
|
|
207
|
-
* `queryInterfaceFactory` seam
|
|
208
|
-
*
|
|
286
|
+
* `queryInterfaceFactory` seam. `implements TableClient` makes a method the SQL
|
|
287
|
+
* accessor has and this class lacks, or one whose signature differs, a build
|
|
288
|
+
* error. Where a method's result is narrowed by `select` / `omit` / `with`, a
|
|
289
|
+
* public overload signature carries the SQL accessor's typing and the
|
|
290
|
+
* implementation below it is typed on the plain row; both return the same
|
|
291
|
+
* projected, relation-loaded rows at run time.
|
|
209
292
|
*/
|
|
210
293
|
class PowqlInterface {
|
|
211
294
|
pool;
|
|
@@ -217,7 +300,10 @@ class PowqlInterface {
|
|
|
217
300
|
defaultLimit;
|
|
218
301
|
warnOnUnlimited;
|
|
219
302
|
onQuery;
|
|
220
|
-
|
|
303
|
+
/** Where this interface's log lines go: the client's logger, else the console. */
|
|
304
|
+
logger;
|
|
305
|
+
/** The logger for advice lines, or `undefined` when `diagnostics` turns advice off. */
|
|
306
|
+
advice;
|
|
221
307
|
constructor(pool, table, schema, middlewares = [], options = {}) {
|
|
222
308
|
this.pool = pool;
|
|
223
309
|
this.table = table;
|
|
@@ -239,6 +325,10 @@ class PowqlInterface {
|
|
|
239
325
|
? (warnOpt[table] ?? warnOpt[(0, schema_js_1.snakeToCamel)(table)]) !== false
|
|
240
326
|
: warnOpt !== false;
|
|
241
327
|
this.onQuery = options._onQuery;
|
|
328
|
+
// Resolved by the client once and passed in; an interface built directly
|
|
329
|
+
// resolves here, at construction, as QueryInterface does.
|
|
330
|
+
this.logger = options.logger ?? logger_js_1.consoleLogger;
|
|
331
|
+
this.advice = (0, diagnostics_js_1.resolveDiagnostics)(options.diagnostics).advice ? this.logger : undefined;
|
|
242
332
|
}
|
|
243
333
|
// -------------------------------------------------------------------------
|
|
244
334
|
// Column / value helpers
|
|
@@ -338,11 +428,44 @@ class PowqlInterface {
|
|
|
338
428
|
*/
|
|
339
429
|
param(value, params, col) {
|
|
340
430
|
let tagged = value;
|
|
341
|
-
|
|
431
|
+
const labels = col ? this.enumLabels(col) : undefined;
|
|
432
|
+
if (labels && value !== null && value !== undefined) {
|
|
433
|
+
// An enum column takes one of its labels, in a write and in a filter
|
|
434
|
+
// alike, as PostgreSQL does (22P02 there).
|
|
435
|
+
this.assertEnumLabel(col, labels, value);
|
|
436
|
+
}
|
|
437
|
+
else if ((0, json_value_js_1.isJsonValue)(value)) {
|
|
438
|
+
// `jsonValue(v)`: serialized with JSON.stringify, as on the SQL engines.
|
|
439
|
+
tagged = new powdb_shared_js_1.PowdbJsonParam(value.value, col?.name);
|
|
440
|
+
}
|
|
441
|
+
else if (col && typeof value === 'string' && (0, powdb_shared_js_1.isDateColumn)(col)) {
|
|
442
|
+
// Date text against a Date column: the epoch micros the column stores,
|
|
443
|
+
// as PostgreSQL reads the same text against a date / timestamp.
|
|
444
|
+
tagged = isoTextMicros(value, col, this.table);
|
|
445
|
+
}
|
|
446
|
+
else if (col &&
|
|
447
|
+
(0, powdb_shared_js_1.isDateColumn)(col) &&
|
|
448
|
+
(typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean')) {
|
|
449
|
+
// The column holds epoch MICROseconds, so a number bound as-is names a
|
|
450
|
+
// different instant (a millisecond count lands in 1970) and matches
|
|
451
|
+
// nothing. PostgreSQL refuses a number against a date / timestamp too.
|
|
452
|
+
throw new errors_js_1.ValidationError(`Invalid value for the Date column "${col.name}" on "${this.table}": expected a Date or ISO 8601 text, ` +
|
|
453
|
+
`got ${describeOperand(value)}. Pass \`new Date(ms)\` for a millisecond timestamp.`);
|
|
454
|
+
}
|
|
455
|
+
else if (col && typeof value === 'string' && this.isIntegerCol(col)) {
|
|
456
|
+
// Integer text against an integer column, as PostgreSQL reads it; an int8
|
|
457
|
+
// value past 2^53 keeps every digit as a bigint.
|
|
458
|
+
tagged = integerTextValue(value, col, this.table);
|
|
459
|
+
}
|
|
460
|
+
else if (col && typeof value === 'number' && this.isFloatCol(col)) {
|
|
342
461
|
// Float column: force a float-form literal even for an integer value.
|
|
343
462
|
tagged = new powdb_shared_js_1.PowdbFloatParam(value);
|
|
344
463
|
}
|
|
345
|
-
else if (col &&
|
|
464
|
+
else if (col &&
|
|
465
|
+
value !== null &&
|
|
466
|
+
typeof value === 'object' &&
|
|
467
|
+
!(value instanceof Date) &&
|
|
468
|
+
(0, powdb_shared_js_1.isJsonColumn)(col, this.schema.enums)) {
|
|
346
469
|
// json document column: a JS object/array is serialized to canonical JSON
|
|
347
470
|
// text and stored as a json document (a JS string passes through raw, same
|
|
348
471
|
// contract as pg jsonb; `null` stays `null`).
|
|
@@ -362,14 +485,36 @@ class PowqlInterface {
|
|
|
362
485
|
* and coercion stays unambiguous. Non-finite floats are still rejected.
|
|
363
486
|
*/
|
|
364
487
|
writeRef(value, col, params) {
|
|
488
|
+
// A value position (create data, `{ set }`): the SQL engines' rule and message.
|
|
489
|
+
if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value)) {
|
|
490
|
+
throw new errors_js_1.ValidationError((0, expr_js_1.exprNotAllowedMessage)(this.table, col.field, value));
|
|
491
|
+
}
|
|
365
492
|
if (typeof value === 'number' && !Number.isFinite(value) && this.isFloatCol(col)) {
|
|
366
493
|
throw new errors_js_1.ValidationError(`Non-finite value for float column "${col.name}" on "${this.table}".`);
|
|
367
494
|
}
|
|
368
495
|
return this.param(value, params, col);
|
|
369
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* A PowQL `int` column declared as an integer type (not a Date, which is
|
|
499
|
+
* stored as int micros, and not a column whose declared type is something
|
|
500
|
+
* else).
|
|
501
|
+
*/
|
|
502
|
+
isIntegerCol(col) {
|
|
503
|
+
if ((0, powdb_shared_js_1.isDateColumn)(col))
|
|
504
|
+
return false;
|
|
505
|
+
const declared = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
506
|
+
if (declared !== '' && !/int|serial/.test(declared))
|
|
507
|
+
return false;
|
|
508
|
+
try {
|
|
509
|
+
return (0, powdb_shared_js_1.powqlColumnType)(col, this.schema.enums) === 'int';
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
370
515
|
isFloatCol(col) {
|
|
371
516
|
try {
|
|
372
|
-
return (0, powdb_shared_js_1.powqlColumnType)(col) === 'float';
|
|
517
|
+
return (0, powdb_shared_js_1.powqlColumnType)(col, this.schema.enums) === 'float';
|
|
373
518
|
}
|
|
374
519
|
catch {
|
|
375
520
|
return false;
|
|
@@ -467,15 +612,6 @@ class PowqlInterface {
|
|
|
467
612
|
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
|
|
468
613
|
return normalized === args.with ? args : { ...args, with: normalized };
|
|
469
614
|
}
|
|
470
|
-
/**
|
|
471
|
-
* Caller args in canonical form: declared relation spellings, and `take` /
|
|
472
|
-
* `skip` folded into `limit` / `offset`.
|
|
473
|
-
*
|
|
474
|
-
* Same composition as `QueryInterface.normalizeArgs` and here for the same
|
|
475
|
-
* reason the method above is here: nothing about a parallel implementation
|
|
476
|
-
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
477
|
-
* not `skip` pages differently from one that reads both.
|
|
478
|
-
*/
|
|
479
615
|
/**
|
|
480
616
|
* Fill in the deterministic per-relation ordering `stableRelationOrder` asks
|
|
481
617
|
* for, through the shared transform the SQL engines use.
|
|
@@ -485,6 +621,13 @@ class PowqlInterface {
|
|
|
485
621
|
* option whose entire purpose is that they do not. Same resolution as
|
|
486
622
|
* QueryInterface: a per-query flag wins, else the client-level default.
|
|
487
623
|
*/
|
|
624
|
+
/** `take` / `skip` inside every `with` level folded into `limit` / `offset` (shared rule). */
|
|
625
|
+
withRelationPagination(args) {
|
|
626
|
+
if (!args?.with)
|
|
627
|
+
return args;
|
|
628
|
+
const normalized = (0, relation_names_js_1.normalizeWithPagination)(this.schema, this.table, args.with);
|
|
629
|
+
return normalized === args.with ? args : { ...args, with: normalized };
|
|
630
|
+
}
|
|
488
631
|
withStableRelationOrder(args) {
|
|
489
632
|
if (!args?.with)
|
|
490
633
|
return args;
|
|
@@ -494,8 +637,158 @@ class PowqlInterface {
|
|
|
494
637
|
const ordered = (0, relation_names_js_1.applyStableRelationOrderTo)(this.schema, args.with, this.table);
|
|
495
638
|
return ordered === args.with ? args : { ...args, with: ordered };
|
|
496
639
|
}
|
|
497
|
-
|
|
498
|
-
|
|
640
|
+
/**
|
|
641
|
+
* Caller args in canonical form: `undefined` pruned (query/normalize-args.ts),
|
|
642
|
+
* declared relation spellings, stable relation order, and `take` / `skip`
|
|
643
|
+
* folded into `limit` / `offset` at the top level and inside `with`. Same composition as
|
|
644
|
+
* `QueryInterface.normalizeArgs`, run at the top of every public operation,
|
|
645
|
+
* so the two engines agree about what each argument means.
|
|
646
|
+
*
|
|
647
|
+
* `refuseEmptiedWhere` names the single-row read (`findFirst`,
|
|
648
|
+
* `findFirstOrThrow`) that refuses a `where` whose every key was `undefined`.
|
|
649
|
+
*/
|
|
650
|
+
normalizeArgs(args, refuseEmptiedWhere) {
|
|
651
|
+
const pruned = (0, normalize_args_js_1.pruneUndefinedArgs)(this.schema, this.table, args);
|
|
652
|
+
if (pruned !== args) {
|
|
653
|
+
if (refuseEmptiedWhere)
|
|
654
|
+
(0, normalize_args_js_1.assertWhereNotEmptiedByPruning)(this.table, refuseEmptiedWhere, args, pruned);
|
|
655
|
+
(0, normalize_args_js_1.assertCursorNotEmptied)(this.table, args, pruned);
|
|
656
|
+
}
|
|
657
|
+
this.assertQueryOptions(pruned);
|
|
658
|
+
const shaped = pruned;
|
|
659
|
+
return (0, utils_js_1.normalizePagination)(this.withStableRelationOrder(this.withRelationPagination(this.withDeclaredRelationNames(shaped))));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The per-query `timeout` and `relationLoadStrategy`, checked with the SQL
|
|
663
|
+
* engines' rule and message: a timeout is a positive number of ms up to
|
|
664
|
+
* {@link MAX_TIMEOUT_MS} (or 0 for none), a strategy one of the SQL engines' values.
|
|
665
|
+
*/
|
|
666
|
+
assertQueryOptions(args) {
|
|
667
|
+
const { timeout, relationLoadStrategy } = args;
|
|
668
|
+
if (timeout !== undefined &&
|
|
669
|
+
!(typeof timeout === 'number' && Number.isFinite(timeout) && timeout >= 0 && timeout <= MAX_TIMEOUT_MS)) {
|
|
670
|
+
throw new errors_js_1.ValidationError(`\`timeout\` on ${this.table} must be a positive number of milliseconds up to ${MAX_TIMEOUT_MS}, or 0 for ` +
|
|
671
|
+
`no timeout (received ${describeOptionValue(timeout)}). Leave it out for no timeout.`);
|
|
672
|
+
}
|
|
673
|
+
if (relationLoadStrategy !== undefined && !RELATION_LOAD_STRATEGIES.includes(relationLoadStrategy)) {
|
|
674
|
+
throw new errors_js_1.ValidationError(`\`relationLoadStrategy\` must be one of ${RELATION_LOAD_STRATEGIES.map((v) => `'${v}'`).join(', ')} ` +
|
|
675
|
+
`(received ${describeOptionValue(relationLoadStrategy)}).`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Whether `cursor` carries a value, after the SQL engines' cursor checks: a
|
|
680
|
+
* cursor that names no row (`null`, `{}`) is E003, and each value is a row's
|
|
681
|
+
* value under the shared rule (`cursorFieldValue`). Unknown fields are E003.
|
|
682
|
+
*/
|
|
683
|
+
cursorCarriesValues(cursor) {
|
|
684
|
+
(0, normalize_args_js_1.assertCursorWritten)(this.table, 'findMany', cursor);
|
|
685
|
+
if (!(0, normalize_args_js_1.hasCursor)(cursor))
|
|
686
|
+
return false;
|
|
687
|
+
const flat = (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.meta, cursor);
|
|
688
|
+
for (const [key, written] of Object.entries(flat)) {
|
|
689
|
+
if (written === undefined)
|
|
690
|
+
continue;
|
|
691
|
+
const col = this.column(key);
|
|
692
|
+
(0, filters_js_1.cursorFieldValue)(key, written, {
|
|
693
|
+
column: col.name,
|
|
694
|
+
table: this.table,
|
|
695
|
+
kind: (0, filters_js_1.operandColumnKind)(this.meta.pgTypes[col.name]),
|
|
696
|
+
nullable: col.nullable && !this.meta.primaryKey.includes(col.name),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* The SQL engines' cursor placement rule (query/builder.ts `planCursor`),
|
|
703
|
+
* so a cursor they refuse is the same E003 here and only a cursor they would
|
|
704
|
+
* serve reaches PowDB's E017. The page order is `orderBy` plus the
|
|
705
|
+
* tiebreaker; the cursor must carry a value for every column of it, or name
|
|
706
|
+
* one row by a unique key (the shared findUnique rule).
|
|
707
|
+
*/
|
|
708
|
+
assertCursorPlaceable(cursor, orderBy) {
|
|
709
|
+
const meta = this.meta;
|
|
710
|
+
const field = (column) => meta.reverseColumnMap[column] ?? column;
|
|
711
|
+
const flat = (0, compound_unique_js_1.expandCompoundUniqueWhere)(meta, cursor);
|
|
712
|
+
const valued = new Set();
|
|
713
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
714
|
+
if (value !== undefined)
|
|
715
|
+
valued.add(this.column(key).name);
|
|
716
|
+
}
|
|
717
|
+
const terms = [];
|
|
718
|
+
for (const [key, dir] of (0, filters_js_1.orderByEntries)(orderBy).filter(([, d]) => d !== undefined)) {
|
|
719
|
+
if ((0, filters_js_1.isVectorOrderBy)(dir) || (0, filters_js_1.isJsonPathOrderBy)(dir) || (typeof dir !== 'string' && !(0, filters_js_1.isOrderBySpec)(dir))) {
|
|
720
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: \`cursor\` cannot page past the orderBy entry "${key}", which ` +
|
|
721
|
+
'orders by a relation, a JSON path or a vector distance rather than a column. Order the cursor ' +
|
|
722
|
+
'query by columns only, or page it with `offset`.');
|
|
723
|
+
}
|
|
724
|
+
const column = this.column(key).name;
|
|
725
|
+
if (terms.some((t) => t.column === column))
|
|
726
|
+
continue;
|
|
727
|
+
terms.push({ column, desc: (0, filters_js_1.normalizeOrderBy)(dir).dir === 'DESC' });
|
|
728
|
+
}
|
|
729
|
+
const nullable = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
730
|
+
const keys = (0, compound_unique_js_1.uniqueKeyNames)(meta).filter((cols) => cols.every((c) => !nullable(c)));
|
|
731
|
+
const ordered = new Set(terms.map((t) => t.column));
|
|
732
|
+
const tiebreak = keys.some((cols) => cols.every((c) => ordered.has(c)))
|
|
733
|
+
? []
|
|
734
|
+
: keys[0]?.filter((c) => !ordered.has(c));
|
|
735
|
+
if (!tiebreak) {
|
|
736
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: a \`cursor\` page needs an order with no ties, and "${this.table}" ` +
|
|
737
|
+
'has no primary key or unique key on NOT NULL columns to break them. Add a primary key, or page ' +
|
|
738
|
+
'this table with `offset`.');
|
|
739
|
+
}
|
|
740
|
+
const desc = terms.length > 0 ? terms[terms.length - 1].desc : false;
|
|
741
|
+
for (const column of tiebreak)
|
|
742
|
+
terms.push({ column, desc });
|
|
743
|
+
if (terms.every((t) => valued.has(t.column)) || (0, compound_unique_js_1.whereIdentifiesOneRow)(meta, flat))
|
|
744
|
+
return;
|
|
745
|
+
const missing = terms.filter((t) => !valued.has(t.column)).map((t) => `"${field(t.column)}"`);
|
|
746
|
+
const order = terms.map((t) => `${field(t.column)} ${t.desc ? 'desc' : 'asc'}`).join(', ');
|
|
747
|
+
const named = (0, compound_unique_js_1.uniqueKeyNames)(meta).map((cols) => cols.length === 1 ? `{ ${field(cols[0])} }` : `{ ${cols.map(field).join(', ')} }`);
|
|
748
|
+
throw new errors_js_1.ValidationError(`findMany on "${this.table}" refused: the \`cursor\` has no value for ${missing.join(', ')}, which the ` +
|
|
749
|
+
`page order (${order}) needs, and it does not name one row by a unique key. Give the cursor a value ` +
|
|
750
|
+
`for every ordered field, or name the cursor row by ${named.length > 0 ? named.join(' or ') : 'a unique key'}.`);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* A paginated `findMany` (`limit` / `offset`) whose `orderBy` can tie gets
|
|
754
|
+
* a tiebreaker appended in the last term's direction (the primary key, else
|
|
755
|
+
* the first unique key on NOT NULL columns), as on the SQL engines, so a page
|
|
756
|
+
* boundary cannot fall between tied rows in a different order on each page.
|
|
757
|
+
* Returns `args` itself when the order is already total or nothing pages.
|
|
758
|
+
*/
|
|
759
|
+
completePageOrder(args) {
|
|
760
|
+
if (args.orderBy === undefined || (args.limit === undefined && args.offset === undefined))
|
|
761
|
+
return args;
|
|
762
|
+
if (args.distinct && args.distinct.length > 0)
|
|
763
|
+
return args;
|
|
764
|
+
const meta = this.meta;
|
|
765
|
+
const entries = (0, filters_js_1.orderByEntries)(args.orderBy).filter(([, d]) => d !== undefined);
|
|
766
|
+
const ordered = new Set();
|
|
767
|
+
let lastDesc = false;
|
|
768
|
+
for (const [key, dir] of entries) {
|
|
769
|
+
lastDesc = false;
|
|
770
|
+
// A JSON-path, vector or relation ordering is not a column of this table.
|
|
771
|
+
if (typeof dir !== 'string' && !(0, filters_js_1.isOrderBySpec)(dir))
|
|
772
|
+
continue;
|
|
773
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
774
|
+
if (column === undefined)
|
|
775
|
+
continue;
|
|
776
|
+
ordered.add(column);
|
|
777
|
+
lastDesc = (0, filters_js_1.normalizeOrderBy)(dir).dir === 'DESC';
|
|
778
|
+
}
|
|
779
|
+
const nullable = (column) => !meta.primaryKey.includes(column) && meta.columns.find((c) => c.name === column)?.nullable === true;
|
|
780
|
+
const keys = (0, compound_unique_js_1.uniqueKeyNames)(meta).filter((cols) => cols.every((c) => !nullable(c)));
|
|
781
|
+
if (keys.length === 0 || keys.some((cols) => cols.every((c) => ordered.has(c))))
|
|
782
|
+
return args;
|
|
783
|
+
const missing = keys[0].filter((c) => !ordered.has(c));
|
|
784
|
+
const dir = lastDesc ? 'desc' : 'asc';
|
|
785
|
+
return {
|
|
786
|
+
...args,
|
|
787
|
+
orderBy: [
|
|
788
|
+
...entries.map(([k, v]) => ({ [k]: v })),
|
|
789
|
+
...missing.map((c) => ({ [meta.reverseColumnMap[c] ?? c]: dir })),
|
|
790
|
+
],
|
|
791
|
+
};
|
|
499
792
|
}
|
|
500
793
|
/**
|
|
501
794
|
* `jsonEncoding` selects between PostgreSQL's `json_build_object` and
|
|
@@ -509,12 +802,34 @@ class PowqlInterface {
|
|
|
509
802
|
return;
|
|
510
803
|
throw new errors_js_1.UnsupportedFeatureError('jsonEncoding', 'PowDB', 'PowQL emits no JSON row encoding for relations, so there is nothing to select; omit the option');
|
|
511
804
|
}
|
|
805
|
+
/** `optimisticLock` is an `update` option; the shared refusal and message from query/writes.ts. */
|
|
806
|
+
assertNoOptimisticLock(args, operation) {
|
|
807
|
+
// The shared check reads only the table name from its builder context.
|
|
808
|
+
(0, writes_js_1.assertNoOptimisticLock)({ table: this.table }, args, operation);
|
|
809
|
+
}
|
|
512
810
|
assertNoForceCustomPlan(args) {
|
|
513
811
|
if (args?.forceCustomPlan !== true)
|
|
514
812
|
return;
|
|
515
813
|
throw new errors_js_1.UnsupportedFeatureError('The forceCustomPlan query option', 'powdb', 'Forcing a per-query custom plan means keeping the statement out of the PostgreSQL plan cache, and PowDB ' +
|
|
516
814
|
'has no such cache to keep it out of. Remove the option, or set it only on PostgreSQL queries.');
|
|
517
815
|
}
|
|
816
|
+
/** `lock` (a row-locking read): PowDB has no row locks. */
|
|
817
|
+
assertNoRowLock(args) {
|
|
818
|
+
if (args?.lock === undefined)
|
|
819
|
+
return;
|
|
820
|
+
throw new errors_js_1.UnsupportedFeatureError('The lock read option', 'powdb', 'PowDB has no row locks: a transaction holds the single writer gate. Remove `lock`; inside ' +
|
|
821
|
+
'$transaction on PowDB, writes are already serialized.');
|
|
822
|
+
}
|
|
823
|
+
/** `_percentile`: PowQL has no percentile aggregate. */
|
|
824
|
+
assertNoPercentile(args) {
|
|
825
|
+
const spec = args._percentile;
|
|
826
|
+
// An empty block asks for nothing, as on the SQL engines.
|
|
827
|
+
if (spec === undefined)
|
|
828
|
+
return;
|
|
829
|
+
if (typeof spec === 'object' && spec !== null && !Object.values(spec).some((v) => v != null))
|
|
830
|
+
return;
|
|
831
|
+
throw new errors_js_1.UnsupportedFeatureError('_percentile', 'powdb', 'Percentile aggregates need PostgreSQL; PowQL has count, sum, avg, min and max only.');
|
|
832
|
+
}
|
|
518
833
|
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
519
834
|
assertIdentifiesOneRow(where) {
|
|
520
835
|
(0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.meta, this.table, where);
|
|
@@ -570,6 +885,11 @@ class PowqlInterface {
|
|
|
570
885
|
* an already-RESOLVED where (relation filters pre-resolved to literal in-lists
|
|
571
886
|
* by {@link resolveRelationFilters}): the relation-key branch below still
|
|
572
887
|
* throws, so an unresolved relation filter can never leak into a join.
|
|
888
|
+
*
|
|
889
|
+
* `NOT` here is PowQL's two-valued complement. A caller's where reaches this
|
|
890
|
+
* only through {@link sqlTrueWhere} (inside {@link resolveRelationFilters},
|
|
891
|
+
* and in {@link applyGlobalFilter}), which leaves `NOT` only where the two
|
|
892
|
+
* readings agree.
|
|
573
893
|
*/
|
|
574
894
|
buildWhere(where, params, alias) {
|
|
575
895
|
if (!where)
|
|
@@ -578,26 +898,36 @@ class PowqlInterface {
|
|
|
578
898
|
for (const [key, value] of Object.entries(where)) {
|
|
579
899
|
if (value === undefined)
|
|
580
900
|
continue;
|
|
581
|
-
if (key === 'AND') {
|
|
582
|
-
const
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
901
|
+
if (key === 'AND' || key === 'OR' || key === 'NOT') {
|
|
902
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, this.table);
|
|
903
|
+
if (branches === null)
|
|
904
|
+
continue;
|
|
905
|
+
if (key === 'OR' && branches.length === 0) {
|
|
906
|
+
parts.push(this.alwaysFalse());
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const built = branches.map((w) => this.buildWhere(w, params, alias));
|
|
910
|
+
// A branch with no condition is TRUE, so the whole `OR` is: a constant
|
|
911
|
+
// condition rather than none, so a `NOT` around it matches no row.
|
|
912
|
+
if (key === 'OR' && built.some((c) => !c)) {
|
|
913
|
+
parts.push(ALWAYS_TRUE);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
const sub = built.filter(Boolean);
|
|
917
|
+
if (!sub.length)
|
|
918
|
+
continue;
|
|
919
|
+
if (key === 'OR')
|
|
589
920
|
parts.push(`(${sub.join(' or ')})`);
|
|
921
|
+
else if (key === 'AND')
|
|
922
|
+
parts.push(`(${sub.join(' and ')})`);
|
|
923
|
+
else
|
|
924
|
+
parts.push(...sub.map((c) => `not (${c})`));
|
|
590
925
|
}
|
|
591
|
-
else if (key
|
|
592
|
-
const sub = this.buildWhere(value, params, alias);
|
|
593
|
-
if (sub)
|
|
594
|
-
parts.push(`not (${sub})`);
|
|
595
|
-
}
|
|
596
|
-
else if (this.meta.relations[key]) {
|
|
926
|
+
else if (Object.hasOwn(this.meta.relations, key)) {
|
|
597
927
|
// Relation filters are pre-resolved to scalar in/notIn by
|
|
598
928
|
// resolveRelationFilters() before buildWhere runs, reaching here means
|
|
599
929
|
// a caller skipped that step (an internal bug, not user error).
|
|
600
|
-
throw new errors_js_1.
|
|
930
|
+
throw new errors_js_1.InvariantError('powql-relation-filter-unresolved', `relation filter "${key}" reached buildWhere unresolved: resolveRelationFilters() must run first.`);
|
|
601
931
|
}
|
|
602
932
|
else {
|
|
603
933
|
// A JsonFilter (or a bare `{ path }`) can compile to zero clauses; skip
|
|
@@ -646,37 +976,158 @@ class PowqlInterface {
|
|
|
646
976
|
'against a bound timestamp needs the fix. A nested `with` (the default nested-projection / join paths) ' +
|
|
647
977
|
'correlates column to column, so it loads the relation without any such comparison.');
|
|
648
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Refuse a value comparison on a `numeric` / `decimal` column. PowDB stores
|
|
981
|
+
* those as `str` (it has no decimal type, and `float` would lose digits), so
|
|
982
|
+
* every comparison, ordering and aggregate the engine could run on them is a
|
|
983
|
+
* TEXT comparison: `'7.50' > '7.5'`, `'10' < '9'`, and `'7.5' = '7.50'` is
|
|
984
|
+
* false; arithmetic on a `str` yields NULL. Null checks, reads and plain
|
|
985
|
+
* writes are unaffected.
|
|
986
|
+
*/
|
|
987
|
+
assertDecimalComparable(col, what) {
|
|
988
|
+
if (!(0, powdb_shared_js_1.isPowdbDecimalTextColumn)(col, this.schema.enums))
|
|
989
|
+
return;
|
|
990
|
+
throw new errors_js_1.UnsupportedFeatureError(`${what} on the numeric column "${col.name}" of "${this.table}"`, 'PowDB', 'PowDB stores numeric/decimal values as text, so the engine would compare, order and aggregate them as ' +
|
|
991
|
+
"strings ('10' < '9', '7.5' != '7.50') and arithmetic on them yields NULL. Reading, writing and `null` " +
|
|
992
|
+
'checks work. To compare, aggregate or do arithmetic, store the value as an integer of minor units ' +
|
|
993
|
+
'(cents) or as a float, or do it in application code.');
|
|
994
|
+
}
|
|
995
|
+
/** The labels of `col`'s enum type, or `undefined` when it is not an enum column. */
|
|
996
|
+
enumLabels(col) {
|
|
997
|
+
return (0, powdb_shared_js_1.powdbEnumLabels)(col, this.schema.enums);
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* E003 unless `value` is one of the enum column's labels. PostgreSQL refuses
|
|
1001
|
+
* any other value (22P02, which Turbine reports as E003) whether it is
|
|
1002
|
+
* written or compared, and the label set is exact: case and spelling count.
|
|
1003
|
+
* The value itself is not echoed, only its kind.
|
|
1004
|
+
*/
|
|
1005
|
+
assertEnumLabel(col, labels, value) {
|
|
1006
|
+
if (typeof value === 'string' && labels.includes(value))
|
|
1007
|
+
return;
|
|
1008
|
+
const shown = labels.slice(0, 10).map((l) => JSON.stringify(l));
|
|
1009
|
+
if (labels.length > shown.length)
|
|
1010
|
+
shown.push(`and ${labels.length - shown.length} more`);
|
|
1011
|
+
throw new errors_js_1.ValidationError(`Invalid value for the enum column "${col.name}" on "${this.table}": expected one of its labels ` +
|
|
1012
|
+
`(${shown.join(', ')}), got ${typeof value === 'string' ? 'a string that is not one of them' : describeOperand(value)}.`);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Refuse an ORDERING of an enum column (orderBy, `_min` / `_max`). PostgreSQL
|
|
1016
|
+
* orders an enum by the position of its labels in the type's declaration;
|
|
1017
|
+
* PowDB stores the label text and would order it alphabetically, a different
|
|
1018
|
+
* answer with no error. Range filters (`gt` / `gte` / `lt` / `lte`) do not come
|
|
1019
|
+
* here: they compile to the exact set of labels on that side of the operand.
|
|
1020
|
+
*/
|
|
1021
|
+
assertEnumOrderable(col, what) {
|
|
1022
|
+
if (!this.enumLabels(col))
|
|
1023
|
+
return;
|
|
1024
|
+
throw new errors_js_1.UnsupportedFeatureError(`${what} on the enum column "${col.name}" of "${this.table}"`, 'PowDB', 'PostgreSQL orders an enum by the order its labels are declared in, and PowDB stores the label text, so ' +
|
|
1025
|
+
'it would order the labels alphabetically. Equality, `in` / `notIn`, `gt` / `gte` / `lt` / `lte` and ' +
|
|
1026
|
+
'groupBy keys work. Order by the label position in application code.');
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* An aggregate over an enum column: `_min` / `_max` order it (see
|
|
1030
|
+
* {@link assertEnumOrderable}); `_sum` / `_avg` have no meaning on a label,
|
|
1031
|
+
* which PostgreSQL refuses as E003 (42883, no such function).
|
|
1032
|
+
*/
|
|
1033
|
+
assertEnumAggregate(col, fn) {
|
|
1034
|
+
if (!this.enumLabels(col))
|
|
1035
|
+
return;
|
|
1036
|
+
if (fn === '_min' || fn === '_max') {
|
|
1037
|
+
this.assertEnumOrderable(col, `\`${fn}\``);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
throw new errors_js_1.ValidationError(`\`${fn}\` on the enum column "${col.name}" of "${this.table}": an enum label is not a number. ` +
|
|
1041
|
+
'Use `_count`, or `_min` / `_max` on another engine.');
|
|
1042
|
+
}
|
|
1043
|
+
/** The E003 for an operand that is not a comparable value for its column. */
|
|
1044
|
+
operandError(field, opName, value, expected) {
|
|
1045
|
+
const position = opName === '=' ? `"${field}"` : `\`${opName}\` on "${field}"`;
|
|
1046
|
+
return new errors_js_1.ValidationError(`Invalid operand for ${position} on "${this.table}": expected ${expected}, got ${describeOperand(value)}. ` +
|
|
1047
|
+
'A column is compared against a string, number, bigint, boolean or Date; pass an operator object ' +
|
|
1048
|
+
'(`{ gt: 5 }`, `{ in: [1, 2] }`) to choose the comparison.');
|
|
1049
|
+
}
|
|
649
1050
|
/** Build a single `field: value | operator` condition. */
|
|
650
1051
|
buildFieldCondition(field, value, params, alias) {
|
|
651
1052
|
const colMeta = this.column(field);
|
|
652
1053
|
const ref = this.ref(field, alias);
|
|
653
1054
|
if (value === null)
|
|
654
1055
|
return `${ref} is null`;
|
|
655
|
-
if (value
|
|
1056
|
+
if (isColumnReference(value)) {
|
|
1057
|
+
throw new errors_js_1.UnsupportedFeatureError('Column references in a filter', 'PowDB', `field "${field}": PowQL has no column-to-column operand in a filter. Compare in application code.`);
|
|
1058
|
+
}
|
|
1059
|
+
if (isScalarOperand(value)) {
|
|
1060
|
+
this.assertDecimalComparable(colMeta, 'Equality');
|
|
656
1061
|
this.assertDatetimePredicateSupported(colMeta);
|
|
657
|
-
return `${ref} = ${this.
|
|
1062
|
+
return `${ref} = ${this.filterOperand(value, params, colMeta)}`;
|
|
658
1063
|
}
|
|
659
1064
|
const op = value;
|
|
660
1065
|
// JSON path / key filters on a json document column compile to PowQL `->`
|
|
661
1066
|
// path filters (≥ 0.12). `isJsonFilter` matches `path`/`equals`/`contains`/
|
|
662
1067
|
// `hasKey`; on a NON-json column those fall through to the scalar operator
|
|
663
1068
|
// path below (e.g. `equals` stays a plain equality), exactly like SQL.
|
|
664
|
-
if ((0, powdb_shared_js_1.isJsonColumn)(colMeta) && (0, filters_js_1.isJsonFilter)(value)) {
|
|
1069
|
+
if ((0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums) && (0, filters_js_1.isJsonFilter)(value)) {
|
|
665
1070
|
(0, powdb_shared_js_1.requireCapability)(this.capabilities, 'jsonDocs', 'JSON path filters');
|
|
666
1071
|
return this.buildJsonPathCondition(colMeta, value, params, alias);
|
|
667
1072
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1073
|
+
if (!Array.isArray(op))
|
|
1074
|
+
rejectUnsupportedFilter(op, field);
|
|
1075
|
+
if (Array.isArray(op) || !Object.keys(op).some((k) => OPERATOR_KEYS.has(k))) {
|
|
1076
|
+
// A json document column compares a whole document by value. Any other
|
|
1077
|
+
// column has no object or array value, so binding one as data compared
|
|
1078
|
+
// against nothing meaningful; it is refused, as on the SQL engines.
|
|
1079
|
+
if (!(0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums))
|
|
1080
|
+
throw this.operandError(field, '=', value, 'a scalar or an operator object');
|
|
1081
|
+
throw jsonDocumentEqualityUnsupported(colMeta);
|
|
673
1082
|
}
|
|
1083
|
+
// The SQL engines' operand rules and messages, so the engines agree on
|
|
1084
|
+
// which operator objects are valid; one naming no comparison
|
|
1085
|
+
// (`{ mode: 'insensitive' }`) is refused instead of compiling to nothing.
|
|
1086
|
+
const operand = {
|
|
1087
|
+
column: colMeta.name,
|
|
1088
|
+
table: this.table,
|
|
1089
|
+
kind: (0, filters_js_1.operandColumnKind)(this.meta.pgTypes[colMeta.name]),
|
|
1090
|
+
};
|
|
1091
|
+
if (!(0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums))
|
|
1092
|
+
(0, filters_js_1.assertOperatorOperands)(op, operand);
|
|
1093
|
+
// On a JSON column a `not` object made only of filter operators is refused,
|
|
1094
|
+
// as on the SQL engines (one spelling, one meaning on every engine).
|
|
1095
|
+
else
|
|
1096
|
+
(0, filters_js_1.assertJsonNotOperand)(op.not, operand);
|
|
674
1097
|
const insensitive = op.mode === 'insensitive';
|
|
675
1098
|
const lhs = insensitive ? `lower(${ref})` : ref;
|
|
1099
|
+
const labels = this.enumLabels(colMeta);
|
|
676
1100
|
const conds = [];
|
|
677
1101
|
for (const [opName, opVal] of Object.entries(op)) {
|
|
678
1102
|
if (opVal === undefined || opName === 'mode')
|
|
679
1103
|
continue;
|
|
1104
|
+
if (labels) {
|
|
1105
|
+
const range = this.enumRangeCondition(colMeta, labels, ref, opName, opVal, insensitive, params);
|
|
1106
|
+
if (range !== undefined) {
|
|
1107
|
+
conds.push(range);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (opName === 'not' && opVal !== null && !isScalarOperand(opVal) && !isColumnReference(opVal)) {
|
|
1112
|
+
// `not` takes an operator object, as on the SQL engines: the rows where
|
|
1113
|
+
// the inner condition is FALSE (a NULL column is neither, so it is
|
|
1114
|
+
// excluded, which PowQL's two-valued `not` alone would not do).
|
|
1115
|
+
if (Array.isArray(opVal) || Object.keys(opVal).length === 0) {
|
|
1116
|
+
throw this.operandError(field, 'not', opVal, 'a scalar, null, or a non-empty operator object');
|
|
1117
|
+
}
|
|
1118
|
+
const inner = { ...(op.mode !== undefined ? { mode: op.mode } : {}), ...opVal };
|
|
1119
|
+
const negated = this.buildWhere(this.sqlFalseWhere({ [field]: inner }), params, alias);
|
|
1120
|
+
conds.push(negated || this.alwaysFalse());
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (opName === 'in' || opName === 'notIn') {
|
|
1124
|
+
if (!Array.isArray(opVal) || opVal.some((v) => v !== null && !isScalarOperand(v))) {
|
|
1125
|
+
throw this.operandError(field, opName, opVal, 'an array of scalars');
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
else if (opVal !== null && !isScalarOperand(opVal)) {
|
|
1129
|
+
throw this.operandError(field, opName, opVal, 'a scalar');
|
|
1130
|
+
}
|
|
680
1131
|
// Every operator below compares the column against a bound literal, which
|
|
681
1132
|
// is the shape a PowDB `datetime` column can only answer from engine 0.20
|
|
682
1133
|
// (see assertDatetimePredicateSupported; `in`/`notIn` reach the engine as
|
|
@@ -686,30 +1137,32 @@ class PowqlInterface {
|
|
|
686
1137
|
// (a compile-time constant, never sent).
|
|
687
1138
|
const comparesNothing = (opVal === null && (opName === 'equals' || opName === 'not')) ||
|
|
688
1139
|
((opName === 'in' || opName === 'notIn') && Array.isArray(opVal) && opVal.length === 0);
|
|
689
|
-
if (!comparesNothing)
|
|
1140
|
+
if (!comparesNothing) {
|
|
1141
|
+
this.assertDecimalComparable(colMeta, `\`${opName}\``);
|
|
690
1142
|
this.assertDatetimePredicateSupported(colMeta);
|
|
1143
|
+
}
|
|
691
1144
|
switch (opName) {
|
|
692
1145
|
case 'equals':
|
|
693
|
-
conds.push(opVal === null ? `${ref} is null` : `${lhs} = ${this.bind(opVal, params, insensitive)}`);
|
|
1146
|
+
conds.push(opVal === null ? `${ref} is null` : `${lhs} = ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
694
1147
|
break;
|
|
695
1148
|
case 'not':
|
|
696
1149
|
// Spell as `lhs != $N` (not `not (lhs = $N)`) so a missing-value row is
|
|
697
1150
|
// EXCLUDED, matching SQL null semantics on PowDB >= 0.18.2. Behavior-neutral
|
|
698
1151
|
// on older engines (pre-0.18.2 `!=` MATCHED missing rows exactly like the
|
|
699
1152
|
// old `not (...)` spelling), so no capability gate is needed.
|
|
700
|
-
conds.push(opVal === null ? `${ref} is not null` : `${lhs} != ${this.bind(opVal, params, insensitive)}`);
|
|
1153
|
+
conds.push(opVal === null ? `${ref} is not null` : `${lhs} != ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
701
1154
|
break;
|
|
702
1155
|
case 'gt':
|
|
703
|
-
conds.push(`${lhs} > ${this.bind(opVal, params, insensitive)}`);
|
|
1156
|
+
conds.push(`${lhs} > ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
704
1157
|
break;
|
|
705
1158
|
case 'gte':
|
|
706
|
-
conds.push(`${lhs} >= ${this.bind(opVal, params, insensitive)}`);
|
|
1159
|
+
conds.push(`${lhs} >= ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
707
1160
|
break;
|
|
708
1161
|
case 'lt':
|
|
709
|
-
conds.push(`${lhs} < ${this.bind(opVal, params, insensitive)}`);
|
|
1162
|
+
conds.push(`${lhs} < ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
710
1163
|
break;
|
|
711
1164
|
case 'lte':
|
|
712
|
-
conds.push(`${lhs} <= ${this.bind(opVal, params, insensitive)}`);
|
|
1165
|
+
conds.push(`${lhs} <= ${this.bind(opVal, params, insensitive, colMeta)}`);
|
|
713
1166
|
break;
|
|
714
1167
|
case 'in':
|
|
715
1168
|
conds.push(this.buildInList(lhs, opVal, params, insensitive, false, colMeta));
|
|
@@ -718,13 +1171,9 @@ class PowqlInterface {
|
|
|
718
1171
|
conds.push(this.buildInList(lhs, opVal, params, insensitive, true, colMeta));
|
|
719
1172
|
break;
|
|
720
1173
|
case 'contains':
|
|
721
|
-
conds.push(`${lhs} like ${this.bindLike(`%${(0, utils_js_1.escapeLike)(String(opVal))}%`, params, insensitive)}`);
|
|
722
|
-
break;
|
|
723
1174
|
case 'startsWith':
|
|
724
|
-
conds.push(`${lhs} like ${this.bindLike(`${(0, utils_js_1.escapeLike)(String(opVal))}%`, params, insensitive)}`);
|
|
725
|
-
break;
|
|
726
1175
|
case 'endsWith':
|
|
727
|
-
conds.push(
|
|
1176
|
+
conds.push(this.buildStringMatch(opName, ref, String(opVal), params, insensitive, `"${field}"`));
|
|
728
1177
|
break;
|
|
729
1178
|
default:
|
|
730
1179
|
throw new errors_js_1.ValidationError(`Unsupported operator "${opName}" on PowDB.`);
|
|
@@ -793,9 +1242,8 @@ class PowqlInterface {
|
|
|
793
1242
|
throw new errors_js_1.UnsupportedFeatureError('JSON containment filters (contains)', 'PowDB', `column "${col.name}": PowQL has no JSON containment operator`);
|
|
794
1243
|
}
|
|
795
1244
|
if (filter.equals !== undefined) {
|
|
796
|
-
if (filter.path === undefined || filter.path.length === 0)
|
|
797
|
-
throw
|
|
798
|
-
}
|
|
1245
|
+
if (filter.path === undefined || filter.path.length === 0)
|
|
1246
|
+
throw jsonDocumentEqualityUnsupported(col);
|
|
799
1247
|
conds.push(filter.equals === null ? `${pathP()} is null` : `${pathP()} = ${this.param(filter.equals, params)}`);
|
|
800
1248
|
}
|
|
801
1249
|
if (filter.hasKey !== undefined) {
|
|
@@ -827,30 +1275,70 @@ class PowqlInterface {
|
|
|
827
1275
|
conds.push(`${pathP()} ${powOp} ${this.param(v, params)}`);
|
|
828
1276
|
}
|
|
829
1277
|
// Substring comparisons against the text at `path`, in the same fixed
|
|
830
|
-
// order as the SQL path
|
|
831
|
-
|
|
1278
|
+
// order as the SQL path, through the same literal matcher as a column.
|
|
1279
|
+
const stringOps = {
|
|
1280
|
+
stringContains: 'contains',
|
|
1281
|
+
stringStartsWith: 'startsWith',
|
|
1282
|
+
stringEndsWith: 'endsWith',
|
|
1283
|
+
};
|
|
1284
|
+
for (const { op, value } of (0, where_js_1.jsonStringEntries)(filter, col.name)) {
|
|
832
1285
|
const insensitive = filter.mode === 'insensitive';
|
|
833
|
-
const
|
|
834
|
-
|
|
1286
|
+
const kind = stringOps[op];
|
|
1287
|
+
if (!kind)
|
|
1288
|
+
throw new errors_js_1.InvariantError('powql-json-operator', `unknown JSON string operator "${op}" on "${col.name}".`);
|
|
1289
|
+
conds.push(this.buildStringMatch(kind, pathP(), value, params, insensitive, `the JSON path on "${col.name}"`));
|
|
835
1290
|
}
|
|
836
1291
|
// Every reachable shape now emits a condition or throws: assertJsonFilterKeys
|
|
837
1292
|
// rejects a filter that compares nothing, so an empty list here would be a
|
|
838
1293
|
// generator bug rather than a user error, and must not silently widen the
|
|
839
1294
|
// query the way it used to.
|
|
840
1295
|
if (!conds.length) {
|
|
841
|
-
throw new errors_js_1.
|
|
1296
|
+
throw new errors_js_1.InvariantError('powql-json-filter-empty', `JSON filter on "${col.name}" compiled to no condition.`);
|
|
842
1297
|
}
|
|
843
1298
|
return conds.length > 1 ? `(${conds.join(' and ')})` : conds[0];
|
|
844
1299
|
}
|
|
845
1300
|
/** Bind a value, lowercasing for case-insensitive comparisons. */
|
|
846
|
-
bind(value, params, insensitive) {
|
|
847
|
-
const ph = this.
|
|
1301
|
+
bind(value, params, insensitive, col) {
|
|
1302
|
+
const ph = this.filterOperand(value, params, col);
|
|
848
1303
|
return insensitive ? `lower(${ph})` : ph;
|
|
849
1304
|
}
|
|
850
|
-
/**
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1305
|
+
/**
|
|
1306
|
+
* A bound filter operand. An integral number compared with a float column is
|
|
1307
|
+
* cast to float in the statement: the typed wire carries an integral number as
|
|
1308
|
+
* an int, and the engine compares int with float by type tag inside `not`,
|
|
1309
|
+
* `or` and `in` (on 0.20 `not (.rating = 4)` matches the 4.0 row and
|
|
1310
|
+
* `.rating in (4, 5)` matches nothing).
|
|
1311
|
+
*/
|
|
1312
|
+
filterOperand(value, params, col) {
|
|
1313
|
+
const ph = this.param(value, params, col);
|
|
1314
|
+
return col && typeof value === 'number' && Number.isInteger(value) && this.isFloatCol(col) ? `(${ph} + 0.0)` : ph;
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* `contains` / `startsWith` / `endsWith` against `expr` (a column or a JSON
|
|
1318
|
+
* path), matching `value` literally.
|
|
1319
|
+
*
|
|
1320
|
+
* PowQL's `like` has NO escape character: `%` and `_` in the pattern are
|
|
1321
|
+
* always wildcards and `\` is an ordinary character. So a prefix or suffix
|
|
1322
|
+
* test never goes through `like`: it compares the leading or trailing
|
|
1323
|
+
* `length($v)` characters with `substring`, which is exact for every value
|
|
1324
|
+
* on every engine version (and sidesteps the pre-0.27 matcher, which missed
|
|
1325
|
+
* text containing a literal `%`). `contains` has no such form, since PowQL
|
|
1326
|
+
* has no position function, so a value carrying `%` or `_` is refused rather
|
|
1327
|
+
* than sent as a pattern that means something else.
|
|
1328
|
+
*/
|
|
1329
|
+
buildStringMatch(op, expr, value, params, insensitive, target) {
|
|
1330
|
+
const fold = (e) => (insensitive ? `lower(${e})` : e);
|
|
1331
|
+
if (op === 'contains') {
|
|
1332
|
+
if (/[%_]/.test(value)) {
|
|
1333
|
+
throw new errors_js_1.UnsupportedFeatureError('`contains` with a value containing `%` or `_`', 'PowDB', `${target} on "${this.table}": PowQL's \`like\` has no escape character, so those characters would act ` +
|
|
1334
|
+
'as wildcards and match other rows. `startsWith` and `endsWith` match such values exactly; for a ' +
|
|
1335
|
+
'substring, filter in application code.');
|
|
1336
|
+
}
|
|
1337
|
+
return `${fold(expr)} like ${fold(this.param(`%${value}%`, params))}`;
|
|
1338
|
+
}
|
|
1339
|
+
const ph = this.param(value, params);
|
|
1340
|
+
const start = op === 'startsWith' ? '1' : `length(${expr}) - length(${ph}) + 1`;
|
|
1341
|
+
return `(length(${expr}) >= length(${ph}) and ${fold(`substring(${expr}, ${start}, length(${ph}))`)} = ${fold(ph)})`;
|
|
854
1342
|
}
|
|
855
1343
|
/**
|
|
856
1344
|
* `lhs [not] in ($1, $2, …)`, empty list collapses to a constant.
|
|
@@ -858,15 +1346,41 @@ class PowqlInterface {
|
|
|
858
1346
|
* A PowDB-native `datetime` column takes the expanded form instead (see
|
|
859
1347
|
* {@link buildDatetimeInList}): its `in` list is still broken upstream at 0.20.
|
|
860
1348
|
*/
|
|
1349
|
+
/**
|
|
1350
|
+
* The enum-specific shapes of one operator on an enum column, or `undefined`
|
|
1351
|
+
* for an operator compiled the ordinary way (equality, `in` / `notIn`, `not`,
|
|
1352
|
+
* whose labels {@link param} checks).
|
|
1353
|
+
*
|
|
1354
|
+
* `gt` / `gte` / `lt` / `lte` compile to `in` over the labels on that side of
|
|
1355
|
+
* the operand in DECLARATION order, which is what PostgreSQL compares; the
|
|
1356
|
+
* text comparison PowDB would run is alphabetical, a different answer. A null
|
|
1357
|
+
* operand matches no row, as `col > NULL` does. Case-insensitive matching and
|
|
1358
|
+
* `contains` / `startsWith` / `endsWith` are E003: PostgreSQL has no `lower`
|
|
1359
|
+
* or `LIKE` on an enum (42883).
|
|
1360
|
+
*/
|
|
1361
|
+
enumRangeCondition(col, labels, ref, opName, opVal, insensitive, params) {
|
|
1362
|
+
if (insensitive || opName === 'contains' || opName === 'startsWith' || opName === 'endsWith') {
|
|
1363
|
+
throw new errors_js_1.ValidationError(`${insensitive ? "`mode: 'insensitive'`" : `\`${opName}\``} on the enum column "${col.name}" of ` +
|
|
1364
|
+
`"${this.table}": an enum compares whole labels exactly. Use \`equals\`, \`in\` or \`notIn\` with its labels.`);
|
|
1365
|
+
}
|
|
1366
|
+
if (opName !== 'gt' && opName !== 'gte' && opName !== 'lt' && opName !== 'lte')
|
|
1367
|
+
return undefined;
|
|
1368
|
+
if (opVal === null)
|
|
1369
|
+
return this.alwaysFalse();
|
|
1370
|
+
this.assertEnumLabel(col, labels, opVal);
|
|
1371
|
+
const at = labels.indexOf(opVal);
|
|
1372
|
+
const side = labels.filter((_, i) => opName === 'gt' ? i > at : opName === 'gte' ? i >= at : opName === 'lt' ? i < at : i <= at);
|
|
1373
|
+
return this.buildInList(ref, side, params, false, false, col);
|
|
1374
|
+
}
|
|
861
1375
|
buildInList(lhs, values, params, insensitive, negate, col) {
|
|
862
1376
|
if (!Array.isArray(values) || values.length === 0) {
|
|
863
1377
|
// `in []` matches nothing; `not in []` matches everything (SQL parity requires a
|
|
864
1378
|
// missing-value row to match `notIn []`, so NO presence guard is appended here).
|
|
865
|
-
return negate ?
|
|
1379
|
+
return negate ? ALWAYS_TRUE : this.alwaysFalse();
|
|
866
1380
|
}
|
|
867
1381
|
if (col && (0, powdb_shared_js_1.isPowdbDatetimeColumn)(col))
|
|
868
1382
|
return this.buildDatetimeInList(lhs, values, params, negate, col);
|
|
869
|
-
const items = values.map((v) => this.bind(v, params, insensitive)).join(', ');
|
|
1383
|
+
const items = values.map((v) => this.bind(v, params, insensitive, col)).join(', ');
|
|
870
1384
|
if (negate) {
|
|
871
1385
|
// PowQL `not in` matches missing-value rows, so append `and lhs is not null`
|
|
872
1386
|
// for SQL null-semantics parity (non-empty list only, per the empty-list note above).
|
|
@@ -947,21 +1461,33 @@ class PowqlInterface {
|
|
|
947
1461
|
return col && (0, powdb_shared_js_1.isPowdbDatetimeColumn)(col) ? exports.MAX_POWQL_DATETIME_TERMS : MAX_RELATION_KEYS;
|
|
948
1462
|
}
|
|
949
1463
|
/**
|
|
950
|
-
* Pre-resolve every relation filter
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
*
|
|
954
|
-
* emits only `in (<literal list>)`.
|
|
1464
|
+
* Pre-resolve every relation filter in a where clause into a plain scalar
|
|
1465
|
+
* condition on the **local key**, by running the inner predicate as its own
|
|
1466
|
+
* query and materializing the matching keys as a literal list. The compiled
|
|
1467
|
+
* where is then relation-free and `buildWhere` emits only `in (<literal list>)`.
|
|
955
1468
|
*
|
|
956
1469
|
* Why not an IN-subquery (`.k in (Target filter <e> { .fk })`)? PowDB's
|
|
957
1470
|
* executor caches a subquery's result by **plan shape, ignoring the literal**,
|
|
958
1471
|
* so a second subquery of the same shape with a different value returns the
|
|
959
1472
|
* first one's stale rows (reproduced live on the embedded engine; the
|
|
960
1473
|
* single-statement literal `in (list)` form is always correct). Resolving
|
|
961
|
-
* client-side trades extra round-trips for correctness, and recurses
|
|
1474
|
+
* client-side trades extra round-trips for correctness, and recurses: nested
|
|
962
1475
|
* relation filters in the inner predicate resolve when the target query runs.
|
|
1476
|
+
*
|
|
1477
|
+
* `skip` is the query's `skipGlobalFilters`, forwarded to every target query
|
|
1478
|
+
* so the target's global filter bounds the related rows exactly as the SQL
|
|
1479
|
+
* engines' EXISTS does, and an opt-out reaches the targets as it does there.
|
|
1480
|
+
*/
|
|
1481
|
+
async resolveRelationFilters(where, timeout, skip) {
|
|
1482
|
+
if (!where)
|
|
1483
|
+
return where;
|
|
1484
|
+
return this.resolveRelationFiltersIn(this.sqlTrueWhere(where), timeout, skip);
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* {@link resolveRelationFilters} over a where already in {@link sqlTrueWhere}'s
|
|
1488
|
+
* form (`AND` / `OR` arrays, `NOT` objects).
|
|
963
1489
|
*/
|
|
964
|
-
async
|
|
1490
|
+
async resolveRelationFiltersIn(where, timeout, skip) {
|
|
965
1491
|
if (!where)
|
|
966
1492
|
return where;
|
|
967
1493
|
const scalar = {};
|
|
@@ -970,76 +1496,195 @@ class PowqlInterface {
|
|
|
970
1496
|
if (value === undefined)
|
|
971
1497
|
continue;
|
|
972
1498
|
if (key === 'AND' || key === 'OR') {
|
|
973
|
-
scalar[key] = await Promise.all(value.map((w) => this.
|
|
974
|
-
|
|
975
|
-
else if (key === 'NOT') {
|
|
976
|
-
scalar[key] = await this.resolveRelationFilters(value, timeout);
|
|
1499
|
+
scalar[key] = await Promise.all(value.map((w) => this.resolveRelationFiltersIn(w, timeout, skip)));
|
|
1500
|
+
continue;
|
|
977
1501
|
}
|
|
978
|
-
|
|
979
|
-
|
|
1502
|
+
if (key === 'NOT') {
|
|
1503
|
+
scalar[key] = await this.resolveRelationFiltersIn(value, timeout, skip);
|
|
1504
|
+
continue;
|
|
980
1505
|
}
|
|
981
|
-
|
|
1506
|
+
const rel = (0, utils_js_1.resolveRelationDef)(this.meta.relations, key);
|
|
1507
|
+
if (!rel) {
|
|
982
1508
|
scalar[key] = value;
|
|
1509
|
+
continue;
|
|
983
1510
|
}
|
|
1511
|
+
const cond = await this.resolveRelationCondition(rel, key, value, timeout, skip);
|
|
1512
|
+
if (cond)
|
|
1513
|
+
relConds.push(cond);
|
|
984
1514
|
}
|
|
985
1515
|
if (!relConds.length)
|
|
986
1516
|
return scalar;
|
|
987
1517
|
const hasScalar = Object.keys(scalar).length > 0;
|
|
988
1518
|
return { AND: [...(hasScalar ? [scalar] : []), ...relConds] };
|
|
989
1519
|
}
|
|
990
|
-
/**
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1520
|
+
/**
|
|
1521
|
+
* Resolve one relation filter to a relation-free condition on the local key,
|
|
1522
|
+
* with the SQL engines' meaning for every branch:
|
|
1523
|
+
*
|
|
1524
|
+
* some / is EXISTS a related row matching the inner where
|
|
1525
|
+
* none / isNot NOT EXISTS such a row (true for a NULL local key)
|
|
1526
|
+
* every NOT EXISTS a related row for which the inner where is FALSE
|
|
1527
|
+
* is: null NOT EXISTS any related row
|
|
1528
|
+
* isNot: null EXISTS a related row
|
|
1529
|
+
*
|
|
1530
|
+
* A bare object on a to-one relation is an implicit `is` (the shared
|
|
1531
|
+
* {@link normalizeRelationFilter} rule). Branches combine with AND. A shape
|
|
1532
|
+
* that names no branch, or names something else, is refused: compiling it to
|
|
1533
|
+
* nothing would match every row, and a mutation carrying it would write them.
|
|
1534
|
+
* Returns `null` when every branch key is `undefined` (an absent filter).
|
|
1535
|
+
*/
|
|
1536
|
+
async resolveRelationCondition(rel, key, value, timeout, skip) {
|
|
1537
|
+
const shapeError = (detail) => new errors_js_1.ValidationError(`Invalid relation filter "${key}" on "${this.table}": ${detail}. ` +
|
|
1538
|
+
(rel.type === 'belongsTo' || rel.type === 'hasOne'
|
|
1539
|
+
? `A to-one relation takes \`{ is: {...} }\`, \`{ isNot: {...} }\`, a bare where object, or \`is: null\`.`
|
|
1540
|
+
: 'A to-many relation takes `{ some: {...} }`, `{ none: {...} }` or `{ every: {...} }`.'));
|
|
1541
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
1542
|
+
throw shapeError(`expected an object, got ${describeOperand(value)}`);
|
|
1543
|
+
}
|
|
1544
|
+
// The shared rule; its first parameter is unused, so it needs no SQL builder context.
|
|
1545
|
+
const filter = (0, where_js_1.normalizeRelationFilter)(undefined, rel, value);
|
|
1546
|
+
const keys = Object.keys(filter);
|
|
1547
|
+
if (keys.length === 0)
|
|
1548
|
+
throw shapeError('the filter object is empty');
|
|
1549
|
+
const unknown = keys.filter((k) => !RELATION_FILTER_KEYS.includes(k));
|
|
1550
|
+
if (unknown.length)
|
|
1551
|
+
throw shapeError(`unrecognized key ${unknown.map((k) => `"${k}"`).join(', ')}`);
|
|
1552
|
+
if (keys.every((k) => filter[k] === undefined))
|
|
1553
|
+
return null;
|
|
1554
|
+
// The shared value rule: `some` / `none` / `every` take a where object,
|
|
1555
|
+
// `is` / `isNot` a where object or null. Checked for every branch before
|
|
1556
|
+
// any of them runs a query.
|
|
1557
|
+
for (const branch of RELATION_FILTER_KEYS) {
|
|
1558
|
+
const problem = (0, where_compile_js_1.relationFilterValueProblem)(branch, filter[branch]);
|
|
1559
|
+
if (problem !== null)
|
|
1560
|
+
throw shapeError(problem);
|
|
1561
|
+
}
|
|
1562
|
+
// The shared cardinality rule: `is` / `isNot` on a to-many relation.
|
|
1563
|
+
const cardinality = (0, where_compile_js_1.relationCardinalityProblem)(rel, filter);
|
|
1564
|
+
if (cardinality !== null)
|
|
1565
|
+
throw shapeError(cardinality);
|
|
1566
|
+
const conds = [];
|
|
1567
|
+
for (const branch of RELATION_FILTER_KEYS) {
|
|
1568
|
+
const inner = filter[branch];
|
|
1569
|
+
if (inner === undefined)
|
|
1570
|
+
continue;
|
|
1571
|
+
const where = inner;
|
|
1572
|
+
switch (branch) {
|
|
1573
|
+
case 'some':
|
|
1574
|
+
conds.push(await this.relationMatch(rel, where, true, timeout, skip));
|
|
1575
|
+
break;
|
|
1576
|
+
case 'none':
|
|
1577
|
+
conds.push(await this.relationMatch(rel, where, false, timeout, skip));
|
|
1578
|
+
break;
|
|
1579
|
+
case 'every': {
|
|
1580
|
+
// `every` over a where that compiles to no condition (`{}`, `AND: []`)
|
|
1581
|
+
// holds for every parent, as in SQL. Otherwise the parent fails when
|
|
1582
|
+
// one related row makes the predicate FALSE; a row that leaves it
|
|
1583
|
+
// unknown (a NULL column) does not count against it.
|
|
1584
|
+
const targetQi = this.relationTargetQi(rel);
|
|
1585
|
+
if (targetQi.compilesToNothing(where))
|
|
1586
|
+
break;
|
|
1587
|
+
const violates = targetQi.sqlFalseWhere(where);
|
|
1588
|
+
conds.push(await this.relationMatch(rel, violates, false, timeout, skip));
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
case 'is':
|
|
1592
|
+
conds.push(await this.relationMatch(rel, where ?? {}, where !== null, timeout, skip));
|
|
1593
|
+
break;
|
|
1594
|
+
case 'isNot':
|
|
1595
|
+
conds.push(await this.relationMatch(rel, where ?? {}, where === null, timeout, skip));
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (conds.length === 0)
|
|
994
1600
|
return {};
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1601
|
+
return conds.length === 1 ? conds[0] : { AND: conds };
|
|
1602
|
+
}
|
|
1603
|
+
/** A fresh interface over a relation's target table, for an internal fetch. */
|
|
1604
|
+
relationTargetQi(rel) {
|
|
1605
|
+
if (!this.schema.tables[rel.to]) {
|
|
1606
|
+
throw new errors_js_1.ValidationError(`Relation "${rel.name}" targets unknown table "${rel.to}".`);
|
|
999
1607
|
}
|
|
1608
|
+
return new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* `EXISTS` (or `NOT EXISTS`, when `exists` is false) a related row matching
|
|
1612
|
+
* `inner`, as a condition on this table's local key.
|
|
1613
|
+
*
|
|
1614
|
+
* The NOT EXISTS form keeps a NULL local key: such a row has no related row,
|
|
1615
|
+
* so it qualifies. PowQL's `not in` alone would drop it.
|
|
1616
|
+
*/
|
|
1617
|
+
async relationMatch(rel, inner, exists, timeout, skip) {
|
|
1618
|
+
const { localField, keys } = rel.type === 'manyToMany'
|
|
1619
|
+
? await this.manyToManySourceKeys(rel, inner, timeout, skip)
|
|
1620
|
+
: await this.directRelationKeys(rel, inner, timeout, skip);
|
|
1621
|
+
if (exists)
|
|
1622
|
+
return { [localField]: { in: keys } };
|
|
1623
|
+
const localCol = this.column(localField);
|
|
1624
|
+
if (!localCol.nullable)
|
|
1625
|
+
return { [localField]: { notIn: keys } };
|
|
1626
|
+
return { OR: [{ [localField]: null }, { [localField]: { notIn: keys } }] };
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* hasMany / hasOne / belongsTo: the local field, and the distinct correlation
|
|
1630
|
+
* keys of the target rows matching `inner` (inside the target's global filter).
|
|
1631
|
+
* hasMany/hasOne: local = our referenceKey, collected = the child's foreignKey.
|
|
1632
|
+
* belongsTo: local = our foreignKey, collected = the target's referenceKey.
|
|
1633
|
+
*/
|
|
1634
|
+
async directRelationKeys(rel, inner, timeout, skip) {
|
|
1000
1635
|
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
1001
1636
|
const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
1002
1637
|
if (fk.length > 1 || rk.length > 1) {
|
|
1003
1638
|
throw new errors_js_1.UnsupportedFeatureError('composite-key relation filters', 'PowDB', `relation "${rel.name}" uses a composite key, PowQL has no tuple-\`in\` to express it`);
|
|
1004
1639
|
}
|
|
1005
|
-
const
|
|
1006
|
-
if (!targetMeta)
|
|
1007
|
-
throw new errors_js_1.ValidationError(`Relation "${rel.name}" targets unknown table "${rel.to}".`);
|
|
1008
|
-
// hasMany/hasOne: localKey = our referenceKey, collect the child's foreignKey.
|
|
1009
|
-
// belongsTo: localKey = our foreignKey, collect the target's referenceKey.
|
|
1640
|
+
const targetQi = this.relationTargetQi(rel);
|
|
1010
1641
|
const localCol = rel.type === 'belongsTo' ? fk[0] : rk[0];
|
|
1011
1642
|
const childCol = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
1012
1643
|
const localField = this.meta.reverseColumnMap[localCol] ?? localCol;
|
|
1013
|
-
const childField =
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1644
|
+
const childField = targetQi.meta.reverseColumnMap[childCol] ?? childCol;
|
|
1645
|
+
const keys = await targetQi.collectRelationKeys(inner, childField, rel.name, timeout, skip);
|
|
1646
|
+
return { localField, keys };
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* The distinct non-null values of `field` over this table's rows matching
|
|
1650
|
+
* `where` (inside this table's global filter, honouring `skip`), refused
|
|
1651
|
+
* past {@link MAX_RELATION_FILTER_KEYS}.
|
|
1652
|
+
*/
|
|
1653
|
+
async collectRelationKeys(where, field, relName, timeout, skip) {
|
|
1654
|
+
const rows = await this.findMany({
|
|
1655
|
+
where: where,
|
|
1656
|
+
select: { [field]: true },
|
|
1657
|
+
timeout,
|
|
1658
|
+
...(skip !== undefined ? { skipGlobalFilters: skip } : {}),
|
|
1659
|
+
});
|
|
1660
|
+
return this.distinctKeys(rows.map((r) => r[field]), relName);
|
|
1661
|
+
}
|
|
1662
|
+
/** Dedupe correlation keys by value (a Date or bigint by its value, not identity) and enforce the cap. */
|
|
1663
|
+
distinctKeys(values, relName) {
|
|
1664
|
+
const byKey = new Map();
|
|
1665
|
+
for (const v of values) {
|
|
1666
|
+
const k = this.joinKey(v);
|
|
1667
|
+
if (k !== null && !byKey.has(k))
|
|
1668
|
+
byKey.set(k, v);
|
|
1669
|
+
}
|
|
1670
|
+
if (byKey.size > exports.MAX_RELATION_FILTER_KEYS) {
|
|
1671
|
+
throw new errors_js_1.UnsupportedFeatureError(`A relation filter on "${relName}" matching ${byKey.size} distinct keys`, 'PowDB', `PowDB resolves a relation filter into a literal list of keys, which the engine checks row by row, so ` +
|
|
1672
|
+
`the cost grows with rows times keys; Turbine refuses lists over ${exports.MAX_RELATION_FILTER_KEYS}. Narrow the ` +
|
|
1673
|
+
'inner predicate, or query from the related side instead (filter the related table and load the parent ' +
|
|
1674
|
+
'with `with`).');
|
|
1675
|
+
}
|
|
1676
|
+
return [...byKey.values()];
|
|
1677
|
+
}
|
|
1678
|
+
/** manyToMany: the source key field, and the source keys linked (through the junction) to targets matching `inner`. */
|
|
1679
|
+
async manyToManySourceKeys(rel, inner, timeout, skip) {
|
|
1034
1680
|
const through = rel.through;
|
|
1035
1681
|
if (!through)
|
|
1036
1682
|
throw new errors_js_1.ValidationError(`manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
|
|
1037
1683
|
const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
|
|
1038
1684
|
const targetJ = (0, schema_js_1.normalizeKeyColumns)(through.targetKey);
|
|
1039
1685
|
const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
throw new errors_js_1.ValidationError(`Relation "${rel.name}" targets unknown table "${rel.to}".`);
|
|
1686
|
+
const targetQi = this.relationTargetQi(rel);
|
|
1687
|
+
const targetMeta = targetQi.meta;
|
|
1043
1688
|
if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
|
|
1044
1689
|
throw new errors_js_1.UnsupportedFeatureError('composite-key manyToMany filters', 'PowDB', `relation "${rel.name}", PowQL has no tuple-\`in\` for composite junction/target keys`);
|
|
1045
1690
|
}
|
|
@@ -1047,50 +1692,158 @@ class PowqlInterface {
|
|
|
1047
1692
|
const targetJCol = targetJ[0];
|
|
1048
1693
|
const sourceRefCol = sourceRef[0];
|
|
1049
1694
|
const targetPkCol = targetMeta.primaryKey[0];
|
|
1050
|
-
const
|
|
1695
|
+
const localField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
1051
1696
|
const sourceRefColMeta = this.meta.columns.find((c) => c.name === sourceRefCol);
|
|
1052
1697
|
const targetPkField = targetMeta.reverseColumnMap[targetPkCol] ?? targetPkCol;
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
select: { [targetPkField]: true },
|
|
1058
|
-
timeout,
|
|
1059
|
-
});
|
|
1060
|
-
return [...new Set(rows.map((r) => r[targetPkField]).filter((v) => v != null))];
|
|
1061
|
-
};
|
|
1062
|
-
// Junction source keys linking any of `targetPks` (literal IN-list, never a subquery).
|
|
1698
|
+
const targetPks = await targetQi.collectRelationKeys(inner, targetPkField, rel.name, timeout, skip);
|
|
1699
|
+
if (!targetPks.length)
|
|
1700
|
+
return { localField, keys: [] };
|
|
1701
|
+
// Junction source keys linking any of the targets (literal in-lists, never a subquery).
|
|
1063
1702
|
const junctionMeta = this.schema.tables[through.table];
|
|
1064
1703
|
const targetJColMeta = junctionMeta?.columns.find((c) => c.name === targetJCol);
|
|
1065
1704
|
const junctionChunk = this.keyChunkSize(junctionMeta, targetJCol);
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1705
|
+
const sources = [];
|
|
1706
|
+
for (let i = 0; i < targetPks.length; i += junctionChunk) {
|
|
1707
|
+
const chunk = targetPks.slice(i, i + junctionChunk);
|
|
1708
|
+
const params = [];
|
|
1709
|
+
const keyClause = this.inClause(this.colRefName(targetJCol), chunk, params, targetJColMeta);
|
|
1710
|
+
const { rows } = await this.exec(`${(0, powdb_shared_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} { ${this.colRefName(sourceJCol)} }`, params, timeout, 'findMany');
|
|
1711
|
+
for (const r of rows) {
|
|
1712
|
+
const v = r[sourceJCol];
|
|
1713
|
+
if (v != null)
|
|
1714
|
+
sources.push(sourceRefColMeta ? coerceScalar(String(v), sourceRefColMeta.tsType) : v);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return { localField, keys: this.distinctKeys(sources, rel.name) };
|
|
1718
|
+
}
|
|
1719
|
+
/**
|
|
1720
|
+
* Rows where `where` is TRUE under SQL three-valued logic. PowQL's own
|
|
1721
|
+
* evaluation already excludes a NULL from every comparison, so this differs
|
|
1722
|
+
* from `where` only under `NOT`, where PowQL takes the plain complement and
|
|
1723
|
+
* SQL keeps a NULL comparison unknown.
|
|
1724
|
+
*
|
|
1725
|
+
* Combinators are read by the shared {@link combinatorBranches} rule, so a
|
|
1726
|
+
* shape the SQL engines refuse is refused here (E003), and the output uses
|
|
1727
|
+
* one spelling: `AND` / `OR` arrays and `NOT` objects. `OR: []` stays as
|
|
1728
|
+
* written and compiles to no row.
|
|
1729
|
+
*/
|
|
1730
|
+
sqlTrueWhere(where) {
|
|
1731
|
+
const parts = [];
|
|
1732
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1733
|
+
if (value === undefined)
|
|
1734
|
+
continue;
|
|
1735
|
+
if (isCombinatorKey(key)) {
|
|
1736
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, this.table);
|
|
1737
|
+
if (branches === null)
|
|
1738
|
+
continue;
|
|
1739
|
+
if (key === 'NOT') {
|
|
1740
|
+
for (const branch of branches) {
|
|
1741
|
+
// A branch that compiles to no condition is skipped, as on the SQL engines.
|
|
1742
|
+
if (!this.compilesToNothing(branch)) {
|
|
1743
|
+
parts.push(this.sqlFalseWhere(branch));
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
else if (key === 'OR' || branches.length > 0) {
|
|
1748
|
+
parts.push({ [key]: branches.map((w) => this.sqlTrueWhere(w)) });
|
|
1079
1749
|
}
|
|
1080
1750
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1751
|
+
else {
|
|
1752
|
+
parts.push({ [key]: value });
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (parts.length === 0)
|
|
1756
|
+
return {};
|
|
1757
|
+
return (parts.length === 1 ? parts[0] : { AND: parts });
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Rows where `where` is FALSE under SQL three-valued logic, the complement
|
|
1761
|
+
* PowQL's two-valued `not` cannot express: a comparison against a NULL
|
|
1762
|
+
* column is unknown, which is neither TRUE nor FALSE, so the row belongs to
|
|
1763
|
+
* neither set. Relation filters (EXISTS) are two-valued and negate plainly.
|
|
1764
|
+
*
|
|
1765
|
+
* A sub-where that compiles to no condition is TRUE, so it is FALSE for no
|
|
1766
|
+
* row and contributes nothing; `OR: []` is FALSE for every row.
|
|
1767
|
+
*/
|
|
1768
|
+
sqlFalseWhere(where) {
|
|
1769
|
+
const parts = [];
|
|
1770
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1771
|
+
if (value === undefined)
|
|
1772
|
+
continue;
|
|
1773
|
+
if (isCombinatorKey(key)) {
|
|
1774
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, this.table);
|
|
1775
|
+
if (branches === null)
|
|
1776
|
+
continue;
|
|
1777
|
+
if (key === 'OR' && branches.length === 0) {
|
|
1778
|
+
parts.push(this.trueWhere());
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
const live = branches.filter((w) => !this.compilesToNothing(w));
|
|
1782
|
+
// An `OR` with a branch that has no condition is TRUE: FALSE for no row.
|
|
1783
|
+
if (live.length === 0 || (key === 'OR' && live.length < branches.length))
|
|
1784
|
+
continue;
|
|
1785
|
+
if (key === 'AND')
|
|
1786
|
+
parts.push({ OR: live.map((w) => this.sqlFalseWhere(w)) });
|
|
1787
|
+
else if (key === 'OR')
|
|
1788
|
+
parts.push({ AND: live.map((w) => this.sqlFalseWhere(w)) });
|
|
1789
|
+
else
|
|
1790
|
+
for (const w of live)
|
|
1791
|
+
parts.push(this.sqlTrueWhere(w));
|
|
1792
|
+
}
|
|
1793
|
+
else if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, key)) {
|
|
1794
|
+
parts.push({ NOT: { [key]: value } });
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
// A column leaf is FALSE when the column is non-null and the leaf does
|
|
1798
|
+
// not hold; on a NULL column only an explicit `not: null` (or an empty
|
|
1799
|
+
// `in`) makes it FALSE, every other comparison is unknown there.
|
|
1800
|
+
const op = value !== null && typeof value === 'object' ? value : undefined;
|
|
1801
|
+
const falseAtNull = op !== undefined && (op.not === null || (Array.isArray(op.in) && op.in.length === 0));
|
|
1802
|
+
const branch = { AND: [{ [key]: { not: null } }, { NOT: { [key]: value } }] };
|
|
1803
|
+
parts.push(falseAtNull ? { OR: [branch, { [key]: null }] } : branch);
|
|
1804
|
+
}
|
|
1085
1805
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1806
|
+
// A where with no condition is TRUE for every row, so FALSE for none.
|
|
1807
|
+
if (parts.length === 0)
|
|
1808
|
+
return this.falseWhere();
|
|
1809
|
+
return (parts.length === 1 ? parts[0] : { OR: parts });
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Whether a where compiles to no condition, as {@link buildWhere} and the
|
|
1813
|
+
* SQL where walk both read it: only absent keys, and `AND` / `NOT` whose
|
|
1814
|
+
* branches all compile to nothing (`AND: []`, `NOT: [{}]`). Any `OR` is a
|
|
1815
|
+
* condition: `OR: []` matches no row, and an `OR` with a branch that has no
|
|
1816
|
+
* condition is constant TRUE (`OR: [{}]`), which `NOT` negates. So is any
|
|
1817
|
+
* column or relation key.
|
|
1818
|
+
*/
|
|
1819
|
+
compilesToNothing(where) {
|
|
1820
|
+
if (where === null || typeof where !== 'object' || Array.isArray(where))
|
|
1821
|
+
return false;
|
|
1822
|
+
for (const [key, value] of Object.entries(where)) {
|
|
1823
|
+
if (value === undefined)
|
|
1824
|
+
continue;
|
|
1825
|
+
if (!isCombinatorKey(key))
|
|
1826
|
+
return false;
|
|
1827
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, this.table);
|
|
1828
|
+
if (branches === null)
|
|
1829
|
+
continue;
|
|
1830
|
+
if (key === 'OR')
|
|
1831
|
+
return false;
|
|
1832
|
+
if (!branches.every((w) => this.compilesToNothing(w)))
|
|
1833
|
+
return false;
|
|
1088
1834
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1837
|
+
/** A where clause that matches every row (the primary key is null or not null). */
|
|
1838
|
+
trueWhere() {
|
|
1839
|
+
const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name ?? 'id';
|
|
1840
|
+
const field = this.meta.reverseColumnMap[pk] ?? pk;
|
|
1841
|
+
return { OR: [{ [field]: null }, { [field]: { not: null } }] };
|
|
1842
|
+
}
|
|
1843
|
+
/** A where clause that matches no row (an empty `in` on the primary key). */
|
|
1844
|
+
falseWhere() {
|
|
1845
|
+
const pk = this.meta.primaryKey[0] ?? this.meta.columns[0]?.name ?? 'id';
|
|
1846
|
+
return { [this.meta.reverseColumnMap[pk] ?? pk]: { in: [] } };
|
|
1094
1847
|
}
|
|
1095
1848
|
// -------------------------------------------------------------------------
|
|
1096
1849
|
// Projection / order
|
|
@@ -1099,8 +1852,9 @@ class PowqlInterface {
|
|
|
1099
1852
|
* Resolve the set of columns to project, honouring `select` / `omit` and the
|
|
1100
1853
|
* query-level `includePii` opt-in. PII-tagged (`defineSchema` `pii: true`)
|
|
1101
1854
|
* columns are EXCLUDED from a default (or omit-only) projection unless
|
|
1102
|
-
* `includePii` is
|
|
1103
|
-
*
|
|
1855
|
+
* `includePii: UNSAFE` is passed, and a `select` naming one without it is
|
|
1856
|
+
* refused (E003), as on the SQL engines. Untagged tables project exactly as
|
|
1857
|
+
* before.
|
|
1104
1858
|
*/
|
|
1105
1859
|
/**
|
|
1106
1860
|
* Resolve one projection field name, or throw. PowQL already refused an
|
|
@@ -1164,6 +1918,23 @@ class PowqlInterface {
|
|
|
1164
1918
|
const forcedPk = [];
|
|
1165
1919
|
const hasSelect = select && Object.keys(select).length;
|
|
1166
1920
|
if (hasSelect) {
|
|
1921
|
+
// A PII column named in `select` is refused unless the read passes
|
|
1922
|
+
// `includePii: UNSAFE`, as on the SQL engines. A key the loader added
|
|
1923
|
+
// itself (a PII-tagged correlation column) carries the stitch-only mark.
|
|
1924
|
+
if (!includePii) {
|
|
1925
|
+
const pii = this.piiColumnNames();
|
|
1926
|
+
if (pii.size) {
|
|
1927
|
+
const stitchOnly = (0, batched_loader_js_1.stitchOnlyFields)(select);
|
|
1928
|
+
for (const [key, on] of Object.entries(select)) {
|
|
1929
|
+
if (!on || stitchOnly?.has(key))
|
|
1930
|
+
continue;
|
|
1931
|
+
const column = this.projectionColumn(key, 'select');
|
|
1932
|
+
if (pii.has(column) && !pk.has(column)) {
|
|
1933
|
+
throw new errors_js_1.ValidationError((0, relations_js_1.piiSelectRefusedMessage)(this.table, this.meta.reverseColumnMap[column] ?? column));
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1167
1938
|
const picked = new Set(Object.entries(select)
|
|
1168
1939
|
.filter(([, v]) => v)
|
|
1169
1940
|
.map(([k]) => this.projectionColumn(k, 'select')));
|
|
@@ -1213,6 +1984,34 @@ class PowqlInterface {
|
|
|
1213
1984
|
* every consumer of the key (relation stitching, reselect) has finished with
|
|
1214
1985
|
* it. A no-op, and free, when nothing was forced.
|
|
1215
1986
|
*/
|
|
1987
|
+
/**
|
|
1988
|
+
* The columns of this table that the post-fetch relation loaders read off
|
|
1989
|
+
* each parent row for `withClause`: a belongsTo's foreign key, a hasMany /
|
|
1990
|
+
* hasOne / manyToMany's reference key, and the same for every relation a
|
|
1991
|
+
* `_count` names. An unknown name contributes nothing; the loader raises its
|
|
1992
|
+
* E005.
|
|
1993
|
+
*/
|
|
1994
|
+
loaderParentKeyColumns(withClause) {
|
|
1995
|
+
const out = new Set();
|
|
1996
|
+
const add = (rel) => {
|
|
1997
|
+
const cols = rel.type === 'belongsTo' ? rel.foreignKey : rel.referenceKey;
|
|
1998
|
+
for (const c of (0, schema_js_1.normalizeKeyColumns)(cols))
|
|
1999
|
+
out.add(c);
|
|
2000
|
+
};
|
|
2001
|
+
for (const [relName, opt] of Object.entries(withClause)) {
|
|
2002
|
+
if (!opt)
|
|
2003
|
+
continue;
|
|
2004
|
+
if (relName === '_count') {
|
|
2005
|
+
for (const rel of (0, batched_loader_js_1.resolveCountRelations)(this.meta, opt))
|
|
2006
|
+
add(rel);
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
const rel = (0, utils_js_1.ownLookup)(this.meta.relations, relName);
|
|
2010
|
+
if (rel)
|
|
2011
|
+
add(rel);
|
|
2012
|
+
}
|
|
2013
|
+
return [...out];
|
|
2014
|
+
}
|
|
1216
2015
|
stripForcedPk(entities, forcedPk) {
|
|
1217
2016
|
if (forcedPk.length === 0 || entities.length === 0)
|
|
1218
2017
|
return;
|
|
@@ -1353,6 +2152,8 @@ class PowqlInterface {
|
|
|
1353
2152
|
if (spec.nulls === 'first') {
|
|
1354
2153
|
throw new errors_js_1.UnsupportedFeatureError('NULLS FIRST placement', 'PowDB', `field "${field}": PowDB orders NULLs / missing keys LAST in both directions`);
|
|
1355
2154
|
}
|
|
2155
|
+
this.assertDecimalComparable(this.column(field), 'Ordering');
|
|
2156
|
+
this.assertEnumOrderable(this.column(field), 'Ordering');
|
|
1356
2157
|
return `${this.ref(field, alias)} ${this.dirKeyword(spec.sort, `orderBy on "${field}"`)}`;
|
|
1357
2158
|
}
|
|
1358
2159
|
// Name the actual feature in the refusal, a pick-row ordering
|
|
@@ -1369,6 +2170,8 @@ class PowqlInterface {
|
|
|
1369
2170
|
: 'object-valued ordering';
|
|
1370
2171
|
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `field "${field}"`);
|
|
1371
2172
|
}
|
|
2173
|
+
this.assertDecimalComparable(this.column(field), 'Ordering');
|
|
2174
|
+
this.assertEnumOrderable(this.column(field), 'Ordering');
|
|
1372
2175
|
return `${this.ref(field, alias)} ${this.dirKeyword(dir, `orderBy on "${field}"`)}`;
|
|
1373
2176
|
});
|
|
1374
2177
|
return ` order ${parts.join(', ')}`;
|
|
@@ -1376,7 +2179,7 @@ class PowqlInterface {
|
|
|
1376
2179
|
/** Compile one {@link JsonPathOrderBy} entry to `order .col->$n asc` (+ optional numeric cast). */
|
|
1377
2180
|
buildJsonPathOrder(field, spec, params, alias) {
|
|
1378
2181
|
const col = this.column(field);
|
|
1379
|
-
if (!(0, powdb_shared_js_1.isJsonColumn)(col)) {
|
|
2182
|
+
if (!(0, powdb_shared_js_1.isJsonColumn)(col, this.schema.enums)) {
|
|
1380
2183
|
throw new errors_js_1.UnsupportedFeatureError('JSON-path ordering', 'PowDB', `field "${field}" is not a json column`);
|
|
1381
2184
|
}
|
|
1382
2185
|
(0, powdb_shared_js_1.requireCapability)(this.capabilities, 'jsonDocs', 'JSON path ordering');
|
|
@@ -1496,16 +2299,21 @@ class PowqlInterface {
|
|
|
1496
2299
|
// `QueryInterface.executeWithMiddleware`. The option surface is the CORE
|
|
1497
2300
|
// one because these args ARE core's args; PowDB reads them, it does not
|
|
1498
2301
|
// define them.
|
|
1499
|
-
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args);
|
|
2302
|
+
(0, option_surface_js_1.warnUnknownQueryOptions)(this.table, action, args, this.advice ?? null);
|
|
1500
2303
|
if (this.middlewares.length === 0)
|
|
1501
2304
|
return executor();
|
|
2305
|
+
// A copy, as on the SQL engines: the statement is built from `args`, not
|
|
2306
|
+
// from what middleware sees, so `next()` throws on a changed copy rather
|
|
2307
|
+
// than doing nothing.
|
|
2308
|
+
const params = (0, utils_js_1.readOnlyMiddlewareParams)(this.table, action, args);
|
|
1502
2309
|
let index = 0;
|
|
1503
2310
|
const next = async (p) => {
|
|
2311
|
+
(0, utils_js_1.assertMiddlewareArgsKept)(params, p);
|
|
1504
2312
|
if (index < this.middlewares.length)
|
|
1505
2313
|
return this.middlewares[index++](p, next);
|
|
1506
2314
|
return executor();
|
|
1507
2315
|
};
|
|
1508
|
-
return next(
|
|
2316
|
+
return next(params);
|
|
1509
2317
|
}
|
|
1510
2318
|
/** Map raw rows to typed entities. `native` is the wire that ACTUALLY served
|
|
1511
2319
|
* this result (threaded from {@link execOnce}, not the pool-level capability),
|
|
@@ -1515,15 +2323,13 @@ class PowqlInterface {
|
|
|
1515
2323
|
* coerces its string cells correctly. Defaults to the pool capability for the
|
|
1516
2324
|
* rare caller with no per-result flag (hand-built test pools). */
|
|
1517
2325
|
shape(rows, native = Boolean(this.capabilities.nativeRaw)) {
|
|
1518
|
-
return rows.map((r) => (0, powdb_shared_js_1.rowToEntity)(r, this.meta, native));
|
|
2326
|
+
return rows.map((r) => (0, powdb_shared_js_1.rowToEntity)(r, this.meta, native, this.schema.enums));
|
|
1519
2327
|
}
|
|
1520
|
-
// -------------------------------------------------------------------------
|
|
1521
|
-
// Reads
|
|
1522
|
-
// -------------------------------------------------------------------------
|
|
1523
2328
|
async findMany(args = {}) {
|
|
1524
2329
|
this.assertNoForceCustomPlan(args);
|
|
2330
|
+
this.assertNoRowLock(args);
|
|
1525
2331
|
this.assertNoJsonEncoding(args);
|
|
1526
|
-
args = this.normalizeArgs(args);
|
|
2332
|
+
args = this.completePageOrder(this.normalizeArgs(args));
|
|
1527
2333
|
return this.withMiddleware('findMany', args, async () => {
|
|
1528
2334
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
1529
2335
|
// is correct on every engine version: PowDB's projection fast path returned
|
|
@@ -1539,7 +2345,7 @@ class PowqlInterface {
|
|
|
1539
2345
|
if (linkPlans.length)
|
|
1540
2346
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1541
2347
|
if (residualWith) {
|
|
1542
|
-
await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
2348
|
+
await this.loadRelations(entities, residualWith, args.timeout, 0, { args, resolvedWhere }, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'), args.skipGlobalFilters);
|
|
1543
2349
|
}
|
|
1544
2350
|
// LAST: every consumer of the internally-forced PK (relation stitching,
|
|
1545
2351
|
// the loaders' correlation) has finished with it by here.
|
|
@@ -1561,7 +2367,11 @@ class PowqlInterface {
|
|
|
1561
2367
|
* to the pre-0.18 output (no alias, `.col` refs).
|
|
1562
2368
|
*/
|
|
1563
2369
|
async buildFind(args, params) {
|
|
1564
|
-
|
|
2370
|
+
// A cursor is checked with the SQL engines' rules first, so an invalid one
|
|
2371
|
+
// is E003 on every engine; a valid one is an engine gap (E017).
|
|
2372
|
+
const cursor = args.cursor;
|
|
2373
|
+
if (this.cursorCarriesValues(cursor)) {
|
|
2374
|
+
this.assertCursorPlaceable(cursor, args.orderBy);
|
|
1565
2375
|
throw new errors_js_1.UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
|
|
1566
2376
|
}
|
|
1567
2377
|
// `distinct` names COLUMNS: Postgres compiles it to `SELECT DISTINCT ON
|
|
@@ -1574,16 +2384,18 @@ class PowqlInterface {
|
|
|
1574
2384
|
// dialect seam exists to prevent.
|
|
1575
2385
|
//
|
|
1576
2386
|
// Refused with the same E017 the other non-Postgres engines already raise
|
|
1577
|
-
// (query/builder.ts's `
|
|
2387
|
+
// (query/builder.ts's `distinctOn` capability gate) and in the same
|
|
1578
2388
|
// words, so `distinct` now means one thing across every engine and matches
|
|
1579
2389
|
// what the README documents. Refused BEFORE the column names are resolved,
|
|
1580
2390
|
// matching that gate's position, so `distinct: ['nope']` reports the
|
|
1581
|
-
// unsupported feature rather than the typo on every engine alike.
|
|
2391
|
+
// unsupported feature rather than the typo on every engine alike. The
|
|
2392
|
+
// element shape is checked first, as there: a non-string element is E003.
|
|
1582
2393
|
if (args.distinct?.length) {
|
|
2394
|
+
(0, filters_js_1.dedupeColumnList)(this.meta, args.distinct);
|
|
1583
2395
|
throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', 'PowDB', "findMany({ distinct }) requires PostgreSQL: PowQL's `distinct` is row-wide, not per-column. " +
|
|
1584
2396
|
'Group in application code, or use groupBy({ by }) for one row per combination.');
|
|
1585
2397
|
}
|
|
1586
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2398
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
1587
2399
|
const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1588
2400
|
// Partition the `with` clause: nested-projection blocks vs loader residue.
|
|
1589
2401
|
// A relation whose field name collides with a projected parent column stays
|
|
@@ -1594,6 +2406,8 @@ class PowqlInterface {
|
|
|
1594
2406
|
// with a `distinct` present and testing for it again would be dead code
|
|
1595
2407
|
// implying the option is still accepted.
|
|
1596
2408
|
const withClause = args.with;
|
|
2409
|
+
if (withClause)
|
|
2410
|
+
this.assertCountTopLevelOnly(withClause, 0);
|
|
1597
2411
|
const nestedPlans = [];
|
|
1598
2412
|
const linkPlans = [];
|
|
1599
2413
|
let residualWith = withClause;
|
|
@@ -1615,7 +2429,7 @@ class PowqlInterface {
|
|
|
1615
2429
|
// inside, only when a genuine candidate reaches it), else it stays on the
|
|
1616
2430
|
// loaders.
|
|
1617
2431
|
const linkPlan = rel && !cols.includes(relName)
|
|
1618
|
-
? await this.planLinkPathRelation(relName, rel, opt, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'), cols, linkPlans.length + 1)
|
|
2432
|
+
? await this.planLinkPathRelation(relName, rel, opt, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'), cols, linkPlans.length + 1, args.skipGlobalFilters)
|
|
1619
2433
|
: null;
|
|
1620
2434
|
if (linkPlan)
|
|
1621
2435
|
linkPlans.push(linkPlan);
|
|
@@ -1624,17 +2438,34 @@ class PowqlInterface {
|
|
|
1624
2438
|
}
|
|
1625
2439
|
residualWith = Object.keys(residue).length ? residue : undefined;
|
|
1626
2440
|
}
|
|
2441
|
+
// The loaders (and the native join) read each parent's correlation key off
|
|
2442
|
+
// the fetched row, so a `select` / `omit` that leaves it out would stitch
|
|
2443
|
+
// nothing. Fetch it anyway and take it back off afterwards, as the SQL
|
|
2444
|
+
// engines' batched loader does.
|
|
2445
|
+
if (residualWith) {
|
|
2446
|
+
for (const column of this.loaderParentKeyColumns(residualWith)) {
|
|
2447
|
+
if (cols.includes(column))
|
|
2448
|
+
continue;
|
|
2449
|
+
cols.push(column);
|
|
2450
|
+
forcedPk.push(column);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
1627
2453
|
const nest = nestedPlans.length > 0 || linkPlans.length > 0;
|
|
1628
2454
|
const alias = nest ? 't0' : undefined;
|
|
1629
2455
|
let where = this.buildWhere(resolvedWhere, params, alias);
|
|
1630
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
|
|
2456
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout), alias);
|
|
1631
2457
|
const filter = where ? ` filter ${where}` : '';
|
|
1632
2458
|
const order = this.buildOrder(args.orderBy, params, alias);
|
|
1633
2459
|
const limit = this.effectiveLimit(args);
|
|
1634
2460
|
this.assertPagination(limit, args.offset, 'findMany');
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2461
|
+
// Once per table per process, as on the SQL engines: a transaction builds
|
|
2462
|
+
// its own interfaces, so a per-interface flag repeated it in each one.
|
|
2463
|
+
if (limit === undefined && this.warnOnUnlimited && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unlimitedFindMany, this.table)) {
|
|
2464
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `findMany on "${this.table}" has no limit: this scans the whole table.`, {
|
|
2465
|
+
code: 'unlimited-find-many',
|
|
2466
|
+
table: this.table,
|
|
2467
|
+
operation: 'findMany',
|
|
2468
|
+
});
|
|
1638
2469
|
}
|
|
1639
2470
|
const limitClause = limit !== undefined ? ` limit ${this.param(limit, params)}` : '';
|
|
1640
2471
|
const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
|
|
@@ -1645,7 +2476,7 @@ class PowqlInterface {
|
|
|
1645
2476
|
const aliasCtr = { n: 1 };
|
|
1646
2477
|
const parts = cols.map((c) => `${(0, powdb_shared_js_1.quotePowqlIdent)(c)}: t0.${(0, powdb_shared_js_1.quotePowqlIdent)(c)}`);
|
|
1647
2478
|
for (const plan of nestedPlans) {
|
|
1648
|
-
parts.push(await this.buildNestedBlock(plan, 't0', aliasCtr, params, args.timeout));
|
|
2479
|
+
parts.push(await this.buildNestedBlock(plan, 't0', aliasCtr, params, args.timeout, args.skipGlobalFilters));
|
|
1649
2480
|
}
|
|
1650
2481
|
// Scalar link-path hops (flat native-typed fields; never a JSON block).
|
|
1651
2482
|
for (const plan of linkPlans)
|
|
@@ -1684,7 +2515,7 @@ class PowqlInterface {
|
|
|
1684
2515
|
// written with `take` / `skip` would report a plan for a DIFFERENT
|
|
1685
2516
|
// statement than the one that executes, which is the one thing a
|
|
1686
2517
|
// diagnostic must not do.
|
|
1687
|
-
args = this.normalizeArgs(args);
|
|
2518
|
+
args = this.completePageOrder(this.normalizeArgs(args));
|
|
1688
2519
|
const params = [];
|
|
1689
2520
|
const { powql } = await this.buildFind(args, params);
|
|
1690
2521
|
const { rows } = await this.exec(`explain ${powql}`, params, args.timeout, 'explain');
|
|
@@ -1697,6 +2528,7 @@ class PowqlInterface {
|
|
|
1697
2528
|
}
|
|
1698
2529
|
async findUnique(args) {
|
|
1699
2530
|
this.assertNoForceCustomPlan(args);
|
|
2531
|
+
this.assertNoRowLock(args);
|
|
1700
2532
|
this.assertNoJsonEncoding(args);
|
|
1701
2533
|
args = this.normalizeArgs(args);
|
|
1702
2534
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
@@ -1721,17 +2553,21 @@ class PowqlInterface {
|
|
|
1721
2553
|
if (linkPlans.length)
|
|
1722
2554
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1723
2555
|
if (residualWith)
|
|
1724
|
-
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
2556
|
+
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'), args.skipGlobalFilters);
|
|
1725
2557
|
// See findMany: the strip is last, after every consumer of the key.
|
|
1726
2558
|
this.stripForcedPk(entities, forcedPk);
|
|
1727
2559
|
return entities[0];
|
|
1728
2560
|
});
|
|
1729
2561
|
}
|
|
1730
2562
|
async findFirst(args = {}) {
|
|
1731
|
-
args = this.normalizeArgs(args);
|
|
2563
|
+
args = this.normalizeArgs(args, 'findFirst');
|
|
1732
2564
|
this.assertNoForceCustomPlan(args);
|
|
2565
|
+
this.assertNoRowLock(args);
|
|
1733
2566
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1734
|
-
|
|
2567
|
+
// An `offset` makes this a page of one, which gets the page tiebreaker
|
|
2568
|
+
// (the row would otherwise depend on which tied rows were skipped).
|
|
2569
|
+
const first = { ...args, limit: 1 };
|
|
2570
|
+
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind(args.offset === undefined ? first : this.completePageOrder(first), 'findFirst');
|
|
1735
2571
|
if (!rows.length)
|
|
1736
2572
|
return null;
|
|
1737
2573
|
const entities = this.shape(rows, native);
|
|
@@ -1740,7 +2576,7 @@ class PowqlInterface {
|
|
|
1740
2576
|
if (linkPlans.length)
|
|
1741
2577
|
this.attachLinkRows(entities, linkPlans, native);
|
|
1742
2578
|
if (residualWith)
|
|
1743
|
-
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
2579
|
+
await this.loadRelations(entities, residualWith, args.timeout, 0, undefined, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'), args.skipGlobalFilters);
|
|
1744
2580
|
// See findMany: the strip is last, after every consumer of the key.
|
|
1745
2581
|
this.stripForcedPk(entities, forcedPk);
|
|
1746
2582
|
return entities[0];
|
|
@@ -1753,6 +2589,7 @@ class PowqlInterface {
|
|
|
1753
2589
|
return row;
|
|
1754
2590
|
}
|
|
1755
2591
|
async findFirstOrThrow(args = {}) {
|
|
2592
|
+
args = this.normalizeArgs(args, 'findFirstOrThrow');
|
|
1756
2593
|
const row = await this.findFirst(args);
|
|
1757
2594
|
if (!row)
|
|
1758
2595
|
throw new errors_js_1.NotFoundError({ table: this.table, where: (args.where ?? {}) });
|
|
@@ -1772,10 +2609,30 @@ class PowqlInterface {
|
|
|
1772
2609
|
* the default `'batched'` strategy) keeps the loaders. Output is byte-equal
|
|
1773
2610
|
* either way (the join reuses the same stitch / shape helpers).
|
|
1774
2611
|
*/
|
|
1775
|
-
async loadRelations(parents, withClause, timeout, depth = 0, parent, includePii = false) {
|
|
2612
|
+
async loadRelations(parents, withClause, timeout, depth = 0, parent, includePii = false, skip) {
|
|
1776
2613
|
if (depth >= 10) {
|
|
1777
2614
|
throw new errors_js_1.ValidationError(`Nested 'with' on PowDB exceeded depth 10 (relation cycle?).`);
|
|
1778
2615
|
}
|
|
2616
|
+
// Every relation name and the `_count` spec are checked BEFORE the
|
|
2617
|
+
// zero-parent return, so whether a `with` is accepted never depends on
|
|
2618
|
+
// whether the base query found rows: the SQL engines compile it either way.
|
|
2619
|
+
// Same errors as the SQL engines: E005 for an unknown relation (and for a
|
|
2620
|
+
// `_count` below the top level), E003 for a `_count` naming a to-one.
|
|
2621
|
+
const countSpec = withClause._count;
|
|
2622
|
+
if (countSpec) {
|
|
2623
|
+
if (depth > 0) {
|
|
2624
|
+
throw new errors_js_1.RelationError(`Unknown relation "_count" on table "${this.table}". ` +
|
|
2625
|
+
`${(0, utils_js_1.availableClause)(Object.keys(this.meta.relations), 'It has no relations.')} ` +
|
|
2626
|
+
'(`_count` is supported on the top-level `with` only, on every relationLoadStrategy.)');
|
|
2627
|
+
}
|
|
2628
|
+
(0, batched_loader_js_1.resolveCountRelations)(this.meta, countSpec);
|
|
2629
|
+
}
|
|
2630
|
+
for (const [relName, opt] of Object.entries(withClause)) {
|
|
2631
|
+
if (opt && relName !== '_count' && !(0, utils_js_1.ownLookup)(this.meta.relations, relName)) {
|
|
2632
|
+
throw new errors_js_1.RelationError(`Unknown relation "${relName}" on table "${this.table}". ` +
|
|
2633
|
+
(0, utils_js_1.availableClause)(Object.keys(this.meta.relations), 'It has no relations.'));
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
1779
2636
|
if (!parents.length)
|
|
1780
2637
|
return;
|
|
1781
2638
|
// The resolved strategy is 'join' only for an EXPLICIT 'join' (per-query arg
|
|
@@ -1785,14 +2642,12 @@ class PowqlInterface {
|
|
|
1785
2642
|
// key, …) never triggers the capability's E017.
|
|
1786
2643
|
const strategyIsJoin = parent ? this.resolveStrategy(parent.args) === 'join' : false;
|
|
1787
2644
|
for (const [relName, opt] of Object.entries(withClause)) {
|
|
1788
|
-
if (!opt)
|
|
2645
|
+
if (!opt || relName === '_count')
|
|
1789
2646
|
continue;
|
|
1790
|
-
const rel = this.meta.relations
|
|
1791
|
-
if (!rel)
|
|
1792
|
-
throw new errors_js_1.ValidationError(`Unknown relation "${relName}" on "${this.table}".`);
|
|
2647
|
+
const rel = (0, utils_js_1.ownLookup)(this.meta.relations, relName);
|
|
1793
2648
|
if (strategyIsJoin && parent && this.joinEligible(rel, opt, parent.args)) {
|
|
1794
2649
|
if (this.capabilities.serverJoins) {
|
|
1795
|
-
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
2650
|
+
await this.loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii, skip);
|
|
1796
2651
|
continue;
|
|
1797
2652
|
}
|
|
1798
2653
|
// An otherwise-eligible relation the engine cannot join: a PER-QUERY
|
|
@@ -1804,7 +2659,7 @@ class PowqlInterface {
|
|
|
1804
2659
|
}
|
|
1805
2660
|
}
|
|
1806
2661
|
if (rel.type === 'manyToMany') {
|
|
1807
|
-
await this.loadManyToMany(parents, rel, relName, opt, timeout, includePii);
|
|
2662
|
+
await this.loadManyToMany(parents, rel, relName, opt, timeout, includePii, skip);
|
|
1808
2663
|
continue;
|
|
1809
2664
|
}
|
|
1810
2665
|
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
@@ -1859,7 +2714,7 @@ class PowqlInterface {
|
|
|
1859
2714
|
if (userSelect) {
|
|
1860
2715
|
fetchOptions = {
|
|
1861
2716
|
...options,
|
|
1862
|
-
select: { ...userSelect, [childKeyField]: true },
|
|
2717
|
+
select: (0, batched_loader_js_1.markStitchOnly)({ ...userSelect, [childKeyField]: true }, [childKeyField]),
|
|
1863
2718
|
};
|
|
1864
2719
|
}
|
|
1865
2720
|
else if (userOmit) {
|
|
@@ -1892,6 +2747,9 @@ class PowqlInterface {
|
|
|
1892
2747
|
// Re-entering the public findMany, so the opt-in has to be spelled
|
|
1893
2748
|
// in its PUBLIC form (the sentinel); a plain `true` is refused there.
|
|
1894
2749
|
includePii: includePii ? types_js_1.UNSAFE : undefined,
|
|
2750
|
+
// The query's opt-out reaches the related table, as on the SQL engines;
|
|
2751
|
+
// without one the target's own global filter applies inside findMany.
|
|
2752
|
+
...(skip !== undefined ? { skipGlobalFilters: skip } : {}),
|
|
1895
2753
|
}));
|
|
1896
2754
|
for (const child of children) {
|
|
1897
2755
|
const k = this.joinKey(child[childKeyField]);
|
|
@@ -1920,6 +2778,175 @@ class PowqlInterface {
|
|
|
1920
2778
|
: PowqlInterface.pageOf(matches, relLimit, relOffset);
|
|
1921
2779
|
}
|
|
1922
2780
|
}
|
|
2781
|
+
// Last, so `_count` is the final key on each row, as on the SQL engines.
|
|
2782
|
+
if (countSpec)
|
|
2783
|
+
await this.loadRelationCounts(parents, countSpec, timeout, skip);
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Refuse a `_count` anywhere below the top-level `with`, as the SQL engines
|
|
2787
|
+
* do (E005, their message). Checked over the whole tree before anything runs,
|
|
2788
|
+
* because a loader re-enters the child table's public findMany, where a
|
|
2789
|
+
* nested `_count` would otherwise read as top level and be counted.
|
|
2790
|
+
*/
|
|
2791
|
+
assertCountTopLevelOnly(withClause, depth) {
|
|
2792
|
+
if (depth >= 10)
|
|
2793
|
+
return;
|
|
2794
|
+
if (depth > 0 && withClause._count) {
|
|
2795
|
+
throw new errors_js_1.RelationError(`Unknown relation "_count" on table "${this.table}". ` +
|
|
2796
|
+
`${(0, utils_js_1.availableClause)(Object.keys(this.meta.relations), 'It has no relations.')} ` +
|
|
2797
|
+
'(`_count` is supported on the top-level `with` only, on every relationLoadStrategy.)');
|
|
2798
|
+
}
|
|
2799
|
+
for (const [relName, opt] of Object.entries(withClause)) {
|
|
2800
|
+
if (!opt || opt === true || relName === '_count')
|
|
2801
|
+
continue;
|
|
2802
|
+
const nested = opt.with;
|
|
2803
|
+
const rel = (0, utils_js_1.ownLookup)(this.meta.relations, relName);
|
|
2804
|
+
if (!rel || !nested || typeof nested !== 'object' || !this.schema.tables[rel.to])
|
|
2805
|
+
continue;
|
|
2806
|
+
new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions()).assertCountTopLevelOnly(nested, depth + 1);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* The reserved `with: { _count: ... }` key: one count per selected to-many
|
|
2811
|
+
* relation, attached as `_count: { rel: n }` (0 for a parent with no rows),
|
|
2812
|
+
* which is the SQL engines' output. The relations are selected by the shared
|
|
2813
|
+
* `resolveCountRelations`, so `true` counts every to-many relation and the
|
|
2814
|
+
* refusals match. Each count honours the counted table's global filter the
|
|
2815
|
+
* way the SQL engines' correlated `COUNT(*)` does: hasMany counts the
|
|
2816
|
+
* surviving child rows, manyToMany counts junction rows whose TARGET
|
|
2817
|
+
* survives. One grouped query per relation per key chunk.
|
|
2818
|
+
*/
|
|
2819
|
+
async loadRelationCounts(parents, countSpec, timeout, skip) {
|
|
2820
|
+
const rels = (0, batched_loader_js_1.resolveCountRelations)(this.meta, countSpec);
|
|
2821
|
+
const countsOf = new Map();
|
|
2822
|
+
for (const parent of parents) {
|
|
2823
|
+
const counts = {};
|
|
2824
|
+
for (const rel of rels)
|
|
2825
|
+
counts[rel.name] = 0;
|
|
2826
|
+
const row = parent;
|
|
2827
|
+
delete row._count;
|
|
2828
|
+
row._count = counts;
|
|
2829
|
+
countsOf.set(parent, counts);
|
|
2830
|
+
}
|
|
2831
|
+
for (const rel of rels) {
|
|
2832
|
+
const { parentKeyField, byKey } = rel.type === 'manyToMany'
|
|
2833
|
+
? await this.countManyToMany(parents, rel, timeout, skip)
|
|
2834
|
+
: await this.countHasMany(parents, rel, timeout, skip);
|
|
2835
|
+
for (const parent of parents) {
|
|
2836
|
+
const k = this.joinKey(parent[parentKeyField]);
|
|
2837
|
+
countsOf.get(parent)[rel.name] = (k == null ? undefined : byKey.get(k)) ?? 0;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
/** Child rows per parent key for a hasMany `_count`, through the child's own groupBy. */
|
|
2842
|
+
async countHasMany(parents, rel, timeout, skip) {
|
|
2843
|
+
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
2844
|
+
const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
2845
|
+
if (fk.length > 1 || rk.length > 1) {
|
|
2846
|
+
throw new errors_js_1.UnsupportedFeatureError('composite-key relation _count', 'PowDB', `relation "${rel.name}"`);
|
|
2847
|
+
}
|
|
2848
|
+
const targetMeta = this.schema.tables[rel.to];
|
|
2849
|
+
if (!targetMeta)
|
|
2850
|
+
throw new errors_js_1.ValidationError(`Relation "${rel.name}" targets unknown table "${rel.to}".`);
|
|
2851
|
+
const parentKeyField = this.meta.reverseColumnMap[rk[0]] ?? rk[0];
|
|
2852
|
+
const childKeyField = targetMeta.reverseColumnMap[fk[0]] ?? fk[0];
|
|
2853
|
+
const keys = [
|
|
2854
|
+
...new Set(parents.map((p) => p[parentKeyField]).filter((k) => k != null)),
|
|
2855
|
+
];
|
|
2856
|
+
const byKey = new Map();
|
|
2857
|
+
const childQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
2858
|
+
const chunkSize = this.keyChunkSize(targetMeta, fk[0]);
|
|
2859
|
+
for (let i = 0; i < keys.length; i += chunkSize) {
|
|
2860
|
+
const groups = (await childQi.groupBy({
|
|
2861
|
+
by: [childKeyField],
|
|
2862
|
+
where: { [childKeyField]: { in: keys.slice(i, i + chunkSize) } },
|
|
2863
|
+
_count: true,
|
|
2864
|
+
timeout,
|
|
2865
|
+
// The key is only a stitch key and never returned, so a PII-tagged
|
|
2866
|
+
// foreign key does not make the count a PII read.
|
|
2867
|
+
includePii: types_js_1.UNSAFE,
|
|
2868
|
+
...(skip !== undefined ? { skipGlobalFilters: skip } : {}),
|
|
2869
|
+
}));
|
|
2870
|
+
for (const g of groups) {
|
|
2871
|
+
const k = this.joinKey(g[childKeyField]);
|
|
2872
|
+
if (k != null)
|
|
2873
|
+
byKey.set(k, Number(g._count ?? 0));
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return { parentKeyField, byKey };
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* Junction rows per parent key for a manyToMany `_count`. When the target
|
|
2880
|
+
* table has a global filter in force, only junction rows whose target row
|
|
2881
|
+
* survives it are counted, as the SQL engines' `EXISTS` on the target does.
|
|
2882
|
+
*/
|
|
2883
|
+
async countManyToMany(parents, rel, timeout, skip) {
|
|
2884
|
+
const through = rel.through;
|
|
2885
|
+
if (!through)
|
|
2886
|
+
throw new errors_js_1.ValidationError(`manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
|
|
2887
|
+
const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
|
|
2888
|
+
const targetJ = (0, schema_js_1.normalizeKeyColumns)(through.targetKey);
|
|
2889
|
+
const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
2890
|
+
const targetMeta = this.schema.tables[rel.to];
|
|
2891
|
+
if (!targetMeta)
|
|
2892
|
+
throw new errors_js_1.ValidationError(`Relation "${rel.name}" targets unknown table "${rel.to}".`);
|
|
2893
|
+
if (sourceJ.length > 1 || targetJ.length > 1 || sourceRef.length > 1 || targetMeta.primaryKey.length > 1) {
|
|
2894
|
+
throw new errors_js_1.UnsupportedFeatureError('composite-key relation _count', 'PowDB', `relation "${rel.name}"`);
|
|
2895
|
+
}
|
|
2896
|
+
const parentKeyField = this.meta.reverseColumnMap[sourceRef[0]] ?? sourceRef[0];
|
|
2897
|
+
const keys = [
|
|
2898
|
+
...new Set(parents.map((p) => p[parentKeyField]).filter((k) => k != null)),
|
|
2899
|
+
];
|
|
2900
|
+
// (1) Junction rows for these parents.
|
|
2901
|
+
const junctionMeta = this.schema.tables[through.table];
|
|
2902
|
+
const sourceJColMeta = junctionMeta?.columns.find((c) => c.name === sourceJ[0]);
|
|
2903
|
+
const pairs = [];
|
|
2904
|
+
const chunkSize = this.keyChunkSize(junctionMeta, sourceJ[0]);
|
|
2905
|
+
for (let i = 0; i < keys.length; i += chunkSize) {
|
|
2906
|
+
const params = [];
|
|
2907
|
+
const keyClause = this.inClause(this.colRefName(sourceJ[0]), keys.slice(i, i + chunkSize), params, sourceJColMeta);
|
|
2908
|
+
const powql = `${(0, powdb_shared_js_1.quotePowqlIdent)(through.table)} filter ${keyClause} ` +
|
|
2909
|
+
`{ ${this.colRefName(sourceJ[0])}, ${this.colRefName(targetJ[0])} }`;
|
|
2910
|
+
const { rows } = await this.exec(powql, params, timeout, 'findMany');
|
|
2911
|
+
for (const row of rows) {
|
|
2912
|
+
const sk = this.joinKey(row[sourceJ[0]]);
|
|
2913
|
+
const tk = this.joinKey(row[targetJ[0]]);
|
|
2914
|
+
if (sk != null && tk != null)
|
|
2915
|
+
pairs.push([sk, tk, row[targetJ[0]]]);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
// (2) With a target global filter in force, keep the targets that survive it.
|
|
2919
|
+
const gf = (0, where_js_1.resolveGlobalFilterFrom)(this.options.globalFilters, rel.to, (0, types_js_1.resolveSkipGlobalFilters)(skip), this.schema.tables);
|
|
2920
|
+
let surviving = null;
|
|
2921
|
+
if (gf && pairs.length) {
|
|
2922
|
+
surviving = new Set();
|
|
2923
|
+
const pkCol = targetMeta.primaryKey[0];
|
|
2924
|
+
const pkField = targetMeta.reverseColumnMap[pkCol] ?? pkCol;
|
|
2925
|
+
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.loaderChildOptions());
|
|
2926
|
+
const targets = [...new Map(pairs.map(([, tk, raw]) => [tk, raw])).values()];
|
|
2927
|
+
const targetChunk = this.keyChunkSize(targetMeta, pkCol);
|
|
2928
|
+
for (let i = 0; i < targets.length; i += targetChunk) {
|
|
2929
|
+
const rows = (await targetQi.findMany({
|
|
2930
|
+
where: { [pkField]: { in: targets.slice(i, i + targetChunk) } },
|
|
2931
|
+
select: { [pkField]: true },
|
|
2932
|
+
timeout,
|
|
2933
|
+
includePii: types_js_1.UNSAFE,
|
|
2934
|
+
...(skip !== undefined ? { skipGlobalFilters: skip } : {}),
|
|
2935
|
+
}));
|
|
2936
|
+
for (const row of rows) {
|
|
2937
|
+
const k = this.joinKey(row[pkField]);
|
|
2938
|
+
if (k != null)
|
|
2939
|
+
surviving.add(k);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
const byKey = new Map();
|
|
2944
|
+
for (const [sk, tk] of pairs) {
|
|
2945
|
+
if (surviving && !surviving.has(tk))
|
|
2946
|
+
continue;
|
|
2947
|
+
byKey.set(sk, (byKey.get(sk) ?? 0) + 1);
|
|
2948
|
+
}
|
|
2949
|
+
return { parentKeyField, byKey };
|
|
1923
2950
|
}
|
|
1924
2951
|
/**
|
|
1925
2952
|
* manyToMany nested read, a three-hop batched loader (no `json_agg`/join
|
|
@@ -1929,7 +2956,7 @@ class PowqlInterface {
|
|
|
1929
2956
|
* single-key N+1 loaders; the junction's source/target columns must be single
|
|
1930
2957
|
* (composite junction keys would need PowQL tuple-`in`, which it lacks).
|
|
1931
2958
|
*/
|
|
1932
|
-
async loadManyToMany(parents, rel, relName, opt, timeout, includePii = false) {
|
|
2959
|
+
async loadManyToMany(parents, rel, relName, opt, timeout, includePii = false, skip) {
|
|
1933
2960
|
const through = rel.through;
|
|
1934
2961
|
if (!through)
|
|
1935
2962
|
throw new errors_js_1.ValidationError(`manyToMany relation "${relName}" is missing its junction (\`through\`).`);
|
|
@@ -2016,7 +3043,7 @@ class PowqlInterface {
|
|
|
2016
3043
|
if (userSelect) {
|
|
2017
3044
|
fetchOptions = {
|
|
2018
3045
|
...options,
|
|
2019
|
-
select: { ...userSelect, [targetPkField]: true },
|
|
3046
|
+
select: (0, batched_loader_js_1.markStitchOnly)({ ...userSelect, [targetPkField]: true }, [targetPkField]),
|
|
2020
3047
|
};
|
|
2021
3048
|
}
|
|
2022
3049
|
else if (userOmit) {
|
|
@@ -2049,6 +3076,7 @@ class PowqlInterface {
|
|
|
2049
3076
|
timeout: options.timeout ?? timeout,
|
|
2050
3077
|
// Public findMany, so the sentinel form. See loadRelation above.
|
|
2051
3078
|
includePii: includePii ? types_js_1.UNSAFE : undefined,
|
|
3079
|
+
...(skip !== undefined ? { skipGlobalFilters: skip } : {}),
|
|
2052
3080
|
}));
|
|
2053
3081
|
for (const t of targets) {
|
|
2054
3082
|
const pk = String(t[targetPkField]);
|
|
@@ -2186,9 +3214,9 @@ class PowqlInterface {
|
|
|
2186
3214
|
return tableMeta.indexes.some((idx) => idx.unique && !idx.docPath && idx.columns.length === 1 && idx.columns[0] === col);
|
|
2187
3215
|
}
|
|
2188
3216
|
/** Dispatch one eligible relation to the correct native-join loader. */
|
|
2189
|
-
async loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false) {
|
|
3217
|
+
async loadRelationViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false, skip) {
|
|
2190
3218
|
if (rel.type === 'manyToMany') {
|
|
2191
|
-
await this.loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii);
|
|
3219
|
+
await this.loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii, skip);
|
|
2192
3220
|
return;
|
|
2193
3221
|
}
|
|
2194
3222
|
const options = (opt === true ? {} : opt);
|
|
@@ -2207,7 +3235,7 @@ class PowqlInterface {
|
|
|
2207
3235
|
const parentKeyField = this.meta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
|
|
2208
3236
|
const params = [];
|
|
2209
3237
|
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
2210
|
-
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout);
|
|
3238
|
+
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 'c', params, options.timeout ?? timeout, skip);
|
|
2211
3239
|
const order = targetQi.buildOrder(options.orderBy, params, 'c');
|
|
2212
3240
|
// No limit/offset clause: on the join they would bound the TOTAL child
|
|
2213
3241
|
// count across every parent. The relation's page is per parent (pageOf).
|
|
@@ -2233,7 +3261,7 @@ class PowqlInterface {
|
|
|
2233
3261
|
* → the already-fetched side (alias `p`), correlating `__tpk` from the
|
|
2234
3262
|
* junction's source key. Always a list, stitched exactly like the loader.
|
|
2235
3263
|
*/
|
|
2236
|
-
async loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false) {
|
|
3264
|
+
async loadManyToManyViaJoin(parents, rel, relName, opt, parent, timeout, includePii = false, skip) {
|
|
2237
3265
|
const through = rel.through;
|
|
2238
3266
|
if (!through)
|
|
2239
3267
|
throw new errors_js_1.ValidationError(`manyToMany relation "${relName}" is missing its junction (\`through\`).`);
|
|
@@ -2249,7 +3277,7 @@ class PowqlInterface {
|
|
|
2249
3277
|
const parentRefField = this.meta.reverseColumnMap[sourceRefCol] ?? sourceRefCol;
|
|
2250
3278
|
const params = [];
|
|
2251
3279
|
const { cols: childCols, forcedPk: childForcedPk } = this.joinChildCols(targetQi, options, includePii);
|
|
2252
|
-
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 't', params, options.timeout ?? timeout);
|
|
3280
|
+
const filter = await this.joinFilter(targetQi, parent.resolvedWhere, options.where, 't', params, options.timeout ?? timeout, skip);
|
|
2253
3281
|
const proj = this.joinProjection(childCols, `j.${(0, powdb_shared_js_1.quotePowqlIdent)(sourceJCol)}`, 't');
|
|
2254
3282
|
const powql = `${targetQi.qt} as t ` +
|
|
2255
3283
|
`join ${(0, powdb_shared_js_1.quotePowqlIdent)(through.table)} as j on t.${(0, powdb_shared_js_1.quotePowqlIdent)(targetPkCol)} = j.${(0, powdb_shared_js_1.quotePowqlIdent)(targetJCol)} ` +
|
|
@@ -2293,14 +3321,18 @@ class PowqlInterface {
|
|
|
2293
3321
|
* to literal in-lists before the base query ran); the relation where is resolved
|
|
2294
3322
|
* on the target the same way before qualifying, so a nested relation filter in
|
|
2295
3323
|
* the relation `where` never reaches the join unresolved. Params bind in order.
|
|
3324
|
+
* Each side carries its own table's global filter under the query's `skip`,
|
|
3325
|
+
* so the join returns exactly the rows the loaders would.
|
|
2296
3326
|
*/
|
|
2297
|
-
async joinFilter(targetQi, parentResolvedWhere, relWhere, childAlias, params, timeout) {
|
|
3327
|
+
async joinFilter(targetQi, parentResolvedWhere, relWhere, childAlias, params, timeout, skip) {
|
|
2298
3328
|
const parts = [];
|
|
2299
|
-
const
|
|
3329
|
+
const parentGf = await this.globalFilterWhere(skip, timeout);
|
|
3330
|
+
const pw = this.applyGlobalFilter(this.buildWhere(parentResolvedWhere, params, 'p'), params, parentGf, 'p');
|
|
2300
3331
|
if (pw)
|
|
2301
3332
|
parts.push(pw);
|
|
2302
|
-
const relResolved = await targetQi.resolveRelationFilters(relWhere, timeout);
|
|
2303
|
-
const
|
|
3333
|
+
const relResolved = await targetQi.resolveRelationFilters(relWhere, timeout, skip);
|
|
3334
|
+
const targetGf = await targetQi.globalFilterWhere(skip, timeout);
|
|
3335
|
+
const rw = targetQi.applyGlobalFilter(targetQi.buildWhere(relResolved, params, childAlias), params, targetGf, childAlias);
|
|
2304
3336
|
if (rw)
|
|
2305
3337
|
parts.push(rw);
|
|
2306
3338
|
return parts.length ? ` filter ${parts.join(' and ')}` : '';
|
|
@@ -2442,8 +3474,12 @@ class PowqlInterface {
|
|
|
2442
3474
|
* Aliases share the parent statement's counter, so arbitrarily deep and
|
|
2443
3475
|
* self-referential trees stay collision-free (the same discipline as the SQL
|
|
2444
3476
|
* engine's json_agg subqueries).
|
|
3477
|
+
*
|
|
3478
|
+
* The target's global filter is ANDed into the block's filter at every level,
|
|
3479
|
+
* under the query's `skip`, exactly as the SQL engines AND it into each
|
|
3480
|
+
* relation subquery: a block that omitted it handed back another tenant's rows.
|
|
2445
3481
|
*/
|
|
2446
|
-
async buildNestedBlock(plan, parentAlias, aliasCtr, params, timeout) {
|
|
3482
|
+
async buildNestedBlock(plan, parentAlias, aliasCtr, params, timeout, skip) {
|
|
2447
3483
|
const { rel, relName, targetQi, options, single } = plan;
|
|
2448
3484
|
const alias = `t${aliasCtr.n++}`;
|
|
2449
3485
|
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey)[0];
|
|
@@ -2454,8 +3490,9 @@ class PowqlInterface {
|
|
|
2454
3490
|
// child where chains behind it as a parenthesized `and` group, which the
|
|
2455
3491
|
// grammar accepts even when the group itself contains `or`.
|
|
2456
3492
|
const correlation = `${alias}.${(0, powdb_shared_js_1.quotePowqlIdent)(childKeyCol)} = ${parentAlias}.${(0, powdb_shared_js_1.quotePowqlIdent)(parentKeyCol)}`;
|
|
2457
|
-
const resolved = await targetQi.resolveRelationFilters(options.where, timeout);
|
|
2458
|
-
const
|
|
3493
|
+
const resolved = await targetQi.resolveRelationFilters(options.where, timeout, skip);
|
|
3494
|
+
const targetGf = await targetQi.globalFilterWhere(skip, timeout);
|
|
3495
|
+
const childWhere = targetQi.applyGlobalFilter(targetQi.buildWhere(resolved, params, alias), params, targetGf, alias);
|
|
2459
3496
|
const filter = childWhere ? `${correlation} and (${childWhere})` : correlation;
|
|
2460
3497
|
const order = targetQi.buildOrder(options.orderBy, params, alias);
|
|
2461
3498
|
const limitClause = single
|
|
@@ -2466,7 +3503,7 @@ class PowqlInterface {
|
|
|
2466
3503
|
const offsetClause = !single && options.offset ? ` offset ${this.param(options.offset, params)}` : '';
|
|
2467
3504
|
const parts = plan.cols.map((c) => `${(0, powdb_shared_js_1.quotePowqlIdent)(c)}: ${alias}.${(0, powdb_shared_js_1.quotePowqlIdent)(c)}`);
|
|
2468
3505
|
for (const child of plan.children) {
|
|
2469
|
-
parts.push(await targetQi.buildNestedBlock(child, alias, aliasCtr, params, timeout));
|
|
3506
|
+
parts.push(await targetQi.buildNestedBlock(child, alias, aliasCtr, params, timeout, skip));
|
|
2470
3507
|
}
|
|
2471
3508
|
return (`${(0, powdb_shared_js_1.quotePowqlIdent)(relName)}: ${targetQi.qt} as ${alias} ` +
|
|
2472
3509
|
`filter ${filter}${order}${limitClause}${offsetClause} { ${parts.join(', ')} }`);
|
|
@@ -2497,7 +3534,7 @@ class PowqlInterface {
|
|
|
2497
3534
|
}
|
|
2498
3535
|
}
|
|
2499
3536
|
const items = Array.isArray(value) ? value : [];
|
|
2500
|
-
const shaped = items.map((o) => (0, powdb_shared_js_1.rowToEntity)(o, plan.targetQi.meta, true));
|
|
3537
|
+
const shaped = items.map((o) => (0, powdb_shared_js_1.rowToEntity)(o, plan.targetQi.meta, true, this.schema.enums));
|
|
2501
3538
|
for (const child of shaped) {
|
|
2502
3539
|
for (const sub of plan.children)
|
|
2503
3540
|
plan.targetQi.attachOneNested(child, sub);
|
|
@@ -2581,12 +3618,13 @@ class PowqlInterface {
|
|
|
2581
3618
|
* link path would regress a hot path for no gain. Requires: single-column
|
|
2582
3619
|
* belongsTo; no relation `with` / `where` / `distinct` / `orderBy` /
|
|
2583
3620
|
* `limit` / `offset` (a scalar path has no per-hop filter/order and cannot
|
|
2584
|
-
* reproduce those, such inputs stay on the loader for exact parity);
|
|
3621
|
+
* reproduce those, such inputs stay on the loader for exact parity); no
|
|
3622
|
+
* global filter on the target, for the same reason; a link
|
|
2585
3623
|
* name and all projected columns that are bare identifiers (a quoted segment in
|
|
2586
3624
|
* a dotted link path is outside the verified spelling, fall back); and a
|
|
2587
3625
|
* DECLARED link that verifiably matches (`findMatchingLink`).
|
|
2588
3626
|
*/
|
|
2589
|
-
async planLinkPathRelation(relName, rel, opt, includePii, parentCols, index) {
|
|
3627
|
+
async planLinkPathRelation(relName, rel, opt, includePii, parentCols, index, skip) {
|
|
2590
3628
|
if (!this.capabilities.linkPaths)
|
|
2591
3629
|
return null;
|
|
2592
3630
|
if (rel.type !== 'belongsTo')
|
|
@@ -2603,6 +3641,10 @@ class PowqlInterface {
|
|
|
2603
3641
|
if (options.orderBy || options.limit !== undefined || options.offset)
|
|
2604
3642
|
return null;
|
|
2605
3643
|
const targetQi = new PowqlInterface(this.pool, rel.to, this.schema, [], this.options);
|
|
3644
|
+
// A link hop has no per-hop filter, so it cannot carry the target's global
|
|
3645
|
+
// filter; such a relation stays on the loaders, which apply it.
|
|
3646
|
+
if (targetQi.hasGlobalFilter(skip))
|
|
3647
|
+
return null;
|
|
2606
3648
|
// `projectionPlan`, not `projectedColumns`: the plan is what carries
|
|
2607
3649
|
// `forcedPk`, and reading the column LIST alone is what made this path's
|
|
2608
3650
|
// strip dead. See the pkProjected line below.
|
|
@@ -2680,7 +3722,7 @@ class PowqlInterface {
|
|
|
2680
3722
|
raw[c] = row[`${plan.keyPrefix}${c}`];
|
|
2681
3723
|
delete row[`${plan.keyPrefix}${c}`];
|
|
2682
3724
|
}
|
|
2683
|
-
const child = (0, powdb_shared_js_1.rowToEntity)(raw, plan.targetQi.meta, native);
|
|
3725
|
+
const child = (0, powdb_shared_js_1.rowToEntity)(raw, plan.targetQi.meta, native, this.schema.enums);
|
|
2684
3726
|
if (child[pkField] == null) {
|
|
2685
3727
|
row[plan.relName] = null;
|
|
2686
3728
|
continue;
|
|
@@ -2731,6 +3773,42 @@ class PowqlInterface {
|
|
|
2731
3773
|
}
|
|
2732
3774
|
return out;
|
|
2733
3775
|
}
|
|
3776
|
+
/**
|
|
3777
|
+
* Fill every omitted column that declares a default, since PowDB has no
|
|
3778
|
+
* column defaults of its own (its only generated value is `auto`, which is
|
|
3779
|
+
* left to the engine). Without this an omitted defaulted column was written
|
|
3780
|
+
* as NULL, silently, or failed NOT NULL where every SQL engine applied the
|
|
3781
|
+
* default.
|
|
3782
|
+
*
|
|
3783
|
+
* "Omitted" means absent or `undefined` under BOTH spellings, exactly the
|
|
3784
|
+
* columns a SQL insert leaves to the database; an explicit `null` stays
|
|
3785
|
+
* `null`. The value comes from the column's declared default expression,
|
|
3786
|
+
* evaluated per row (`now()` is the insert time). An expression this cannot
|
|
3787
|
+
* evaluate faithfully, or metadata that does not carry the expression, is
|
|
3788
|
+
* refused rather than guessed.
|
|
3789
|
+
*/
|
|
3790
|
+
applyColumnDefaults(data) {
|
|
3791
|
+
let out;
|
|
3792
|
+
for (const col of this.meta.columns) {
|
|
3793
|
+
if (!col.hasDefault || col.isGenerated || this.meta.primaryKey.includes(col.name))
|
|
3794
|
+
continue;
|
|
3795
|
+
if (data[col.field] !== undefined || data[col.name] !== undefined)
|
|
3796
|
+
continue;
|
|
3797
|
+
const expr = col.defaultValue;
|
|
3798
|
+
const value = expr == null ? NO_DEFAULT : emulateColumnDefault(expr, col, this.schema.enums);
|
|
3799
|
+
if (value === NO_DEFAULT) {
|
|
3800
|
+
throw new errors_js_1.UnsupportedFeatureError(`The default of column "${col.name}" on "${this.table}"`, 'PowDB', (expr == null
|
|
3801
|
+
? 'The schema metadata marks the column as defaulted but does not carry the default expression'
|
|
3802
|
+
: `The default expression \`${expr}\` has no client-side equivalent`) +
|
|
3803
|
+
', and PowDB has no column defaults, so the value cannot be applied. Supply the field explicitly ' +
|
|
3804
|
+
'on create (or build the metadata with defineSchema, which records literal, now(), current_date and ' +
|
|
3805
|
+
'gen_random_uuid() defaults).');
|
|
3806
|
+
}
|
|
3807
|
+
out ??= { ...data };
|
|
3808
|
+
out[col.field] = value;
|
|
3809
|
+
}
|
|
3810
|
+
return out ?? data;
|
|
3811
|
+
}
|
|
2734
3812
|
/**
|
|
2735
3813
|
* The table name as a PowQL type reference, backtick-quoted when it is a
|
|
2736
3814
|
* reserved word (e.g. a table named `order`). Used in every emitted
|
|
@@ -2740,12 +3818,13 @@ class PowqlInterface {
|
|
|
2740
3818
|
return (0, powdb_shared_js_1.quotePowqlIdent)(this.table);
|
|
2741
3819
|
}
|
|
2742
3820
|
async create(args) {
|
|
3821
|
+
args = this.normalizeArgs(args);
|
|
2743
3822
|
return this.withMiddleware('create', args, async () => {
|
|
2744
3823
|
const plan = this.writeReturnPlan(args);
|
|
2745
3824
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
|
|
2746
3825
|
return this.nestedCreate(args);
|
|
2747
3826
|
}
|
|
2748
|
-
const data = this.applyPkDefault(args.data);
|
|
3827
|
+
const data = this.applyColumnDefaults(this.applyPkDefault(args.data));
|
|
2749
3828
|
const assigns = this.scalarData(data);
|
|
2750
3829
|
const params = [];
|
|
2751
3830
|
const body = assigns
|
|
@@ -2806,16 +3885,28 @@ class PowqlInterface {
|
|
|
2806
3885
|
}
|
|
2807
3886
|
}
|
|
2808
3887
|
async createMany(args) {
|
|
3888
|
+
args = this.normalizeArgs(args);
|
|
2809
3889
|
return this.withMiddleware('createMany', args, async () => {
|
|
3890
|
+
if (args.onConflict !== undefined) {
|
|
3891
|
+
throw new errors_js_1.UnsupportedFeatureError('createMany({ onConflict })', 'powdb', "PowQL's upsert statement takes one row. Call upsert() per row inside $transaction.");
|
|
3892
|
+
}
|
|
3893
|
+
// Validated as every engine validates it; it acts only on onConflict's
|
|
3894
|
+
// update, which is refused above.
|
|
3895
|
+
(0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
2810
3896
|
if (args.skipDuplicates) {
|
|
2811
3897
|
// PowQL's `insert … returning` has no conflict clause, so there is no
|
|
2812
3898
|
// faithful skip-duplicates form; refuse rather than silently insert.
|
|
2813
3899
|
throw new errors_js_1.UnsupportedFeatureError('createMany({ skipDuplicates: true })', 'powdb', 'PowQL insert has no ON CONFLICT DO NOTHING equivalent, filter duplicates before inserting.');
|
|
2814
3900
|
}
|
|
2815
|
-
|
|
2816
|
-
|
|
3901
|
+
// Resolved before the write, so a bad projection writes nothing.
|
|
3902
|
+
const plan = this.writeReturnPlan(args);
|
|
3903
|
+
const supplied = args.data.map((d) => this.applyPkDefault(d));
|
|
3904
|
+
if (!supplied.length)
|
|
2817
3905
|
return [];
|
|
2818
|
-
|
|
3906
|
+
// Uniformity is judged on what the caller supplied, as the SQL engines
|
|
3907
|
+
// judge it; defaults are then filled per row.
|
|
3908
|
+
this.assertUniformCreateManyRows(supplied);
|
|
3909
|
+
const inputs = supplied.map((d) => this.applyColumnDefaults(d));
|
|
2819
3910
|
const params = [];
|
|
2820
3911
|
const tuples = inputs.map((d) => {
|
|
2821
3912
|
const assigns = this.scalarData(d);
|
|
@@ -2823,10 +3914,11 @@ class PowqlInterface {
|
|
|
2823
3914
|
});
|
|
2824
3915
|
// Multi-row insert with `returning` hands back every inserted row in one round-trip.
|
|
2825
3916
|
const { rows, native } = await this.exec(`insert ${this.qt} ${tuples.join(', ')} returning`, params, args.timeout, 'createMany');
|
|
2826
|
-
return this.shape(rows, native).map((r) => this.
|
|
3917
|
+
return this.shape(rows, native).map((r) => this.shapeWriteRow(r, plan));
|
|
2827
3918
|
});
|
|
2828
3919
|
}
|
|
2829
3920
|
async update(args) {
|
|
3921
|
+
args = this.normalizeArgs(args);
|
|
2830
3922
|
return this.withMiddleware('update', args, async () => {
|
|
2831
3923
|
const plan = this.writeReturnPlan(args);
|
|
2832
3924
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
|
|
@@ -2840,13 +3932,13 @@ class PowqlInterface {
|
|
|
2840
3932
|
// counts as the key it is (engine parity with the SQL buildUpdate).
|
|
2841
3933
|
const userWhere = this.expandedWhere(args.where);
|
|
2842
3934
|
const params = [];
|
|
2843
|
-
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
3935
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout, args.skipGlobalFilters);
|
|
2844
3936
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2845
3937
|
// `false` here refused an empty where even WITH the opt-in, while every
|
|
2846
3938
|
// SQL engine accepted it: verified by probe on buildUpdate/buildDelete.
|
|
2847
|
-
this.assertCompiledWhere(where, allowFullTableScan, 'update');
|
|
3939
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'update', userWhere);
|
|
2848
3940
|
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'update');
|
|
2849
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
3941
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
2850
3942
|
let setClause = this.buildUpdateAssignments(args.data, params);
|
|
2851
3943
|
// Optimistic locking, matching the SQL engines exactly: bump the version
|
|
2852
3944
|
// column in the SET, add `version = expected` to the filter, and treat
|
|
@@ -2881,12 +3973,15 @@ class PowqlInterface {
|
|
|
2881
3973
|
});
|
|
2882
3974
|
}
|
|
2883
3975
|
async updateMany(args) {
|
|
3976
|
+
args = this.normalizeArgs(args);
|
|
3977
|
+
(0, writes_js_1.assertNoBulkWritePaging)(this.table, 'updateMany', args);
|
|
3978
|
+
this.assertNoOptimisticLock(args, 'updateMany');
|
|
2884
3979
|
return this.withMiddleware('updateMany', args, async () => {
|
|
2885
3980
|
const params = [];
|
|
2886
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
3981
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
2887
3982
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2888
|
-
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'updateMany');
|
|
2889
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
3983
|
+
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'updateMany', args.where);
|
|
3984
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
2890
3985
|
const setClause = this.buildUpdateAssignments(args.data, params);
|
|
2891
3986
|
const filter = where ? ` filter ${where}` : '';
|
|
2892
3987
|
const { rowCount } = await this.exec(`${this.qt}${filter} update { ${setClause} }`, params, args.timeout, 'updateMany');
|
|
@@ -2902,25 +3997,29 @@ class PowqlInterface {
|
|
|
2902
3997
|
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
2903
3998
|
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not updateMany()`);
|
|
2904
3999
|
}
|
|
4000
|
+
// PowQL update assignments take values and the atomic operators only.
|
|
4001
|
+
if ((0, expr_js_1.isSqlExpr)(value) || (0, column_ref_js_1.isColumnRef)(value)) {
|
|
4002
|
+
throw new errors_js_1.UnsupportedFeatureError(`update expressions (expr / col() in update data, "${this.table}.${field}")`, 'powdb', 'Compute the value in JavaScript and write it as a plain value, or use the atomic operators ' +
|
|
4003
|
+
'(increment, decrement, multiply, divide).');
|
|
4004
|
+
}
|
|
2905
4005
|
const colMeta = this.column(field);
|
|
2906
4006
|
const ref = this.ref(field);
|
|
2907
4007
|
const col = (0, powdb_shared_js_1.quotePowqlIdent)(colMeta.name);
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
else
|
|
2921
|
-
throw new errors_js_1.ValidationError(`Unsupported update operator on "${field}".`);
|
|
4008
|
+
const op = this.setOperatorOf(colMeta, value);
|
|
4009
|
+
if (op === 'set') {
|
|
4010
|
+
parts.push(`${col} := ${this.writeRef(value.set, colMeta, params)}`);
|
|
4011
|
+
}
|
|
4012
|
+
else if (op !== null) {
|
|
4013
|
+
this.assertDecimalComparable(colMeta, `\`${op}\``);
|
|
4014
|
+
// Same rule and message as the SQL engines' buildSetClauses.
|
|
4015
|
+
const operand = value[op];
|
|
4016
|
+
if (typeof operand !== 'number' || !Number.isFinite(operand)) {
|
|
4017
|
+
throw new errors_js_1.ValidationError(`update operator "${op}" on "${this.table}.${field}" requires a finite number, got ${typeof operand}`);
|
|
4018
|
+
}
|
|
4019
|
+
parts.push(`${col} := ${ref} ${POWQL_ARITHMETIC[op]} ${this.writeRef(operand, colMeta, params)}`);
|
|
2922
4020
|
}
|
|
2923
4021
|
else {
|
|
4022
|
+
this.assertBindableSetValue(field, colMeta, value);
|
|
2924
4023
|
parts.push(`${col} := ${this.writeRef(value, colMeta, params)}`);
|
|
2925
4024
|
}
|
|
2926
4025
|
}
|
|
@@ -2928,6 +4027,40 @@ class PowqlInterface {
|
|
|
2928
4027
|
throw new errors_js_1.ValidationError(`update on "${this.table}" has no fields to set.`);
|
|
2929
4028
|
return parts.join(', ');
|
|
2930
4029
|
}
|
|
4030
|
+
/**
|
|
4031
|
+
* The atomic operator an update value names, or `null` for a plain value.
|
|
4032
|
+
* The SQL engines' `setOperatorOf` rule: a plain object with EXACTLY one key
|
|
4033
|
+
* from {@link UPDATE_OPERATOR_KEYS}, and on a json column only `set`, since
|
|
4034
|
+
* an object there IS the document (`{ increment: 1 }` is the document
|
|
4035
|
+
* `{"increment":1}`, and `{ set: doc }` writes a document shaped like
|
|
4036
|
+
* `{ set: ... }`).
|
|
4037
|
+
*/
|
|
4038
|
+
setOperatorOf(colMeta, value) {
|
|
4039
|
+
if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
|
|
4040
|
+
return null;
|
|
4041
|
+
const keys = Object.keys(value);
|
|
4042
|
+
if (keys.length !== 1 || !filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
4043
|
+
return null;
|
|
4044
|
+
const op = keys[0];
|
|
4045
|
+
if (op !== 'set' && (0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums))
|
|
4046
|
+
return null;
|
|
4047
|
+
return op;
|
|
4048
|
+
}
|
|
4049
|
+
/**
|
|
4050
|
+
* A one-key plain object that is not an update operator, written to a
|
|
4051
|
+
* non-json column, is a misspelled operator (`{ incremnt: 1 }`): refused with
|
|
4052
|
+
* the SQL engines' message rather than stored as text.
|
|
4053
|
+
*/
|
|
4054
|
+
assertBindableSetValue(field, colMeta, value) {
|
|
4055
|
+
if (!(0, filters_js_1.isUnmatchedPlainObject)(value) || (0, powdb_shared_js_1.isJsonColumn)(colMeta, this.schema.enums))
|
|
4056
|
+
return;
|
|
4057
|
+
const keys = Object.keys(value);
|
|
4058
|
+
if (keys.length !== 1 || filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
4059
|
+
return;
|
|
4060
|
+
throw new errors_js_1.ValidationError(`Unknown update operator "${keys[0]}" on "${this.table}.${field}". ` +
|
|
4061
|
+
`Supported: ${[...filters_js_1.UPDATE_OPERATOR_KEYS].join(', ')}. ` +
|
|
4062
|
+
'A plain object is only written as a value on a json/jsonb column.');
|
|
4063
|
+
}
|
|
2931
4064
|
// -------------------------------------------------------------------------
|
|
2932
4065
|
// Nested writes, create/update whose `data` carries relation ops (create,
|
|
2933
4066
|
// connect, connectOrCreate, disconnect, set, delete, update, upsert). Reuses
|
|
@@ -2984,7 +4117,9 @@ class PowqlInterface {
|
|
|
2984
4117
|
this.pool);
|
|
2985
4118
|
const ctx = {
|
|
2986
4119
|
schema: this.schema,
|
|
2987
|
-
|
|
4120
|
+
// The nested engine's internal `updateMany` for `set` reaches the
|
|
4121
|
+
// privilege sentinel through the same seam the SQL engines use.
|
|
4122
|
+
tx: (0, builder_js_1.unlockNestedWriteTx)(tx),
|
|
2988
4123
|
scopedConnect: this.options?.scopedConnect === true,
|
|
2989
4124
|
};
|
|
2990
4125
|
// Plant the single-writer re-entrancy marker for the implicit tx's
|
|
@@ -3021,19 +4156,21 @@ class PowqlInterface {
|
|
|
3021
4156
|
const tx = {
|
|
3022
4157
|
table: (name) => new PowqlInterface(this.pool, name, this.schema, this.middlewares, opts),
|
|
3023
4158
|
};
|
|
3024
|
-
return { schema: this.schema, tx: tx };
|
|
4159
|
+
return { schema: this.schema, tx: (0, builder_js_1.unlockNestedWriteTx)(tx) };
|
|
3025
4160
|
}
|
|
3026
4161
|
async delete(args) {
|
|
4162
|
+
args = this.normalizeArgs(args);
|
|
4163
|
+
this.assertNoOptimisticLock(args, 'delete');
|
|
3027
4164
|
return this.withMiddleware('delete', args, async () => {
|
|
3028
4165
|
const plan = this.writeReturnPlan(args);
|
|
3029
4166
|
const allowFullTableScan = (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan');
|
|
3030
4167
|
const userWhere = this.expandedWhere(args.where);
|
|
3031
4168
|
const params = [];
|
|
3032
|
-
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
4169
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout, args.skipGlobalFilters);
|
|
3033
4170
|
let where = this.buildWhere(resolvedWhere, params);
|
|
3034
|
-
this.assertCompiledWhere(where, allowFullTableScan, 'delete');
|
|
4171
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'delete', userWhere);
|
|
3035
4172
|
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'delete');
|
|
3036
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
4173
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
3037
4174
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
3038
4175
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
3039
4176
|
const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
|
|
@@ -3043,18 +4180,23 @@ class PowqlInterface {
|
|
|
3043
4180
|
});
|
|
3044
4181
|
}
|
|
3045
4182
|
async deleteMany(args) {
|
|
4183
|
+
args = this.normalizeArgs(args);
|
|
4184
|
+
(0, writes_js_1.assertNoBulkWritePaging)(this.table, 'deleteMany', args);
|
|
4185
|
+
this.assertNoOptimisticLock(args, 'deleteMany');
|
|
3046
4186
|
return this.withMiddleware('deleteMany', args, async () => {
|
|
3047
4187
|
const params = [];
|
|
3048
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
4188
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
3049
4189
|
let where = this.buildWhere(resolvedWhere, params);
|
|
3050
|
-
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'deleteMany');
|
|
3051
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
4190
|
+
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'deleteMany', args.where);
|
|
4191
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
3052
4192
|
const filter = where ? ` filter ${where}` : '';
|
|
3053
4193
|
const { rowCount } = await this.exec(`${this.qt}${filter} delete`, params, args.timeout, 'deleteMany');
|
|
3054
4194
|
return { count: rowCount };
|
|
3055
4195
|
});
|
|
3056
4196
|
}
|
|
3057
4197
|
async upsert(args) {
|
|
4198
|
+
args = this.normalizeArgs(args);
|
|
4199
|
+
this.assertNoOptimisticLock(args, 'upsert');
|
|
3058
4200
|
return this.withMiddleware('upsert', args, async () => {
|
|
3059
4201
|
// The single-row rule for `upsert`, shared with the SQL engines through
|
|
3060
4202
|
// query/compound-unique.ts. PowqlInterface is a PARALLEL implementation,
|
|
@@ -3065,7 +4207,7 @@ class PowqlInterface {
|
|
|
3065
4207
|
const upsertWhere = (this.expandedWhere(args.where) ?? {});
|
|
3066
4208
|
this.assertMutationIdentifiesOneRow(upsertWhere, false, 'upsert');
|
|
3067
4209
|
const plan = this.writeReturnPlan(args);
|
|
3068
|
-
const createData = this.applyPkDefault(args.create);
|
|
4210
|
+
const createData = this.applyColumnDefaults(this.applyPkDefault(args.create));
|
|
3069
4211
|
const pkCol = this.meta.primaryKey[0];
|
|
3070
4212
|
const conflictColumns = Object.keys(upsertWhere)
|
|
3071
4213
|
.filter((k) => upsertWhere[k] !== undefined)
|
|
@@ -3085,15 +4227,18 @@ class PowqlInterface {
|
|
|
3085
4227
|
// as a NotFoundError. The lookup runs its find and its update through the
|
|
3086
4228
|
// filtered interface, so another tenant's row is invisible to it: the
|
|
3087
4229
|
// insert then collides on the key and fails, and nothing is overwritten.
|
|
3088
|
-
const filtered = this.
|
|
4230
|
+
const filtered = this.hasGlobalFilter(args.skipGlobalFilters);
|
|
4231
|
+
// The shape the SQL engines answer with ONE conflict statement (`where`
|
|
4232
|
+
// pinned to `create`, a non-empty scalar update).
|
|
4233
|
+
const conflictShape = (0, compound_unique_js_1.upsertWhereMatchesCreate)(this.meta, upsertWhere, (args.create ?? {})) &&
|
|
4234
|
+
!(0, nested_write_js_1.hasRelationFields)(args.create, this.meta) &&
|
|
4235
|
+
!(0, nested_write_js_1.hasRelationFields)(updateData, this.meta) &&
|
|
4236
|
+
Object.values(updateData).some((v) => v !== undefined);
|
|
3089
4237
|
const native = this.meta.primaryKey.length === 1 &&
|
|
3090
4238
|
pkCol !== undefined &&
|
|
3091
4239
|
conflictColumns.length === 1 &&
|
|
3092
4240
|
conflictColumns[0] === pkCol &&
|
|
3093
|
-
|
|
3094
|
-
!(0, nested_write_js_1.hasRelationFields)(args.create, this.meta) &&
|
|
3095
|
-
!(0, nested_write_js_1.hasRelationFields)(updateData, this.meta) &&
|
|
3096
|
-
Object.values(updateData).some((v) => v !== undefined) &&
|
|
4241
|
+
conflictShape &&
|
|
3097
4242
|
!filtered;
|
|
3098
4243
|
if (!native) {
|
|
3099
4244
|
return this.upsertLookupFirst(upsertWhere, createData, updateData, {
|
|
@@ -3101,6 +4246,9 @@ class PowqlInterface {
|
|
|
3101
4246
|
omit: args.omit,
|
|
3102
4247
|
skipGlobalFilters: args.skipGlobalFilters,
|
|
3103
4248
|
timeout: args.timeout,
|
|
4249
|
+
// That statement carries the global filter on its conflict branch, so a
|
|
4250
|
+
// key held by a row the filter hides writes nothing there (E001).
|
|
4251
|
+
hiddenConflictIsNotFound: conflictShape && filtered,
|
|
3104
4252
|
});
|
|
3105
4253
|
}
|
|
3106
4254
|
const params = [];
|
|
@@ -3145,7 +4293,7 @@ class PowqlInterface {
|
|
|
3145
4293
|
* is refused whether or not the row exists (the SQL engines do the same).
|
|
3146
4294
|
*/
|
|
3147
4295
|
async upsertLookupFirst(where, createData, updateData, shape) {
|
|
3148
|
-
const { skipGlobalFilters, timeout, ...projection } = shape;
|
|
4296
|
+
const { skipGlobalFilters, timeout, hiddenConflictIsNotFound, ...projection } = shape;
|
|
3149
4297
|
const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
|
|
3150
4298
|
const time = timeout === undefined ? {} : { timeout };
|
|
3151
4299
|
this.scalarData((0, nested_write_js_1.extractRelationFields)(createData, this.meta).scalars);
|
|
@@ -3162,6 +4310,21 @@ class PowqlInterface {
|
|
|
3162
4310
|
return existing;
|
|
3163
4311
|
return (await tbl.update({ where, data: updateData, ...projection, ...skip, ...time }));
|
|
3164
4312
|
}
|
|
4313
|
+
if (hiddenConflictIsNotFound) {
|
|
4314
|
+
// `where` is pinned to `create`, so the insert below collides exactly
|
|
4315
|
+
// when the global filter is what hid the row: say so, as the SQL engines do.
|
|
4316
|
+
const hidden = await tbl.findUnique({ where, skipGlobalFilters: types_js_1.UNSAFE, ...time });
|
|
4317
|
+
if (hidden) {
|
|
4318
|
+
throw new errors_js_1.NotFoundError({
|
|
4319
|
+
table: this.table,
|
|
4320
|
+
where,
|
|
4321
|
+
operation: 'upsert',
|
|
4322
|
+
message: `upsert on "${this.table}" wrote nothing: the row that conflicts on the given key is excluded by the ` +
|
|
4323
|
+
`configured global filter for "${this.table}", so it was neither updated (the filter hides it) nor ` +
|
|
4324
|
+
'inserted (the key is taken). Pass `skipGlobalFilters: UNSAFE` to address it anyway.',
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
3165
4328
|
return (await tbl.create({ data: createData, ...projection, ...time }));
|
|
3166
4329
|
};
|
|
3167
4330
|
return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
|
|
@@ -3171,11 +4334,12 @@ class PowqlInterface {
|
|
|
3171
4334
|
// -------------------------------------------------------------------------
|
|
3172
4335
|
async count(args = {}) {
|
|
3173
4336
|
this.assertNoForceCustomPlan(args);
|
|
4337
|
+
args = this.normalizeArgs(args);
|
|
3174
4338
|
return this.withMiddleware('count', (args ?? {}), async () => {
|
|
3175
4339
|
const params = [];
|
|
3176
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
4340
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
3177
4341
|
let where = this.buildWhere(resolvedWhere, params);
|
|
3178
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
4342
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
3179
4343
|
const filter = where ? ` filter ${where}` : '';
|
|
3180
4344
|
const { rows } = await this.exec(`count(${this.qt}${filter})`, params, args.timeout, 'count');
|
|
3181
4345
|
return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
|
|
@@ -3235,12 +4399,14 @@ class PowqlInterface {
|
|
|
3235
4399
|
}
|
|
3236
4400
|
async aggregate(args) {
|
|
3237
4401
|
this.assertNoForceCustomPlan(args);
|
|
4402
|
+
this.assertNoPercentile(args);
|
|
4403
|
+
args = this.normalizeArgs(args);
|
|
3238
4404
|
return this.withMiddleware('aggregate', args, async () => {
|
|
3239
4405
|
const result = {};
|
|
3240
4406
|
const filterParams = [];
|
|
3241
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
4407
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
3242
4408
|
let where = this.buildWhere(resolvedWhere, filterParams);
|
|
3243
|
-
where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
|
|
4409
|
+
where = this.applyGlobalFilter(where, filterParams, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
3244
4410
|
const filter = where ? ` filter ${where}` : '';
|
|
3245
4411
|
const plan = [];
|
|
3246
4412
|
if (args._count) {
|
|
@@ -3277,6 +4443,8 @@ class PowqlInterface {
|
|
|
3277
4443
|
result[fn] = acc;
|
|
3278
4444
|
const powfn = fn.slice(1); // sum/avg/min/max
|
|
3279
4445
|
for (const field of Object.keys(spec).filter((f) => spec[f])) {
|
|
4446
|
+
this.assertDecimalComparable(this.column(field), `\`${fn}\``);
|
|
4447
|
+
this.assertEnumAggregate(this.column(field), fn);
|
|
3280
4448
|
// Same PII contract as the SQL engines: _min/_max return a stored cell.
|
|
3281
4449
|
if (fn === '_min' || fn === '_max') {
|
|
3282
4450
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
@@ -3285,6 +4453,8 @@ class PowqlInterface {
|
|
|
3285
4453
|
plan.push({
|
|
3286
4454
|
scalar: `${powfn}(${this.qt}${filter} { ${ref} })`,
|
|
3287
4455
|
inner: `${powfn}(${ref})`,
|
|
4456
|
+
...(fn === '_min' || fn === '_max' ? { col: this.column(field) } : {}),
|
|
4457
|
+
...(fn === '_sum' && isInt8Column(this.column(field)) ? { exact: true } : {}),
|
|
3288
4458
|
assign: (v) => {
|
|
3289
4459
|
acc[field] = v;
|
|
3290
4460
|
},
|
|
@@ -3293,10 +4463,16 @@ class PowqlInterface {
|
|
|
3293
4463
|
}
|
|
3294
4464
|
// A scalar cell and a grouped cell decode through the same wire decoder,
|
|
3295
4465
|
// so one coercion serves both forms.
|
|
3296
|
-
const
|
|
4466
|
+
const decode = (p, cell, native) => p.col
|
|
4467
|
+
? decodeAggregateCell(cell, p.col, native, this.schema.enums)
|
|
4468
|
+
: p.exact
|
|
4469
|
+
? exactIntegerSumCell(cell)
|
|
4470
|
+
: cell == null || cell === 'null'
|
|
4471
|
+
? null
|
|
4472
|
+
: Number(cell);
|
|
3297
4473
|
if (plan.length > 1 && this.groupsAggregatesInOneStatement) {
|
|
3298
4474
|
const proj = plan.map((p, i) => `agg_${i}: ${p.inner}`).join(', ');
|
|
3299
|
-
const { rows } = await this.exec(`${this.qt}${filter} group 1 { ${proj} }`, [...filterParams], args.timeout, 'aggregate');
|
|
4475
|
+
const { rows, native } = await this.exec(`${this.qt}${filter} group 1 { ${proj} }`, [...filterParams], args.timeout, 'aggregate');
|
|
3300
4476
|
// Zero groups means the filter matched no row. The per-field statements
|
|
3301
4477
|
// below answer that case instead of a synthesized one, because what an
|
|
3302
4478
|
// aggregate over NO rows returns is engine-versioned (`sum` answered 0
|
|
@@ -3305,19 +4481,23 @@ class PowqlInterface {
|
|
|
3305
4481
|
const row = rows.length === 1 ? rows[0] : undefined;
|
|
3306
4482
|
if (row) {
|
|
3307
4483
|
for (const [i, p] of plan.entries())
|
|
3308
|
-
p.assign(
|
|
4484
|
+
p.assign(decode(p, row[`agg_${i}`], native));
|
|
3309
4485
|
return result;
|
|
3310
4486
|
}
|
|
3311
4487
|
}
|
|
3312
4488
|
for (const p of plan) {
|
|
3313
|
-
const { rows } = await this.exec(p.scalar, [...filterParams], args.timeout, 'aggregate');
|
|
3314
|
-
p.assign(
|
|
4489
|
+
const { rows, native } = await this.exec(p.scalar, [...filterParams], args.timeout, 'aggregate');
|
|
4490
|
+
p.assign(decode(p, rows[0]?.value, native));
|
|
3315
4491
|
}
|
|
3316
4492
|
return result;
|
|
3317
4493
|
});
|
|
3318
4494
|
}
|
|
3319
4495
|
async groupBy(args) {
|
|
4496
|
+
// The SQL engines' `by` refusals, before anything reads `by`.
|
|
4497
|
+
(0, aggregates_js_1.assertGroupByKeys)(this.table, args?.by);
|
|
3320
4498
|
this.assertNoForceCustomPlan(args);
|
|
4499
|
+
this.assertNoPercentile(args);
|
|
4500
|
+
args = this.normalizeArgs(args);
|
|
3321
4501
|
return this.withMiddleware('groupBy', args, async () => {
|
|
3322
4502
|
// DISTINCT ON has no PowQL equivalent (no DISTINCT ON row source).
|
|
3323
4503
|
if (args.distinctOn) {
|
|
@@ -3338,9 +4518,9 @@ class PowqlInterface {
|
|
|
3338
4518
|
// a per-call legacy fallback on a native-capable pool still decodes right.
|
|
3339
4519
|
const emitNative = Boolean(this.capabilities.nativeRaw);
|
|
3340
4520
|
const params = [];
|
|
3341
|
-
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
4521
|
+
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout, args.skipGlobalFilters);
|
|
3342
4522
|
let where = this.buildWhere(resolvedWhere, params);
|
|
3343
|
-
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
4523
|
+
where = this.applyGlobalFilter(where, params, await this.globalFilterWhere(args.skipGlobalFilters, args.timeout));
|
|
3344
4524
|
const filter = where ? ` filter ${where}` : '';
|
|
3345
4525
|
// Result-key namespace, mirroring the SQL builder's `claimResultKey`
|
|
3346
4526
|
// (query/builder.ts): a group-key / aggregate output-name collision (with
|
|
@@ -3362,6 +4542,8 @@ class PowqlInterface {
|
|
|
3362
4542
|
for (const entry of args.by) {
|
|
3363
4543
|
if (typeof entry === 'string') {
|
|
3364
4544
|
const col = this.column(entry);
|
|
4545
|
+
// Text grouping would put '7.5' and '7.50' in different groups.
|
|
4546
|
+
this.assertDecimalComparable(col, 'A groupBy key');
|
|
3365
4547
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, entry, col.name, 'groupBy `by` key', (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
3366
4548
|
// Keyed by the canonical FIELD, never by whichever of the column's
|
|
3367
4549
|
// two legal spellings the caller wrote (`column` accepts both), so
|
|
@@ -3378,7 +4560,7 @@ class PowqlInterface {
|
|
|
3378
4560
|
}
|
|
3379
4561
|
else {
|
|
3380
4562
|
const col = this.column(entry.field);
|
|
3381
|
-
if (!(0, powdb_shared_js_1.isJsonColumn)(col)) {
|
|
4563
|
+
if (!(0, powdb_shared_js_1.isJsonColumn)(col, this.schema.enums)) {
|
|
3382
4564
|
throw new errors_js_1.ValidationError(`groupBy JSON group key on "${entry.field}" (table "${this.table}") requires a json column.`);
|
|
3383
4565
|
}
|
|
3384
4566
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, entry.field, col.name, 'groupBy JSON `by` key', (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
@@ -3405,6 +4587,7 @@ class PowqlInterface {
|
|
|
3405
4587
|
// Aggregates: `agg_N` internal aliases (PowQL rejects reserved-word
|
|
3406
4588
|
// aliases like `count:`). `aggInner` lets HAVING re-emit the exact inner
|
|
3407
4589
|
// expression by user key; `aggOrderExprs` lets orderBy reference the alias.
|
|
4590
|
+
// `col` is set for a column `_min` / `_max`: a stored cell, decoded by its column.
|
|
3408
4591
|
const aggReaders = [];
|
|
3409
4592
|
const aggOrderExprs = new Map();
|
|
3410
4593
|
const aggInner = new Map();
|
|
@@ -3470,6 +4653,8 @@ class PowqlInterface {
|
|
|
3470
4653
|
const alias = `agg_${aggN++}`;
|
|
3471
4654
|
if (target === true) {
|
|
3472
4655
|
const col = this.column(key);
|
|
4656
|
+
this.assertDecimalComparable(col, `\`${fn}\``);
|
|
4657
|
+
this.assertEnumAggregate(col, fn);
|
|
3473
4658
|
if (fn === '_min' || fn === '_max') {
|
|
3474
4659
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, key, col.name, `groupBy ${fn}`, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
3475
4660
|
}
|
|
@@ -3478,13 +4663,19 @@ class PowqlInterface {
|
|
|
3478
4663
|
proj.push(`${alias}: ${powfn}(${inner})`);
|
|
3479
4664
|
// Canonical field, so the result bucket and the keys `orderBy` /
|
|
3480
4665
|
// `having` accept are the same whichever spelling was requested.
|
|
3481
|
-
aggReaders.push({
|
|
4666
|
+
aggReaders.push({
|
|
4667
|
+
alias,
|
|
4668
|
+
outKey: `${fn}:${col.field}`,
|
|
4669
|
+
numeric: fn === '_sum' || fn === '_avg',
|
|
4670
|
+
...(fn === '_min' || fn === '_max' ? { col } : {}),
|
|
4671
|
+
...(fn === '_sum' && isInt8Column(col) ? { exact: true } : {}),
|
|
4672
|
+
});
|
|
3482
4673
|
aggOrderExprs.set(`${fn}:${col.field}`, `.${alias}`);
|
|
3483
4674
|
aggInner.set(col.field, inner);
|
|
3484
4675
|
}
|
|
3485
4676
|
else {
|
|
3486
4677
|
const col = this.column(target.field);
|
|
3487
|
-
if (!(0, powdb_shared_js_1.isJsonColumn)(col)) {
|
|
4678
|
+
if (!(0, powdb_shared_js_1.isJsonColumn)(col, this.schema.enums)) {
|
|
3488
4679
|
throw new errors_js_1.ValidationError(`groupBy ${fn} target "${key}" on "${target.field}" (table "${this.table}") requires a json column.`);
|
|
3489
4680
|
}
|
|
3490
4681
|
if (fn === '_min' || fn === '_max') {
|
|
@@ -3543,7 +4734,15 @@ class PowqlInterface {
|
|
|
3543
4734
|
}
|
|
3544
4735
|
for (const a of aggReaders) {
|
|
3545
4736
|
const cell = raw[a.alias];
|
|
3546
|
-
const v =
|
|
4737
|
+
const v = a.col
|
|
4738
|
+
? decodeAggregateCell(cell, a.col, resultNative, this.schema.enums)
|
|
4739
|
+
: a.exact
|
|
4740
|
+
? exactIntegerSumCell(cell)
|
|
4741
|
+
: cell == null || cell === 'null'
|
|
4742
|
+
? null
|
|
4743
|
+
: a.numeric
|
|
4744
|
+
? Number(cell)
|
|
4745
|
+
: cell;
|
|
3547
4746
|
if (a.outKey === '_count')
|
|
3548
4747
|
out._count = v ?? 0;
|
|
3549
4748
|
else {
|
|
@@ -3617,6 +4816,11 @@ class PowqlInterface {
|
|
|
3617
4816
|
`Supported: ${[...POWQL_HAVING_AGG_FNS.keys()].join(', ')}.`);
|
|
3618
4817
|
}
|
|
3619
4818
|
const inner = this.lookupGroupKey(aggInner, key) ?? this.ref(key);
|
|
4819
|
+
const column = this.meta.columns.find((c) => c.field === key || c.name === key);
|
|
4820
|
+
if (column && fn !== 'count')
|
|
4821
|
+
this.assertDecimalComparable(column, `\`${aggKey}\` in having`);
|
|
4822
|
+
if (column && fn !== 'count')
|
|
4823
|
+
this.assertEnumAggregate(column, aggKey);
|
|
3620
4824
|
conds.push(cmp(`${fn}(${inner})`, filter));
|
|
3621
4825
|
}
|
|
3622
4826
|
}
|
|
@@ -3679,6 +4883,9 @@ class PowqlInterface {
|
|
|
3679
4883
|
if (!expr) {
|
|
3680
4884
|
throw new errors_js_1.ValidationError(`Unknown field "${key}" in groupBy orderBy on table "${this.table}". Orderable keys: ${validKeys()}.`);
|
|
3681
4885
|
}
|
|
4886
|
+
const keyColumn = this.meta.columns.find((c) => c.field === key || c.name === key);
|
|
4887
|
+
if (keyColumn)
|
|
4888
|
+
this.assertEnumOrderable(keyColumn, 'Ordering a groupBy');
|
|
3682
4889
|
parts.push(`${expr} ${this.groupOrderDir(value, key)}`);
|
|
3683
4890
|
}
|
|
3684
4891
|
return parts.length ? ` order ${parts.join(', ')}` : '';
|
|
@@ -3701,15 +4908,68 @@ class PowqlInterface {
|
|
|
3701
4908
|
async *findManyStream() {
|
|
3702
4909
|
throw new errors_js_1.UnsupportedFeatureError('cursor streaming (findManyStream)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
|
|
3703
4910
|
}
|
|
3704
|
-
// The batch-yielding sibling needs its OWN stub. `PowqlInterface` is a
|
|
3705
|
-
// parallel implementation of QueryInterface's surface rather than a subclass,
|
|
3706
|
-
// so a method that exists on one and not the other is not a type error here:
|
|
3707
|
-
// it is `undefined` at runtime, and the caller gets `TypeError: not a
|
|
3708
|
-
// function` instead of the typed E017 that tells them what to do instead.
|
|
3709
4911
|
// biome-ignore lint/correctness/useYield: intentionally throws before yielding, PowDB has no server cursor.
|
|
3710
4912
|
async *findManyStreamBatches() {
|
|
3711
4913
|
throw new errors_js_1.UnsupportedFeatureError('cursor streaming (findManyStreamBatches)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
|
|
3712
4914
|
}
|
|
4915
|
+
// Deferred queries. `db.pipeline()` and `$transaction([...])` take a
|
|
4916
|
+
// prebuilt statement, and a PowDB read is several round trips (relation
|
|
4917
|
+
// filters resolve first, loaders run after), so there is no single statement
|
|
4918
|
+
// to hand over. Each stub throws the typed refusal when called, which is
|
|
4919
|
+
// before the pipeline or transaction starts.
|
|
4920
|
+
deferredUnsupported(method) {
|
|
4921
|
+
throw new errors_js_1.UnsupportedFeatureError(`deferred query (${method})`, 'PowDB', 'db.pipeline() and $transaction([...]) take deferred queries; run the calls inside ' +
|
|
4922
|
+
'db.$transaction(async (tx) => ...) instead');
|
|
4923
|
+
}
|
|
4924
|
+
buildFindMany(_args) {
|
|
4925
|
+
return this.deferredUnsupported('buildFindMany');
|
|
4926
|
+
}
|
|
4927
|
+
buildFindFirst(_args) {
|
|
4928
|
+
return this.deferredUnsupported('buildFindFirst');
|
|
4929
|
+
}
|
|
4930
|
+
buildFindFirstOrThrow(_args) {
|
|
4931
|
+
return this.deferredUnsupported('buildFindFirstOrThrow');
|
|
4932
|
+
}
|
|
4933
|
+
buildFindUnique(_args) {
|
|
4934
|
+
return this.deferredUnsupported('buildFindUnique');
|
|
4935
|
+
}
|
|
4936
|
+
buildFindUniqueOrThrow(_args) {
|
|
4937
|
+
return this.deferredUnsupported('buildFindUniqueOrThrow');
|
|
4938
|
+
}
|
|
4939
|
+
buildCount(_args) {
|
|
4940
|
+
return this.deferredUnsupported('buildCount');
|
|
4941
|
+
}
|
|
4942
|
+
buildAggregate(_args) {
|
|
4943
|
+
return this.deferredUnsupported('buildAggregate');
|
|
4944
|
+
}
|
|
4945
|
+
buildGroupBy(_args) {
|
|
4946
|
+
return this.deferredUnsupported('buildGroupBy');
|
|
4947
|
+
}
|
|
4948
|
+
buildCreate(_args) {
|
|
4949
|
+
return this.deferredUnsupported('buildCreate');
|
|
4950
|
+
}
|
|
4951
|
+
buildCreateMany(_args) {
|
|
4952
|
+
return this.deferredUnsupported('buildCreateMany');
|
|
4953
|
+
}
|
|
4954
|
+
buildUpdate(_args) {
|
|
4955
|
+
return this.deferredUnsupported('buildUpdate');
|
|
4956
|
+
}
|
|
4957
|
+
buildUpdateMany(_args) {
|
|
4958
|
+
return this.deferredUnsupported('buildUpdateMany');
|
|
4959
|
+
}
|
|
4960
|
+
buildDelete(_args) {
|
|
4961
|
+
return this.deferredUnsupported('buildDelete');
|
|
4962
|
+
}
|
|
4963
|
+
buildDeleteMany(_args) {
|
|
4964
|
+
return this.deferredUnsupported('buildDeleteMany');
|
|
4965
|
+
}
|
|
4966
|
+
buildUpsert(_args) {
|
|
4967
|
+
return this.deferredUnsupported('buildUpsert');
|
|
4968
|
+
}
|
|
4969
|
+
/** SQL template cache counters. PowDB keeps no statement cache, so every counter is zero. */
|
|
4970
|
+
cacheStats() {
|
|
4971
|
+
return { hits: 0, misses: 0, hitRate: 0, size: 0 };
|
|
4972
|
+
}
|
|
3713
4973
|
// -------------------------------------------------------------------------
|
|
3714
4974
|
// Reselect helper (upsert only, PowDB's upsert has no `returning`)
|
|
3715
4975
|
// -------------------------------------------------------------------------
|
|
@@ -3726,15 +4986,6 @@ class PowqlInterface {
|
|
|
3726
4986
|
});
|
|
3727
4987
|
return rows.length ? this.shape(rows, native)[0] : null;
|
|
3728
4988
|
}
|
|
3729
|
-
/**
|
|
3730
|
-
* Empty-where guard, blocks accidental whole-table writes. Mirrors the SQL
|
|
3731
|
-
* path's `assertMutationHasPredicate` (query/builder.ts): it gates on the
|
|
3732
|
-
* *compiled* PowQL filter fragment, NOT the shape of the `where` object. A
|
|
3733
|
-
* `where` whose conditions all evaporate during compilation, `{}`,
|
|
3734
|
-
* `{ id: undefined }`, `{ OR: [] }`, `{ AND: [] }`, `{ NOT: {} }`,
|
|
3735
|
-
* `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
|
|
3736
|
-
* because emitting a filter-less write would hit every row.
|
|
3737
|
-
*/
|
|
3738
4989
|
/**
|
|
3739
4990
|
* AND this table's configured global filter (soft-delete / multi-tenancy)
|
|
3740
4991
|
* onto an ALREADY-COMPILED user filter, pushing its params after the user's.
|
|
@@ -3754,8 +5005,7 @@ class PowqlInterface {
|
|
|
3754
5005
|
* `alias` is forwarded so the nested/native-join paths qualify the filter's
|
|
3755
5006
|
* column references the same way they qualify the user's.
|
|
3756
5007
|
*/
|
|
3757
|
-
applyGlobalFilter(compiledUserWhere, params,
|
|
3758
|
-
const gf = (0, where_js_1.resolveGlobalFilterFrom)(this.options.globalFilters, this.table, (0, types_js_1.resolveSkipGlobalFilters)(skip));
|
|
5008
|
+
applyGlobalFilter(compiledUserWhere, params, gf, alias) {
|
|
3759
5009
|
if (!gf)
|
|
3760
5010
|
return compiledUserWhere;
|
|
3761
5011
|
const gfWhere = this.buildWhere(gf, params, alias);
|
|
@@ -3765,17 +5015,312 @@ class PowqlInterface {
|
|
|
3765
5015
|
return gfWhere;
|
|
3766
5016
|
return `(${compiledUserWhere}) and (${gfWhere})`;
|
|
3767
5017
|
}
|
|
3768
|
-
|
|
5018
|
+
/**
|
|
5019
|
+
* Tables whose global filter is being resolved by an operation above this
|
|
5020
|
+
* interface. A relation filter inside a global filter runs its own query on
|
|
5021
|
+
* the related table, which applies THAT table's filter, so two filters that
|
|
5022
|
+
* reach each other through relations would resolve forever; each table's
|
|
5023
|
+
* filter applies once per chain instead.
|
|
5024
|
+
*/
|
|
5025
|
+
get globalFilterStack() {
|
|
5026
|
+
return this.options._globalFilterStack ?? [];
|
|
5027
|
+
}
|
|
5028
|
+
/** Whether this table's global filter constrains an operation under `skip`. */
|
|
5029
|
+
hasGlobalFilter(skip) {
|
|
5030
|
+
if (this.globalFilterStack.includes(this.table))
|
|
5031
|
+
return false;
|
|
5032
|
+
const gf = (0, where_js_1.resolveGlobalFilterFrom)(this.options.globalFilters, this.table, (0, types_js_1.resolveSkipGlobalFilters)(skip), this.schema.tables);
|
|
5033
|
+
return gf != null && !this.compilesToNothing(gf);
|
|
5034
|
+
}
|
|
5035
|
+
/**
|
|
5036
|
+
* This table's global filter for one operation, ready for
|
|
5037
|
+
* {@link applyGlobalFilter}: read under SQL three-valued logic, with every
|
|
5038
|
+
* relation filter in it (`posts: { some: ... }`, `user: { is: ... }`)
|
|
5039
|
+
* resolved to a literal key list by the same client-side resolution a
|
|
5040
|
+
* caller's `where` gets. The related table's own global filter bounds those
|
|
5041
|
+
* lookups, as on the SQL engines, and `skip` reaches them too.
|
|
5042
|
+
*/
|
|
5043
|
+
async globalFilterWhere(skip, timeout) {
|
|
5044
|
+
const stack = this.globalFilterStack;
|
|
5045
|
+
if (stack.includes(this.table))
|
|
5046
|
+
return undefined;
|
|
5047
|
+
const gf = (0, where_js_1.resolveGlobalFilterFrom)(this.options.globalFilters, this.table, (0, types_js_1.resolveSkipGlobalFilters)(skip), this.schema.tables);
|
|
5048
|
+
if (!gf)
|
|
5049
|
+
return undefined;
|
|
5050
|
+
if (!this.namesRelation(gf))
|
|
5051
|
+
return this.sqlTrueWhere(gf);
|
|
5052
|
+
const scoped = new PowqlInterface(this.pool, this.table, this.schema, [], {
|
|
5053
|
+
...this.loaderChildOptions(),
|
|
5054
|
+
_globalFilterStack: [...stack, this.table],
|
|
5055
|
+
});
|
|
5056
|
+
return scoped.resolveRelationFilters(gf, timeout, skip);
|
|
5057
|
+
}
|
|
5058
|
+
/** Whether `where` holds a relation filter at any combinator depth. */
|
|
5059
|
+
namesRelation(where) {
|
|
5060
|
+
if (where === null || typeof where !== 'object' || Array.isArray(where))
|
|
5061
|
+
return false;
|
|
5062
|
+
for (const [key, value] of Object.entries(where)) {
|
|
5063
|
+
if (value === undefined)
|
|
5064
|
+
continue;
|
|
5065
|
+
if (isCombinatorKey(key)) {
|
|
5066
|
+
const branches = (0, where_compile_js_1.combinatorBranches)(key, value, this.table);
|
|
5067
|
+
if (branches?.some((w) => this.namesRelation(w)))
|
|
5068
|
+
return true;
|
|
5069
|
+
}
|
|
5070
|
+
else if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, key)) {
|
|
5071
|
+
return true;
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
return false;
|
|
5075
|
+
}
|
|
5076
|
+
/**
|
|
5077
|
+
* Empty-where guard, blocks accidental whole-table writes. Mirrors the SQL
|
|
5078
|
+
* path's `assertMutationHasPredicate` (query/where.ts): it gates on the
|
|
5079
|
+
* *compiled* PowQL filter fragment, NOT the shape of the `where` object, so a
|
|
5080
|
+
* `where` whose conditions all evaporate during compilation (`{}`,
|
|
5081
|
+
* `{ id: undefined }`, `{ AND: [] }`, `{ NOT: {} }`) is refused. A where that
|
|
5082
|
+
* compiles but is TRUE for every row (`NOT: { OR: [] }`, `id: { notIn: [] }`)
|
|
5083
|
+
* writes every row just the same, so `userWhere`, the caller's where before
|
|
5084
|
+
* relation filters were resolved, is folded by the SQL engines' rule
|
|
5085
|
+
* (`foldWhereFor`) and refused too.
|
|
5086
|
+
*
|
|
5087
|
+
* The fold also reads the schema's NOT NULL columns, as the SQL guard does:
|
|
5088
|
+
* `{ name: { not: null } }` or `NOT: { name: null }` on a NOT NULL column is
|
|
5089
|
+
* TRUE for every row (PowDB's `required` holds the same guarantee). It does
|
|
5090
|
+
* NOT trust foreign keys: PowDB declares none, so an orphan row is possible
|
|
5091
|
+
* and a belongsTo `is: {}` stays a condition.
|
|
5092
|
+
*/
|
|
5093
|
+
assertCompiledWhere(compiledWhere, allow, action, userWhere) {
|
|
3769
5094
|
if (allow === true)
|
|
3770
5095
|
return;
|
|
3771
|
-
if (compiledWhere.length > 0
|
|
5096
|
+
if (compiledWhere.length > 0 &&
|
|
5097
|
+
(userWhere === undefined ||
|
|
5098
|
+
(0, where_compile_js_1.foldWhereFor)(this.schema.tables, this.table, userWhere, POWDB_GUARD_FOLD) !== 'true')) {
|
|
3772
5099
|
return;
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
'or check that your filter values are defined.');
|
|
5100
|
+
}
|
|
5101
|
+
throw new errors_js_1.ValidationError((0, where_js_1.emptyMutationWhereMessage)(action, this.table));
|
|
3776
5102
|
}
|
|
3777
5103
|
}
|
|
3778
5104
|
exports.PowqlInterface = PowqlInterface;
|
|
5105
|
+
/**
|
|
5106
|
+
* The mass-mutation guard's fold options on PowDB (see `assertCompiledWhere`):
|
|
5107
|
+
* NOT NULL columns count, foreign keys do not (PowDB enforces none).
|
|
5108
|
+
*/
|
|
5109
|
+
const POWDB_GUARD_FOLD = {
|
|
5110
|
+
notNull: { foreignKeys: false, hasGlobalFilter: () => true },
|
|
5111
|
+
};
|
|
5112
|
+
/** Marks a default expression {@link emulateColumnDefault} cannot evaluate. */
|
|
5113
|
+
const NO_DEFAULT = Symbol('turbine.powdb.noDefault');
|
|
5114
|
+
/**
|
|
5115
|
+
* Evaluate a column's SQL default expression client-side, for one insert.
|
|
5116
|
+
*
|
|
5117
|
+
* Covers what `defineSchema` accepts (NULL, TRUE / FALSE, numeric and quoted
|
|
5118
|
+
* string literals, now() / current_timestamp, current_date, gen_random_uuid())
|
|
5119
|
+
* plus the trailing `::type` cast and wrapping parentheses introspection adds.
|
|
5120
|
+
* Anything else returns {@link NO_DEFAULT}: a guessed value would be a silent
|
|
5121
|
+
* divergence from the engine the schema was written for.
|
|
5122
|
+
*/
|
|
5123
|
+
function emulateColumnDefault(expr, col, enums) {
|
|
5124
|
+
let e = expr.trim();
|
|
5125
|
+
for (;;) {
|
|
5126
|
+
const unwrapped = e.replace(/::[a-z_][a-z0-9_ ]*(\(\d+(,\s*\d+)?\))?(\[\])?$/i, '').trim();
|
|
5127
|
+
const stripped = /^\((.*)\)$/s.test(unwrapped) ? unwrapped.slice(1, -1).trim() : unwrapped;
|
|
5128
|
+
if (stripped === e)
|
|
5129
|
+
break;
|
|
5130
|
+
e = stripped;
|
|
5131
|
+
}
|
|
5132
|
+
const ts = (0, powdb_shared_js_1.baseTsType)(col.tsType);
|
|
5133
|
+
const upper = e.toUpperCase();
|
|
5134
|
+
if (upper === 'NULL')
|
|
5135
|
+
return null;
|
|
5136
|
+
if (upper === 'TRUE' || upper === 'FALSE')
|
|
5137
|
+
return ts === 'boolean' ? upper === 'TRUE' : NO_DEFAULT;
|
|
5138
|
+
if (['NOW()', 'CURRENT_TIMESTAMP', 'LOCALTIMESTAMP', 'TRANSACTION_TIMESTAMP()', 'STATEMENT_TIMESTAMP()'].includes(upper)) {
|
|
5139
|
+
return ts === 'Date' ? new Date() : NO_DEFAULT;
|
|
5140
|
+
}
|
|
5141
|
+
if (upper === 'CURRENT_DATE') {
|
|
5142
|
+
if (ts !== 'Date')
|
|
5143
|
+
return NO_DEFAULT;
|
|
5144
|
+
const now = new Date();
|
|
5145
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
5146
|
+
}
|
|
5147
|
+
if (upper === 'GEN_RANDOM_UUID()' || upper === 'UUID_GENERATE_V4()')
|
|
5148
|
+
return ts === 'string' ? (0, node_crypto_1.randomUUID)() : NO_DEFAULT;
|
|
5149
|
+
const quoted = /^'((?:[^']|'')*)'$/s.exec(e);
|
|
5150
|
+
const literal = quoted ? quoted[1].replace(/''/g, "'") : /^-?\d+(\.\d+)?$/.test(e) ? e : undefined;
|
|
5151
|
+
if (literal === undefined)
|
|
5152
|
+
return NO_DEFAULT;
|
|
5153
|
+
// An enum default is one of its labels, stored as the label text.
|
|
5154
|
+
const labels = (0, powdb_shared_js_1.powdbEnumLabels)(col, enums);
|
|
5155
|
+
if (labels)
|
|
5156
|
+
return labels.includes(literal) ? literal : NO_DEFAULT;
|
|
5157
|
+
if ((0, powdb_shared_js_1.isJsonColumn)(col, enums)) {
|
|
5158
|
+
try {
|
|
5159
|
+
return JSON.parse(literal);
|
|
5160
|
+
}
|
|
5161
|
+
catch {
|
|
5162
|
+
return NO_DEFAULT;
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
switch (ts) {
|
|
5166
|
+
case 'string':
|
|
5167
|
+
return literal;
|
|
5168
|
+
case 'number': {
|
|
5169
|
+
const n = Number(literal);
|
|
5170
|
+
if (literal.trim() === '' || !Number.isFinite(n))
|
|
5171
|
+
return NO_DEFAULT;
|
|
5172
|
+
// An int8 default past 2^53 keeps every digit, as the column does.
|
|
5173
|
+
return isInt8Column(col) && /^-?\d+$/.test(literal) && !Number.isSafeInteger(n) ? BigInt(literal) : n;
|
|
5174
|
+
}
|
|
5175
|
+
case 'bigint':
|
|
5176
|
+
return /^-?\d+$/.test(literal) ? BigInt(literal) : NO_DEFAULT;
|
|
5177
|
+
case 'boolean': {
|
|
5178
|
+
const b = literal.toLowerCase();
|
|
5179
|
+
if (['t', 'true', 'y', 'yes', 'on', '1'].includes(b))
|
|
5180
|
+
return true;
|
|
5181
|
+
if (['f', 'false', 'n', 'no', 'off', '0'].includes(b))
|
|
5182
|
+
return false;
|
|
5183
|
+
return NO_DEFAULT;
|
|
5184
|
+
}
|
|
5185
|
+
case 'Date': {
|
|
5186
|
+
// A zone-less literal is UTC, as Turbine reads a zone-less timestamp.
|
|
5187
|
+
const zoned = /^\d{4}-\d\d-\d\d$/.test(literal) || /([zZ]|[+-]\d\d(:?\d\d)?)$/.test(literal);
|
|
5188
|
+
const d = new Date(zoned ? literal : `${literal.replace(' ', 'T')}Z`);
|
|
5189
|
+
return quoted && !Number.isNaN(d.getTime()) ? d : NO_DEFAULT;
|
|
5190
|
+
}
|
|
5191
|
+
default:
|
|
5192
|
+
return NO_DEFAULT;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
/**
|
|
5196
|
+
* Decode a `_min` / `_max` cell, which is a stored value of `col`, through the
|
|
5197
|
+
* same per-column coercion a row read uses: a string stays a string, a Date
|
|
5198
|
+
* column's micros become a Date, a bigint keeps its digits. Reading it as a
|
|
5199
|
+
* number turned every text minimum into null and every date into micros.
|
|
5200
|
+
*/
|
|
5201
|
+
/**
|
|
5202
|
+
* Whether `col` is a 64-bit integer column (`int8` / `bigint`), the type whose
|
|
5203
|
+
* `SUM` PostgreSQL widens to `numeric` and the SQL engines return as exact
|
|
5204
|
+
* decimal text rather than a JS number (query/aggregates.ts,
|
|
5205
|
+
* `isExactNumericType`). A column read from a live PowDB catalog reports the
|
|
5206
|
+
* engine's `int` and is not recognised, so its sum stays a number.
|
|
5207
|
+
*/
|
|
5208
|
+
function isInt8Column(col) {
|
|
5209
|
+
const pgType = col.pgType ?? '';
|
|
5210
|
+
const paren = pgType.indexOf('(');
|
|
5211
|
+
const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
|
|
5212
|
+
return base === 'int8' || base === 'bigint';
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* A `_sum` over an int8 column, as PostgreSQL returns it: the exact decimal
|
|
5216
|
+
* text, so a total past 2^53 keeps every digit. The native wire delivers the
|
|
5217
|
+
* engine's i64 as a `bigint` and the legacy wire as text; both are exact.
|
|
5218
|
+
*/
|
|
5219
|
+
function exactIntegerSumCell(cell) {
|
|
5220
|
+
if (cell === undefined || cell === null || cell === 'null')
|
|
5221
|
+
return null;
|
|
5222
|
+
if (typeof cell === 'bigint')
|
|
5223
|
+
return cell.toString();
|
|
5224
|
+
if (typeof cell === 'number' && Number.isInteger(cell))
|
|
5225
|
+
return BigInt(cell).toString();
|
|
5226
|
+
return String(cell);
|
|
5227
|
+
}
|
|
5228
|
+
/**
|
|
5229
|
+
* Whole-document equality on a json column (a bare object or array, or
|
|
5230
|
+
* `equals` with no `path`): PowQL has no document comparison, so it is an
|
|
5231
|
+
* engine gap (E017) on every spelling, never a value bound as data.
|
|
5232
|
+
*/
|
|
5233
|
+
function jsonDocumentEqualityUnsupported(col) {
|
|
5234
|
+
return new errors_js_1.UnsupportedFeatureError('JSON document equality (equals without path)', 'PowDB', `column "${col.name}": pass a \`path\` to compare a specific json value; PowQL has no whole-document comparison`);
|
|
5235
|
+
}
|
|
5236
|
+
const INT64_MIN = -(2n ** 63n);
|
|
5237
|
+
const INT64_MAX = 2n ** 63n - 1n;
|
|
5238
|
+
/**
|
|
5239
|
+
* The value integer text names for an integer column: a number when it is a
|
|
5240
|
+
* safe integer, else a bigint, so an int8 past 2^53 keeps every digit. Text
|
|
5241
|
+
* that is not an integer is refused (E003), as PostgreSQL refuses it.
|
|
5242
|
+
*/
|
|
5243
|
+
function integerTextValue(text, col, table) {
|
|
5244
|
+
const trimmed = text.trim();
|
|
5245
|
+
const shown = JSON.stringify(text.length > 40 ? `${text.slice(0, 40)}...` : text);
|
|
5246
|
+
if (!/^[+-]?\d+$/.test(trimmed)) {
|
|
5247
|
+
throw new errors_js_1.ValidationError(`Invalid value ${shown} for the integer column "${col.name}" on "${table}": expected an integer. ` +
|
|
5248
|
+
"Pass a number, a bigint, or integer text such as '42'.");
|
|
5249
|
+
}
|
|
5250
|
+
const big = BigInt(trimmed);
|
|
5251
|
+
if (big < INT64_MIN || big > INT64_MAX) {
|
|
5252
|
+
throw new errors_js_1.ValidationError(`Invalid value ${shown} for the integer column "${col.name}" on "${table}": it is outside the 64-bit range.`);
|
|
5253
|
+
}
|
|
5254
|
+
const n = Number(big);
|
|
5255
|
+
return Number.isSafeInteger(n) ? n : big;
|
|
5256
|
+
}
|
|
5257
|
+
/** ISO 8601 date or date-time text: date, optional time and fraction, optional zone. */
|
|
5258
|
+
const ISO_DATE_TEXT = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
|
|
5259
|
+
/**
|
|
5260
|
+
* The epoch micros a Date column stores for `text`, read the way PostgreSQL
|
|
5261
|
+
* reads the same text against a `date` / `timestamp` column, so
|
|
5262
|
+
* `{ birthday: '2000-02-29' }` matches what `{ birthday: new Date(...) }` does.
|
|
5263
|
+
*
|
|
5264
|
+
* - A `date` column takes the calendar day and ignores any time or zone
|
|
5265
|
+
* (PostgreSQL's date input does the same), at UTC midnight, which is how
|
|
5266
|
+
* Turbine reads and writes a date.
|
|
5267
|
+
* - Any other Date column takes the full instant; text with no zone is UTC,
|
|
5268
|
+
* Turbine's zone-less convention. Up to microsecond precision is kept.
|
|
5269
|
+
*
|
|
5270
|
+
* Only ISO 8601 text is read. PostgreSQL also accepts words (`now`,
|
|
5271
|
+
* `infinity`) and other layouts; those are refused (E003) rather than guessed.
|
|
5272
|
+
*/
|
|
5273
|
+
function isoTextMicros(text, col, table) {
|
|
5274
|
+
const refuse = (why) => new errors_js_1.ValidationError(`Invalid date text ${JSON.stringify(text.length > 40 ? `${text.slice(0, 40)}...` : text)} for the Date ` +
|
|
5275
|
+
`column "${col.name}" on "${table}": ${why}. PowDB stores the column as epoch microseconds, so text is ` +
|
|
5276
|
+
'read as ISO 8601 (YYYY-MM-DD, optionally a time and a zone). Pass a Date for anything else.');
|
|
5277
|
+
const m = ISO_DATE_TEXT.exec(text.trim());
|
|
5278
|
+
if (!m)
|
|
5279
|
+
throw refuse('it is not ISO 8601');
|
|
5280
|
+
const [, y, mo, d, hh, mi, ss, frac, zone] = m;
|
|
5281
|
+
const isDate = (col.pgType ?? '').trim().toLowerCase() === 'date';
|
|
5282
|
+
const day = new Date(0);
|
|
5283
|
+
day.setUTCFullYear(Number(y), Number(mo) - 1, Number(d));
|
|
5284
|
+
if (day.getUTCFullYear() !== Number(y) || day.getUTCMonth() !== Number(mo) - 1 || day.getUTCDate() !== Number(d)) {
|
|
5285
|
+
throw refuse('it names no calendar day');
|
|
5286
|
+
}
|
|
5287
|
+
if (isDate)
|
|
5288
|
+
return BigInt(day.getTime()) * 1000n;
|
|
5289
|
+
// PostgreSQL reads zone-less text against timestamptz in the SESSION time
|
|
5290
|
+
// zone, a server setting PowDB has no counterpart for, so no single reading
|
|
5291
|
+
// matches it; a plain `timestamp` has no zone and is read as UTC.
|
|
5292
|
+
if (!zone && /^timestamptz$|with time zone/i.test((col.pgType ?? '').trim())) {
|
|
5293
|
+
throw refuse('it has no zone, and PostgreSQL would read it in the session time zone; add Z or an offset');
|
|
5294
|
+
}
|
|
5295
|
+
const hours = Number(hh ?? 0);
|
|
5296
|
+
const minutes = Number(mi ?? 0);
|
|
5297
|
+
const seconds = Number(ss ?? 0);
|
|
5298
|
+
if (hours > 23 || minutes > 59 || seconds > 59)
|
|
5299
|
+
throw refuse('its time is out of range');
|
|
5300
|
+
let offsetMinutes = 0;
|
|
5301
|
+
if (zone && zone.toUpperCase() !== 'Z') {
|
|
5302
|
+
const digits = zone.slice(1).replace(':', '');
|
|
5303
|
+
const oh = Number(digits.slice(0, 2));
|
|
5304
|
+
const om = Number(digits.slice(2) || 0);
|
|
5305
|
+
if (oh > 15 || om > 59)
|
|
5306
|
+
throw refuse('its zone offset is out of range');
|
|
5307
|
+
offsetMinutes = (zone[0] === '-' ? -1 : 1) * (oh * 60 + om);
|
|
5308
|
+
}
|
|
5309
|
+
const ms = day.getTime() + ((hours * 60 + minutes - offsetMinutes) * 60 + seconds) * 1000;
|
|
5310
|
+
// Microseconds past the second, from the first six fraction digits (PostgreSQL rounds the rest).
|
|
5311
|
+
const fracMicros = frac ? Math.round(Number(`0.${frac}`) * 1e6) : 0;
|
|
5312
|
+
return BigInt(ms) * 1000n + BigInt(fracMicros);
|
|
5313
|
+
}
|
|
5314
|
+
function decodeAggregateCell(cell, col, native, enums) {
|
|
5315
|
+
if (cell === undefined || cell === null)
|
|
5316
|
+
return null;
|
|
5317
|
+
if (native)
|
|
5318
|
+
return (0, powdb_shared_js_1.coerceNativeValue)(cell, col);
|
|
5319
|
+
// Legacy string wire: an aggregate over no rows renders the bareword `null`.
|
|
5320
|
+
if (typeof cell !== 'string')
|
|
5321
|
+
return cell;
|
|
5322
|
+
return cell === 'null' ? null : (0, powdb_shared_js_1.coerceValue)(cell, col, enums);
|
|
5323
|
+
}
|
|
3779
5324
|
/** Coerce a group-key scalar string by the column's TS type (numbers/bools). */
|
|
3780
5325
|
function coerceScalar(raw, tsType) {
|
|
3781
5326
|
const ts = (0, powdb_shared_js_1.baseTsType)(tsType);
|