tplm-lang 0.3.0 → 0.3.2
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.
|
@@ -12,6 +12,58 @@
|
|
|
12
12
|
import { collectBranches, } from "./table-spec.js";
|
|
13
13
|
// Module-level reference to ordering provider for definition-order sorting
|
|
14
14
|
let currentOrderingProvider;
|
|
15
|
+
// ---
|
|
16
|
+
// DATE NORMALIZATION
|
|
17
|
+
// ---
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a raw data value: converts Date objects to formatted strings
|
|
20
|
+
* so they display cleanly as headers and work correctly with Set-based
|
|
21
|
+
* deduplication and value-based cell lookup.
|
|
22
|
+
*
|
|
23
|
+
* Dates at midnight UTC (date-only) become "YYYY-MM-DD".
|
|
24
|
+
* Timestamps with a time component become "YYYY-MM-DD HH:MM:SS".
|
|
25
|
+
*/
|
|
26
|
+
function normalizeDimValue(value) {
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
const iso = value.toISOString();
|
|
29
|
+
return iso.endsWith("T00:00:00.000Z")
|
|
30
|
+
? iso.slice(0, 10)
|
|
31
|
+
: iso.slice(0, 19).replace("T", " ");
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively normalize all Date values in query result rows.
|
|
37
|
+
* This ensures Date objects are converted to formatted strings before
|
|
38
|
+
* any downstream processing (header extraction, cell indexing, etc.).
|
|
39
|
+
*/
|
|
40
|
+
function normalizeResultRow(row) {
|
|
41
|
+
const normalized = {};
|
|
42
|
+
for (const key of Object.keys(row)) {
|
|
43
|
+
const val = row[key];
|
|
44
|
+
if (Array.isArray(val)) {
|
|
45
|
+
normalized[key] = val.map((item) => item && typeof item === "object" && !Array.isArray(item) && !(item instanceof Date)
|
|
46
|
+
? normalizeResultRow(item)
|
|
47
|
+
: normalizeDimValue(item));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
normalized[key] = normalizeDimValue(val);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Normalize all Date objects in query results to formatted strings.
|
|
57
|
+
* Applied once at the entry point so all downstream code sees
|
|
58
|
+
* consistent string values instead of raw Date objects.
|
|
59
|
+
*/
|
|
60
|
+
function normalizeQueryResults(results) {
|
|
61
|
+
const normalized = new Map();
|
|
62
|
+
for (const [queryId, rows] of results) {
|
|
63
|
+
normalized.set(queryId, rows.map(normalizeResultRow));
|
|
64
|
+
}
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
15
67
|
/**
|
|
16
68
|
* Build a GridSpec from a TableSpec and query results.
|
|
17
69
|
*
|
|
@@ -33,6 +85,10 @@ export function buildGridSpec(spec, plan, results, malloyQueriesOrOptions) {
|
|
|
33
85
|
}
|
|
34
86
|
// Set module-level ordering provider for definition-order sorting
|
|
35
87
|
currentOrderingProvider = orderingProvider;
|
|
88
|
+
// Normalize Date objects in results to formatted strings before any processing.
|
|
89
|
+
// Malloy's toObject() returns JS Date objects for date/timestamp columns, which
|
|
90
|
+
// cause issues with Set-based dedup, value comparison, and display formatting.
|
|
91
|
+
results = normalizeQueryResults(results);
|
|
36
92
|
try {
|
|
37
93
|
// Build maps of query ID to special handling flags
|
|
38
94
|
const invertedQueries = new Set();
|
|
@@ -322,6 +322,9 @@ class TPLParser extends CstParser {
|
|
|
322
322
|
this.SUBRULE(this.whereExpression);
|
|
323
323
|
});
|
|
324
324
|
// Simplified WHERE expression - captures tokens until ROWS
|
|
325
|
+
// Must accept all tokens that can appear in Malloy filter expressions,
|
|
326
|
+
// including @ for date literals (@2025-01-01), minus for negative numbers
|
|
327
|
+
// and date components, colon for timestamps, and comma for IN lists.
|
|
325
328
|
whereExpression = this.RULE('whereExpression', () => {
|
|
326
329
|
this.AT_LEAST_ONE(() => {
|
|
327
330
|
this.OR([
|
|
@@ -336,6 +339,10 @@ class TPLParser extends CstParser {
|
|
|
336
339
|
{ ALT: () => this.CONSUME(Dot) },
|
|
337
340
|
{ ALT: () => this.CONSUME(LParen) },
|
|
338
341
|
{ ALT: () => this.CONSUME(RParen) },
|
|
342
|
+
{ ALT: () => this.CONSUME(At) }, // Malloy date literals: @2025-01-01
|
|
343
|
+
{ ALT: () => this.CONSUME(Minus) }, // Negative numbers, date separators
|
|
344
|
+
{ ALT: () => this.CONSUME(Colon) }, // Timestamp separators: 10:30:00
|
|
345
|
+
{ ALT: () => this.CONSUME(CommaPunct) }, // IN lists, multiple conditions
|
|
339
346
|
]);
|
|
340
347
|
});
|
|
341
348
|
});
|
|
@@ -939,7 +946,9 @@ class TPLToAstVisitor extends BaseTPLVisitor {
|
|
|
939
946
|
return this.visit(ctx.whereExpression[0]);
|
|
940
947
|
}
|
|
941
948
|
whereExpression(ctx) {
|
|
942
|
-
// Reconstruct the WHERE expression from tokens
|
|
949
|
+
// Reconstruct the WHERE expression from tokens, preserving original spacing.
|
|
950
|
+
// This is important for date literals like @2025-01-01 where tokens (@, 2025, -, 01)
|
|
951
|
+
// must not have spaces inserted between them.
|
|
943
952
|
const allTokens = [];
|
|
944
953
|
for (const key of Object.keys(ctx)) {
|
|
945
954
|
const tokens = ctx[key];
|
|
@@ -947,9 +956,17 @@ class TPLToAstVisitor extends BaseTPLVisitor {
|
|
|
947
956
|
allTokens.push(...tokens);
|
|
948
957
|
}
|
|
949
958
|
}
|
|
950
|
-
// Sort by position
|
|
959
|
+
// Sort by position in the original input
|
|
951
960
|
allTokens.sort((a, b) => a.startOffset - b.startOffset);
|
|
952
|
-
|
|
961
|
+
// Rebuild using original offsets to preserve spacing
|
|
962
|
+
if (allTokens.length === 0)
|
|
963
|
+
return '';
|
|
964
|
+
let result = allTokens[0].image;
|
|
965
|
+
for (let i = 1; i < allTokens.length; i++) {
|
|
966
|
+
const gap = allTokens[i].startOffset - (allTokens[i - 1].startOffset + allTokens[i - 1].image.length);
|
|
967
|
+
result += (gap > 0 ? ' '.repeat(gap) : '') + allTokens[i].image;
|
|
968
|
+
}
|
|
969
|
+
return result;
|
|
953
970
|
}
|
|
954
971
|
axis(ctx) {
|
|
955
972
|
const groups = ctx.groups.map((g) => this.visit(g));
|