@dbsp/core 1.10.0 → 1.10.1
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 +2849 -2827
- package/dist/index.js +717 -687
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3219,375 +3219,788 @@ function batchValues(data, columns, types, opts) {
|
|
|
3219
3219
|
});
|
|
3220
3220
|
}
|
|
3221
3221
|
|
|
3222
|
-
// src/dx/
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
function toExpressionIntent(input) {
|
|
3227
|
-
if (input instanceof ExpressionRef) return input.intent;
|
|
3228
|
-
if (typeof input === "object" && input !== null && "__expr" in input && input.__expr === true && "intent" in input) {
|
|
3229
|
-
return input.intent;
|
|
3222
|
+
// src/dx/column-utils.ts
|
|
3223
|
+
function getColumnName(field) {
|
|
3224
|
+
if (typeof field === "string") {
|
|
3225
|
+
return field;
|
|
3230
3226
|
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3227
|
+
const colName = field[COLUMN_META];
|
|
3228
|
+
if (colName === void 0) {
|
|
3229
|
+
throw new Error("Invalid ColumnRef: missing COLUMN_META");
|
|
3233
3230
|
}
|
|
3234
|
-
return
|
|
3231
|
+
return colName;
|
|
3235
3232
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
constructor(
|
|
3240
|
-
this.
|
|
3233
|
+
|
|
3234
|
+
// src/dx/window-functions.ts
|
|
3235
|
+
var WindowBuilder = class _WindowBuilder {
|
|
3236
|
+
constructor(fnKind, partitions = [], orders = []) {
|
|
3237
|
+
this.fnKind = fnKind;
|
|
3238
|
+
this.partitions = partitions;
|
|
3239
|
+
this.orders = orders;
|
|
3241
3240
|
}
|
|
3241
|
+
fnKind;
|
|
3242
|
+
partitions;
|
|
3243
|
+
orders;
|
|
3242
3244
|
/**
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3245
|
+
* Create a ranking window builder (row_number, rank, dense_rank)
|
|
3246
|
+
* @internal Use factory functions instead: rowNumber(), rank(), denseRank()
|
|
3245
3247
|
*/
|
|
3246
|
-
|
|
3247
|
-
return new
|
|
3248
|
-
...this.intent,
|
|
3249
|
-
as: alias
|
|
3250
|
-
});
|
|
3251
|
-
}
|
|
3252
|
-
/** WHERE: expr = value */
|
|
3253
|
-
eq(value) {
|
|
3254
|
-
return { kind: "expression", expr: this.intent, operator: "eq", value };
|
|
3255
|
-
}
|
|
3256
|
-
/** WHERE: expr != value */
|
|
3257
|
-
neq(value) {
|
|
3258
|
-
return { kind: "expression", expr: this.intent, operator: "neq", value };
|
|
3248
|
+
static ranking(fn2) {
|
|
3249
|
+
return new _WindowBuilder({ type: "ranking", fn: fn2 });
|
|
3259
3250
|
}
|
|
3260
|
-
/**
|
|
3261
|
-
|
|
3262
|
-
|
|
3251
|
+
/**
|
|
3252
|
+
* Create an aggregate window builder (sum, avg, count, min, max)
|
|
3253
|
+
* @internal Use factory functions instead: sum(field), avg(field), etc.
|
|
3254
|
+
*/
|
|
3255
|
+
static aggregate(fn2, field) {
|
|
3256
|
+
return new _WindowBuilder({
|
|
3257
|
+
type: "aggregate",
|
|
3258
|
+
fn: fn2,
|
|
3259
|
+
...field !== void 0 ? { field } : {}
|
|
3260
|
+
});
|
|
3263
3261
|
}
|
|
3264
|
-
/**
|
|
3265
|
-
|
|
3266
|
-
|
|
3262
|
+
/**
|
|
3263
|
+
* Create an offset window builder (lag, lead)
|
|
3264
|
+
* @internal Use factory functions instead: lag(field), lead(field)
|
|
3265
|
+
*/
|
|
3266
|
+
static offset(fn2, field) {
|
|
3267
|
+
return new _WindowBuilder({ type: "offset", fn: fn2, field });
|
|
3267
3268
|
}
|
|
3268
|
-
/**
|
|
3269
|
-
|
|
3270
|
-
|
|
3269
|
+
/**
|
|
3270
|
+
* Add partition field(s) to the OVER clause.
|
|
3271
|
+
* Multiple calls APPEND fields (not replace).
|
|
3272
|
+
* Supports both string field names and ColumnRef (DX-040).
|
|
3273
|
+
*
|
|
3274
|
+
* @example
|
|
3275
|
+
* sum('amount').partitionBy('user_id').partitionBy('category')
|
|
3276
|
+
* // → PARTITION BY "user_id", "category"
|
|
3277
|
+
*
|
|
3278
|
+
* @example DX-040 with ColumnRef
|
|
3279
|
+
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3280
|
+
*/
|
|
3281
|
+
partitionBy(...fields) {
|
|
3282
|
+
const fieldNames = fields.map(
|
|
3283
|
+
(f) => typeof f === "string" ? f : getColumnName(f)
|
|
3284
|
+
);
|
|
3285
|
+
return new _WindowBuilder(
|
|
3286
|
+
this.fnKind,
|
|
3287
|
+
[...this.partitions, ...fieldNames],
|
|
3288
|
+
this.orders
|
|
3289
|
+
);
|
|
3271
3290
|
}
|
|
3272
|
-
/**
|
|
3273
|
-
|
|
3274
|
-
|
|
3291
|
+
/**
|
|
3292
|
+
* Add order field to the OVER clause.
|
|
3293
|
+
* Multiple calls APPEND fields (not replace).
|
|
3294
|
+
* Supports both string field names and ColumnRef (DX-040).
|
|
3295
|
+
*
|
|
3296
|
+
* @param field - Column name or ColumnRef to order by
|
|
3297
|
+
* @param direction - Sort direction: 'asc' (default) or 'desc'
|
|
3298
|
+
*
|
|
3299
|
+
* @example
|
|
3300
|
+
* rowNumber().orderBy('created_at').orderBy('id', 'desc')
|
|
3301
|
+
* // → ORDER BY "created_at" ASC, "id" DESC
|
|
3302
|
+
*
|
|
3303
|
+
* @example DX-040 with ColumnRef
|
|
3304
|
+
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3305
|
+
*/
|
|
3306
|
+
orderBy(field, direction = "asc") {
|
|
3307
|
+
const fieldName = typeof field === "string" ? field : getColumnName(field);
|
|
3308
|
+
return new _WindowBuilder(this.fnKind, this.partitions, [
|
|
3309
|
+
...this.orders,
|
|
3310
|
+
{ field: fieldName, direction }
|
|
3311
|
+
]);
|
|
3275
3312
|
}
|
|
3276
3313
|
/**
|
|
3277
|
-
*
|
|
3278
|
-
*
|
|
3279
|
-
* Returns a new ExpressionRef — does not mutate.
|
|
3314
|
+
* Finalize the window expression with an alias.
|
|
3315
|
+
* Returns ExpressionSpec for use in columns().
|
|
3280
3316
|
*
|
|
3281
|
-
* @
|
|
3282
|
-
*
|
|
3283
|
-
* @
|
|
3317
|
+
* @param alias - Required alias for the result column
|
|
3318
|
+
*
|
|
3319
|
+
* @example
|
|
3320
|
+
* columns(['id', rowNumber().orderBy('date').as('rn')])
|
|
3284
3321
|
*/
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3322
|
+
as(alias) {
|
|
3323
|
+
return {
|
|
3324
|
+
__expr: true,
|
|
3325
|
+
intent: this.toWindowIntent(alias)
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Convert builder state to WindowIntent — produces the correct discriminated branch.
|
|
3330
|
+
* - ranking → RankingWindowIntent (no field)
|
|
3331
|
+
* - aggregate → AggregateWindowIntent (field required; COUNT uses '*' when omitted)
|
|
3332
|
+
* - offset → OffsetWindowIntent (field required)
|
|
3333
|
+
*/
|
|
3334
|
+
toWindowIntent(alias) {
|
|
3335
|
+
const over = {};
|
|
3336
|
+
if (this.partitions.length > 0) {
|
|
3337
|
+
over.partitionBy = this.partitions;
|
|
3290
3338
|
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3339
|
+
if (this.orders.length > 0) {
|
|
3340
|
+
over.orderBy = this.orders;
|
|
3341
|
+
}
|
|
3342
|
+
if (this.fnKind.type === "ranking") {
|
|
3343
|
+
return {
|
|
3344
|
+
kind: "window",
|
|
3345
|
+
function: this.fnKind.fn,
|
|
3346
|
+
alias,
|
|
3347
|
+
over
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
if (this.fnKind.type === "offset") {
|
|
3351
|
+
return {
|
|
3352
|
+
kind: "window",
|
|
3353
|
+
function: this.fnKind.fn,
|
|
3354
|
+
field: this.fnKind.field,
|
|
3355
|
+
alias,
|
|
3356
|
+
over
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
const { field } = this.fnKind;
|
|
3360
|
+
return {
|
|
3361
|
+
kind: "window",
|
|
3362
|
+
function: this.fnKind.fn,
|
|
3363
|
+
...field !== void 0 && { field },
|
|
3364
|
+
alias,
|
|
3365
|
+
over
|
|
3366
|
+
};
|
|
3295
3367
|
}
|
|
3296
3368
|
};
|
|
3297
|
-
function
|
|
3298
|
-
return
|
|
3299
|
-
kind: "ref",
|
|
3300
|
-
column
|
|
3301
|
-
});
|
|
3369
|
+
function rowNumber() {
|
|
3370
|
+
return WindowBuilder.ranking("row_number");
|
|
3302
3371
|
}
|
|
3303
|
-
function
|
|
3304
|
-
return
|
|
3305
|
-
kind: "param",
|
|
3306
|
-
value
|
|
3307
|
-
});
|
|
3372
|
+
function rank() {
|
|
3373
|
+
return WindowBuilder.ranking("rank");
|
|
3308
3374
|
}
|
|
3309
|
-
function
|
|
3310
|
-
|
|
3311
|
-
throw new Error(`Invalid type name: ${typeName}`);
|
|
3312
|
-
}
|
|
3313
|
-
return new ExpressionRef({
|
|
3314
|
-
kind: "cast",
|
|
3315
|
-
expr: expr.intent,
|
|
3316
|
-
typeName
|
|
3317
|
-
});
|
|
3375
|
+
function denseRank() {
|
|
3376
|
+
return WindowBuilder.ranking("dense_rank");
|
|
3318
3377
|
}
|
|
3319
|
-
function
|
|
3320
|
-
|
|
3321
|
-
throw new Error(`Invalid operator: ${operator}`);
|
|
3322
|
-
}
|
|
3323
|
-
return new ExpressionRef({
|
|
3324
|
-
kind: "customOp",
|
|
3325
|
-
operator,
|
|
3326
|
-
left: toExpressionIntent(left),
|
|
3327
|
-
right: toExpressionIntent(right)
|
|
3328
|
-
});
|
|
3378
|
+
function wSum(field) {
|
|
3379
|
+
return WindowBuilder.aggregate("sum", field);
|
|
3329
3380
|
}
|
|
3330
|
-
function
|
|
3331
|
-
|
|
3332
|
-
throw new Error(`Invalid function name: ${name}`);
|
|
3333
|
-
}
|
|
3334
|
-
const regularArgs = [];
|
|
3335
|
-
const orderByArgs = [];
|
|
3336
|
-
for (const arg of args) {
|
|
3337
|
-
if (isAggOrderByArg(arg)) {
|
|
3338
|
-
orderByArgs.push(arg);
|
|
3339
|
-
} else {
|
|
3340
|
-
regularArgs.push(toExpressionIntent(arg));
|
|
3341
|
-
}
|
|
3342
|
-
}
|
|
3343
|
-
const intent = {
|
|
3344
|
-
kind: "customFn",
|
|
3345
|
-
name,
|
|
3346
|
-
args: regularArgs,
|
|
3347
|
-
...orderByArgs.length > 0 ? { aggOrderBy: orderByArgs } : {}
|
|
3348
|
-
};
|
|
3349
|
-
return new ExpressionRef(intent);
|
|
3381
|
+
function wAvg(field) {
|
|
3382
|
+
return WindowBuilder.aggregate("avg", field);
|
|
3350
3383
|
}
|
|
3351
|
-
function
|
|
3352
|
-
return
|
|
3353
|
-
kind: "literal",
|
|
3354
|
-
value
|
|
3355
|
-
});
|
|
3384
|
+
function wCount(field) {
|
|
3385
|
+
return WindowBuilder.aggregate("count", field);
|
|
3356
3386
|
}
|
|
3357
|
-
function
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
return
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3387
|
+
function wMin(field) {
|
|
3388
|
+
return WindowBuilder.aggregate("min", field);
|
|
3389
|
+
}
|
|
3390
|
+
function wMax(field) {
|
|
3391
|
+
return WindowBuilder.aggregate("max", field);
|
|
3392
|
+
}
|
|
3393
|
+
function lag(field) {
|
|
3394
|
+
return WindowBuilder.offset("lag", field);
|
|
3395
|
+
}
|
|
3396
|
+
function lead(field) {
|
|
3397
|
+
return WindowBuilder.offset("lead", field);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// src/dx/filters.ts
|
|
3401
|
+
function distinct(field) {
|
|
3402
|
+
return { field, distinct: true };
|
|
3403
|
+
}
|
|
3404
|
+
function isDistinctField(value) {
|
|
3405
|
+
return typeof value === "object" && value !== null && "field" in value && typeof value.field === "string" && "distinct" in value && value.distinct === true;
|
|
3406
|
+
}
|
|
3407
|
+
function createComparisonFilter(operator) {
|
|
3408
|
+
return (field, value) => ({
|
|
3409
|
+
kind: "comparison",
|
|
3410
|
+
field: getColumnName(field),
|
|
3411
|
+
operator,
|
|
3412
|
+
value
|
|
3365
3413
|
});
|
|
3366
3414
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3415
|
+
var eq = createComparisonFilter("eq");
|
|
3416
|
+
var neq = createComparisonFilter("neq");
|
|
3417
|
+
var gt = createComparisonFilter("gt");
|
|
3418
|
+
var gte = createComparisonFilter("gte");
|
|
3419
|
+
var lt = createComparisonFilter("lt");
|
|
3420
|
+
var lte = createComparisonFilter("lte");
|
|
3421
|
+
var isDistinctFrom = createComparisonFilter("isDistinctFrom");
|
|
3422
|
+
function like(field, pattern, options) {
|
|
3423
|
+
const caseInsensitive = typeof options === "boolean" ? options : options?.caseInsensitive;
|
|
3424
|
+
const escapeChar = typeof options === "object" ? options.escape : void 0;
|
|
3425
|
+
const intent = {
|
|
3426
|
+
kind: "like",
|
|
3427
|
+
field: getColumnName(field),
|
|
3428
|
+
pattern
|
|
3429
|
+
};
|
|
3430
|
+
const withCi = caseInsensitive !== void 0 ? { ...intent, caseInsensitive } : intent;
|
|
3431
|
+
if (escapeChar !== void 0) {
|
|
3432
|
+
return { ...withCi, escape: escapeChar };
|
|
3370
3433
|
}
|
|
3371
|
-
return
|
|
3372
|
-
kind: "namedArg",
|
|
3373
|
-
name,
|
|
3374
|
-
value: toExpressionIntent(value)
|
|
3375
|
-
});
|
|
3434
|
+
return withCi;
|
|
3376
3435
|
}
|
|
3377
|
-
function
|
|
3378
|
-
return
|
|
3436
|
+
function inArray(field, values) {
|
|
3437
|
+
return { kind: "in", field: getColumnName(field), values };
|
|
3379
3438
|
}
|
|
3380
|
-
function
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3439
|
+
function inSubquery(field, query) {
|
|
3440
|
+
const expr = "build" in query ? query.build() : query;
|
|
3441
|
+
return {
|
|
3442
|
+
kind: "in",
|
|
3443
|
+
field: getColumnName(field),
|
|
3444
|
+
subquery: expr.toIntent()
|
|
3445
|
+
};
|
|
3385
3446
|
}
|
|
3386
|
-
function
|
|
3387
|
-
return
|
|
3447
|
+
function any(field, values) {
|
|
3448
|
+
return { kind: "any", field: getColumnName(field), values };
|
|
3388
3449
|
}
|
|
3389
|
-
function
|
|
3390
|
-
return {
|
|
3450
|
+
function isNull(field) {
|
|
3451
|
+
return { kind: "null", field: getColumnName(field), operator: "isNull" };
|
|
3391
3452
|
}
|
|
3392
|
-
function
|
|
3393
|
-
|
|
3394
|
-
return fn("array_agg", colExpr, ...rest);
|
|
3453
|
+
function isNotNull(field) {
|
|
3454
|
+
return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
|
|
3395
3455
|
}
|
|
3396
|
-
function
|
|
3397
|
-
const
|
|
3398
|
-
return
|
|
3456
|
+
function and(...conditions) {
|
|
3457
|
+
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
3458
|
+
return { kind: "and", conditions: flatConditions };
|
|
3399
3459
|
}
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3460
|
+
function or(...conditions) {
|
|
3461
|
+
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
3462
|
+
return { kind: "or", conditions: flatConditions };
|
|
3463
|
+
}
|
|
3464
|
+
function not(condition) {
|
|
3465
|
+
return { kind: "not", condition };
|
|
3466
|
+
}
|
|
3467
|
+
function exists(relation, options) {
|
|
3468
|
+
const result = { kind: "exists", relation };
|
|
3469
|
+
if (options?.where !== void 0) {
|
|
3470
|
+
result.where = options.where;
|
|
3405
3471
|
}
|
|
3406
|
-
if (
|
|
3407
|
-
|
|
3472
|
+
if (options?.recursive !== void 0) {
|
|
3473
|
+
result.recursive = options.recursive;
|
|
3408
3474
|
}
|
|
3409
|
-
if (
|
|
3410
|
-
|
|
3475
|
+
if (options?.include !== void 0) {
|
|
3476
|
+
result.include = options.include;
|
|
3411
3477
|
}
|
|
3412
|
-
return
|
|
3478
|
+
return result;
|
|
3413
3479
|
}
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3480
|
+
function notExists(relation, options) {
|
|
3481
|
+
const result = {
|
|
3482
|
+
kind: "notExists",
|
|
3483
|
+
relation
|
|
3484
|
+
};
|
|
3485
|
+
if (options?.where !== void 0) {
|
|
3486
|
+
result.where = options.where;
|
|
3418
3487
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
...this.branches,
|
|
3422
|
-
{ condition, result: toResultIntent(thenValue) }
|
|
3423
|
-
]);
|
|
3488
|
+
if (options?.recursive !== void 0) {
|
|
3489
|
+
result.recursive = options.recursive;
|
|
3424
3490
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
kind: "case",
|
|
3428
|
-
when: this.branches,
|
|
3429
|
-
else: toResultIntent(elseValue)
|
|
3430
|
-
};
|
|
3431
|
-
return new ExpressionRef(intent);
|
|
3491
|
+
if (options?.include !== void 0) {
|
|
3492
|
+
result.include = options.include;
|
|
3432
3493
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
3494
|
+
return result;
|
|
3495
|
+
}
|
|
3496
|
+
function rawExists(sq) {
|
|
3497
|
+
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
3498
|
+
return { kind: "rawExists", subquery: intent };
|
|
3499
|
+
}
|
|
3500
|
+
function rawNotExists(sq) {
|
|
3501
|
+
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
3502
|
+
return { kind: "rawNotExists", subquery: intent };
|
|
3503
|
+
}
|
|
3504
|
+
function getRelationName(rel) {
|
|
3505
|
+
const meta = rel[RELATION_META];
|
|
3506
|
+
if (!meta) {
|
|
3507
|
+
throw new Error("Invalid RelationRef: missing RELATION_META");
|
|
3435
3508
|
}
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3509
|
+
return meta.target;
|
|
3510
|
+
}
|
|
3511
|
+
function every(relation, filter) {
|
|
3512
|
+
const relationName = getRelationName(relation);
|
|
3513
|
+
const where = filter(relation);
|
|
3514
|
+
return {
|
|
3515
|
+
kind: "relationFilter",
|
|
3516
|
+
relation: relationName,
|
|
3517
|
+
where,
|
|
3518
|
+
mode: "every"
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
function none(relation, filter) {
|
|
3522
|
+
const relationName = getRelationName(relation);
|
|
3523
|
+
const where = filter(relation);
|
|
3524
|
+
return {
|
|
3525
|
+
kind: "relationFilter",
|
|
3526
|
+
relation: relationName,
|
|
3527
|
+
where,
|
|
3528
|
+
mode: "none"
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
function some(relation, filter) {
|
|
3532
|
+
const relationName = getRelationName(relation);
|
|
3533
|
+
const where = filter(relation);
|
|
3534
|
+
return {
|
|
3535
|
+
kind: "relationFilter",
|
|
3536
|
+
relation: relationName,
|
|
3537
|
+
where,
|
|
3538
|
+
mode: "some"
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
function coalesce(fields, as) {
|
|
3542
|
+
if (fields.length === 0) {
|
|
3543
|
+
throw new Error("coalesce() requires at least one field");
|
|
3442
3544
|
}
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3545
|
+
if (!as || as.trim() === "") {
|
|
3546
|
+
throw new Error("coalesce() requires a non-empty alias");
|
|
3547
|
+
}
|
|
3548
|
+
for (const f of fields) {
|
|
3549
|
+
validateIdentifier(f, "column");
|
|
3550
|
+
}
|
|
3551
|
+
validateIdentifier(as, "column");
|
|
3552
|
+
return {
|
|
3553
|
+
__expr: true,
|
|
3554
|
+
intent: { kind: "coalesce", fields, as }
|
|
3555
|
+
};
|
|
3446
3556
|
}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
function requireAdapter(adapter, operationName) {
|
|
3450
|
-
if (!adapter) {
|
|
3557
|
+
function raw(sqlFragment, as) {
|
|
3558
|
+
if (typeof as !== "string" || as.trim().length === 0) {
|
|
3451
3559
|
throw new InvalidOperationError(
|
|
3452
|
-
|
|
3453
|
-
"
|
|
3560
|
+
"raw",
|
|
3561
|
+
"raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
|
|
3454
3562
|
);
|
|
3455
3563
|
}
|
|
3456
|
-
|
|
3564
|
+
validateIdentifier(as, "column");
|
|
3565
|
+
return {
|
|
3566
|
+
__expr: true,
|
|
3567
|
+
intent: { kind: "raw", sql: sqlFragment, as }
|
|
3568
|
+
};
|
|
3457
3569
|
}
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
this.schemaName = schemaName;
|
|
3570
|
+
function col(column, alias) {
|
|
3571
|
+
validateIdentifier(column, "column");
|
|
3572
|
+
validateIdentifier(alias, "column");
|
|
3573
|
+
return {
|
|
3574
|
+
__expr: true,
|
|
3575
|
+
intent: { kind: "columnAlias", column, alias }
|
|
3576
|
+
};
|
|
3577
|
+
}
|
|
3578
|
+
function relationColumn(relation, column, as) {
|
|
3579
|
+
if (!relation) {
|
|
3580
|
+
throw new Error("relationColumn() requires a non-empty relation path");
|
|
3470
3581
|
}
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3582
|
+
for (const segment of relation.split(".")) {
|
|
3583
|
+
validateIdentifier(segment, "relation");
|
|
3584
|
+
}
|
|
3585
|
+
if (column !== "*") {
|
|
3586
|
+
validateIdentifier(column, "column");
|
|
3587
|
+
}
|
|
3588
|
+
validateIdentifier(as, "column");
|
|
3589
|
+
return {
|
|
3590
|
+
__expr: true,
|
|
3591
|
+
intent: { kind: "relationColumn", relation, column, as }
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
var SQL_RAW_MARKER = /* @__PURE__ */ Symbol.for("dbsp:raw-sql");
|
|
3595
|
+
function sql(sqlFragment) {
|
|
3596
|
+
if (!sqlFragment || sqlFragment.trim() === "") {
|
|
3597
|
+
throw new Error("sql() requires a non-empty SQL fragment");
|
|
3598
|
+
}
|
|
3599
|
+
return { [SQL_RAW_MARKER]: true, sql: sqlFragment };
|
|
3600
|
+
}
|
|
3601
|
+
function isSqlRaw(value) {
|
|
3602
|
+
return typeof value === "object" && value !== null && SQL_RAW_MARKER in value && value[SQL_RAW_MARKER] === true;
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// src/dx/expressions.ts
|
|
3606
|
+
var OPERATOR_PATTERN = /^[a-zA-Z_<>=!@#%^&|~*+\-/.]+$/;
|
|
3607
|
+
var FUNCTION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
|
|
3608
|
+
var TYPE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\[\])?$/;
|
|
3609
|
+
function toExpressionIntent(input) {
|
|
3610
|
+
if (input instanceof ExpressionRef) return input.intent;
|
|
3611
|
+
if (typeof input === "object" && input !== null && "__expr" in input && input.__expr === true && "intent" in input) {
|
|
3612
|
+
return input.intent;
|
|
3613
|
+
}
|
|
3614
|
+
if (typeof input === "string") {
|
|
3615
|
+
return { kind: "ref", column: input };
|
|
3616
|
+
}
|
|
3617
|
+
return { kind: "param", value: input };
|
|
3618
|
+
}
|
|
3619
|
+
var ExpressionRef = class _ExpressionRef {
|
|
3620
|
+
__expr = true;
|
|
3621
|
+
intent;
|
|
3622
|
+
constructor(intent) {
|
|
3623
|
+
this.intent = intent;
|
|
3487
3624
|
}
|
|
3488
3625
|
/**
|
|
3489
|
-
*
|
|
3490
|
-
*
|
|
3491
|
-
* @param columnName - The name of the index column in the CTE.
|
|
3626
|
+
* Set an alias for this expression in SELECT.
|
|
3627
|
+
* Returns a new ExpressionRef — does not mutate.
|
|
3492
3628
|
*/
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3629
|
+
as(alias) {
|
|
3630
|
+
return new _ExpressionRef({
|
|
3631
|
+
...this.intent,
|
|
3632
|
+
as: alias
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
/** WHERE: expr = value */
|
|
3636
|
+
eq(value) {
|
|
3637
|
+
return { kind: "expression", expr: this.intent, operator: "eq", value };
|
|
3638
|
+
}
|
|
3639
|
+
/** WHERE: expr != value */
|
|
3640
|
+
neq(value) {
|
|
3641
|
+
return { kind: "expression", expr: this.intent, operator: "neq", value };
|
|
3642
|
+
}
|
|
3643
|
+
/** WHERE: expr > value */
|
|
3644
|
+
gt(value) {
|
|
3645
|
+
return { kind: "expression", expr: this.intent, operator: "gt", value };
|
|
3646
|
+
}
|
|
3647
|
+
/** WHERE: expr >= value */
|
|
3648
|
+
gte(value) {
|
|
3649
|
+
return { kind: "expression", expr: this.intent, operator: "gte", value };
|
|
3650
|
+
}
|
|
3651
|
+
/** WHERE: expr < value */
|
|
3652
|
+
lt(value) {
|
|
3653
|
+
return { kind: "expression", expr: this.intent, operator: "lt", value };
|
|
3654
|
+
}
|
|
3655
|
+
/** WHERE: expr <= value */
|
|
3656
|
+
lte(value) {
|
|
3657
|
+
return { kind: "expression", expr: this.intent, operator: "lte", value };
|
|
3496
3658
|
}
|
|
3497
3659
|
/**
|
|
3498
|
-
*
|
|
3660
|
+
* Add a FILTER (WHERE ...) clause to this function expression.
|
|
3661
|
+
* Valid on `fn()` expressions (customFn kind), including DISTINCT calls
|
|
3662
|
+
* produced by `fn(aggName, distinct(field))` (still customFn kind).
|
|
3663
|
+
* Returns a new ExpressionRef — does not mutate.
|
|
3499
3664
|
*
|
|
3500
|
-
*
|
|
3665
|
+
* NOTE: the FILTER clause is only emitted when the expression is used in
|
|
3666
|
+
* column position (`.columns([...])`) — the adapter patches it in at that
|
|
3667
|
+
* one SELECT-list branch (compiler.ts). Using `.filter()` inside
|
|
3668
|
+
* `.orderBy()`, `.having()`, `cast()`, or any other nested context compiles
|
|
3669
|
+
* successfully but silently drops the FILTER clause (tracked separately,
|
|
3670
|
+
* not specific to DISTINCT).
|
|
3671
|
+
*
|
|
3672
|
+
* @example fn('array_agg', ref('name')).filter(eq('active', true))
|
|
3673
|
+
* → array_agg("name") FILTER (WHERE "active" = $1) // in .columns()
|
|
3674
|
+
* @example fn('count', distinct('id')).filter(eq('active', true))
|
|
3675
|
+
* → count(DISTINCT id) FILTER (WHERE "active" = $1) // in .columns()
|
|
3676
|
+
* @throws Error if called on any other expression kind
|
|
3501
3677
|
*/
|
|
3502
|
-
|
|
3503
|
-
if (
|
|
3504
|
-
throw new
|
|
3505
|
-
|
|
3506
|
-
"CTE requires a data source \u2014 call .fromUnnest() first"
|
|
3678
|
+
filter(condition) {
|
|
3679
|
+
if (this.intent.kind !== "customFn") {
|
|
3680
|
+
throw new Error(
|
|
3681
|
+
`filter() can only be used on function expressions created with fn(). Got kind: '${this.intent.kind}'`
|
|
3507
3682
|
);
|
|
3508
3683
|
}
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
...this.indexColumnName !== void 0 && {
|
|
3514
|
-
indexColumn: this.indexColumnName
|
|
3515
|
-
}
|
|
3516
|
-
};
|
|
3517
|
-
return new CteQueryBuilder(
|
|
3518
|
-
cteIntent,
|
|
3519
|
-
selectBuilder,
|
|
3520
|
-
this.adapter,
|
|
3521
|
-
this.schemaName
|
|
3522
|
-
);
|
|
3684
|
+
return new _ExpressionRef({
|
|
3685
|
+
...this.intent,
|
|
3686
|
+
filter: condition
|
|
3687
|
+
});
|
|
3523
3688
|
}
|
|
3524
3689
|
};
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3690
|
+
function ref2(column) {
|
|
3691
|
+
return new ExpressionRef({
|
|
3692
|
+
kind: "ref",
|
|
3693
|
+
column
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
function param(value) {
|
|
3697
|
+
return new ExpressionRef({
|
|
3698
|
+
kind: "param",
|
|
3699
|
+
value
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
function cast(expr, typeName) {
|
|
3703
|
+
if (!TYPE_NAME_PATTERN.test(typeName)) {
|
|
3704
|
+
throw new Error(`Invalid type name: ${typeName}`);
|
|
3535
3705
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
};
|
|
3706
|
+
return new ExpressionRef({
|
|
3707
|
+
kind: "cast",
|
|
3708
|
+
expr: expr.intent,
|
|
3709
|
+
typeName
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
function op(operator, left, right) {
|
|
3713
|
+
if (!operator || !OPERATOR_PATTERN.test(operator)) {
|
|
3714
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
3546
3715
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3716
|
+
return new ExpressionRef({
|
|
3717
|
+
kind: "customOp",
|
|
3718
|
+
operator,
|
|
3719
|
+
left: toExpressionIntent(left),
|
|
3720
|
+
right: toExpressionIntent(right)
|
|
3721
|
+
});
|
|
3722
|
+
}
|
|
3723
|
+
function fn(name, ...args) {
|
|
3724
|
+
if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
|
|
3725
|
+
throw new Error(`Invalid function name: ${name}`);
|
|
3549
3726
|
}
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3727
|
+
const regularArgs = [];
|
|
3728
|
+
const orderByArgs = [];
|
|
3729
|
+
let distinct2 = false;
|
|
3730
|
+
for (let i = 0; i < args.length; i++) {
|
|
3731
|
+
const arg = args[i];
|
|
3732
|
+
if (isDistinctField2(arg)) {
|
|
3733
|
+
if (i !== 0) {
|
|
3734
|
+
throw new Error(
|
|
3735
|
+
`fn('${name}', ...): distinct() is only supported as the first argument \u2014 SQL DISTINCT applies to the whole argument list, not a single position.`
|
|
3736
|
+
);
|
|
3737
|
+
}
|
|
3738
|
+
const field = arg.field;
|
|
3739
|
+
if (field === "*") {
|
|
3740
|
+
throw new Error(
|
|
3741
|
+
`fn('${name}', distinct('*')): DISTINCT on '*' is not valid SQL \u2014 '*' has no columns to deduplicate on. Provide a specific column, or omit distinct() (e.g. fn('${name}', star())) for a plain star call.`
|
|
3742
|
+
);
|
|
3743
|
+
}
|
|
3744
|
+
distinct2 = true;
|
|
3745
|
+
regularArgs.push(toExpressionIntent(field));
|
|
3746
|
+
} else if (isAggOrderByArg(arg)) {
|
|
3747
|
+
orderByArgs.push(arg);
|
|
3748
|
+
} else {
|
|
3749
|
+
regularArgs.push(toExpressionIntent(arg));
|
|
3750
|
+
}
|
|
3566
3751
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3752
|
+
const intent = {
|
|
3753
|
+
kind: "customFn",
|
|
3754
|
+
name,
|
|
3755
|
+
args: regularArgs,
|
|
3756
|
+
...distinct2 ? { distinct: true } : {},
|
|
3757
|
+
...orderByArgs.length > 0 ? { aggOrderBy: orderByArgs } : {}
|
|
3758
|
+
};
|
|
3759
|
+
return new ExpressionRef(intent);
|
|
3760
|
+
}
|
|
3761
|
+
function literal(value) {
|
|
3762
|
+
return new ExpressionRef({
|
|
3763
|
+
kind: "literal",
|
|
3764
|
+
value
|
|
3765
|
+
});
|
|
3766
|
+
}
|
|
3767
|
+
function unary(operator, expr) {
|
|
3768
|
+
if (!operator || !OPERATOR_PATTERN.test(operator)) {
|
|
3769
|
+
throw new Error(`Invalid operator: ${operator}`);
|
|
3579
3770
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3771
|
+
return new ExpressionRef({
|
|
3772
|
+
kind: "unary",
|
|
3773
|
+
operator,
|
|
3774
|
+
operand: toExpressionIntent(expr)
|
|
3775
|
+
});
|
|
3776
|
+
}
|
|
3777
|
+
function namedArg(name, value) {
|
|
3778
|
+
if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
|
|
3779
|
+
throw new Error(`namedArg: invalid argument name: ${name}`);
|
|
3585
3780
|
}
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3781
|
+
return new ExpressionRef({
|
|
3782
|
+
kind: "namedArg",
|
|
3783
|
+
name,
|
|
3784
|
+
value: toExpressionIntent(value)
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
function star() {
|
|
3788
|
+
return new ExpressionRef({ kind: "star" });
|
|
3789
|
+
}
|
|
3790
|
+
function array(...items) {
|
|
3791
|
+
return new ExpressionRef({
|
|
3792
|
+
kind: "array",
|
|
3793
|
+
elements: items.map(toExpressionIntent)
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
function isDistinctField2(input) {
|
|
3797
|
+
return !Array.isArray(input) && !(input instanceof ExpressionRef) && isDistinctField(input);
|
|
3798
|
+
}
|
|
3799
|
+
function isAggOrderByArg(input) {
|
|
3800
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof ExpressionRef) && "__aggOrderBy" in input && input.__aggOrderBy === true;
|
|
3801
|
+
}
|
|
3802
|
+
function aggOrderBy(field, direction = "asc") {
|
|
3803
|
+
return { __aggOrderBy: true, field, direction };
|
|
3804
|
+
}
|
|
3805
|
+
function arrayAgg(col2, ...rest) {
|
|
3806
|
+
const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
|
|
3807
|
+
return fn("array_agg", colExpr, ...rest);
|
|
3808
|
+
}
|
|
3809
|
+
function stringAgg(col2, separator, ...rest) {
|
|
3810
|
+
const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
|
|
3811
|
+
return fn("string_agg", colExpr, separator, ...rest);
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
// src/dx/case-when-builder.ts
|
|
3815
|
+
function toResultIntent(value) {
|
|
3816
|
+
if (value instanceof ExpressionRef) {
|
|
3817
|
+
return value.intent;
|
|
3818
|
+
}
|
|
3819
|
+
if (value === null || value === void 0) {
|
|
3820
|
+
return { kind: "literal", value: null };
|
|
3821
|
+
}
|
|
3822
|
+
if (typeof value === "string") {
|
|
3823
|
+
return { kind: "ref", column: value };
|
|
3824
|
+
}
|
|
3825
|
+
return { kind: "literal", value };
|
|
3826
|
+
}
|
|
3827
|
+
var CaseBuilder = class _CaseBuilder {
|
|
3828
|
+
branches;
|
|
3829
|
+
constructor(branches) {
|
|
3830
|
+
this.branches = branches;
|
|
3831
|
+
}
|
|
3832
|
+
when(condition, thenValue) {
|
|
3833
|
+
return new _CaseBuilder([
|
|
3834
|
+
...this.branches,
|
|
3835
|
+
{ condition, result: toResultIntent(thenValue) }
|
|
3836
|
+
]);
|
|
3837
|
+
}
|
|
3838
|
+
else(elseValue) {
|
|
3839
|
+
const intent = {
|
|
3840
|
+
kind: "case",
|
|
3841
|
+
when: this.branches,
|
|
3842
|
+
else: toResultIntent(elseValue)
|
|
3843
|
+
};
|
|
3844
|
+
return new ExpressionRef(intent);
|
|
3845
|
+
}
|
|
3846
|
+
as(alias) {
|
|
3847
|
+
return this.toExpr().as(alias);
|
|
3848
|
+
}
|
|
3849
|
+
toExpr() {
|
|
3850
|
+
const intent = {
|
|
3851
|
+
kind: "case",
|
|
3852
|
+
when: this.branches
|
|
3853
|
+
};
|
|
3854
|
+
return new ExpressionRef(intent);
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
function caseWhen(condition, thenValue) {
|
|
3858
|
+
return new CaseBuilder([{ condition, result: toResultIntent(thenValue) }]);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
// src/dx/builder-utils.ts
|
|
3862
|
+
function requireAdapter(adapter, operationName) {
|
|
3863
|
+
if (!adapter) {
|
|
3864
|
+
throw new InvalidOperationError(
|
|
3865
|
+
operationName,
|
|
3866
|
+
"This operation requires an adapter \u2014 pass an adapter when creating the ORM"
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
return adapter;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
// src/dx/cte-builder.ts
|
|
3873
|
+
var CteBuilder = class {
|
|
3874
|
+
cteName;
|
|
3875
|
+
adapter;
|
|
3876
|
+
schemaName;
|
|
3877
|
+
unnestColumns;
|
|
3878
|
+
indexColumnName;
|
|
3879
|
+
constructor(name, adapter, schemaName) {
|
|
3880
|
+
this.cteName = name;
|
|
3881
|
+
this.adapter = adapter;
|
|
3882
|
+
this.schemaName = schemaName;
|
|
3883
|
+
}
|
|
3884
|
+
/**
|
|
3885
|
+
* Provide the column arrays for the unnest() CTE.
|
|
3886
|
+
* All arrays must have the same length.
|
|
3887
|
+
*
|
|
3888
|
+
* @param columns - A record of column name → array of values.
|
|
3889
|
+
*/
|
|
3890
|
+
fromUnnest(columns) {
|
|
3891
|
+
const lengths = Object.values(columns).map((arr) => arr.length);
|
|
3892
|
+
if (lengths.length > 1 && !lengths.every((l) => l === lengths[0])) {
|
|
3893
|
+
throw new InvalidOperationError(
|
|
3894
|
+
"withCte",
|
|
3895
|
+
`Array length mismatch in CTE unnest columns: lengths are [${lengths.join(", ")}]`
|
|
3896
|
+
);
|
|
3897
|
+
}
|
|
3898
|
+
this.unnestColumns = columns;
|
|
3899
|
+
return this;
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Add a 0-based ordinality index column (uses WITH ORDINALITY).
|
|
3903
|
+
*
|
|
3904
|
+
* @param columnName - The name of the index column in the CTE.
|
|
3905
|
+
*/
|
|
3906
|
+
withIndex(columnName) {
|
|
3907
|
+
this.indexColumnName = columnName;
|
|
3908
|
+
return this;
|
|
3909
|
+
}
|
|
3910
|
+
/**
|
|
3911
|
+
* Attach an outer query and produce a CteQueryBuilder for execution.
|
|
3912
|
+
*
|
|
3913
|
+
* @param selectBuilder - A QueryBuilder (from orm.select(...))
|
|
3914
|
+
*/
|
|
3915
|
+
query(selectBuilder) {
|
|
3916
|
+
if (!this.unnestColumns) {
|
|
3917
|
+
throw new InvalidOperationError(
|
|
3918
|
+
"withCte",
|
|
3919
|
+
"CTE requires a data source \u2014 call .fromUnnest() first"
|
|
3920
|
+
);
|
|
3921
|
+
}
|
|
3922
|
+
const cteIntent = {
|
|
3923
|
+
kind: "unnestCte",
|
|
3924
|
+
name: this.cteName,
|
|
3925
|
+
columns: this.unnestColumns,
|
|
3926
|
+
...this.indexColumnName !== void 0 && {
|
|
3927
|
+
indexColumn: this.indexColumnName
|
|
3928
|
+
}
|
|
3929
|
+
};
|
|
3930
|
+
return new CteQueryBuilder(
|
|
3931
|
+
cteIntent,
|
|
3932
|
+
selectBuilder,
|
|
3933
|
+
this.adapter,
|
|
3934
|
+
this.schemaName
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
var CteQueryBuilder = class {
|
|
3939
|
+
cteIntent;
|
|
3940
|
+
outerBuilder;
|
|
3941
|
+
adapter;
|
|
3942
|
+
schemaName;
|
|
3943
|
+
constructor(cteIntent, outerBuilder, adapter, schemaName) {
|
|
3944
|
+
this.cteIntent = cteIntent;
|
|
3945
|
+
this.outerBuilder = outerBuilder;
|
|
3946
|
+
this.adapter = adapter;
|
|
3947
|
+
this.schemaName = schemaName;
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3950
|
+
* Build the CteQueryIntent AST.
|
|
3951
|
+
*/
|
|
3952
|
+
buildIntent() {
|
|
3953
|
+
const queryIntent = this.outerBuilder.buildIntent();
|
|
3954
|
+
return {
|
|
3955
|
+
kind: "cteQuery",
|
|
3956
|
+
ctes: [this.cteIntent],
|
|
3957
|
+
query: queryIntent
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
requireAdapter() {
|
|
3961
|
+
return requireAdapter(this.adapter, "withCte");
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* Compile to SQL and return an observability dump.
|
|
3965
|
+
*/
|
|
3966
|
+
dump() {
|
|
3967
|
+
const adapter = this.requireAdapter();
|
|
3968
|
+
const intent = this.buildIntent();
|
|
3969
|
+
const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
|
|
3970
|
+
const compiled = adapter.compileCteQuery(
|
|
3971
|
+
intent,
|
|
3972
|
+
compileOptions
|
|
3973
|
+
);
|
|
3974
|
+
return {
|
|
3975
|
+
sql: compiled.sql,
|
|
3976
|
+
params: compiled.parameters,
|
|
3977
|
+
intent
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Execute the CTE query and return all results.
|
|
3982
|
+
*/
|
|
3983
|
+
async all() {
|
|
3984
|
+
const adapter = this.requireAdapter();
|
|
3985
|
+
const intent = this.buildIntent();
|
|
3986
|
+
const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
|
|
3987
|
+
const compiled = adapter.compileCteQuery(
|
|
3988
|
+
intent,
|
|
3989
|
+
compileOptions
|
|
3990
|
+
);
|
|
3991
|
+
return adapter.execute(compiled);
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Alias for all().
|
|
3995
|
+
*/
|
|
3996
|
+
execute() {
|
|
3997
|
+
return this.all();
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
|
|
4001
|
+
// src/dx/feature-checkers.ts
|
|
4002
|
+
var DEFAULT_FEATURE_CHECKERS = Object.freeze([
|
|
4003
|
+
// -----------------------------------------------------------------------
|
|
3591
4004
|
// Schema-level features
|
|
3592
4005
|
// -----------------------------------------------------------------------
|
|
3593
4006
|
{
|
|
@@ -3861,389 +4274,6 @@ var DEFAULT_FEATURE_CHECKERS = Object.freeze([
|
|
|
3861
4274
|
}
|
|
3862
4275
|
]);
|
|
3863
4276
|
|
|
3864
|
-
// src/dx/column-utils.ts
|
|
3865
|
-
function getColumnName(field) {
|
|
3866
|
-
if (typeof field === "string") {
|
|
3867
|
-
return field;
|
|
3868
|
-
}
|
|
3869
|
-
const colName = field[COLUMN_META];
|
|
3870
|
-
if (colName === void 0) {
|
|
3871
|
-
throw new Error("Invalid ColumnRef: missing COLUMN_META");
|
|
3872
|
-
}
|
|
3873
|
-
return colName;
|
|
3874
|
-
}
|
|
3875
|
-
|
|
3876
|
-
// src/dx/window-functions.ts
|
|
3877
|
-
var WindowBuilder = class _WindowBuilder {
|
|
3878
|
-
constructor(fnKind, partitions = [], orders = []) {
|
|
3879
|
-
this.fnKind = fnKind;
|
|
3880
|
-
this.partitions = partitions;
|
|
3881
|
-
this.orders = orders;
|
|
3882
|
-
}
|
|
3883
|
-
fnKind;
|
|
3884
|
-
partitions;
|
|
3885
|
-
orders;
|
|
3886
|
-
/**
|
|
3887
|
-
* Create a ranking window builder (row_number, rank, dense_rank)
|
|
3888
|
-
* @internal Use factory functions instead: rowNumber(), rank(), denseRank()
|
|
3889
|
-
*/
|
|
3890
|
-
static ranking(fn2) {
|
|
3891
|
-
return new _WindowBuilder({ type: "ranking", fn: fn2 });
|
|
3892
|
-
}
|
|
3893
|
-
/**
|
|
3894
|
-
* Create an aggregate window builder (sum, avg, count, min, max)
|
|
3895
|
-
* @internal Use factory functions instead: sum(field), avg(field), etc.
|
|
3896
|
-
*/
|
|
3897
|
-
static aggregate(fn2, field) {
|
|
3898
|
-
return new _WindowBuilder({
|
|
3899
|
-
type: "aggregate",
|
|
3900
|
-
fn: fn2,
|
|
3901
|
-
...field !== void 0 ? { field } : {}
|
|
3902
|
-
});
|
|
3903
|
-
}
|
|
3904
|
-
/**
|
|
3905
|
-
* Create an offset window builder (lag, lead)
|
|
3906
|
-
* @internal Use factory functions instead: lag(field), lead(field)
|
|
3907
|
-
*/
|
|
3908
|
-
static offset(fn2, field) {
|
|
3909
|
-
return new _WindowBuilder({ type: "offset", fn: fn2, field });
|
|
3910
|
-
}
|
|
3911
|
-
/**
|
|
3912
|
-
* Add partition field(s) to the OVER clause.
|
|
3913
|
-
* Multiple calls APPEND fields (not replace).
|
|
3914
|
-
* Supports both string field names and ColumnRef (DX-040).
|
|
3915
|
-
*
|
|
3916
|
-
* @example
|
|
3917
|
-
* sum('amount').partitionBy('user_id').partitionBy('category')
|
|
3918
|
-
* // → PARTITION BY "user_id", "category"
|
|
3919
|
-
*
|
|
3920
|
-
* @example DX-040 with ColumnRef
|
|
3921
|
-
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3922
|
-
*/
|
|
3923
|
-
partitionBy(...fields) {
|
|
3924
|
-
const fieldNames = fields.map(
|
|
3925
|
-
(f) => typeof f === "string" ? f : getColumnName(f)
|
|
3926
|
-
);
|
|
3927
|
-
return new _WindowBuilder(
|
|
3928
|
-
this.fnKind,
|
|
3929
|
-
[...this.partitions, ...fieldNames],
|
|
3930
|
-
this.orders
|
|
3931
|
-
);
|
|
3932
|
-
}
|
|
3933
|
-
/**
|
|
3934
|
-
* Add order field to the OVER clause.
|
|
3935
|
-
* Multiple calls APPEND fields (not replace).
|
|
3936
|
-
* Supports both string field names and ColumnRef (DX-040).
|
|
3937
|
-
*
|
|
3938
|
-
* @param field - Column name or ColumnRef to order by
|
|
3939
|
-
* @param direction - Sort direction: 'asc' (default) or 'desc'
|
|
3940
|
-
*
|
|
3941
|
-
* @example
|
|
3942
|
-
* rowNumber().orderBy('created_at').orderBy('id', 'desc')
|
|
3943
|
-
* // → ORDER BY "created_at" ASC, "id" DESC
|
|
3944
|
-
*
|
|
3945
|
-
* @example DX-040 with ColumnRef
|
|
3946
|
-
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3947
|
-
*/
|
|
3948
|
-
orderBy(field, direction = "asc") {
|
|
3949
|
-
const fieldName = typeof field === "string" ? field : getColumnName(field);
|
|
3950
|
-
return new _WindowBuilder(this.fnKind, this.partitions, [
|
|
3951
|
-
...this.orders,
|
|
3952
|
-
{ field: fieldName, direction }
|
|
3953
|
-
]);
|
|
3954
|
-
}
|
|
3955
|
-
/**
|
|
3956
|
-
* Finalize the window expression with an alias.
|
|
3957
|
-
* Returns ExpressionSpec for use in columns().
|
|
3958
|
-
*
|
|
3959
|
-
* @param alias - Required alias for the result column
|
|
3960
|
-
*
|
|
3961
|
-
* @example
|
|
3962
|
-
* columns(['id', rowNumber().orderBy('date').as('rn')])
|
|
3963
|
-
*/
|
|
3964
|
-
as(alias) {
|
|
3965
|
-
return {
|
|
3966
|
-
__expr: true,
|
|
3967
|
-
intent: this.toWindowIntent(alias)
|
|
3968
|
-
};
|
|
3969
|
-
}
|
|
3970
|
-
/**
|
|
3971
|
-
* Convert builder state to WindowIntent — produces the correct discriminated branch.
|
|
3972
|
-
* - ranking → RankingWindowIntent (no field)
|
|
3973
|
-
* - aggregate → AggregateWindowIntent (field required; COUNT uses '*' when omitted)
|
|
3974
|
-
* - offset → OffsetWindowIntent (field required)
|
|
3975
|
-
*/
|
|
3976
|
-
toWindowIntent(alias) {
|
|
3977
|
-
const over = {};
|
|
3978
|
-
if (this.partitions.length > 0) {
|
|
3979
|
-
over.partitionBy = this.partitions;
|
|
3980
|
-
}
|
|
3981
|
-
if (this.orders.length > 0) {
|
|
3982
|
-
over.orderBy = this.orders;
|
|
3983
|
-
}
|
|
3984
|
-
if (this.fnKind.type === "ranking") {
|
|
3985
|
-
return {
|
|
3986
|
-
kind: "window",
|
|
3987
|
-
function: this.fnKind.fn,
|
|
3988
|
-
alias,
|
|
3989
|
-
over
|
|
3990
|
-
};
|
|
3991
|
-
}
|
|
3992
|
-
if (this.fnKind.type === "offset") {
|
|
3993
|
-
return {
|
|
3994
|
-
kind: "window",
|
|
3995
|
-
function: this.fnKind.fn,
|
|
3996
|
-
field: this.fnKind.field,
|
|
3997
|
-
alias,
|
|
3998
|
-
over
|
|
3999
|
-
};
|
|
4000
|
-
}
|
|
4001
|
-
const { field } = this.fnKind;
|
|
4002
|
-
return {
|
|
4003
|
-
kind: "window",
|
|
4004
|
-
function: this.fnKind.fn,
|
|
4005
|
-
...field !== void 0 && { field },
|
|
4006
|
-
alias,
|
|
4007
|
-
over
|
|
4008
|
-
};
|
|
4009
|
-
}
|
|
4010
|
-
};
|
|
4011
|
-
function rowNumber() {
|
|
4012
|
-
return WindowBuilder.ranking("row_number");
|
|
4013
|
-
}
|
|
4014
|
-
function rank() {
|
|
4015
|
-
return WindowBuilder.ranking("rank");
|
|
4016
|
-
}
|
|
4017
|
-
function denseRank() {
|
|
4018
|
-
return WindowBuilder.ranking("dense_rank");
|
|
4019
|
-
}
|
|
4020
|
-
function wSum(field) {
|
|
4021
|
-
return WindowBuilder.aggregate("sum", field);
|
|
4022
|
-
}
|
|
4023
|
-
function wAvg(field) {
|
|
4024
|
-
return WindowBuilder.aggregate("avg", field);
|
|
4025
|
-
}
|
|
4026
|
-
function wCount(field) {
|
|
4027
|
-
return WindowBuilder.aggregate("count", field);
|
|
4028
|
-
}
|
|
4029
|
-
function wMin(field) {
|
|
4030
|
-
return WindowBuilder.aggregate("min", field);
|
|
4031
|
-
}
|
|
4032
|
-
function wMax(field) {
|
|
4033
|
-
return WindowBuilder.aggregate("max", field);
|
|
4034
|
-
}
|
|
4035
|
-
function lag(field) {
|
|
4036
|
-
return WindowBuilder.offset("lag", field);
|
|
4037
|
-
}
|
|
4038
|
-
function lead(field) {
|
|
4039
|
-
return WindowBuilder.offset("lead", field);
|
|
4040
|
-
}
|
|
4041
|
-
|
|
4042
|
-
// src/dx/filters.ts
|
|
4043
|
-
function distinct(field) {
|
|
4044
|
-
return { field, distinct: true };
|
|
4045
|
-
}
|
|
4046
|
-
function isDistinctField(value) {
|
|
4047
|
-
return typeof value === "object" && value !== null && "field" in value && "distinct" in value && value.distinct === true;
|
|
4048
|
-
}
|
|
4049
|
-
function createComparisonFilter(operator) {
|
|
4050
|
-
return (field, value) => ({
|
|
4051
|
-
kind: "comparison",
|
|
4052
|
-
field: getColumnName(field),
|
|
4053
|
-
operator,
|
|
4054
|
-
value
|
|
4055
|
-
});
|
|
4056
|
-
}
|
|
4057
|
-
var eq = createComparisonFilter("eq");
|
|
4058
|
-
var neq = createComparisonFilter("neq");
|
|
4059
|
-
var gt = createComparisonFilter("gt");
|
|
4060
|
-
var gte = createComparisonFilter("gte");
|
|
4061
|
-
var lt = createComparisonFilter("lt");
|
|
4062
|
-
var lte = createComparisonFilter("lte");
|
|
4063
|
-
var isDistinctFrom = createComparisonFilter("isDistinctFrom");
|
|
4064
|
-
function like(field, pattern, options) {
|
|
4065
|
-
const caseInsensitive = typeof options === "boolean" ? options : options?.caseInsensitive;
|
|
4066
|
-
const escapeChar = typeof options === "object" ? options.escape : void 0;
|
|
4067
|
-
const intent = {
|
|
4068
|
-
kind: "like",
|
|
4069
|
-
field: getColumnName(field),
|
|
4070
|
-
pattern
|
|
4071
|
-
};
|
|
4072
|
-
const withCi = caseInsensitive !== void 0 ? { ...intent, caseInsensitive } : intent;
|
|
4073
|
-
if (escapeChar !== void 0) {
|
|
4074
|
-
return { ...withCi, escape: escapeChar };
|
|
4075
|
-
}
|
|
4076
|
-
return withCi;
|
|
4077
|
-
}
|
|
4078
|
-
function inArray(field, values) {
|
|
4079
|
-
return { kind: "in", field: getColumnName(field), values };
|
|
4080
|
-
}
|
|
4081
|
-
function inSubquery(field, query) {
|
|
4082
|
-
const expr = "build" in query ? query.build() : query;
|
|
4083
|
-
return {
|
|
4084
|
-
kind: "in",
|
|
4085
|
-
field: getColumnName(field),
|
|
4086
|
-
subquery: expr.toIntent()
|
|
4087
|
-
};
|
|
4088
|
-
}
|
|
4089
|
-
function any(field, values) {
|
|
4090
|
-
return { kind: "any", field: getColumnName(field), values };
|
|
4091
|
-
}
|
|
4092
|
-
function isNull(field) {
|
|
4093
|
-
return { kind: "null", field: getColumnName(field), operator: "isNull" };
|
|
4094
|
-
}
|
|
4095
|
-
function isNotNull(field) {
|
|
4096
|
-
return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
|
|
4097
|
-
}
|
|
4098
|
-
function and(...conditions) {
|
|
4099
|
-
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
4100
|
-
return { kind: "and", conditions: flatConditions };
|
|
4101
|
-
}
|
|
4102
|
-
function or(...conditions) {
|
|
4103
|
-
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
4104
|
-
return { kind: "or", conditions: flatConditions };
|
|
4105
|
-
}
|
|
4106
|
-
function not(condition) {
|
|
4107
|
-
return { kind: "not", condition };
|
|
4108
|
-
}
|
|
4109
|
-
function exists(relation, options) {
|
|
4110
|
-
const result = { kind: "exists", relation };
|
|
4111
|
-
if (options?.where !== void 0) {
|
|
4112
|
-
result.where = options.where;
|
|
4113
|
-
}
|
|
4114
|
-
if (options?.recursive !== void 0) {
|
|
4115
|
-
result.recursive = options.recursive;
|
|
4116
|
-
}
|
|
4117
|
-
if (options?.include !== void 0) {
|
|
4118
|
-
result.include = options.include;
|
|
4119
|
-
}
|
|
4120
|
-
return result;
|
|
4121
|
-
}
|
|
4122
|
-
function notExists(relation, options) {
|
|
4123
|
-
const result = {
|
|
4124
|
-
kind: "notExists",
|
|
4125
|
-
relation
|
|
4126
|
-
};
|
|
4127
|
-
if (options?.where !== void 0) {
|
|
4128
|
-
result.where = options.where;
|
|
4129
|
-
}
|
|
4130
|
-
if (options?.recursive !== void 0) {
|
|
4131
|
-
result.recursive = options.recursive;
|
|
4132
|
-
}
|
|
4133
|
-
if (options?.include !== void 0) {
|
|
4134
|
-
result.include = options.include;
|
|
4135
|
-
}
|
|
4136
|
-
return result;
|
|
4137
|
-
}
|
|
4138
|
-
function rawExists(sq) {
|
|
4139
|
-
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
4140
|
-
return { kind: "rawExists", subquery: intent };
|
|
4141
|
-
}
|
|
4142
|
-
function rawNotExists(sq) {
|
|
4143
|
-
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
4144
|
-
return { kind: "rawNotExists", subquery: intent };
|
|
4145
|
-
}
|
|
4146
|
-
function getRelationName(rel) {
|
|
4147
|
-
const meta = rel[RELATION_META];
|
|
4148
|
-
if (!meta) {
|
|
4149
|
-
throw new Error("Invalid RelationRef: missing RELATION_META");
|
|
4150
|
-
}
|
|
4151
|
-
return meta.target;
|
|
4152
|
-
}
|
|
4153
|
-
function every(relation, filter) {
|
|
4154
|
-
const relationName = getRelationName(relation);
|
|
4155
|
-
const where = filter(relation);
|
|
4156
|
-
return {
|
|
4157
|
-
kind: "relationFilter",
|
|
4158
|
-
relation: relationName,
|
|
4159
|
-
where,
|
|
4160
|
-
mode: "every"
|
|
4161
|
-
};
|
|
4162
|
-
}
|
|
4163
|
-
function none(relation, filter) {
|
|
4164
|
-
const relationName = getRelationName(relation);
|
|
4165
|
-
const where = filter(relation);
|
|
4166
|
-
return {
|
|
4167
|
-
kind: "relationFilter",
|
|
4168
|
-
relation: relationName,
|
|
4169
|
-
where,
|
|
4170
|
-
mode: "none"
|
|
4171
|
-
};
|
|
4172
|
-
}
|
|
4173
|
-
function some(relation, filter) {
|
|
4174
|
-
const relationName = getRelationName(relation);
|
|
4175
|
-
const where = filter(relation);
|
|
4176
|
-
return {
|
|
4177
|
-
kind: "relationFilter",
|
|
4178
|
-
relation: relationName,
|
|
4179
|
-
where,
|
|
4180
|
-
mode: "some"
|
|
4181
|
-
};
|
|
4182
|
-
}
|
|
4183
|
-
function coalesce(fields, as) {
|
|
4184
|
-
if (fields.length === 0) {
|
|
4185
|
-
throw new Error("coalesce() requires at least one field");
|
|
4186
|
-
}
|
|
4187
|
-
if (!as || as.trim() === "") {
|
|
4188
|
-
throw new Error("coalesce() requires a non-empty alias");
|
|
4189
|
-
}
|
|
4190
|
-
for (const f of fields) {
|
|
4191
|
-
validateIdentifier(f, "column");
|
|
4192
|
-
}
|
|
4193
|
-
validateIdentifier(as, "column");
|
|
4194
|
-
return {
|
|
4195
|
-
__expr: true,
|
|
4196
|
-
intent: { kind: "coalesce", fields, as }
|
|
4197
|
-
};
|
|
4198
|
-
}
|
|
4199
|
-
function raw(sqlFragment, as) {
|
|
4200
|
-
if (typeof as !== "string" || as.trim().length === 0) {
|
|
4201
|
-
throw new InvalidOperationError(
|
|
4202
|
-
"raw",
|
|
4203
|
-
"raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
|
|
4204
|
-
);
|
|
4205
|
-
}
|
|
4206
|
-
validateIdentifier(as, "column");
|
|
4207
|
-
return {
|
|
4208
|
-
__expr: true,
|
|
4209
|
-
intent: { kind: "raw", sql: sqlFragment, as }
|
|
4210
|
-
};
|
|
4211
|
-
}
|
|
4212
|
-
function col(column, alias) {
|
|
4213
|
-
validateIdentifier(column, "column");
|
|
4214
|
-
validateIdentifier(alias, "column");
|
|
4215
|
-
return {
|
|
4216
|
-
__expr: true,
|
|
4217
|
-
intent: { kind: "columnAlias", column, alias }
|
|
4218
|
-
};
|
|
4219
|
-
}
|
|
4220
|
-
function relationColumn(relation, column, as) {
|
|
4221
|
-
if (!relation) {
|
|
4222
|
-
throw new Error("relationColumn() requires a non-empty relation path");
|
|
4223
|
-
}
|
|
4224
|
-
for (const segment of relation.split(".")) {
|
|
4225
|
-
validateIdentifier(segment, "relation");
|
|
4226
|
-
}
|
|
4227
|
-
if (column !== "*") {
|
|
4228
|
-
validateIdentifier(column, "column");
|
|
4229
|
-
}
|
|
4230
|
-
validateIdentifier(as, "column");
|
|
4231
|
-
return {
|
|
4232
|
-
__expr: true,
|
|
4233
|
-
intent: { kind: "relationColumn", relation, column, as }
|
|
4234
|
-
};
|
|
4235
|
-
}
|
|
4236
|
-
var SQL_RAW_MARKER = /* @__PURE__ */ Symbol.for("dbsp:raw-sql");
|
|
4237
|
-
function sql(sqlFragment) {
|
|
4238
|
-
if (!sqlFragment || sqlFragment.trim() === "") {
|
|
4239
|
-
throw new Error("sql() requires a non-empty SQL fragment");
|
|
4240
|
-
}
|
|
4241
|
-
return { [SQL_RAW_MARKER]: true, sql: sqlFragment };
|
|
4242
|
-
}
|
|
4243
|
-
function isSqlRaw(value) {
|
|
4244
|
-
return typeof value === "object" && value !== null && SQL_RAW_MARKER in value && value[SQL_RAW_MARKER] === true;
|
|
4245
|
-
}
|
|
4246
|
-
|
|
4247
4277
|
// src/dx/full-text-search.ts
|
|
4248
4278
|
function fullTextSearch({
|
|
4249
4279
|
query,
|