tplm-lang 0.3.5 → 0.3.6
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/compiler/grid-spec-builder.js +3 -5
- package/dist/compiler/malloy-generator.js +19 -2
- package/dist/compiler/multi-query-utils.d.ts +6 -1
- package/dist/compiler/multi-query-utils.js +11 -6
- package/dist/index.cjs +19 -15
- package/dist/renderer/grid-renderer.js +23 -18
- package/package.json +1 -1
|
@@ -461,15 +461,13 @@ function buildSiblingHeaders(node, plan, results, axis, currentPath, depth, pare
|
|
|
461
461
|
* Format an aggregate name for display (e.g., "births sum" from "births", "sum")
|
|
462
462
|
*/
|
|
463
463
|
function formatAggregateName(measure, aggregation) {
|
|
464
|
-
// For count/n without a measure, just return
|
|
465
|
-
// This handles cases like standalone "count" or "n"
|
|
464
|
+
// For count/n without a measure, just return "N"
|
|
466
465
|
if (!measure || measure === "__pending__") {
|
|
467
466
|
return aggregation === "count" ? "N" : aggregation;
|
|
468
467
|
}
|
|
469
|
-
// For count
|
|
470
|
-
// count doesn't really bind to a measure in Malloy
|
|
468
|
+
// For field-bound count (distinct count), show "field N"
|
|
471
469
|
if (aggregation === "count") {
|
|
472
|
-
return
|
|
470
|
+
return `${measure} N`;
|
|
473
471
|
}
|
|
474
472
|
return `${measure} ${aggregation}`;
|
|
475
473
|
}
|
|
@@ -110,16 +110,33 @@ function buildNestedPivot(colGroupings, aggregates, indent) {
|
|
|
110
110
|
function buildAggregates(aggregates, indent = 2) {
|
|
111
111
|
const pad = ' '.repeat(indent);
|
|
112
112
|
const aggs = aggregates.map(a => {
|
|
113
|
-
return `${a.name} is ${a.measure
|
|
113
|
+
return `${a.name} is ${buildAggExpr(a.measure, a.aggregation)}`;
|
|
114
114
|
});
|
|
115
115
|
return `${pad}aggregate: ${aggs.join(', ')}`;
|
|
116
116
|
}
|
|
117
|
+
/** Build a Malloy aggregate expression (mirrors multi-query-utils buildAggExpression) */
|
|
118
|
+
function buildAggExpr(measure, aggregation) {
|
|
119
|
+
if (aggregation === 'count') {
|
|
120
|
+
if (measure && measure !== '__pending__' && measure !== '') {
|
|
121
|
+
return `count(${escapeForMalloy(measure)})`;
|
|
122
|
+
}
|
|
123
|
+
return 'count()';
|
|
124
|
+
}
|
|
125
|
+
if (!measure || measure === '__pending__')
|
|
126
|
+
return 'count()';
|
|
127
|
+
const methodMap = { mean: 'avg', stdev: 'stddev', pct: 'sum', pctn: 'count', pctsum: 'sum' };
|
|
128
|
+
return `${measure}.${methodMap[aggregation] ?? aggregation}()`;
|
|
129
|
+
}
|
|
130
|
+
const MALLOY_RESERVED = new Set(['all', 'and', 'as', 'asc', 'avg', 'by', 'case', 'cast', 'count', 'day', 'desc', 'dimension', 'else', 'end', 'exclude', 'extend', 'false', 'from', 'group', 'having', 'hour', 'import', 'is', 'join', 'limit', 'max', 'measure', 'min', 'minute', 'month', 'nest', 'not', 'now', 'null', 'number', 'on', 'or', 'order', 'pick', 'quarter', 'run', 'second', 'source', 'sum', 'then', 'true', 'week', 'when', 'where', 'year']);
|
|
131
|
+
function escapeForMalloy(name) {
|
|
132
|
+
return MALLOY_RESERVED.has(name.toLowerCase()) ? `\`${name}\`` : name;
|
|
133
|
+
}
|
|
117
134
|
/**
|
|
118
135
|
* Build total aggregate for column totals.
|
|
119
136
|
*/
|
|
120
137
|
function buildTotalAggregate(aggregates, label) {
|
|
121
138
|
const aggs = aggregates.map(a => {
|
|
122
|
-
return `${a.name} is ${a.measure
|
|
139
|
+
return `${a.name} is ${buildAggExpr(a.measure, a.aggregation)}`;
|
|
123
140
|
});
|
|
124
141
|
const nestName = label ? `"${label}"` : '"total"';
|
|
125
142
|
return ` nest: total is { aggregate: ${aggs.join(', ')} }`;
|
|
@@ -16,7 +16,12 @@ export declare function escapeFieldName(name: string): string;
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function escapeWhereExpression(expr: string): string;
|
|
18
18
|
/**
|
|
19
|
-
* Build a Malloy aggregate expression from measure and aggregation
|
|
19
|
+
* Build a Malloy aggregate expression from measure and aggregation.
|
|
20
|
+
*
|
|
21
|
+
* Count semantics:
|
|
22
|
+
* - Standalone `count` or `n` (no measure) → `count()` — counts all rows
|
|
23
|
+
* - Field-bound `user_id.count` (with measure) → `count(user_id)` — counts distinct values
|
|
24
|
+
* This matches Malloy's native semantics where count(field) = COUNT(DISTINCT field).
|
|
20
25
|
*/
|
|
21
26
|
export declare function buildAggExpression(measure: string, aggregation: string): string;
|
|
22
27
|
/**
|
|
@@ -86,16 +86,21 @@ const AGG_METHOD_MAP = {
|
|
|
86
86
|
pctsum: 'sum',
|
|
87
87
|
};
|
|
88
88
|
/**
|
|
89
|
-
* Build a Malloy aggregate expression from measure and aggregation
|
|
89
|
+
* Build a Malloy aggregate expression from measure and aggregation.
|
|
90
|
+
*
|
|
91
|
+
* Count semantics:
|
|
92
|
+
* - Standalone `count` or `n` (no measure) → `count()` — counts all rows
|
|
93
|
+
* - Field-bound `user_id.count` (with measure) → `count(user_id)` — counts distinct values
|
|
94
|
+
* This matches Malloy's native semantics where count(field) = COUNT(DISTINCT field).
|
|
90
95
|
*/
|
|
91
96
|
export function buildAggExpression(measure, aggregation) {
|
|
92
97
|
const malloyMethod = AGG_METHOD_MAP[aggregation] ?? aggregation;
|
|
93
|
-
// Handle count
|
|
94
|
-
// count() counts all rows, not a specific field
|
|
95
|
-
// When users write "income.count" or "income.n", they semantically mean "count"
|
|
96
|
-
// since you can't "count" a measure - you can only count rows
|
|
98
|
+
// Handle count: standalone = row count, field-bound = distinct count
|
|
97
99
|
if (aggregation === 'count') {
|
|
98
|
-
|
|
100
|
+
if (measure && measure !== '__pending__' && measure !== '') {
|
|
101
|
+
return `count(${escapeFieldName(measure)})`; // distinct count of field
|
|
102
|
+
}
|
|
103
|
+
return 'count()'; // standalone row count
|
|
99
104
|
}
|
|
100
105
|
// Handle other aggregations without a measure (use placeholder)
|
|
101
106
|
// Also handle the __pending__ placeholder used for standalone count
|
package/dist/index.cjs
CHANGED
|
@@ -6536,6 +6536,9 @@ var AGG_METHOD_MAP = {
|
|
|
6536
6536
|
function buildAggExpression(measure, aggregation) {
|
|
6537
6537
|
const malloyMethod = AGG_METHOD_MAP[aggregation] ?? aggregation;
|
|
6538
6538
|
if (aggregation === "count") {
|
|
6539
|
+
if (measure && measure !== "__pending__" && measure !== "") {
|
|
6540
|
+
return `count(${escapeFieldName(measure)})`;
|
|
6541
|
+
}
|
|
6539
6542
|
return "count()";
|
|
6540
6543
|
}
|
|
6541
6544
|
if (!measure || measure === "__pending__") {
|
|
@@ -8760,7 +8763,7 @@ function formatAggregateName(measure, aggregation) {
|
|
|
8760
8763
|
return aggregation === "count" ? "N" : aggregation;
|
|
8761
8764
|
}
|
|
8762
8765
|
if (aggregation === "count") {
|
|
8763
|
-
return
|
|
8766
|
+
return `${measure} N`;
|
|
8764
8767
|
}
|
|
8765
8768
|
return `${measure} ${aggregation}`;
|
|
8766
8769
|
}
|
|
@@ -10206,6 +10209,17 @@ ${ordinalPick}`);
|
|
|
10206
10209
|
}
|
|
10207
10210
|
|
|
10208
10211
|
// packages/renderer/grid-renderer.ts
|
|
10212
|
+
function formatAggDisplayName(agg) {
|
|
10213
|
+
if (agg.label) return agg.label;
|
|
10214
|
+
if (agg.aggregation === "count") {
|
|
10215
|
+
if (agg.measure && agg.measure !== "__pending__" && agg.measure !== "") {
|
|
10216
|
+
return `${agg.measure} N`;
|
|
10217
|
+
}
|
|
10218
|
+
return "N";
|
|
10219
|
+
}
|
|
10220
|
+
if (!agg.measure || agg.measure === "__pending__") return agg.aggregation;
|
|
10221
|
+
return `${agg.measure} ${agg.aggregation}`;
|
|
10222
|
+
}
|
|
10209
10223
|
function renderGridToHTML(grid, options = {}) {
|
|
10210
10224
|
const {
|
|
10211
10225
|
tableClass = "tpl-table",
|
|
@@ -10288,7 +10302,7 @@ function renderColumnHeaders(grid, lines, showDimensionLabels) {
|
|
|
10288
10302
|
lines.push('<th class="tpl-corner"></th>');
|
|
10289
10303
|
}
|
|
10290
10304
|
}
|
|
10291
|
-
lines.push(`<th>${escapeHTML(grid.aggregates[0]
|
|
10305
|
+
lines.push(`<th>${escapeHTML(grid.aggregates[0] ? formatAggDisplayName(grid.aggregates[0]) : "Value")}</th>`);
|
|
10292
10306
|
lines.push("</tr>");
|
|
10293
10307
|
lines.push("</thead>");
|
|
10294
10308
|
return;
|
|
@@ -10320,7 +10334,7 @@ function renderColumnHeaders(grid, lines, showDimensionLabels) {
|
|
|
10320
10334
|
lines.push("<th>Value</th>");
|
|
10321
10335
|
} else {
|
|
10322
10336
|
for (const agg of grid.aggregates) {
|
|
10323
|
-
lines.push(`<th>${escapeHTML(agg
|
|
10337
|
+
lines.push(`<th>${escapeHTML(formatAggDisplayName(agg))}</th>`);
|
|
10324
10338
|
}
|
|
10325
10339
|
}
|
|
10326
10340
|
lines.push("</tr>");
|
|
@@ -10537,23 +10551,13 @@ function buildHumanPath(rowValues, colValues, aggregateName, aggregates) {
|
|
|
10537
10551
|
if (aggInfo?.label) {
|
|
10538
10552
|
displayAgg = aggInfo.label;
|
|
10539
10553
|
} else if (aggInfo) {
|
|
10540
|
-
|
|
10541
|
-
displayAgg = "count";
|
|
10542
|
-
} else {
|
|
10543
|
-
displayAgg = `${aggInfo.measure}.${aggInfo.aggregation}`;
|
|
10544
|
-
}
|
|
10554
|
+
displayAgg = formatAggDisplayName(aggInfo);
|
|
10545
10555
|
} else {
|
|
10546
10556
|
displayAgg = aggregateName.replace(/^__pending___/, "").replace(/_/g, ".");
|
|
10547
10557
|
}
|
|
10548
10558
|
} else if (aggregates.length === 1) {
|
|
10549
10559
|
const aggInfo = aggregates[0];
|
|
10550
|
-
|
|
10551
|
-
displayAgg = aggInfo.label;
|
|
10552
|
-
} else if (aggInfo.aggregation === "count") {
|
|
10553
|
-
displayAgg = "count";
|
|
10554
|
-
} else {
|
|
10555
|
-
displayAgg = `${aggInfo.measure}.${aggInfo.aggregation}`;
|
|
10556
|
-
}
|
|
10560
|
+
displayAgg = formatAggDisplayName(aggInfo);
|
|
10557
10561
|
}
|
|
10558
10562
|
if (displayAgg) {
|
|
10559
10563
|
return dimPath ? `${dimPath} \u2192 ${displayAgg}` : displayAgg;
|
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
* Key simplification: GridSpec already has the complete header hierarchy
|
|
7
7
|
* with pre-computed spans, so we don't need to reconstruct structure.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Format an aggregate's display name.
|
|
11
|
+
* - Standalone count/n → "N"
|
|
12
|
+
* - field.count (distinct count) → "field N"
|
|
13
|
+
* - field.sum → "field sum"
|
|
14
|
+
*/
|
|
15
|
+
function formatAggDisplayName(agg) {
|
|
16
|
+
if (agg.label)
|
|
17
|
+
return agg.label;
|
|
18
|
+
if (agg.aggregation === 'count') {
|
|
19
|
+
if (agg.measure && agg.measure !== '__pending__' && agg.measure !== '') {
|
|
20
|
+
return `${agg.measure} N`;
|
|
21
|
+
}
|
|
22
|
+
return 'N';
|
|
23
|
+
}
|
|
24
|
+
if (!agg.measure || agg.measure === '__pending__')
|
|
25
|
+
return agg.aggregation;
|
|
26
|
+
return `${agg.measure} ${agg.aggregation}`;
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Render a GridSpec to HTML.
|
|
11
30
|
*/
|
|
@@ -118,7 +137,7 @@ function renderColumnHeaders(grid, lines, showDimensionLabels) {
|
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
// Single aggregate header
|
|
121
|
-
lines.push(`<th>${escapeHTML(grid.aggregates[0]
|
|
140
|
+
lines.push(`<th>${escapeHTML(grid.aggregates[0] ? formatAggDisplayName(grid.aggregates[0]) : 'Value')}</th>`);
|
|
122
141
|
lines.push('</tr>');
|
|
123
142
|
lines.push('</thead>');
|
|
124
143
|
return;
|
|
@@ -159,7 +178,7 @@ function renderColumnHeaders(grid, lines, showDimensionLabels) {
|
|
|
159
178
|
else {
|
|
160
179
|
// Aggregates are not on row axis, render each aggregate as a column header
|
|
161
180
|
for (const agg of grid.aggregates) {
|
|
162
|
-
lines.push(`<th>${escapeHTML(agg
|
|
181
|
+
lines.push(`<th>${escapeHTML(formatAggDisplayName(agg))}</th>`);
|
|
163
182
|
}
|
|
164
183
|
}
|
|
165
184
|
lines.push('</tr>');
|
|
@@ -505,13 +524,7 @@ function buildHumanPath(rowValues, colValues, aggregateName, aggregates) {
|
|
|
505
524
|
displayAgg = aggInfo.label;
|
|
506
525
|
}
|
|
507
526
|
else if (aggInfo) {
|
|
508
|
-
|
|
509
|
-
if (aggInfo.aggregation === 'count') {
|
|
510
|
-
displayAgg = 'count';
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
displayAgg = `${aggInfo.measure}.${aggInfo.aggregation}`;
|
|
514
|
-
}
|
|
527
|
+
displayAgg = formatAggDisplayName(aggInfo);
|
|
515
528
|
}
|
|
516
529
|
else {
|
|
517
530
|
// Fallback: clean up the internal name
|
|
@@ -523,15 +536,7 @@ function buildHumanPath(rowValues, colValues, aggregateName, aggregates) {
|
|
|
523
536
|
else if (aggregates.length === 1) {
|
|
524
537
|
// Single aggregate - still show it in tooltip
|
|
525
538
|
const aggInfo = aggregates[0];
|
|
526
|
-
|
|
527
|
-
displayAgg = aggInfo.label;
|
|
528
|
-
}
|
|
529
|
-
else if (aggInfo.aggregation === 'count') {
|
|
530
|
-
displayAgg = 'count';
|
|
531
|
-
}
|
|
532
|
-
else {
|
|
533
|
-
displayAgg = `${aggInfo.measure}.${aggInfo.aggregation}`;
|
|
534
|
-
}
|
|
539
|
+
displayAgg = formatAggDisplayName(aggInfo);
|
|
535
540
|
}
|
|
536
541
|
if (displayAgg) {
|
|
537
542
|
return dimPath ? `${dimPath} → ${displayAgg}` : displayAgg;
|