@dbsp/adapter-pgsql 1.10.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -123,6 +123,7 @@ interface PlanDecision {
123
123
  readonly targetColumn?: ColumnListInput;
124
124
  readonly targetTable?: string;
125
125
  readonly function?: string;
126
+ readonly distinct?: boolean;
126
127
  readonly args?: readonly unknown[];
127
128
  readonly conditions?: readonly PlanDecision[];
128
129
  readonly columns?: readonly string[];
@@ -173,6 +174,13 @@ interface PlanDecision {
173
174
  readonly jsonMode?: 'json' | 'text';
174
175
  readonly selectColumn?: string;
175
176
  readonly aggregate?: string;
177
+ /**
178
+ * Apply DISTINCT to a scalar subquery's aggregate (e.g. AVG(DISTINCT price)).
179
+ * Deliberately NOT named `distinct` — that field means "this decision's own
180
+ * query-level DISTINCT modifier" and `assertNoDroppedDecisionModifiers`
181
+ * (subquery-emission.ts) rejects it as unsupported on subquery decisions.
182
+ */
183
+ readonly aggregateDistinct?: boolean;
176
184
  readonly subqueryOperator?: string;
177
185
  readonly filterCondition?: PlanDecision;
178
186
  readonly expressionIntent?: unknown;
@@ -1165,6 +1173,8 @@ interface Decision {
1165
1173
  readonly targetColumn?: ColumnListInput;
1166
1174
  readonly targetTable?: string;
1167
1175
  readonly function?: string;
1176
+ /** Apply DISTINCT to a SELECT-list aggregate (e.g. COUNT(DISTINCT col)). */
1177
+ readonly distinct?: boolean;
1168
1178
  readonly args?: readonly unknown[];
1169
1179
  readonly conditions?: readonly Decision[];
1170
1180
  readonly columns?: readonly string[];
@@ -1201,6 +1211,15 @@ interface Decision {
1201
1211
  readonly cycleDetection?: boolean;
1202
1212
  readonly selectColumn?: string;
1203
1213
  readonly aggregate?: string;
1214
+ /**
1215
+ * Apply DISTINCT to a scalar subquery's aggregate (e.g. AVG(DISTINCT price)).
1216
+ * Deliberately NOT named `distinct` — `assertNoDroppedDecisionModifiers`
1217
+ * (subquery-emission.ts) treats a top-level `distinct === true` on ANY
1218
+ * subquery decision as an unsupported query-level DISTINCT modifier and
1219
+ * throws. This field is scoped to the aggregate projection only, so it
1220
+ * must not collide with that generic guard.
1221
+ */
1222
+ readonly aggregateDistinct?: boolean;
1204
1223
  readonly subqueryOperator?: string;
1205
1224
  readonly traversal?: string;
1206
1225
  readonly traversals?: readonly {
package/dist/index.js CHANGED
@@ -1268,6 +1268,7 @@ function compileExpressionIntent(intent, ctx, state) {
1268
1268
  return sortBy(colNode, ob.direction === "desc" ? "DESC" : "ASC");
1269
1269
  }) : void 0;
1270
1270
  return funcCall(nameParts, argNodes, {
1271
+ distinct: i.distinct === true,
1271
1272
  ...orderByNodes ? { orderBy: orderByNodes } : {}
1272
1273
  });
1273
1274
  }
@@ -1488,6 +1489,7 @@ function handleAggregateExpression(expr, rootTable, decisions, applyFilter) {
1488
1489
  table: rootTable
1489
1490
  };
1490
1491
  if (aggAs) decision.alias = aggAs;
1492
+ if (aggDistinct === true) decision.distinct = true;
1491
1493
  decisions.push(decision);
1492
1494
  } else if (aggFunc === "count" && !aggField) {
1493
1495
  const decision = {
@@ -1496,10 +1498,11 @@ function handleAggregateExpression(expr, rootTable, decisions, applyFilter) {
1496
1498
  column: "*",
1497
1499
  table: rootTable
1498
1500
  };
1501
+ if (aggDistinct === true) decision.distinct = true;
1499
1502
  if (aggAs) decision.alias = aggAs;
1500
1503
  applyFilter(decision, aggFilter, rootTable);
1501
1504
  decisions.push(decision);
1502
- } else if (aggFunc === "count" && aggDistinct && aggField) {
1505
+ } else if (aggFunc === "count" && aggDistinct === true && aggField) {
1503
1506
  const decision = {
1504
1507
  type: "selectFunction",
1505
1508
  function: "countDistinct",
@@ -1517,6 +1520,7 @@ function handleAggregateExpression(expr, rootTable, decisions, applyFilter) {
1517
1520
  };
1518
1521
  if (aggField) decision.column = aggField;
1519
1522
  if (aggAs) decision.alias = aggAs;
1523
+ if (aggDistinct === true) decision.distinct = true;
1520
1524
  applyFilter(decision, aggFilter, rootTable);
1521
1525
  decisions.push(decision);
1522
1526
  }
@@ -1797,6 +1801,9 @@ function convertSelect(select, rootTable) {
1797
1801
  if (agg.as) {
1798
1802
  aggDecision.alias = agg.as;
1799
1803
  }
1804
+ if (agg.distinct === true) {
1805
+ aggDecision.distinct = true;
1806
+ }
1800
1807
  applyFilterCondition(aggDecision, agg.filter, rootTable);
1801
1808
  decisions.push(aggDecision);
1802
1809
  }
@@ -2127,6 +2134,7 @@ function convertSubquery(cond) {
2127
2134
  const targetTable = subquery.from;
2128
2135
  let selectColumn = "*";
2129
2136
  let aggregate;
2137
+ let aggregateDistinct;
2130
2138
  const select = subquery.select;
2131
2139
  if (select) {
2132
2140
  if ("type" in select && select.type === "aggregate") {
@@ -2134,6 +2142,7 @@ function convertSubquery(cond) {
2134
2142
  if (agg) {
2135
2143
  aggregate = agg.function;
2136
2144
  selectColumn = agg.field ?? "*";
2145
+ aggregateDistinct = agg.distinct === true ? true : void 0;
2137
2146
  }
2138
2147
  } else if ("fields" in select && select.fields?.length) {
2139
2148
  selectColumn = select.fields[0];
@@ -2167,6 +2176,7 @@ function convertSubquery(cond) {
2167
2176
  // Provenance: original QueryIntent for validation in buildPredicateSubquerySelect
2168
2177
  subqueryIntent: subquery,
2169
2178
  ...aggregate && { aggregate },
2179
+ ...aggregateDistinct && { aggregateDistinct },
2170
2180
  ...subConditions.length > 0 && { conditions: subConditions },
2171
2181
  ...rawLimit != null && { limit: rawLimit },
2172
2182
  ...rawOrderBy && rawOrderBy.length > 0 && {
@@ -4564,21 +4574,19 @@ function buildSubqueryFromIntent(intent, paramOffset, naming = identityNaming, s
4564
4574
  if (select?.type === "aggregate" && select.aggregates && select.aggregates.length > 0) {
4565
4575
  targetList = select.aggregates.map((agg) => {
4566
4576
  let aggNode;
4567
- if (!agg.field || agg.field === "*") {
4568
- aggNode = {
4569
- FuncCall: {
4570
- funcname: [{ String: { sval: agg.function.toLowerCase() } }],
4571
- agg_star: true
4572
- }
4573
- };
4577
+ const field = agg.field;
4578
+ if (!field || field === "*") {
4579
+ if (agg.distinct === true) {
4580
+ throw new Error(
4581
+ `${agg.function}(DISTINCT *) is not valid SQL \u2014 PostgreSQL does not support DISTINCT on a star aggregate; provide a specific column.`
4582
+ );
4583
+ }
4584
+ aggNode = funcCall(agg.function.toLowerCase(), [], { star: true });
4574
4585
  } else {
4575
- const aggArg = columnRef(agg.field, innerAlias, void 0, naming);
4576
- aggNode = {
4577
- FuncCall: {
4578
- funcname: [{ String: { sval: agg.function.toLowerCase() } }],
4579
- args: [aggArg]
4580
- }
4581
- };
4586
+ const aggArg = columnRef(field, innerAlias, void 0, naming);
4587
+ aggNode = funcCall(agg.function.toLowerCase(), [aggArg], {
4588
+ distinct: agg.distinct === true
4589
+ });
4582
4590
  }
4583
4591
  return { ResTarget: { val: aggNode } };
4584
4592
  });
@@ -5222,6 +5230,8 @@ function assertNoDroppedDecisionModifiers(decision, use) {
5222
5230
  if (d.having != null) unsupported.push("HAVING");
5223
5231
  if (d.offset != null) unsupported.push("OFFSET");
5224
5232
  if (d.distinct === true) unsupported.push("DISTINCT");
5233
+ if (d.aggregateDistinct === true && d.aggregate == null)
5234
+ unsupported.push("aggregate DISTINCT without an aggregate function");
5225
5235
  if (Array.isArray(d.distinctOn) && d.distinctOn.length > 0)
5226
5236
  unsupported.push("DISTINCT ON");
5227
5237
  if (Array.isArray(d.include) && d.include.length > 0)
@@ -5271,6 +5281,11 @@ function buildPredicateSubquerySelect(use, sourceIntent, decision, ctx, state, d
5271
5281
  let targetVal;
5272
5282
  if (aggregate) {
5273
5283
  if (selectColumn === "*") {
5284
+ if (decision.aggregateDistinct === true) {
5285
+ throw new Error(
5286
+ `${aggregate}(DISTINCT *) is not valid SQL \u2014 PostgreSQL does not support DISTINCT on a star aggregate; provide a specific column.`
5287
+ );
5288
+ }
5274
5289
  targetVal = {
5275
5290
  FuncCall: {
5276
5291
  funcname: [{ String: { sval: aggregate.toLowerCase() } }],
@@ -5287,7 +5302,8 @@ function buildPredicateSubquerySelect(use, sourceIntent, decision, ctx, state, d
5287
5302
  targetVal = {
5288
5303
  FuncCall: {
5289
5304
  funcname: [{ String: { sval: aggregate.toLowerCase() } }],
5290
- args: [aggArg]
5305
+ args: [aggArg],
5306
+ ...decision.aggregateDistinct === true && { agg_distinct: true }
5291
5307
  }
5292
5308
  };
5293
5309
  }
@@ -6503,7 +6519,13 @@ var init_handlers = __esm({
6503
6519
 
6504
6520
  // src/handlers/expression/aggregate.ts
6505
6521
  function buildAggregate(funcName, column, distinct, ctx, filterNode) {
6506
- const isCountStar = funcName === "count" && (!column || column === "*");
6522
+ const isStarColumn = !column || column === "*";
6523
+ if (distinct && isStarColumn) {
6524
+ throw new Error(
6525
+ `${funcName}(DISTINCT *) is not valid SQL \u2014 PostgreSQL does not support DISTINCT on a star aggregate; provide a specific column.`
6526
+ );
6527
+ }
6528
+ const isCountStar = funcName === "count" && isStarColumn;
6507
6529
  if (isCountStar) {
6508
6530
  return funcCall(funcName, [], {
6509
6531
  star: true,
@@ -6535,7 +6557,7 @@ function createSimpleAggregateHandler(funcName, types) {
6535
6557
  return buildAggregate(
6536
6558
  funcName,
6537
6559
  decision.column,
6538
- false,
6560
+ decision.distinct === true,
6539
6561
  ctx,
6540
6562
  decision.filterWhere
6541
6563
  );
@@ -6551,7 +6573,7 @@ var init_aggregate = __esm({
6551
6573
  types: ["count", "COUNT"],
6552
6574
  compile(decision, ctx, _state) {
6553
6575
  const column = decision.column;
6554
- const distinct = decision.operator === "countDistinct" || Boolean(decision.args?.[0]);
6576
+ const distinct = decision.distinct === true || decision.operator === "countDistinct";
6555
6577
  return buildAggregate("count", column, distinct, ctx, decision.filterWhere);
6556
6578
  }
6557
6579
  };
@@ -6583,7 +6605,7 @@ var init_aggregate = __esm({
6583
6605
  return buildAggregate(
6584
6606
  funcName,
6585
6607
  decision.column,
6586
- false,
6608
+ decision.distinct === true,
6587
6609
  ctx,
6588
6610
  decision.filterWhere
6589
6611
  );
@@ -9189,6 +9211,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
9189
9211
  targetColumn: pd.targetColumn ?? derivedFkColumns.targetColumn,
9190
9212
  targetTable: pd.targetTable,
9191
9213
  function: pd.function,
9214
+ distinct: pd.distinct,
9192
9215
  args: pd.args,
9193
9216
  columns: pd.columns,
9194
9217
  values: pd.values,
@@ -9226,6 +9249,7 @@ function mapToHandlerDecision(pd, rootTable, defaultPk, deriveFk) {
9226
9249
  subqueryOperator: pd.subqueryOperator,
9227
9250
  selectColumn: pd.selectColumn,
9228
9251
  aggregate: pd.aggregate,
9252
+ aggregateDistinct: pd.aggregateDistinct,
9229
9253
  columnAliases: pd.columnAliases,
9230
9254
  escape: pd.escape,
9231
9255
  subqueryIntent: pd.subqueryIntent
@@ -10054,13 +10078,13 @@ var PlanCompiler = class _PlanCompiler {
10054
10078
  const renumbered = renumberParamRefsInAst(innerResult.ast, paramOffset);
10055
10079
  return { ast: renumbered, parameters: innerResult.parameters };
10056
10080
  }
10057
- compileGenericNqlFunction(functionName, args, ctx, state) {
10081
+ compileGenericNqlFunction(functionName, args, ctx, state, distinct = false) {
10058
10082
  assertNqlSelectScalarFunctionAllowed(functionName);
10059
10083
  validateIdentifier(functionName, "function");
10060
10084
  const argNodes = args.map(
10061
10085
  (arg) => this.compileNqlFunctionArg(arg, ctx, state)
10062
10086
  );
10063
- return funcCall(functionName, argNodes);
10087
+ return funcCall(functionName, argNodes, { distinct });
10064
10088
  }
10065
10089
  compileNqlFunctionArg(arg, ctx, state) {
10066
10090
  if (isNqlBindingRef(arg)) {
@@ -10139,6 +10163,7 @@ var PlanCompiler = class _PlanCompiler {
10139
10163
  "NQL aggregate expression requires a function name"
10140
10164
  );
10141
10165
  }
10166
+ const isDistinct = record.distinct === true;
10142
10167
  const aggregateArgs = [];
10143
10168
  if (record.field === "*") {
10144
10169
  aggregateArgs.push({ kind: "star" });
@@ -10148,7 +10173,13 @@ var PlanCompiler = class _PlanCompiler {
10148
10173
  if (Array.isArray(record.extraArgs)) {
10149
10174
  aggregateArgs.push(...record.extraArgs);
10150
10175
  }
10151
- return this.compileGenericNqlFunction(fn4, aggregateArgs, ctx, state);
10176
+ return this.compileGenericNqlFunction(
10177
+ fn4,
10178
+ aggregateArgs,
10179
+ ctx,
10180
+ state,
10181
+ isDistinct
10182
+ );
10152
10183
  }
10153
10184
  case "arithmetic": {
10154
10185
  const operator = record.operator;
@@ -10355,7 +10386,8 @@ var PlanCompiler = class _PlanCompiler {
10355
10386
  funcType,
10356
10387
  decision.args ?? [],
10357
10388
  ctx,
10358
- state
10389
+ state,
10390
+ decision.distinct === true
10359
10391
  );
10360
10392
  this.state.paramIndex = state.paramIndex;
10361
10393
  targetList.push({