@atscript/db-sql-tools 0.1.128 → 0.1.129

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.cjs CHANGED
@@ -339,19 +339,27 @@ function buildAggExpr(dialect, expr) {
339
339
  return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr))}`;
340
340
  }
341
341
  /**
342
- * Renders `$having`. A key that names an aggregate alias (`$as`, else
343
- * `fn_field`) renders the aggregate expression itself — `SUM("amount") > ?`
344
- * — because PostgreSQL does not allow a SELECT alias in HAVING (MySQL and
345
- * SQLite tolerate it, so the expression form keeps all three identical).
346
- * Other keys (grouped columns) render as plain columns.
342
+ * ` HAVING <predicate>` (leading space) + params for `controls.$having`, or
343
+ * `undefined` when there is nothing to render. Shared by the row and the
344
+ * count builders so both filter the same group set.
345
+ *
346
+ * A key that names an aggregate alias (`$as`, else `fn_field`) renders the
347
+ * aggregate expression itself — `SUM("amount") > ?` — because PostgreSQL does
348
+ * not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
349
+ * expression form keeps all three identical). Other keys (grouped columns)
350
+ * render as plain columns.
347
351
  */
348
- function buildHaving(dialect, controls) {
352
+ function havingClause(dialect, controls) {
349
353
  const having = controls.$having;
350
- const aggregates = controls.$select?.aggregates;
351
- if (!aggregates?.length) return buildWhere(dialect, having);
354
+ if (!having) return void 0;
352
355
  const exprByAlias = /* @__PURE__ */ new Map();
353
- for (const expr of aggregates) exprByAlias.set((0, _atscript_db_agg.resolveAlias)(expr), aggFnSql(dialect, expr));
354
- return (0, _uniqu_core.walkFilter)(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) })) ?? EMPTY_AND;
356
+ for (const expr of controls.$select?.aggregates ?? []) exprByAlias.set((0, _atscript_db_agg.resolveAlias)(expr), aggFnSql(dialect, expr));
357
+ const fragment = (0, _uniqu_core.walkFilter)(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) }));
358
+ if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
359
+ return {
360
+ sql: ` HAVING ${fragment.sql}`,
361
+ params: fragment.params
362
+ };
355
363
  }
356
364
  /**
357
365
  * Builds a SELECT ... GROUP BY statement with aggregate functions.
@@ -369,12 +377,10 @@ function buildAggregateSelect(dialect, table, where, controls) {
369
377
  const groupCols = groupBy.map((f) => dialect.quoteIdentifier(f)).join(", ");
370
378
  sql += ` GROUP BY ${groupCols}`;
371
379
  }
372
- if (controls.$having) {
373
- const havingFragment = buildHaving(dialect, controls);
374
- if (havingFragment.sql !== EMPTY_AND.sql) {
375
- sql += ` HAVING ${havingFragment.sql}`;
376
- params.push(...havingFragment.params);
377
- }
380
+ const having = havingClause(dialect, controls);
381
+ if (having) {
382
+ sql += having.sql;
383
+ params.push(...having.params);
378
384
  }
379
385
  if (controls.$sort) {
380
386
  const orderParts = [];
@@ -396,19 +402,23 @@ function buildAggregateSelect(dialect, table, where, controls) {
396
402
  });
397
403
  }
398
404
  /**
399
- * Builds a COUNT query for the number of distinct groups.
400
- * Returns `{ count: N }` when executed.
405
+ * Builds a COUNT query for the number of distinct groups — the groups that
406
+ * survive `$having` when one is given (the same predicate the row query
407
+ * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
408
+ * executed.
401
409
  */
402
410
  function buildAggregateCount(dialect, table, where, controls) {
403
411
  const groupFields = controls.$groupBy;
404
- if (!groupFields?.length) return finalizeParams(dialect, {
405
- sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
412
+ const having = havingClause(dialect, controls);
413
+ const countCol = `COUNT(*) AS ${dialect.quoteIdentifier("count")}`;
414
+ if (!groupFields?.length && !having) return finalizeParams(dialect, {
415
+ sql: `SELECT ${countCol} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
406
416
  params: where.params
407
417
  });
408
- const groupCols = groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ");
418
+ const groupBy = groupFields?.length ? ` GROUP BY ${groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ")}` : "";
409
419
  return finalizeParams(dialect, {
410
- sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`,
411
- params: where.params
420
+ sql: `SELECT ${countCol} FROM (SELECT ${groupBy ? "1" : "COUNT(*)"} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
421
+ params: [...where.params, ...having?.params ?? []]
412
422
  });
413
423
  }
414
424
  //#endregion
package/dist/index.d.cts CHANGED
@@ -215,8 +215,10 @@ declare const AGG_FN_SQL: Record<string, string>;
215
215
  */
216
216
  declare function buildAggregateSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
217
217
  /**
218
- * Builds a COUNT query for the number of distinct groups.
219
- * Returns `{ count: N }` when executed.
218
+ * Builds a COUNT query for the number of distinct groups — the groups that
219
+ * survive `$having` when one is given (the same predicate the row query
220
+ * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
221
+ * executed.
220
222
  */
221
223
  declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
222
224
  //#endregion
package/dist/index.d.mts CHANGED
@@ -215,8 +215,10 @@ declare const AGG_FN_SQL: Record<string, string>;
215
215
  */
216
216
  declare function buildAggregateSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
217
217
  /**
218
- * Builds a COUNT query for the number of distinct groups.
219
- * Returns `{ count: N }` when executed.
218
+ * Builds a COUNT query for the number of distinct groups — the groups that
219
+ * survive `$having` when one is given (the same predicate the row query
220
+ * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
221
+ * executed.
220
222
  */
221
223
  declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
222
224
  //#endregion
package/dist/index.mjs CHANGED
@@ -338,19 +338,27 @@ function buildAggExpr(dialect, expr) {
338
338
  return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`;
339
339
  }
340
340
  /**
341
- * Renders `$having`. A key that names an aggregate alias (`$as`, else
342
- * `fn_field`) renders the aggregate expression itself — `SUM("amount") > ?`
343
- * — because PostgreSQL does not allow a SELECT alias in HAVING (MySQL and
344
- * SQLite tolerate it, so the expression form keeps all three identical).
345
- * Other keys (grouped columns) render as plain columns.
341
+ * ` HAVING <predicate>` (leading space) + params for `controls.$having`, or
342
+ * `undefined` when there is nothing to render. Shared by the row and the
343
+ * count builders so both filter the same group set.
344
+ *
345
+ * A key that names an aggregate alias (`$as`, else `fn_field`) renders the
346
+ * aggregate expression itself — `SUM("amount") > ?` — because PostgreSQL does
347
+ * not allow a SELECT alias in HAVING (MySQL and SQLite tolerate it, so the
348
+ * expression form keeps all three identical). Other keys (grouped columns)
349
+ * render as plain columns.
346
350
  */
347
- function buildHaving(dialect, controls) {
351
+ function havingClause(dialect, controls) {
348
352
  const having = controls.$having;
349
- const aggregates = controls.$select?.aggregates;
350
- if (!aggregates?.length) return buildWhere(dialect, having);
353
+ if (!having) return void 0;
351
354
  const exprByAlias = /* @__PURE__ */ new Map();
352
- for (const expr of aggregates) exprByAlias.set(resolveAlias(expr), aggFnSql(dialect, expr));
353
- return walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) })) ?? EMPTY_AND;
355
+ for (const expr of controls.$select?.aggregates ?? []) exprByAlias.set(resolveAlias(expr), aggFnSql(dialect, expr));
356
+ const fragment = walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) }));
357
+ if (!fragment || fragment.sql === EMPTY_AND.sql) return void 0;
358
+ return {
359
+ sql: ` HAVING ${fragment.sql}`,
360
+ params: fragment.params
361
+ };
354
362
  }
355
363
  /**
356
364
  * Builds a SELECT ... GROUP BY statement with aggregate functions.
@@ -368,12 +376,10 @@ function buildAggregateSelect(dialect, table, where, controls) {
368
376
  const groupCols = groupBy.map((f) => dialect.quoteIdentifier(f)).join(", ");
369
377
  sql += ` GROUP BY ${groupCols}`;
370
378
  }
371
- if (controls.$having) {
372
- const havingFragment = buildHaving(dialect, controls);
373
- if (havingFragment.sql !== EMPTY_AND.sql) {
374
- sql += ` HAVING ${havingFragment.sql}`;
375
- params.push(...havingFragment.params);
376
- }
379
+ const having = havingClause(dialect, controls);
380
+ if (having) {
381
+ sql += having.sql;
382
+ params.push(...having.params);
377
383
  }
378
384
  if (controls.$sort) {
379
385
  const orderParts = [];
@@ -395,19 +401,23 @@ function buildAggregateSelect(dialect, table, where, controls) {
395
401
  });
396
402
  }
397
403
  /**
398
- * Builds a COUNT query for the number of distinct groups.
399
- * Returns `{ count: N }` when executed.
404
+ * Builds a COUNT query for the number of distinct groups — the groups that
405
+ * survive `$having` when one is given (the same predicate the row query
406
+ * renders, so `$count` agrees with the row set). Returns `{ count: N }` when
407
+ * executed.
400
408
  */
401
409
  function buildAggregateCount(dialect, table, where, controls) {
402
410
  const groupFields = controls.$groupBy;
403
- if (!groupFields?.length) return finalizeParams(dialect, {
404
- sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
411
+ const having = havingClause(dialect, controls);
412
+ const countCol = `COUNT(*) AS ${dialect.quoteIdentifier("count")}`;
413
+ if (!groupFields?.length && !having) return finalizeParams(dialect, {
414
+ sql: `SELECT ${countCol} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
405
415
  params: where.params
406
416
  });
407
- const groupCols = groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ");
417
+ const groupBy = groupFields?.length ? ` GROUP BY ${groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ")}` : "";
408
418
  return finalizeParams(dialect, {
409
- sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`,
410
- params: where.params
419
+ sql: `SELECT ${countCol} FROM (SELECT ${groupBy ? "1" : "COUNT(*)"} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}${groupBy}${having?.sql ?? ""}) AS ${dialect.quoteIdentifier("_groups")}`,
420
+ params: [...where.params, ...having?.params ?? []]
411
421
  });
412
422
  }
413
423
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sql-tools",
3
- "version": "0.1.128",
3
+ "version": "0.1.129",
4
4
  "description": "Shared SQL builder utilities for @atscript database adapters.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@uniqu/core": "^0.1.8",
45
- "@atscript/db": "^0.1.128"
45
+ "@atscript/db": "^0.1.129"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",