@dbsp/core 1.0.3 → 1.0.4
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/dist/index.d.ts +16 -6
- package/dist/index.js +144 -52
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -2573,15 +2573,20 @@ declare function notExists(relation: string, options?: {
|
|
|
2573
2573
|
* Accepts a SubqueryBuilder (must have `.build()`) or any builder
|
|
2574
2574
|
* exposing `buildIntent(): QueryIntent` (e.g. QueryBuilder).
|
|
2575
2575
|
*
|
|
2576
|
+
* **Limitation:** correlated subqueries (using `outerRef()` inside the inner WHERE)
|
|
2577
|
+
* are NOT supported and will throw at compile time. For correlated EXISTS over an
|
|
2578
|
+
* FK-declared relation, use `exists('relation', { where: ... outerRef(...) })` instead.
|
|
2579
|
+
*
|
|
2576
2580
|
* @param subquery - A SubqueryBuilder or any object with buildIntent()
|
|
2577
2581
|
*
|
|
2578
2582
|
* @example
|
|
2579
|
-
* // EXISTS (SELECT 1 FROM audit_log WHERE audit_log.
|
|
2580
|
-
*
|
|
2583
|
+
* // EXISTS (SELECT 1 FROM audit_log WHERE audit_log.entity_type = 'login')
|
|
2584
|
+
* // Uncorrelated: no reference to the outer row — inner filter is a plain value.
|
|
2585
|
+
* rawExists(subquery('audit_log').select('id').where(eq('entityType', 'login')))
|
|
2581
2586
|
*
|
|
2582
2587
|
* @example
|
|
2583
|
-
* // EXISTS with a full query builder
|
|
2584
|
-
* rawExists(orm.select('sessions').where(and(eq('
|
|
2588
|
+
* // EXISTS with a full query builder — polymorphic table, no FK to source
|
|
2589
|
+
* rawExists(orm.select('sessions').where(and(eq('status', 'active'), gt('expiresAt', new Date()))))
|
|
2585
2590
|
*/
|
|
2586
2591
|
declare function rawExists(sq: SubqueryBuilder | {
|
|
2587
2592
|
buildIntent(): QueryIntent;
|
|
@@ -2593,11 +2598,16 @@ declare function rawExists(sq: SubqueryBuilder | {
|
|
|
2593
2598
|
* Accepts a SubqueryBuilder (must have `.build()`) or any builder
|
|
2594
2599
|
* exposing `buildIntent(): QueryIntent` (e.g. QueryBuilder).
|
|
2595
2600
|
*
|
|
2601
|
+
* **Limitation:** correlated subqueries (using `outerRef()` inside the inner WHERE)
|
|
2602
|
+
* are NOT supported and will throw at compile time. For correlated NOT EXISTS over an
|
|
2603
|
+
* FK-declared relation, use `notExists('relation', { where: ... outerRef(...) })` instead.
|
|
2604
|
+
*
|
|
2596
2605
|
* @param subquery - A SubqueryBuilder or any object with buildIntent()
|
|
2597
2606
|
*
|
|
2598
2607
|
* @example
|
|
2599
|
-
* // NOT EXISTS (SELECT 1 FROM bans WHERE bans.
|
|
2600
|
-
*
|
|
2608
|
+
* // NOT EXISTS (SELECT 1 FROM bans WHERE bans.reason = 'spam')
|
|
2609
|
+
* // Uncorrelated: inner filter is a plain value, no reference to the outer row.
|
|
2610
|
+
* rawNotExists(subquery('bans').select('id').where(eq('reason', 'spam')))
|
|
2601
2611
|
*/
|
|
2602
2612
|
declare function rawNotExists(sq: SubqueryBuilder | {
|
|
2603
2613
|
buildIntent(): QueryIntent;
|
package/dist/index.js
CHANGED
|
@@ -2146,6 +2146,7 @@ function plan(intent, model, options = {}) {
|
|
|
2146
2146
|
throw new Error(`Unknown table: ${intent.from}`);
|
|
2147
2147
|
}
|
|
2148
2148
|
const optimizedWhere = intent.where ? optimizeInToExists(intent.where, intent.from, model) : void 0;
|
|
2149
|
+
const plannedIntent = optimizedWhere !== void 0 && optimizedWhere !== intent.where ? { ...intent, where: optimizedWhere } : intent;
|
|
2149
2150
|
if (optimizedWhere) {
|
|
2150
2151
|
processWhere(optimizedWhere, intent.from, model, state, opts, "where");
|
|
2151
2152
|
}
|
|
@@ -2188,7 +2189,13 @@ function plan(intent, model, options = {}) {
|
|
|
2188
2189
|
decisions: Object.freeze(state.decisions.slice()),
|
|
2189
2190
|
warnings: Object.freeze(state.warnings.slice()),
|
|
2190
2191
|
ctes: Object.freeze(state.ctes.slice()),
|
|
2192
|
+
// intent is ALWAYS the original submitted intent (contract: observable via dump()).
|
|
2193
|
+
// executableIntent carries the optimized WHERE (e.g. IN→EXISTS) when the
|
|
2194
|
+
// optimizer rewrote it, so the adapter compiles the correct SQL from that field.
|
|
2195
|
+
// When no optimization applies, executableIntent is left undefined and the
|
|
2196
|
+
// adapter falls back to intent.
|
|
2191
2197
|
intent,
|
|
2198
|
+
...plannedIntent !== intent && { executableIntent: plannedIntent },
|
|
2192
2199
|
metadata
|
|
2193
2200
|
};
|
|
2194
2201
|
return Object.freeze(report);
|
|
@@ -2326,11 +2333,15 @@ function isSubquerySelectedColumnNonNullable(existsIntent, sourceTable, model) {
|
|
|
2326
2333
|
if (!column) return false;
|
|
2327
2334
|
return !column.nullable;
|
|
2328
2335
|
}
|
|
2329
|
-
function optimizeInToExists(where, sourceTable, model) {
|
|
2336
|
+
function optimizeInToExists(where, sourceTable, model, negated = false) {
|
|
2330
2337
|
switch (where.kind) {
|
|
2331
2338
|
case "in": {
|
|
2332
2339
|
const inWhere = where;
|
|
2333
2340
|
if (!inWhere.subquery) return where;
|
|
2341
|
+
const sq = inWhere.subquery;
|
|
2342
|
+
if (sq.limit != null || sq.orderBy?.length || sq.offset != null || sq.groupBy?.length || sq.having != null || sq.distinct || sq.distinctOn?.length || sq.joins?.length || sq.include?.length || sq.batchValuesSource != null || sq.existsWrap || sq.lock != null || sq.select != null && sq.select.type !== "fields") {
|
|
2343
|
+
return where;
|
|
2344
|
+
}
|
|
2334
2345
|
const subSelect = inWhere.subquery.select;
|
|
2335
2346
|
if (!subSelect || subSelect.type !== "fields") return where;
|
|
2336
2347
|
const fields = "fields" in subSelect ? subSelect.fields : void 0;
|
|
@@ -2350,18 +2361,24 @@ function optimizeInToExists(where, sourceTable, model) {
|
|
|
2350
2361
|
}
|
|
2351
2362
|
}
|
|
2352
2363
|
if (!matchedRelation) return where;
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2364
|
+
const effectiveNegated = negated !== Boolean(inWhere.not);
|
|
2365
|
+
if (effectiveNegated) {
|
|
2366
|
+
const existsIntent = { relation: matchedRelation };
|
|
2367
|
+
if (!isSubquerySelectedColumnNonNullable(existsIntent, sourceTable, model)) {
|
|
2368
|
+
return where;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const targetKind = inWhere.not ? "notExists" : "exists";
|
|
2372
|
+
return {
|
|
2373
|
+
kind: targetKind,
|
|
2355
2374
|
relation: matchedRelation,
|
|
2356
|
-
// Forward the subquery's inner WHERE conditions
|
|
2357
2375
|
...inWhere.subquery.where && { where: inWhere.subquery.where }
|
|
2358
2376
|
};
|
|
2359
|
-
return existsWhere;
|
|
2360
2377
|
}
|
|
2361
2378
|
case "and": {
|
|
2362
2379
|
const andWhere = where;
|
|
2363
2380
|
const optimized = andWhere.conditions.map(
|
|
2364
|
-
(c) => optimizeInToExists(c, sourceTable, model)
|
|
2381
|
+
(c) => optimizeInToExists(c, sourceTable, model, negated)
|
|
2365
2382
|
);
|
|
2366
2383
|
if (optimized.every((c, i) => c === andWhere.conditions[i])) return where;
|
|
2367
2384
|
return { kind: "and", conditions: optimized };
|
|
@@ -2369,7 +2386,7 @@ function optimizeInToExists(where, sourceTable, model) {
|
|
|
2369
2386
|
case "or": {
|
|
2370
2387
|
const orWhere = where;
|
|
2371
2388
|
const optimized = orWhere.conditions.map(
|
|
2372
|
-
(c) => optimizeInToExists(c, sourceTable, model)
|
|
2389
|
+
(c) => optimizeInToExists(c, sourceTable, model, negated)
|
|
2373
2390
|
);
|
|
2374
2391
|
if (optimized.every((c, i) => c === orWhere.conditions[i])) return where;
|
|
2375
2392
|
return { kind: "or", conditions: optimized };
|
|
@@ -2379,22 +2396,10 @@ function optimizeInToExists(where, sourceTable, model) {
|
|
|
2379
2396
|
const optimized = optimizeInToExists(
|
|
2380
2397
|
notWhere.condition,
|
|
2381
2398
|
sourceTable,
|
|
2382
|
-
model
|
|
2399
|
+
model,
|
|
2400
|
+
!negated
|
|
2383
2401
|
);
|
|
2384
2402
|
if (optimized === notWhere.condition) return where;
|
|
2385
|
-
if (optimized.kind === "exists") {
|
|
2386
|
-
if (!isSubquerySelectedColumnNonNullable(
|
|
2387
|
-
optimized,
|
|
2388
|
-
sourceTable,
|
|
2389
|
-
model
|
|
2390
|
-
)) {
|
|
2391
|
-
return where;
|
|
2392
|
-
}
|
|
2393
|
-
return {
|
|
2394
|
-
...optimized,
|
|
2395
|
-
kind: "notExists"
|
|
2396
|
-
};
|
|
2397
|
-
}
|
|
2398
2403
|
return { kind: "not", condition: optimized };
|
|
2399
2404
|
}
|
|
2400
2405
|
default:
|
|
@@ -2476,6 +2481,26 @@ function processWhere(where, sourceTable, model, state, opts, intentPath) {
|
|
|
2476
2481
|
break;
|
|
2477
2482
|
case "expression":
|
|
2478
2483
|
break;
|
|
2484
|
+
// Custom expression — no relation analysis, pass through
|
|
2485
|
+
// Adapter-only kinds: planner records no decisions; the adapter compiles
|
|
2486
|
+
// them directly from the intent. Explicit cases here prevent silent
|
|
2487
|
+
// fallthrough and keep the switch exhaustive.
|
|
2488
|
+
case "rawExists":
|
|
2489
|
+
case "rawNotExists":
|
|
2490
|
+
break;
|
|
2491
|
+
case "subquery":
|
|
2492
|
+
break;
|
|
2493
|
+
case "range":
|
|
2494
|
+
break;
|
|
2495
|
+
case "jsonContains":
|
|
2496
|
+
case "jsonExists":
|
|
2497
|
+
break;
|
|
2498
|
+
default: {
|
|
2499
|
+
const _exhaustive = where;
|
|
2500
|
+
throw new Error(
|
|
2501
|
+
`processWhere: unhandled WhereIntent kind '${_exhaustive.kind}'`
|
|
2502
|
+
);
|
|
2503
|
+
}
|
|
2479
2504
|
}
|
|
2480
2505
|
}
|
|
2481
2506
|
function processRelationFilter(relationPath, sourceTable, model, state, opts, intentPath, nestedWhere, mode) {
|
|
@@ -3012,6 +3037,60 @@ function assertCapability(adapter, capability, operation) {
|
|
|
3012
3037
|
}
|
|
3013
3038
|
|
|
3014
3039
|
// src/dx/batch-values.ts
|
|
3040
|
+
var MULTIWORD_BASE_TYPES = [
|
|
3041
|
+
"timestamp with time zone",
|
|
3042
|
+
"timestamp without time zone",
|
|
3043
|
+
"time with time zone",
|
|
3044
|
+
"time without time zone",
|
|
3045
|
+
"double precision",
|
|
3046
|
+
"character varying",
|
|
3047
|
+
"bit varying"
|
|
3048
|
+
];
|
|
3049
|
+
var IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3050
|
+
function validateTypeName(typeName) {
|
|
3051
|
+
const raw2 = typeName.trim();
|
|
3052
|
+
if (raw2.length === 0) {
|
|
3053
|
+
throw new Error(
|
|
3054
|
+
`batchValues: invalid type name '${typeName}'. Type names must not be empty.`
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
let rest = raw2;
|
|
3058
|
+
if (rest.endsWith("[]")) {
|
|
3059
|
+
rest = rest.slice(0, -2);
|
|
3060
|
+
if (rest.endsWith("[]")) {
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`batchValues: invalid type name '${typeName}'. At most one array suffix "[]" is allowed as a raw type-name input. Use "int4[]" not "int4[][]".`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const modifierMatch = rest.match(/\(([^)]*)\)$/);
|
|
3067
|
+
if (modifierMatch) {
|
|
3068
|
+
const inner = modifierMatch[1] ?? "";
|
|
3069
|
+
if (!/^\d+(?:,\d+)?$/.test(inner)) {
|
|
3070
|
+
throw new Error(
|
|
3071
|
+
`batchValues: invalid type name '${typeName}'. Type modifier must be "(N)" or "(N,M)" with digits only; got "(${inner})".`
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
rest = rest.slice(0, rest.length - modifierMatch[0].length).trimEnd();
|
|
3075
|
+
}
|
|
3076
|
+
const baseLower = rest.toLowerCase();
|
|
3077
|
+
if (MULTIWORD_BASE_TYPES.includes(baseLower)) {
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3080
|
+
const parts = rest.split(".");
|
|
3081
|
+
if (parts.length > 2) {
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`batchValues: invalid type name '${typeName}'. Schema-qualified types allow at most one dot (schema.type).`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
for (const part of parts) {
|
|
3087
|
+
if (!IDENT_RE.test(part)) {
|
|
3088
|
+
throw new Error(
|
|
3089
|
+
`batchValues: invalid type name '${typeName}'. Base type "${part}" is not a valid SQL identifier ([A-Za-z_][A-Za-z0-9_]*) and is not in the multi-word type allowlist.`
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3015
3094
|
function isBatchValuesRef(value) {
|
|
3016
3095
|
return typeof value === "object" && value !== null && "__kind" in value && value.__kind === "batchValues";
|
|
3017
3096
|
}
|
|
@@ -3024,20 +3103,28 @@ function batchValues(data, columns, types, opts) {
|
|
|
3024
3103
|
if (columns.length === 0) {
|
|
3025
3104
|
throw new Error("batchValues: at least one column is required");
|
|
3026
3105
|
}
|
|
3027
|
-
const
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
`batchValues: invalid type name '${invalidType}'. Type names must contain only letters, digits, and underscores.`
|
|
3031
|
-
);
|
|
3106
|
+
const normalizedTypes = types.map((t) => t.trim());
|
|
3107
|
+
for (const t of normalizedTypes) {
|
|
3108
|
+
validateTypeName(t);
|
|
3032
3109
|
}
|
|
3033
|
-
|
|
3110
|
+
const alias = opts?.alias ?? "batch";
|
|
3111
|
+
validateIdentifier(alias, "alias");
|
|
3112
|
+
for (const col2 of columns) {
|
|
3113
|
+
validateIdentifier(col2, "column");
|
|
3114
|
+
}
|
|
3115
|
+
const frozenData = Object.freeze(
|
|
3116
|
+
data.map((row) => Object.freeze([...row]))
|
|
3117
|
+
);
|
|
3118
|
+
const frozenColumns = Object.freeze([...columns]);
|
|
3119
|
+
const frozenTypes = Object.freeze([...normalizedTypes]);
|
|
3120
|
+
return Object.freeze({
|
|
3034
3121
|
__kind: "batchValues",
|
|
3035
|
-
data,
|
|
3036
|
-
columns,
|
|
3037
|
-
types,
|
|
3038
|
-
alias
|
|
3122
|
+
data: frozenData,
|
|
3123
|
+
columns: frozenColumns,
|
|
3124
|
+
types: frozenTypes,
|
|
3125
|
+
alias,
|
|
3039
3126
|
ordinality: opts?.ordinality ?? false
|
|
3040
|
-
};
|
|
3127
|
+
});
|
|
3041
3128
|
}
|
|
3042
3129
|
|
|
3043
3130
|
// src/dx/expressions.ts
|
|
@@ -4018,36 +4105,31 @@ function coalesce(fields, as) {
|
|
|
4018
4105
|
};
|
|
4019
4106
|
}
|
|
4020
4107
|
function raw(sqlFragment, as) {
|
|
4021
|
-
|
|
4022
|
-
throw new Error("raw() requires a non-empty alias");
|
|
4023
|
-
}
|
|
4108
|
+
validateIdentifier(as, "column");
|
|
4024
4109
|
return {
|
|
4025
4110
|
__expr: true,
|
|
4026
4111
|
intent: { kind: "raw", sql: sqlFragment, as }
|
|
4027
4112
|
};
|
|
4028
4113
|
}
|
|
4029
4114
|
function col(column, alias) {
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
if (!alias || alias.trim() === "") {
|
|
4034
|
-
throw new Error("col() requires a non-empty alias");
|
|
4035
|
-
}
|
|
4115
|
+
validateIdentifier(column, "column");
|
|
4116
|
+
validateIdentifier(alias, "column");
|
|
4036
4117
|
return {
|
|
4037
4118
|
__expr: true,
|
|
4038
4119
|
intent: { kind: "columnAlias", column, alias }
|
|
4039
4120
|
};
|
|
4040
4121
|
}
|
|
4041
4122
|
function relationColumn(relation, column, as) {
|
|
4042
|
-
if (!relation
|
|
4123
|
+
if (!relation) {
|
|
4043
4124
|
throw new Error("relationColumn() requires a non-empty relation path");
|
|
4044
4125
|
}
|
|
4045
|
-
|
|
4046
|
-
|
|
4126
|
+
for (const segment of relation.split(".")) {
|
|
4127
|
+
validateIdentifier(segment, "relation");
|
|
4047
4128
|
}
|
|
4048
|
-
if (
|
|
4049
|
-
|
|
4129
|
+
if (column !== "*") {
|
|
4130
|
+
validateIdentifier(column, "column");
|
|
4050
4131
|
}
|
|
4132
|
+
validateIdentifier(as, "column");
|
|
4051
4133
|
return {
|
|
4052
4134
|
__expr: true,
|
|
4053
4135
|
intent: { kind: "relationColumn", relation, column, as }
|
|
@@ -4406,7 +4488,7 @@ async function runAfterMutationHooks(hooks, ctx, result, onHookError) {
|
|
|
4406
4488
|
error,
|
|
4407
4489
|
hook.name || "anonymous",
|
|
4408
4490
|
frozen,
|
|
4409
|
-
"
|
|
4491
|
+
"afterMutation"
|
|
4410
4492
|
);
|
|
4411
4493
|
if (action === "continue") continue;
|
|
4412
4494
|
}
|
|
@@ -4619,7 +4701,8 @@ function subquery(table) {
|
|
|
4619
4701
|
function outerRef(column) {
|
|
4620
4702
|
return {
|
|
4621
4703
|
kind: "ref",
|
|
4622
|
-
column
|
|
4704
|
+
column,
|
|
4705
|
+
outer: true
|
|
4623
4706
|
};
|
|
4624
4707
|
}
|
|
4625
4708
|
function isSubqueryExpression(value) {
|
|
@@ -7056,10 +7139,9 @@ function stream(builder, options) {
|
|
|
7056
7139
|
};
|
|
7057
7140
|
let hookIntent = rawIntent;
|
|
7058
7141
|
try {
|
|
7059
|
-
const afterHookCtx = await
|
|
7060
|
-
hookStore
|
|
7061
|
-
ctx,
|
|
7062
|
-
onHookError
|
|
7142
|
+
const afterHookCtx = await withReentrancyGuard(
|
|
7143
|
+
hookStore,
|
|
7144
|
+
(s) => runBeforeQueryHooks(s.beforeQuery, ctx, onHookError)
|
|
7063
7145
|
);
|
|
7064
7146
|
hookIntent = afterHookCtx.intent;
|
|
7065
7147
|
} catch (error) {
|
|
@@ -7515,6 +7597,13 @@ var QueryBuilderImpl = class _QueryBuilderImpl {
|
|
|
7515
7597
|
"join(batchValuesRef): an `on` condition is required for BatchValues joins"
|
|
7516
7598
|
);
|
|
7517
7599
|
}
|
|
7600
|
+
validateIdentifier(bv.alias, "alias");
|
|
7601
|
+
for (const col2 of bv.columns) {
|
|
7602
|
+
validateIdentifier(col2, "column");
|
|
7603
|
+
}
|
|
7604
|
+
if (opts.as !== void 0) {
|
|
7605
|
+
validateIdentifier(opts.as, "alias");
|
|
7606
|
+
}
|
|
7518
7607
|
const joinIntent = {
|
|
7519
7608
|
batchValues: {
|
|
7520
7609
|
data: bv.data,
|
|
@@ -7530,6 +7619,9 @@ var QueryBuilderImpl = class _QueryBuilderImpl {
|
|
|
7530
7619
|
builder.joinIntents.push(joinIntent);
|
|
7531
7620
|
} else {
|
|
7532
7621
|
validateIdentifier(relationOrTableOrBatch, "table");
|
|
7622
|
+
if (opts?.as !== void 0) {
|
|
7623
|
+
validateIdentifier(opts.as, "alias");
|
|
7624
|
+
}
|
|
7533
7625
|
const joinIntent = opts?.on ? {
|
|
7534
7626
|
table: relationOrTableOrBatch,
|
|
7535
7627
|
on: opts.on,
|