@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.js CHANGED
@@ -3219,375 +3219,788 @@ function batchValues(data, columns, types, opts) {
3219
3219
  });
3220
3220
  }
3221
3221
 
3222
- // src/dx/expressions.ts
3223
- var OPERATOR_PATTERN = /^[a-zA-Z_<>=!@#%^&|~*+\-/.]+$/;
3224
- var FUNCTION_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/;
3225
- var TYPE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_ ]*(\[\])?$/;
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
- if (typeof input === "string") {
3232
- return { kind: "ref", column: input };
3227
+ const colName = field[COLUMN_META];
3228
+ if (colName === void 0) {
3229
+ throw new Error("Invalid ColumnRef: missing COLUMN_META");
3233
3230
  }
3234
- return { kind: "param", value: input };
3231
+ return colName;
3235
3232
  }
3236
- var ExpressionRef = class _ExpressionRef {
3237
- __expr = true;
3238
- intent;
3239
- constructor(intent) {
3240
- this.intent = intent;
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
- * Set an alias for this expression in SELECT.
3244
- * Returns a new ExpressionRef does not mutate.
3245
+ * Create a ranking window builder (row_number, rank, dense_rank)
3246
+ * @internal Use factory functions instead: rowNumber(), rank(), denseRank()
3245
3247
  */
3246
- as(alias) {
3247
- return new _ExpressionRef({
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
- /** WHERE: expr > value */
3261
- gt(value) {
3262
- return { kind: "expression", expr: this.intent, operator: "gt", value };
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
- /** WHERE: expr >= value */
3265
- gte(value) {
3266
- return { kind: "expression", expr: this.intent, operator: "gte", value };
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
- /** WHERE: expr < value */
3269
- lt(value) {
3270
- return { kind: "expression", expr: this.intent, operator: "lt", value };
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
- /** WHERE: expr <= value */
3273
- lte(value) {
3274
- return { kind: "expression", expr: this.intent, operator: "lte", value };
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
- * Add a FILTER (WHERE ...) clause to this function expression.
3278
- * Only valid on `fn()` expressions (customFn kind).
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
- * @example fn('array_agg', ref('name')).filter(eq('active', true))
3282
- * → array_agg("name") FILTER (WHERE "active" = $1)
3283
- * @throws Error if called on non-customFn expressions
3317
+ * @param alias - Required alias for the result column
3318
+ *
3319
+ * @example
3320
+ * columns(['id', rowNumber().orderBy('date').as('rn')])
3284
3321
  */
3285
- filter(condition) {
3286
- if (this.intent.kind !== "customFn") {
3287
- throw new Error(
3288
- `filter() can only be used on function expressions created with fn(). Got kind: '${this.intent.kind}'`
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
- return new _ExpressionRef({
3292
- ...this.intent,
3293
- filter: condition
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 ref2(column) {
3298
- return new ExpressionRef({
3299
- kind: "ref",
3300
- column
3301
- });
3369
+ function rowNumber() {
3370
+ return WindowBuilder.ranking("row_number");
3302
3371
  }
3303
- function param(value) {
3304
- return new ExpressionRef({
3305
- kind: "param",
3306
- value
3307
- });
3372
+ function rank() {
3373
+ return WindowBuilder.ranking("rank");
3308
3374
  }
3309
- function cast(expr, typeName) {
3310
- if (!TYPE_NAME_PATTERN.test(typeName)) {
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 op(operator, left, right) {
3320
- if (!operator || !OPERATOR_PATTERN.test(operator)) {
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 fn(name, ...args) {
3331
- if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
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 literal(value) {
3352
- return new ExpressionRef({
3353
- kind: "literal",
3354
- value
3355
- });
3384
+ function wCount(field) {
3385
+ return WindowBuilder.aggregate("count", field);
3356
3386
  }
3357
- function unary(operator, expr) {
3358
- if (!operator || !OPERATOR_PATTERN.test(operator)) {
3359
- throw new Error(`Invalid operator: ${operator}`);
3360
- }
3361
- return new ExpressionRef({
3362
- kind: "unary",
3363
- operator,
3364
- operand: toExpressionIntent(expr)
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
- function namedArg(name, value) {
3368
- if (!name || !FUNCTION_NAME_PATTERN.test(name)) {
3369
- throw new Error(`namedArg: invalid argument name: ${name}`);
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 new ExpressionRef({
3372
- kind: "namedArg",
3373
- name,
3374
- value: toExpressionIntent(value)
3375
- });
3434
+ return withCi;
3376
3435
  }
3377
- function star() {
3378
- return new ExpressionRef({ kind: "star" });
3436
+ function inArray(field, values) {
3437
+ return { kind: "in", field: getColumnName(field), values };
3379
3438
  }
3380
- function array(...items) {
3381
- return new ExpressionRef({
3382
- kind: "array",
3383
- elements: items.map(toExpressionIntent)
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 isAggOrderByArg(input) {
3387
- return typeof input === "object" && input !== null && !Array.isArray(input) && !(input instanceof ExpressionRef) && "__aggOrderBy" in input && input.__aggOrderBy === true;
3447
+ function any(field, values) {
3448
+ return { kind: "any", field: getColumnName(field), values };
3388
3449
  }
3389
- function aggOrderBy(field, direction = "asc") {
3390
- return { __aggOrderBy: true, field, direction };
3450
+ function isNull(field) {
3451
+ return { kind: "null", field: getColumnName(field), operator: "isNull" };
3391
3452
  }
3392
- function arrayAgg(col2, ...rest) {
3393
- const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3394
- return fn("array_agg", colExpr, ...rest);
3453
+ function isNotNull(field) {
3454
+ return { kind: "null", field: getColumnName(field), operator: "isNotNull" };
3395
3455
  }
3396
- function stringAgg(col2, separator, ...rest) {
3397
- const colExpr = typeof col2 === "string" ? ref2(col2) : col2;
3398
- return fn("string_agg", colExpr, separator, ...rest);
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
- // src/dx/case-when-builder.ts
3402
- function toResultIntent(value) {
3403
- if (value instanceof ExpressionRef) {
3404
- return value.intent;
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 (value === null || value === void 0) {
3407
- return { kind: "literal", value: null };
3472
+ if (options?.recursive !== void 0) {
3473
+ result.recursive = options.recursive;
3408
3474
  }
3409
- if (typeof value === "string") {
3410
- return { kind: "ref", column: value };
3475
+ if (options?.include !== void 0) {
3476
+ result.include = options.include;
3411
3477
  }
3412
- return { kind: "literal", value };
3478
+ return result;
3413
3479
  }
3414
- var CaseBuilder = class _CaseBuilder {
3415
- branches;
3416
- constructor(branches) {
3417
- this.branches = branches;
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
- when(condition, thenValue) {
3420
- return new _CaseBuilder([
3421
- ...this.branches,
3422
- { condition, result: toResultIntent(thenValue) }
3423
- ]);
3488
+ if (options?.recursive !== void 0) {
3489
+ result.recursive = options.recursive;
3424
3490
  }
3425
- else(elseValue) {
3426
- const intent = {
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
- as(alias) {
3434
- return this.toExpr().as(alias);
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
- toExpr() {
3437
- const intent = {
3438
- kind: "case",
3439
- when: this.branches
3440
- };
3441
- return new ExpressionRef(intent);
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
- function caseWhen(condition, thenValue) {
3445
- return new CaseBuilder([{ condition, result: toResultIntent(thenValue) }]);
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
- // src/dx/builder-utils.ts
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
- operationName,
3453
- "This operation requires an adapter \u2014 pass an adapter when creating the ORM"
3560
+ "raw",
3561
+ "raw() requires an alias as second argument, e.g. raw('COUNT(*)', 'count')"
3454
3562
  );
3455
3563
  }
3456
- return adapter;
3564
+ validateIdentifier(as, "column");
3565
+ return {
3566
+ __expr: true,
3567
+ intent: { kind: "raw", sql: sqlFragment, as }
3568
+ };
3457
3569
  }
3458
-
3459
- // src/dx/cte-builder.ts
3460
- var CteBuilder = class {
3461
- cteName;
3462
- adapter;
3463
- schemaName;
3464
- unnestColumns;
3465
- indexColumnName;
3466
- constructor(name, adapter, schemaName) {
3467
- this.cteName = name;
3468
- this.adapter = adapter;
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
- * Provide the column arrays for the unnest() CTE.
3473
- * All arrays must have the same length.
3474
- *
3475
- * @param columns - A record of column name → array of values.
3476
- */
3477
- fromUnnest(columns) {
3478
- const lengths = Object.values(columns).map((arr) => arr.length);
3479
- if (lengths.length > 1 && !lengths.every((l) => l === lengths[0])) {
3480
- throw new InvalidOperationError(
3481
- "withCte",
3482
- `Array length mismatch in CTE unnest columns: lengths are [${lengths.join(", ")}]`
3483
- );
3484
- }
3485
- this.unnestColumns = columns;
3486
- return this;
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
- * Add a 0-based ordinality index column (uses WITH ORDINALITY).
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
- withIndex(columnName) {
3494
- this.indexColumnName = columnName;
3495
- return this;
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
- * Attach an outer query and produce a CteQueryBuilder for execution.
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
- * @param selectBuilder - A QueryBuilder (from orm.select(...))
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
- query(selectBuilder) {
3503
- if (!this.unnestColumns) {
3504
- throw new InvalidOperationError(
3505
- "withCte",
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
- const cteIntent = {
3510
- kind: "unnestCte",
3511
- name: this.cteName,
3512
- columns: this.unnestColumns,
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
- var CteQueryBuilder = class {
3526
- cteIntent;
3527
- outerBuilder;
3528
- adapter;
3529
- schemaName;
3530
- constructor(cteIntent, outerBuilder, adapter, schemaName) {
3531
- this.cteIntent = cteIntent;
3532
- this.outerBuilder = outerBuilder;
3533
- this.adapter = adapter;
3534
- this.schemaName = schemaName;
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
- * Build the CteQueryIntent AST.
3538
- */
3539
- buildIntent() {
3540
- const queryIntent = this.outerBuilder.buildIntent();
3541
- return {
3542
- kind: "cteQuery",
3543
- ctes: [this.cteIntent],
3544
- query: queryIntent
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
- requireAdapter() {
3548
- return requireAdapter(this.adapter, "withCte");
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
- * Compile to SQL and return an observability dump.
3552
- */
3553
- dump() {
3554
- const adapter = this.requireAdapter();
3555
- const intent = this.buildIntent();
3556
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
3557
- const compiled = adapter.compileCteQuery(
3558
- intent,
3559
- compileOptions
3560
- );
3561
- return {
3562
- sql: compiled.sql,
3563
- params: compiled.parameters,
3564
- intent
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
- * Execute the CTE query and return all results.
3569
- */
3570
- async all() {
3571
- const adapter = this.requireAdapter();
3572
- const intent = this.buildIntent();
3573
- const compileOptions = this.schemaName ? { schemaName: this.schemaName } : void 0;
3574
- const compiled = adapter.compileCteQuery(
3575
- intent,
3576
- compileOptions
3577
- );
3578
- return adapter.execute(compiled);
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
- * Alias for all().
3582
- */
3583
- execute() {
3584
- return this.all();
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
- // src/dx/feature-checkers.ts
3589
- var DEFAULT_FEATURE_CHECKERS = Object.freeze([
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,