@dbsp/core 1.9.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 +2851 -2828
- package/dist/index.js +554 -512
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -60,6 +60,7 @@ import * as types_star from "@dbsp/types";
|
|
|
60
60
|
// src/model-impl.ts
|
|
61
61
|
var ModelIRImpl = class {
|
|
62
62
|
tables;
|
|
63
|
+
externalTables;
|
|
63
64
|
relations;
|
|
64
65
|
enums;
|
|
65
66
|
extensions;
|
|
@@ -67,8 +68,11 @@ var ModelIRImpl = class {
|
|
|
67
68
|
// Pre-computed indexes for efficient lookups
|
|
68
69
|
relationsBySource;
|
|
69
70
|
relationsByTarget;
|
|
70
|
-
constructor(tables, relations, enums, extensions, sequences) {
|
|
71
|
+
constructor(tables, relations, enums, extensions, sequences, externalTables) {
|
|
71
72
|
this.tables = Object.freeze(new Map(tables));
|
|
73
|
+
this.externalTables = Object.freeze(
|
|
74
|
+
new Set(externalTables)
|
|
75
|
+
);
|
|
72
76
|
this.relations = Object.freeze(new Map(relations));
|
|
73
77
|
if (enums) {
|
|
74
78
|
this.enums = Object.freeze(new Map(enums));
|
|
@@ -156,6 +160,11 @@ var ModelIRImpl = class {
|
|
|
156
160
|
validate() {
|
|
157
161
|
const errors = [];
|
|
158
162
|
for (const table of this.tables.values()) {
|
|
163
|
+
if (this.externalTables.has(table.name)) {
|
|
164
|
+
errors.push(
|
|
165
|
+
`Table "${table.name}" cannot be both managed and external`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
159
168
|
if (table.primaryKey) {
|
|
160
169
|
const pkCols = Array.isArray(table.primaryKey) ? table.primaryKey : [table.primaryKey];
|
|
161
170
|
for (const pk of pkCols) {
|
|
@@ -169,7 +178,7 @@ var ModelIRImpl = class {
|
|
|
169
178
|
}
|
|
170
179
|
for (const table of this.tables.values()) {
|
|
171
180
|
for (const fk of table.foreignKeys) {
|
|
172
|
-
if (!this.tables.has(fk.references.table)) {
|
|
181
|
+
if (!this.tables.has(fk.references.table) && !this.externalTables.has(fk.references.table)) {
|
|
173
182
|
errors.push(
|
|
174
183
|
`Table "${table.name}" has FK referencing non-existent table "${fk.references.table}"`
|
|
175
184
|
);
|
|
@@ -3210,144 +3219,554 @@ function batchValues(data, columns, types, opts) {
|
|
|
3210
3219
|
});
|
|
3211
3220
|
}
|
|
3212
3221
|
|
|
3213
|
-
// src/dx/
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
function toExpressionIntent(input) {
|
|
3218
|
-
if (input instanceof ExpressionRef) return input.intent;
|
|
3219
|
-
if (typeof input === "object" && input !== null && "__expr" in input && input.__expr === true && "intent" in input) {
|
|
3220
|
-
return input.intent;
|
|
3222
|
+
// src/dx/column-utils.ts
|
|
3223
|
+
function getColumnName(field) {
|
|
3224
|
+
if (typeof field === "string") {
|
|
3225
|
+
return field;
|
|
3221
3226
|
}
|
|
3222
|
-
|
|
3223
|
-
|
|
3227
|
+
const colName = field[COLUMN_META];
|
|
3228
|
+
if (colName === void 0) {
|
|
3229
|
+
throw new Error("Invalid ColumnRef: missing COLUMN_META");
|
|
3224
3230
|
}
|
|
3225
|
-
return
|
|
3231
|
+
return colName;
|
|
3226
3232
|
}
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
constructor(
|
|
3231
|
-
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;
|
|
3232
3240
|
}
|
|
3241
|
+
fnKind;
|
|
3242
|
+
partitions;
|
|
3243
|
+
orders;
|
|
3233
3244
|
/**
|
|
3234
|
-
*
|
|
3235
|
-
*
|
|
3245
|
+
* Create a ranking window builder (row_number, rank, dense_rank)
|
|
3246
|
+
* @internal Use factory functions instead: rowNumber(), rank(), denseRank()
|
|
3236
3247
|
*/
|
|
3237
|
-
|
|
3238
|
-
return new
|
|
3239
|
-
...this.intent,
|
|
3240
|
-
as: alias
|
|
3241
|
-
});
|
|
3242
|
-
}
|
|
3243
|
-
/** WHERE: expr = value */
|
|
3244
|
-
eq(value) {
|
|
3245
|
-
return { kind: "expression", expr: this.intent, operator: "eq", value };
|
|
3246
|
-
}
|
|
3247
|
-
/** WHERE: expr != value */
|
|
3248
|
-
neq(value) {
|
|
3249
|
-
return { kind: "expression", expr: this.intent, operator: "neq", value };
|
|
3248
|
+
static ranking(fn2) {
|
|
3249
|
+
return new _WindowBuilder({ type: "ranking", fn: fn2 });
|
|
3250
3250
|
}
|
|
3251
|
-
/**
|
|
3252
|
-
|
|
3253
|
-
|
|
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
|
+
});
|
|
3254
3261
|
}
|
|
3255
|
-
/**
|
|
3256
|
-
|
|
3257
|
-
|
|
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 });
|
|
3258
3268
|
}
|
|
3259
|
-
/**
|
|
3260
|
-
|
|
3261
|
-
|
|
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
|
+
);
|
|
3262
3290
|
}
|
|
3263
|
-
/**
|
|
3264
|
-
|
|
3265
|
-
|
|
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
|
+
]);
|
|
3266
3312
|
}
|
|
3267
3313
|
/**
|
|
3268
|
-
*
|
|
3269
|
-
*
|
|
3270
|
-
* Returns a new ExpressionRef — does not mutate.
|
|
3314
|
+
* Finalize the window expression with an alias.
|
|
3315
|
+
* Returns ExpressionSpec for use in columns().
|
|
3271
3316
|
*
|
|
3272
|
-
* @
|
|
3273
|
-
*
|
|
3274
|
-
* @
|
|
3317
|
+
* @param alias - Required alias for the result column
|
|
3318
|
+
*
|
|
3319
|
+
* @example
|
|
3320
|
+
* columns(['id', rowNumber().orderBy('date').as('rn')])
|
|
3275
3321
|
*/
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
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;
|
|
3281
3338
|
}
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
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
|
+
};
|
|
3286
3367
|
}
|
|
3287
3368
|
};
|
|
3288
|
-
function
|
|
3289
|
-
return
|
|
3290
|
-
kind: "ref",
|
|
3291
|
-
column
|
|
3292
|
-
});
|
|
3369
|
+
function rowNumber() {
|
|
3370
|
+
return WindowBuilder.ranking("row_number");
|
|
3293
3371
|
}
|
|
3294
|
-
function
|
|
3295
|
-
return
|
|
3296
|
-
kind: "param",
|
|
3297
|
-
value
|
|
3298
|
-
});
|
|
3372
|
+
function rank() {
|
|
3373
|
+
return WindowBuilder.ranking("rank");
|
|
3299
3374
|
}
|
|
3300
|
-
function
|
|
3301
|
-
|
|
3302
|
-
throw new Error(`Invalid type name: ${typeName}`);
|
|
3303
|
-
}
|
|
3304
|
-
return new ExpressionRef({
|
|
3305
|
-
kind: "cast",
|
|
3306
|
-
expr: expr.intent,
|
|
3307
|
-
typeName
|
|
3308
|
-
});
|
|
3375
|
+
function denseRank() {
|
|
3376
|
+
return WindowBuilder.ranking("dense_rank");
|
|
3309
3377
|
}
|
|
3310
|
-
function
|
|
3311
|
-
|
|
3312
|
-
throw new Error(`Invalid operator: ${operator}`);
|
|
3313
|
-
}
|
|
3314
|
-
return new ExpressionRef({
|
|
3315
|
-
kind: "customOp",
|
|
3316
|
-
operator,
|
|
3317
|
-
left: toExpressionIntent(left),
|
|
3318
|
-
right: toExpressionIntent(right)
|
|
3319
|
-
});
|
|
3378
|
+
function wSum(field) {
|
|
3379
|
+
return WindowBuilder.aggregate("sum", field);
|
|
3320
3380
|
}
|
|
3321
|
-
function
|
|
3322
|
-
|
|
3323
|
-
throw new Error(`Invalid function name: ${name}`);
|
|
3324
|
-
}
|
|
3325
|
-
const regularArgs = [];
|
|
3326
|
-
const orderByArgs = [];
|
|
3327
|
-
for (const arg of args) {
|
|
3328
|
-
if (isAggOrderByArg(arg)) {
|
|
3329
|
-
orderByArgs.push(arg);
|
|
3330
|
-
} else {
|
|
3331
|
-
regularArgs.push(toExpressionIntent(arg));
|
|
3332
|
-
}
|
|
3333
|
-
}
|
|
3334
|
-
const intent = {
|
|
3335
|
-
kind: "customFn",
|
|
3336
|
-
name,
|
|
3337
|
-
args: regularArgs,
|
|
3338
|
-
...orderByArgs.length > 0 ? { aggOrderBy: orderByArgs } : {}
|
|
3339
|
-
};
|
|
3340
|
-
return new ExpressionRef(intent);
|
|
3381
|
+
function wAvg(field) {
|
|
3382
|
+
return WindowBuilder.aggregate("avg", field);
|
|
3341
3383
|
}
|
|
3342
|
-
function
|
|
3343
|
-
return
|
|
3344
|
-
kind: "literal",
|
|
3345
|
-
value
|
|
3346
|
-
});
|
|
3384
|
+
function wCount(field) {
|
|
3385
|
+
return WindowBuilder.aggregate("count", field);
|
|
3347
3386
|
}
|
|
3348
|
-
function
|
|
3349
|
-
|
|
3350
|
-
|
|
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
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
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 };
|
|
3433
|
+
}
|
|
3434
|
+
return withCi;
|
|
3435
|
+
}
|
|
3436
|
+
function inArray(field, values) {
|
|
3437
|
+
return { kind: "in", field: getColumnName(field), values };
|
|
3438
|
+
}
|
|
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
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
function any(field, values) {
|
|
3448
|
+
return { kind: "any", field: getColumnName(field), values };
|
|
3449
|
+
}
|
|
3450
|
+
function isNull(field) {
|
|
3451
|
+
return { kind: "null", field: getColumnName(field), operator: "isNull" };
|
|
3452
|
+
}
|
|
3453
|
+
function isNotNull(field) {
|
|
3454
|
+
return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
|
|
3455
|
+
}
|
|
3456
|
+
function and(...conditions) {
|
|
3457
|
+
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
3458
|
+
return { kind: "and", conditions: flatConditions };
|
|
3459
|
+
}
|
|
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;
|
|
3471
|
+
}
|
|
3472
|
+
if (options?.recursive !== void 0) {
|
|
3473
|
+
result.recursive = options.recursive;
|
|
3474
|
+
}
|
|
3475
|
+
if (options?.include !== void 0) {
|
|
3476
|
+
result.include = options.include;
|
|
3477
|
+
}
|
|
3478
|
+
return result;
|
|
3479
|
+
}
|
|
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;
|
|
3487
|
+
}
|
|
3488
|
+
if (options?.recursive !== void 0) {
|
|
3489
|
+
result.recursive = options.recursive;
|
|
3490
|
+
}
|
|
3491
|
+
if (options?.include !== void 0) {
|
|
3492
|
+
result.include = options.include;
|
|
3493
|
+
}
|
|
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");
|
|
3508
|
+
}
|
|
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");
|
|
3544
|
+
}
|
|
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
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
function raw(sqlFragment, as) {
|
|
3558
|
+
if (typeof as !== "string" || as.trim().length === 0) {
|
|
3559
|
+
throw new InvalidOperationError(
|
|
3560
|
+
"raw",
|
|
3561
|
+
"raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
|
|
3562
|
+
);
|
|
3563
|
+
}
|
|
3564
|
+
validateIdentifier(as, "column");
|
|
3565
|
+
return {
|
|
3566
|
+
__expr: true,
|
|
3567
|
+
intent: { kind: "raw", sql: sqlFragment, as }
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
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");
|
|
3581
|
+
}
|
|
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;
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Set an alias for this expression in SELECT.
|
|
3627
|
+
* Returns a new ExpressionRef — does not mutate.
|
|
3628
|
+
*/
|
|
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 };
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
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.
|
|
3664
|
+
*
|
|
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
|
|
3677
|
+
*/
|
|
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}'`
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
return new _ExpressionRef({
|
|
3685
|
+
...this.intent,
|
|
3686
|
+
filter: condition
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
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}`);
|
|
3705
|
+
}
|
|
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}`);
|
|
3715
|
+
}
|
|
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}`);
|
|
3726
|
+
}
|
|
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
|
+
}
|
|
3751
|
+
}
|
|
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}`);
|
|
3351
3770
|
}
|
|
3352
3771
|
return new ExpressionRef({
|
|
3353
3772
|
kind: "unary",
|
|
@@ -3374,6 +3793,9 @@ function array(...items) {
|
|
|
3374
3793
|
elements: items.map(toExpressionIntent)
|
|
3375
3794
|
});
|
|
3376
3795
|
}
|
|
3796
|
+
function isDistinctField2(input) {
|
|
3797
|
+
return !Array.isArray(input) && !(input instanceof ExpressionRef) && isDistinctField(input);
|
|
3798
|
+
}
|
|
3377
3799
|
function isAggOrderByArg(input) {
|
|
3378
3800
|
return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof ExpressionRef) && "__aggOrderBy" in input && input.__aggOrderBy === true;
|
|
3379
3801
|
}
|
|
@@ -3837,403 +4259,20 @@ var DEFAULT_FEATURE_CHECKERS = Object.freeze([
|
|
|
3837
4259
|
// Row-Level Security
|
|
3838
4260
|
// -----------------------------------------------------------------------
|
|
3839
4261
|
{
|
|
3840
|
-
capability: "supportsDDLRowLevelSecurity",
|
|
3841
|
-
feature: "rowLevelSecurity",
|
|
3842
|
-
detectUsage(model) {
|
|
3843
|
-
if (!model.tables) return [];
|
|
3844
|
-
const usages = [];
|
|
3845
|
-
for (const [tableName, table] of model.tables) {
|
|
3846
|
-
if (table.rlsEnabled || table.policies?.length) {
|
|
3847
|
-
usages.push({ table: tableName, detail: tableName });
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
return usages;
|
|
3851
|
-
}
|
|
3852
|
-
}
|
|
3853
|
-
]);
|
|
3854
|
-
|
|
3855
|
-
// src/dx/column-utils.ts
|
|
3856
|
-
function getColumnName(field) {
|
|
3857
|
-
if (typeof field === "string") {
|
|
3858
|
-
return field;
|
|
3859
|
-
}
|
|
3860
|
-
const colName = field[COLUMN_META];
|
|
3861
|
-
if (colName === void 0) {
|
|
3862
|
-
throw new Error("Invalid ColumnRef: missing COLUMN_META");
|
|
3863
|
-
}
|
|
3864
|
-
return colName;
|
|
3865
|
-
}
|
|
3866
|
-
|
|
3867
|
-
// src/dx/window-functions.ts
|
|
3868
|
-
var WindowBuilder = class _WindowBuilder {
|
|
3869
|
-
constructor(fnKind, partitions = [], orders = []) {
|
|
3870
|
-
this.fnKind = fnKind;
|
|
3871
|
-
this.partitions = partitions;
|
|
3872
|
-
this.orders = orders;
|
|
3873
|
-
}
|
|
3874
|
-
fnKind;
|
|
3875
|
-
partitions;
|
|
3876
|
-
orders;
|
|
3877
|
-
/**
|
|
3878
|
-
* Create a ranking window builder (row_number, rank, dense_rank)
|
|
3879
|
-
* @internal Use factory functions instead: rowNumber(), rank(), denseRank()
|
|
3880
|
-
*/
|
|
3881
|
-
static ranking(fn2) {
|
|
3882
|
-
return new _WindowBuilder({ type: "ranking", fn: fn2 });
|
|
3883
|
-
}
|
|
3884
|
-
/**
|
|
3885
|
-
* Create an aggregate window builder (sum, avg, count, min, max)
|
|
3886
|
-
* @internal Use factory functions instead: sum(field), avg(field), etc.
|
|
3887
|
-
*/
|
|
3888
|
-
static aggregate(fn2, field) {
|
|
3889
|
-
return new _WindowBuilder({
|
|
3890
|
-
type: "aggregate",
|
|
3891
|
-
fn: fn2,
|
|
3892
|
-
...field !== void 0 ? { field } : {}
|
|
3893
|
-
});
|
|
3894
|
-
}
|
|
3895
|
-
/**
|
|
3896
|
-
* Create an offset window builder (lag, lead)
|
|
3897
|
-
* @internal Use factory functions instead: lag(field), lead(field)
|
|
3898
|
-
*/
|
|
3899
|
-
static offset(fn2, field) {
|
|
3900
|
-
return new _WindowBuilder({ type: "offset", fn: fn2, field });
|
|
3901
|
-
}
|
|
3902
|
-
/**
|
|
3903
|
-
* Add partition field(s) to the OVER clause.
|
|
3904
|
-
* Multiple calls APPEND fields (not replace).
|
|
3905
|
-
* Supports both string field names and ColumnRef (DX-040).
|
|
3906
|
-
*
|
|
3907
|
-
* @example
|
|
3908
|
-
* sum('amount').partitionBy('user_id').partitionBy('category')
|
|
3909
|
-
* // → PARTITION BY "user_id", "category"
|
|
3910
|
-
*
|
|
3911
|
-
* @example DX-040 with ColumnRef
|
|
3912
|
-
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3913
|
-
*/
|
|
3914
|
-
partitionBy(...fields) {
|
|
3915
|
-
const fieldNames = fields.map(
|
|
3916
|
-
(f) => typeof f === "string" ? f : getColumnName(f)
|
|
3917
|
-
);
|
|
3918
|
-
return new _WindowBuilder(
|
|
3919
|
-
this.fnKind,
|
|
3920
|
-
[...this.partitions, ...fieldNames],
|
|
3921
|
-
this.orders
|
|
3922
|
-
);
|
|
3923
|
-
}
|
|
3924
|
-
/**
|
|
3925
|
-
* Add order field to the OVER clause.
|
|
3926
|
-
* Multiple calls APPEND fields (not replace).
|
|
3927
|
-
* Supports both string field names and ColumnRef (DX-040).
|
|
3928
|
-
*
|
|
3929
|
-
* @param field - Column name or ColumnRef to order by
|
|
3930
|
-
* @param direction - Sort direction: 'asc' (default) or 'desc'
|
|
3931
|
-
*
|
|
3932
|
-
* @example
|
|
3933
|
-
* rowNumber().orderBy('created_at').orderBy('id', 'desc')
|
|
3934
|
-
* // → ORDER BY "created_at" ASC, "id" DESC
|
|
3935
|
-
*
|
|
3936
|
-
* @example DX-040 with ColumnRef
|
|
3937
|
-
* rank().partitionBy(users.dept).orderBy(users.salary, 'desc')
|
|
3938
|
-
*/
|
|
3939
|
-
orderBy(field, direction = "asc") {
|
|
3940
|
-
const fieldName = typeof field === "string" ? field : getColumnName(field);
|
|
3941
|
-
return new _WindowBuilder(this.fnKind, this.partitions, [
|
|
3942
|
-
...this.orders,
|
|
3943
|
-
{ field: fieldName, direction }
|
|
3944
|
-
]);
|
|
3945
|
-
}
|
|
3946
|
-
/**
|
|
3947
|
-
* Finalize the window expression with an alias.
|
|
3948
|
-
* Returns ExpressionSpec for use in columns().
|
|
3949
|
-
*
|
|
3950
|
-
* @param alias - Required alias for the result column
|
|
3951
|
-
*
|
|
3952
|
-
* @example
|
|
3953
|
-
* columns(['id', rowNumber().orderBy('date').as('rn')])
|
|
3954
|
-
*/
|
|
3955
|
-
as(alias) {
|
|
3956
|
-
return {
|
|
3957
|
-
__expr: true,
|
|
3958
|
-
intent: this.toWindowIntent(alias)
|
|
3959
|
-
};
|
|
3960
|
-
}
|
|
3961
|
-
/**
|
|
3962
|
-
* Convert builder state to WindowIntent — produces the correct discriminated branch.
|
|
3963
|
-
* - ranking → RankingWindowIntent (no field)
|
|
3964
|
-
* - aggregate → AggregateWindowIntent (field required; COUNT uses '*' when omitted)
|
|
3965
|
-
* - offset → OffsetWindowIntent (field required)
|
|
3966
|
-
*/
|
|
3967
|
-
toWindowIntent(alias) {
|
|
3968
|
-
const over = {};
|
|
3969
|
-
if (this.partitions.length > 0) {
|
|
3970
|
-
over.partitionBy = this.partitions;
|
|
3971
|
-
}
|
|
3972
|
-
if (this.orders.length > 0) {
|
|
3973
|
-
over.orderBy = this.orders;
|
|
3974
|
-
}
|
|
3975
|
-
if (this.fnKind.type === "ranking") {
|
|
3976
|
-
return {
|
|
3977
|
-
kind: "window",
|
|
3978
|
-
function: this.fnKind.fn,
|
|
3979
|
-
alias,
|
|
3980
|
-
over
|
|
3981
|
-
};
|
|
3982
|
-
}
|
|
3983
|
-
if (this.fnKind.type === "offset") {
|
|
3984
|
-
return {
|
|
3985
|
-
kind: "window",
|
|
3986
|
-
function: this.fnKind.fn,
|
|
3987
|
-
field: this.fnKind.field,
|
|
3988
|
-
alias,
|
|
3989
|
-
over
|
|
3990
|
-
};
|
|
4262
|
+
capability: "supportsDDLRowLevelSecurity",
|
|
4263
|
+
feature: "rowLevelSecurity",
|
|
4264
|
+
detectUsage(model) {
|
|
4265
|
+
if (!model.tables) return [];
|
|
4266
|
+
const usages = [];
|
|
4267
|
+
for (const [tableName, table] of model.tables) {
|
|
4268
|
+
if (table.rlsEnabled || table.policies?.length) {
|
|
4269
|
+
usages.push({ table: tableName, detail: tableName });
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
return usages;
|
|
3991
4273
|
}
|
|
3992
|
-
const { field } = this.fnKind;
|
|
3993
|
-
return {
|
|
3994
|
-
kind: "window",
|
|
3995
|
-
function: this.fnKind.fn,
|
|
3996
|
-
...field !== void 0 && { field },
|
|
3997
|
-
alias,
|
|
3998
|
-
over
|
|
3999
|
-
};
|
|
4000
|
-
}
|
|
4001
|
-
};
|
|
4002
|
-
function rowNumber() {
|
|
4003
|
-
return WindowBuilder.ranking("row_number");
|
|
4004
|
-
}
|
|
4005
|
-
function rank() {
|
|
4006
|
-
return WindowBuilder.ranking("rank");
|
|
4007
|
-
}
|
|
4008
|
-
function denseRank() {
|
|
4009
|
-
return WindowBuilder.ranking("dense_rank");
|
|
4010
|
-
}
|
|
4011
|
-
function wSum(field) {
|
|
4012
|
-
return WindowBuilder.aggregate("sum", field);
|
|
4013
|
-
}
|
|
4014
|
-
function wAvg(field) {
|
|
4015
|
-
return WindowBuilder.aggregate("avg", field);
|
|
4016
|
-
}
|
|
4017
|
-
function wCount(field) {
|
|
4018
|
-
return WindowBuilder.aggregate("count", field);
|
|
4019
|
-
}
|
|
4020
|
-
function wMin(field) {
|
|
4021
|
-
return WindowBuilder.aggregate("min", field);
|
|
4022
|
-
}
|
|
4023
|
-
function wMax(field) {
|
|
4024
|
-
return WindowBuilder.aggregate("max", field);
|
|
4025
|
-
}
|
|
4026
|
-
function lag(field) {
|
|
4027
|
-
return WindowBuilder.offset("lag", field);
|
|
4028
|
-
}
|
|
4029
|
-
function lead(field) {
|
|
4030
|
-
return WindowBuilder.offset("lead", field);
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
// src/dx/filters.ts
|
|
4034
|
-
function distinct(field) {
|
|
4035
|
-
return { field, distinct: true };
|
|
4036
|
-
}
|
|
4037
|
-
function isDistinctField(value) {
|
|
4038
|
-
return typeof value === "object" && value !== null && "field" in value && "distinct" in value && value.distinct === true;
|
|
4039
|
-
}
|
|
4040
|
-
function createComparisonFilter(operator) {
|
|
4041
|
-
return (field, value) => ({
|
|
4042
|
-
kind: "comparison",
|
|
4043
|
-
field: getColumnName(field),
|
|
4044
|
-
operator,
|
|
4045
|
-
value
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
var eq = createComparisonFilter("eq");
|
|
4049
|
-
var neq = createComparisonFilter("neq");
|
|
4050
|
-
var gt = createComparisonFilter("gt");
|
|
4051
|
-
var gte = createComparisonFilter("gte");
|
|
4052
|
-
var lt = createComparisonFilter("lt");
|
|
4053
|
-
var lte = createComparisonFilter("lte");
|
|
4054
|
-
var isDistinctFrom = createComparisonFilter("isDistinctFrom");
|
|
4055
|
-
function like(field, pattern, options) {
|
|
4056
|
-
const caseInsensitive = typeof options === "boolean" ? options : options?.caseInsensitive;
|
|
4057
|
-
const escapeChar = typeof options === "object" ? options.escape : void 0;
|
|
4058
|
-
const intent = {
|
|
4059
|
-
kind: "like",
|
|
4060
|
-
field: getColumnName(field),
|
|
4061
|
-
pattern
|
|
4062
|
-
};
|
|
4063
|
-
const withCi = caseInsensitive !== void 0 ? { ...intent, caseInsensitive } : intent;
|
|
4064
|
-
if (escapeChar !== void 0) {
|
|
4065
|
-
return { ...withCi, escape: escapeChar };
|
|
4066
|
-
}
|
|
4067
|
-
return withCi;
|
|
4068
|
-
}
|
|
4069
|
-
function inArray(field, values) {
|
|
4070
|
-
return { kind: "in", field: getColumnName(field), values };
|
|
4071
|
-
}
|
|
4072
|
-
function inSubquery(field, query) {
|
|
4073
|
-
const expr = "build" in query ? query.build() : query;
|
|
4074
|
-
return {
|
|
4075
|
-
kind: "in",
|
|
4076
|
-
field: getColumnName(field),
|
|
4077
|
-
subquery: expr.toIntent()
|
|
4078
|
-
};
|
|
4079
|
-
}
|
|
4080
|
-
function any(field, values) {
|
|
4081
|
-
return { kind: "any", field: getColumnName(field), values };
|
|
4082
|
-
}
|
|
4083
|
-
function isNull(field) {
|
|
4084
|
-
return { kind: "null", field: getColumnName(field), operator: "isNull" };
|
|
4085
|
-
}
|
|
4086
|
-
function isNotNull(field) {
|
|
4087
|
-
return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
|
|
4088
|
-
}
|
|
4089
|
-
function and(...conditions) {
|
|
4090
|
-
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
4091
|
-
return { kind: "and", conditions: flatConditions };
|
|
4092
|
-
}
|
|
4093
|
-
function or(...conditions) {
|
|
4094
|
-
const flatConditions = conditions.length === 1 && Array.isArray(conditions[0]) ? conditions[0] : conditions;
|
|
4095
|
-
return { kind: "or", conditions: flatConditions };
|
|
4096
|
-
}
|
|
4097
|
-
function not(condition) {
|
|
4098
|
-
return { kind: "not", condition };
|
|
4099
|
-
}
|
|
4100
|
-
function exists(relation, options) {
|
|
4101
|
-
const result = { kind: "exists", relation };
|
|
4102
|
-
if (options?.where !== void 0) {
|
|
4103
|
-
result.where = options.where;
|
|
4104
|
-
}
|
|
4105
|
-
if (options?.recursive !== void 0) {
|
|
4106
|
-
result.recursive = options.recursive;
|
|
4107
|
-
}
|
|
4108
|
-
if (options?.include !== void 0) {
|
|
4109
|
-
result.include = options.include;
|
|
4110
4274
|
}
|
|
4111
|
-
|
|
4112
|
-
}
|
|
4113
|
-
function notExists(relation, options) {
|
|
4114
|
-
const result = {
|
|
4115
|
-
kind: "notExists",
|
|
4116
|
-
relation
|
|
4117
|
-
};
|
|
4118
|
-
if (options?.where !== void 0) {
|
|
4119
|
-
result.where = options.where;
|
|
4120
|
-
}
|
|
4121
|
-
if (options?.recursive !== void 0) {
|
|
4122
|
-
result.recursive = options.recursive;
|
|
4123
|
-
}
|
|
4124
|
-
if (options?.include !== void 0) {
|
|
4125
|
-
result.include = options.include;
|
|
4126
|
-
}
|
|
4127
|
-
return result;
|
|
4128
|
-
}
|
|
4129
|
-
function rawExists(sq) {
|
|
4130
|
-
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
4131
|
-
return { kind: "rawExists", subquery: intent };
|
|
4132
|
-
}
|
|
4133
|
-
function rawNotExists(sq) {
|
|
4134
|
-
const intent = "buildIntent" in sq ? sq.buildIntent() : sq.build().toIntent();
|
|
4135
|
-
return { kind: "rawNotExists", subquery: intent };
|
|
4136
|
-
}
|
|
4137
|
-
function getRelationName(rel) {
|
|
4138
|
-
const meta = rel[RELATION_META];
|
|
4139
|
-
if (!meta) {
|
|
4140
|
-
throw new Error("Invalid RelationRef: missing RELATION_META");
|
|
4141
|
-
}
|
|
4142
|
-
return meta.target;
|
|
4143
|
-
}
|
|
4144
|
-
function every(relation, filter) {
|
|
4145
|
-
const relationName = getRelationName(relation);
|
|
4146
|
-
const where = filter(relation);
|
|
4147
|
-
return {
|
|
4148
|
-
kind: "relationFilter",
|
|
4149
|
-
relation: relationName,
|
|
4150
|
-
where,
|
|
4151
|
-
mode: "every"
|
|
4152
|
-
};
|
|
4153
|
-
}
|
|
4154
|
-
function none(relation, filter) {
|
|
4155
|
-
const relationName = getRelationName(relation);
|
|
4156
|
-
const where = filter(relation);
|
|
4157
|
-
return {
|
|
4158
|
-
kind: "relationFilter",
|
|
4159
|
-
relation: relationName,
|
|
4160
|
-
where,
|
|
4161
|
-
mode: "none"
|
|
4162
|
-
};
|
|
4163
|
-
}
|
|
4164
|
-
function some(relation, filter) {
|
|
4165
|
-
const relationName = getRelationName(relation);
|
|
4166
|
-
const where = filter(relation);
|
|
4167
|
-
return {
|
|
4168
|
-
kind: "relationFilter",
|
|
4169
|
-
relation: relationName,
|
|
4170
|
-
where,
|
|
4171
|
-
mode: "some"
|
|
4172
|
-
};
|
|
4173
|
-
}
|
|
4174
|
-
function coalesce(fields, as) {
|
|
4175
|
-
if (fields.length === 0) {
|
|
4176
|
-
throw new Error("coalesce() requires at least one field");
|
|
4177
|
-
}
|
|
4178
|
-
if (!as || as.trim() === "") {
|
|
4179
|
-
throw new Error("coalesce() requires a non-empty alias");
|
|
4180
|
-
}
|
|
4181
|
-
for (const f of fields) {
|
|
4182
|
-
validateIdentifier(f, "column");
|
|
4183
|
-
}
|
|
4184
|
-
validateIdentifier(as, "column");
|
|
4185
|
-
return {
|
|
4186
|
-
__expr: true,
|
|
4187
|
-
intent: { kind: "coalesce", fields, as }
|
|
4188
|
-
};
|
|
4189
|
-
}
|
|
4190
|
-
function raw(sqlFragment, as) {
|
|
4191
|
-
if (typeof as !== "string" || as.trim().length === 0) {
|
|
4192
|
-
throw new InvalidOperationError(
|
|
4193
|
-
"raw",
|
|
4194
|
-
"raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
|
|
4195
|
-
);
|
|
4196
|
-
}
|
|
4197
|
-
validateIdentifier(as, "column");
|
|
4198
|
-
return {
|
|
4199
|
-
__expr: true,
|
|
4200
|
-
intent: { kind: "raw", sql: sqlFragment, as }
|
|
4201
|
-
};
|
|
4202
|
-
}
|
|
4203
|
-
function col(column, alias) {
|
|
4204
|
-
validateIdentifier(column, "column");
|
|
4205
|
-
validateIdentifier(alias, "column");
|
|
4206
|
-
return {
|
|
4207
|
-
__expr: true,
|
|
4208
|
-
intent: { kind: "columnAlias", column, alias }
|
|
4209
|
-
};
|
|
4210
|
-
}
|
|
4211
|
-
function relationColumn(relation, column, as) {
|
|
4212
|
-
if (!relation) {
|
|
4213
|
-
throw new Error("relationColumn() requires a non-empty relation path");
|
|
4214
|
-
}
|
|
4215
|
-
for (const segment of relation.split(".")) {
|
|
4216
|
-
validateIdentifier(segment, "relation");
|
|
4217
|
-
}
|
|
4218
|
-
if (column !== "*") {
|
|
4219
|
-
validateIdentifier(column, "column");
|
|
4220
|
-
}
|
|
4221
|
-
validateIdentifier(as, "column");
|
|
4222
|
-
return {
|
|
4223
|
-
__expr: true,
|
|
4224
|
-
intent: { kind: "relationColumn", relation, column, as }
|
|
4225
|
-
};
|
|
4226
|
-
}
|
|
4227
|
-
var SQL_RAW_MARKER = /* @__PURE__ */ Symbol.for("dbsp:raw-sql");
|
|
4228
|
-
function sql(sqlFragment) {
|
|
4229
|
-
if (!sqlFragment || sqlFragment.trim() === "") {
|
|
4230
|
-
throw new Error("sql() requires a non-empty SQL fragment");
|
|
4231
|
-
}
|
|
4232
|
-
return { [SQL_RAW_MARKER]: true, sql: sqlFragment };
|
|
4233
|
-
}
|
|
4234
|
-
function isSqlRaw(value) {
|
|
4235
|
-
return typeof value === "object" && value !== null && SQL_RAW_MARKER in value && value[SQL_RAW_MARKER] === true;
|
|
4236
|
-
}
|
|
4275
|
+
]);
|
|
4237
4276
|
|
|
4238
4277
|
// src/dx/full-text-search.ts
|
|
4239
4278
|
function fullTextSearch({
|
|
@@ -10240,6 +10279,9 @@ function generateCreateIndexSQL(tableName, schemaName, opts) {
|
|
|
10240
10279
|
`INCLUDE (${opts.include.map((c) => quoteIdent(c)).join(", ")})`
|
|
10241
10280
|
);
|
|
10242
10281
|
}
|
|
10282
|
+
if (opts.unique && opts.nullsNotDistinct) {
|
|
10283
|
+
parts.push("NULLS NOT DISTINCT");
|
|
10284
|
+
}
|
|
10243
10285
|
if (opts.with && Object.keys(opts.with).length > 0) {
|
|
10244
10286
|
const withClauses = Object.entries(opts.with).map(([k, v2]) => `${k} = ${v2}`).join(", ");
|
|
10245
10287
|
parts.push(`WITH (${withClauses})`);
|