tplm-lang 0.3.6 → 0.3.7
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.d.ts +5 -0
- package/dist/compiler/grid-spec-builder.js +69 -20
- package/dist/compiler/table-spec.d.ts +16 -0
- package/dist/executor/index.d.ts +2 -0
- package/dist/executor/index.js +10 -1
- package/dist/index.cjs +104 -28
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -3
- package/dist/renderer/grid-renderer.js +9 -3
- package/package.json +1 -1
|
@@ -23,6 +23,11 @@ export interface BuildGridSpecOptions {
|
|
|
23
23
|
malloyQueries?: MalloyQuerySpec[];
|
|
24
24
|
/** Ordering provider for definition-order sorting */
|
|
25
25
|
orderingProvider?: DimensionOrderingProvider;
|
|
26
|
+
/**
|
|
27
|
+
* Raw SQL queries by query ID, captured from the Malloy runtime.
|
|
28
|
+
* When provided, each CellValue will include `sql` and `cellSQL` fields.
|
|
29
|
+
*/
|
|
30
|
+
sqlQueries?: Map<string, string>;
|
|
26
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* Build a GridSpec from a TableSpec and query results.
|
|
@@ -102,12 +102,14 @@ export function buildGridSpec(spec, plan, results, malloyQueriesOrOptions) {
|
|
|
102
102
|
// Handle both old signature (array) and new signature (options object)
|
|
103
103
|
let malloyQueries;
|
|
104
104
|
let orderingProvider;
|
|
105
|
+
let sqlQueries;
|
|
105
106
|
if (Array.isArray(malloyQueriesOrOptions)) {
|
|
106
107
|
malloyQueries = malloyQueriesOrOptions;
|
|
107
108
|
}
|
|
108
109
|
else if (malloyQueriesOrOptions) {
|
|
109
110
|
malloyQueries = malloyQueriesOrOptions.malloyQueries;
|
|
110
111
|
orderingProvider = malloyQueriesOrOptions.orderingProvider;
|
|
112
|
+
sqlQueries = malloyQueriesOrOptions.sqlQueries;
|
|
111
113
|
}
|
|
112
114
|
// Set module-level ordering provider for definition-order sorting
|
|
113
115
|
currentOrderingProvider = orderingProvider;
|
|
@@ -174,7 +176,7 @@ export function buildGridSpec(spec, plan, results, malloyQueriesOrOptions) {
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
// Build cell lookup (value-based)
|
|
177
|
-
const cellLookup = buildCellLookup(spec, plan, results, invertedQueries, flatQueries);
|
|
179
|
+
const cellLookup = buildCellLookup(spec, plan, results, invertedQueries, flatQueries, sqlQueries);
|
|
178
180
|
// Check for totals
|
|
179
181
|
const hasRowTotal = axisHasTotal(spec.rowAxis);
|
|
180
182
|
const hasColTotal = axisHasTotal(spec.colAxis);
|
|
@@ -1023,6 +1025,37 @@ function makeCellKey(rowValues, colValues) {
|
|
|
1023
1025
|
.map(([k, v]) => `${k}=${v}`)
|
|
1024
1026
|
.join("|");
|
|
1025
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Build a cell-specific SQL query by wrapping the base SQL with a WHERE clause
|
|
1030
|
+
* that narrows to the specific dimension values for this cell.
|
|
1031
|
+
*/
|
|
1032
|
+
function buildCellSQL(baseSQL, rowValues, colValues) {
|
|
1033
|
+
const conditions = [];
|
|
1034
|
+
const addCondition = (dim, val) => {
|
|
1035
|
+
if (typeof val === "number") {
|
|
1036
|
+
conditions.push(`${quoteIdentifier(dim)} = ${val}`);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
conditions.push(`${quoteIdentifier(dim)} = '${val.replace(/'/g, "''")}'`);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
for (const [dim, val] of rowValues) {
|
|
1043
|
+
addCondition(dim, val);
|
|
1044
|
+
}
|
|
1045
|
+
for (const [dim, val] of colValues) {
|
|
1046
|
+
addCondition(dim, val);
|
|
1047
|
+
}
|
|
1048
|
+
if (conditions.length === 0)
|
|
1049
|
+
return baseSQL;
|
|
1050
|
+
// Wrap: SELECT * FROM (baseSQL) AS _tpl WHERE conditions
|
|
1051
|
+
return `SELECT * FROM (\n${baseSQL}\n) AS _tpl_base\nWHERE ${conditions.join(" AND ")}`;
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Quote a SQL identifier with double quotes.
|
|
1055
|
+
*/
|
|
1056
|
+
function quoteIdentifier(name) {
|
|
1057
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
1058
|
+
}
|
|
1026
1059
|
/**
|
|
1027
1060
|
* Build a cell lookup function from query results.
|
|
1028
1061
|
*
|
|
@@ -1032,9 +1065,10 @@ function makeCellKey(rowValues, colValues) {
|
|
|
1032
1065
|
*
|
|
1033
1066
|
* @param invertedQueries Set of query IDs that have inverted axes (column dim is outer in Malloy)
|
|
1034
1067
|
* @param flatQueries Set of query IDs that use flat structure (all dims in single group_by)
|
|
1068
|
+
* @param sqlQueries Optional map of query ID → SQL string for attribution
|
|
1035
1069
|
*/
|
|
1036
|
-
function buildCellLookup(spec, plan, results, invertedQueries, flatQueries = new Set()) {
|
|
1037
|
-
// Build an index: cellKey → aggregateName → value
|
|
1070
|
+
function buildCellLookup(spec, plan, results, invertedQueries, flatQueries = new Set(), sqlQueries) {
|
|
1071
|
+
// Build an index: cellKey → aggregateName → CellEntry (value + queryId)
|
|
1038
1072
|
const cellIndex = new Map();
|
|
1039
1073
|
// Index all query results by plan query ID
|
|
1040
1074
|
// Since we now use a unified QueryPlan system, IDs should match directly
|
|
@@ -1078,12 +1112,24 @@ function buildCellLookup(spec, plan, results, invertedQueries, flatQueries = new
|
|
|
1078
1112
|
pathDescription: cellKey,
|
|
1079
1113
|
};
|
|
1080
1114
|
}
|
|
1081
|
-
const
|
|
1115
|
+
const entry = cellData.get(aggName);
|
|
1116
|
+
const value = entry?.value ?? null;
|
|
1117
|
+
// Build SQL attribution if available
|
|
1118
|
+
let sql;
|
|
1119
|
+
let cellSQL;
|
|
1120
|
+
if (sqlQueries && entry?.queryId) {
|
|
1121
|
+
sql = sqlQueries.get(entry.queryId);
|
|
1122
|
+
if (sql) {
|
|
1123
|
+
cellSQL = buildCellSQL(sql, rowValues, colValues);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1082
1126
|
return {
|
|
1083
1127
|
raw: value,
|
|
1084
1128
|
formatted: formatValue(value, agg),
|
|
1085
1129
|
aggregate: aggName,
|
|
1086
1130
|
pathDescription: cellKey,
|
|
1131
|
+
sql,
|
|
1132
|
+
cellSQL,
|
|
1087
1133
|
};
|
|
1088
1134
|
},
|
|
1089
1135
|
};
|
|
@@ -1129,7 +1175,7 @@ function indexQueryResults(data, query, cellIndex, aggregates, isInverted = fals
|
|
|
1129
1175
|
new Map(), // values for malloy nested (will be mapped to correct axis)
|
|
1130
1176
|
cellIndex, 0, isInverted, query.rowGroupings, // logical row groupings
|
|
1131
1177
|
query.colGroupings, // logical col groupings
|
|
1132
|
-
primarySuffix);
|
|
1178
|
+
primarySuffix, query.id);
|
|
1133
1179
|
// Handle merged queries with additional column variants
|
|
1134
1180
|
// Each variant has its own nests (e.g., by_gender vs by_sector_label)
|
|
1135
1181
|
// that need to be indexed separately
|
|
@@ -1147,7 +1193,7 @@ function indexQueryResults(data, query, cellIndex, aggregates, isInverted = fals
|
|
|
1147
1193
|
seenFirstDims.set(firstDim, count + 1);
|
|
1148
1194
|
nestNameSuffix = count > 0 ? `_${count}` : "";
|
|
1149
1195
|
}
|
|
1150
|
-
flattenAndIndex(data, malloyRowGroupings, variantColGroupings, aggregates, new Map(), new Map(), cellIndex, 0, isInverted, query.rowGroupings, variant.colGroupings, nestNameSuffix);
|
|
1196
|
+
flattenAndIndex(data, malloyRowGroupings, variantColGroupings, aggregates, new Map(), new Map(), cellIndex, 0, isInverted, query.rowGroupings, variant.colGroupings, nestNameSuffix, query.id);
|
|
1151
1197
|
}
|
|
1152
1198
|
}
|
|
1153
1199
|
}
|
|
@@ -1192,7 +1238,7 @@ function indexFlatQueryResults(data, query, cellIndex, aggregates) {
|
|
|
1192
1238
|
}
|
|
1193
1239
|
}
|
|
1194
1240
|
// Index aggregate values using the combined row/col values
|
|
1195
|
-
indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex);
|
|
1241
|
+
indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex, query.id);
|
|
1196
1242
|
}
|
|
1197
1243
|
}
|
|
1198
1244
|
/**
|
|
@@ -1205,7 +1251,7 @@ function indexFlatQueryResults(data, query, cellIndex, aggregates) {
|
|
|
1205
1251
|
* @param logicalColGroupings The actual col groupings for cell key building
|
|
1206
1252
|
* @param nestNameSuffix Suffix for nest names (e.g., '_1' for merged query variants)
|
|
1207
1253
|
*/
|
|
1208
|
-
function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggregates, baseOuterValues, baseNestedValues, cellIndex, outerDepth, isInverted = false, logicalRowGroupings, logicalColGroupings, nestNameSuffix = "") {
|
|
1254
|
+
function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggregates, baseOuterValues, baseNestedValues, cellIndex, outerDepth, isInverted = false, logicalRowGroupings, logicalColGroupings, nestNameSuffix = "", queryId) {
|
|
1209
1255
|
for (const row of data) {
|
|
1210
1256
|
// Build outer values - collect ALL outer dimension values from the current row
|
|
1211
1257
|
// This handles the case where multiple dimensions are in the same group_by
|
|
@@ -1234,7 +1280,7 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
1234
1280
|
const nestedKey = `by_${dim}${nestNameSuffix}`;
|
|
1235
1281
|
if (row[nestedKey] && Array.isArray(row[nestedKey])) {
|
|
1236
1282
|
// This dimension is nested - recurse
|
|
1237
|
-
flattenAndIndex(row[nestedKey], malloyOuterGroupings, malloyNestedGroupings, aggregates, currentOuterValues, baseNestedValues, cellIndex, i, isInverted, logicalRowGroupings, logicalColGroupings, nestNameSuffix);
|
|
1283
|
+
flattenAndIndex(row[nestedKey], malloyOuterGroupings, malloyNestedGroupings, aggregates, currentOuterValues, baseNestedValues, cellIndex, i, isInverted, logicalRowGroupings, logicalColGroupings, nestNameSuffix, queryId);
|
|
1238
1284
|
break; // Don't continue with this row, we recursed
|
|
1239
1285
|
}
|
|
1240
1286
|
}
|
|
@@ -1247,7 +1293,7 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
1247
1293
|
continue; // Skipped this row because we recursed into nested data
|
|
1248
1294
|
}
|
|
1249
1295
|
if (malloyNestedGroupings.length > 0) {
|
|
1250
|
-
indexColumnPivots(row, malloyNestedGroupings, aggregates, currentOuterValues, new Map(), cellIndex, 0, isInverted, nestNameSuffix);
|
|
1296
|
+
indexColumnPivots(row, malloyNestedGroupings, aggregates, currentOuterValues, new Map(), cellIndex, 0, isInverted, nestNameSuffix, queryId);
|
|
1251
1297
|
}
|
|
1252
1298
|
else {
|
|
1253
1299
|
// Direct aggregate values
|
|
@@ -1256,10 +1302,10 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
1256
1302
|
if (isInverted) {
|
|
1257
1303
|
indexAggregateValues(row, aggregates, baseNestedValues, // These are actually the row values (empty when no nesting)
|
|
1258
1304
|
currentOuterValues, // These are actually the col values
|
|
1259
|
-
cellIndex);
|
|
1305
|
+
cellIndex, queryId);
|
|
1260
1306
|
}
|
|
1261
1307
|
else {
|
|
1262
|
-
indexAggregateValues(row, aggregates, currentOuterValues, baseNestedValues, cellIndex);
|
|
1308
|
+
indexAggregateValues(row, aggregates, currentOuterValues, baseNestedValues, cellIndex, queryId);
|
|
1263
1309
|
}
|
|
1264
1310
|
}
|
|
1265
1311
|
}
|
|
@@ -1273,7 +1319,7 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
1273
1319
|
* @param nestNameSuffix Suffix for nest names (e.g., '_1' for merged query variants).
|
|
1274
1320
|
* Only applied to the outermost nest (nestedDepth === 0) to match Malloy query structure.
|
|
1275
1321
|
*/
|
|
1276
|
-
function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNestedValues, cellIndex, nestedDepth, isInverted = false, nestNameSuffix = "") {
|
|
1322
|
+
function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNestedValues, cellIndex, nestedDepth, isInverted = false, nestNameSuffix = "", queryId) {
|
|
1277
1323
|
const currentDim = nestedGroupings[nestedDepth];
|
|
1278
1324
|
const remainingDims = nestedGroupings.slice(nestedDepth);
|
|
1279
1325
|
// First try the single dimension key
|
|
@@ -1311,10 +1357,10 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
1311
1357
|
if (isInverted) {
|
|
1312
1358
|
indexAggregateValues(nestedRow, aggregates, currentNestedValues, // These are actually row values
|
|
1313
1359
|
outerValues, // These are actually col values
|
|
1314
|
-
cellIndex);
|
|
1360
|
+
cellIndex, queryId);
|
|
1315
1361
|
}
|
|
1316
1362
|
else {
|
|
1317
|
-
indexAggregateValues(nestedRow, aggregates, outerValues, currentNestedValues, cellIndex);
|
|
1363
|
+
indexAggregateValues(nestedRow, aggregates, outerValues, currentNestedValues, cellIndex, queryId);
|
|
1318
1364
|
}
|
|
1319
1365
|
}
|
|
1320
1366
|
return;
|
|
@@ -1340,7 +1386,7 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
1340
1386
|
}
|
|
1341
1387
|
if (nestedDepth + 1 < nestedGroupings.length) {
|
|
1342
1388
|
// More nesting
|
|
1343
|
-
indexColumnPivots(nestedRow, nestedGroupings, aggregates, outerValues, currentNestedValues, cellIndex, nestedDepth + 1, isInverted, nestNameSuffix);
|
|
1389
|
+
indexColumnPivots(nestedRow, nestedGroupings, aggregates, outerValues, currentNestedValues, cellIndex, nestedDepth + 1, isInverted, nestNameSuffix, queryId);
|
|
1344
1390
|
}
|
|
1345
1391
|
else {
|
|
1346
1392
|
// Leaf - index aggregate values
|
|
@@ -1349,10 +1395,10 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
1349
1395
|
if (isInverted) {
|
|
1350
1396
|
indexAggregateValues(nestedRow, aggregates, currentNestedValues, // These are actually row values
|
|
1351
1397
|
outerValues, // These are actually col values
|
|
1352
|
-
cellIndex);
|
|
1398
|
+
cellIndex, queryId);
|
|
1353
1399
|
}
|
|
1354
1400
|
else {
|
|
1355
|
-
indexAggregateValues(nestedRow, aggregates, outerValues, currentNestedValues, cellIndex);
|
|
1401
|
+
indexAggregateValues(nestedRow, aggregates, outerValues, currentNestedValues, cellIndex, queryId);
|
|
1356
1402
|
}
|
|
1357
1403
|
}
|
|
1358
1404
|
}
|
|
@@ -1360,7 +1406,7 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
1360
1406
|
/**
|
|
1361
1407
|
* Index aggregate values at a specific row/col value combination.
|
|
1362
1408
|
*/
|
|
1363
|
-
function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex) {
|
|
1409
|
+
function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex, queryId) {
|
|
1364
1410
|
const cellKey = makeCellKey(rowValues, colValues);
|
|
1365
1411
|
let cellData = cellIndex.get(cellKey);
|
|
1366
1412
|
if (!cellData) {
|
|
@@ -1370,7 +1416,10 @@ function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex)
|
|
|
1370
1416
|
for (const agg of aggregates) {
|
|
1371
1417
|
const value = row[agg.name];
|
|
1372
1418
|
if (value !== undefined) {
|
|
1373
|
-
cellData.set(agg.name,
|
|
1419
|
+
cellData.set(agg.name, {
|
|
1420
|
+
value: typeof value === "number" ? value : null,
|
|
1421
|
+
queryId,
|
|
1422
|
+
});
|
|
1374
1423
|
}
|
|
1375
1424
|
}
|
|
1376
1425
|
}
|
|
@@ -399,6 +399,22 @@ export interface CellValue {
|
|
|
399
399
|
readonly aggregate: string;
|
|
400
400
|
/** Path description for tooltips */
|
|
401
401
|
readonly pathDescription: string;
|
|
402
|
+
/**
|
|
403
|
+
* The SQL query that produced this cell's data.
|
|
404
|
+
* Only populated when `trackSQL: true` is set.
|
|
405
|
+
* This is the full SQL for the query — multiple cells may share the same SQL
|
|
406
|
+
* when they come from the same underlying query.
|
|
407
|
+
*/
|
|
408
|
+
readonly sql?: string;
|
|
409
|
+
/**
|
|
410
|
+
* A cell-specific SQL query narrowed with WHERE conditions for this cell's
|
|
411
|
+
* exact dimension values. This would return only the row(s) relevant to
|
|
412
|
+
* this specific cell if executed independently.
|
|
413
|
+
* Only populated when `trackSQL: true` is set.
|
|
414
|
+
* Note: For percentage/ACROSS aggregates, the narrowed SQL may not reproduce
|
|
415
|
+
* the same value because window functions depend on the full result set.
|
|
416
|
+
*/
|
|
417
|
+
readonly cellSQL?: string;
|
|
402
418
|
}
|
|
403
419
|
/**
|
|
404
420
|
* Walk the axis tree and call a visitor for each node.
|
package/dist/executor/index.d.ts
CHANGED
package/dist/executor/index.js
CHANGED
|
@@ -273,7 +273,13 @@ export async function executeMalloy(malloySource, options = {}) {
|
|
|
273
273
|
try {
|
|
274
274
|
// Parse and run the Malloy query
|
|
275
275
|
// Note: Malloy has a default rowLimit of 10 - we use a high limit to get all data
|
|
276
|
-
const
|
|
276
|
+
const materializer = runtime.loadQuery(malloySource);
|
|
277
|
+
// Capture SQL before running if requested
|
|
278
|
+
let sql;
|
|
279
|
+
if (options.captureSQL) {
|
|
280
|
+
sql = await materializer.getSQL();
|
|
281
|
+
}
|
|
282
|
+
const result = await materializer.run({ rowLimit: 100000 });
|
|
277
283
|
// Get the data
|
|
278
284
|
const data = result.data.toObject();
|
|
279
285
|
// Write to file if requested
|
|
@@ -281,6 +287,9 @@ export async function executeMalloy(malloySource, options = {}) {
|
|
|
281
287
|
fs.writeFileSync(options.outputPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
282
288
|
console.log(`Results written to: ${options.outputPath}`);
|
|
283
289
|
}
|
|
290
|
+
if (options.captureSQL) {
|
|
291
|
+
return { data: options.raw ? result : data, sql };
|
|
292
|
+
}
|
|
284
293
|
return options.raw ? result : data;
|
|
285
294
|
}
|
|
286
295
|
catch (error) {
|
package/dist/index.cjs
CHANGED
|
@@ -8429,11 +8429,13 @@ function normalizeQueryResults(results) {
|
|
|
8429
8429
|
function buildGridSpec(spec, plan, results, malloyQueriesOrOptions) {
|
|
8430
8430
|
let malloyQueries;
|
|
8431
8431
|
let orderingProvider;
|
|
8432
|
+
let sqlQueries;
|
|
8432
8433
|
if (Array.isArray(malloyQueriesOrOptions)) {
|
|
8433
8434
|
malloyQueries = malloyQueriesOrOptions;
|
|
8434
8435
|
} else if (malloyQueriesOrOptions) {
|
|
8435
8436
|
malloyQueries = malloyQueriesOrOptions.malloyQueries;
|
|
8436
8437
|
orderingProvider = malloyQueriesOrOptions.orderingProvider;
|
|
8438
|
+
sqlQueries = malloyQueriesOrOptions.sqlQueries;
|
|
8437
8439
|
}
|
|
8438
8440
|
currentOrderingProvider2 = orderingProvider;
|
|
8439
8441
|
results = normalizeQueryResults(results);
|
|
@@ -8489,7 +8491,8 @@ function buildGridSpec(spec, plan, results, malloyQueriesOrOptions) {
|
|
|
8489
8491
|
plan,
|
|
8490
8492
|
results,
|
|
8491
8493
|
invertedQueries,
|
|
8492
|
-
flatQueries
|
|
8494
|
+
flatQueries,
|
|
8495
|
+
sqlQueries
|
|
8493
8496
|
);
|
|
8494
8497
|
const hasRowTotal = axisHasTotal(spec.rowAxis);
|
|
8495
8498
|
const hasColTotal = axisHasTotal(spec.colAxis);
|
|
@@ -9081,7 +9084,31 @@ function makeCellKey(rowValues, colValues) {
|
|
|
9081
9084
|
];
|
|
9082
9085
|
return allEntries.sort((a, b) => a[0].localeCompare(b[0])).map(([k, v]) => `${k}=${v}`).join("|");
|
|
9083
9086
|
}
|
|
9084
|
-
function
|
|
9087
|
+
function buildCellSQL(baseSQL, rowValues, colValues) {
|
|
9088
|
+
const conditions = [];
|
|
9089
|
+
const addCondition = (dim, val) => {
|
|
9090
|
+
if (typeof val === "number") {
|
|
9091
|
+
conditions.push(`${quoteIdentifier(dim)} = ${val}`);
|
|
9092
|
+
} else {
|
|
9093
|
+
conditions.push(`${quoteIdentifier(dim)} = '${val.replace(/'/g, "''")}'`);
|
|
9094
|
+
}
|
|
9095
|
+
};
|
|
9096
|
+
for (const [dim, val] of rowValues) {
|
|
9097
|
+
addCondition(dim, val);
|
|
9098
|
+
}
|
|
9099
|
+
for (const [dim, val] of colValues) {
|
|
9100
|
+
addCondition(dim, val);
|
|
9101
|
+
}
|
|
9102
|
+
if (conditions.length === 0) return baseSQL;
|
|
9103
|
+
return `SELECT * FROM (
|
|
9104
|
+
${baseSQL}
|
|
9105
|
+
) AS _tpl_base
|
|
9106
|
+
WHERE ${conditions.join(" AND ")}`;
|
|
9107
|
+
}
|
|
9108
|
+
function quoteIdentifier(name) {
|
|
9109
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
9110
|
+
}
|
|
9111
|
+
function buildCellLookup(spec, plan, results, invertedQueries, flatQueries = /* @__PURE__ */ new Set(), sqlQueries) {
|
|
9085
9112
|
const cellIndex = /* @__PURE__ */ new Map();
|
|
9086
9113
|
const DEBUG = process.env.DEBUG_GRID === "true";
|
|
9087
9114
|
for (const query of plan.queries) {
|
|
@@ -9130,12 +9157,23 @@ function buildCellLookup(spec, plan, results, invertedQueries, flatQueries = /*
|
|
|
9130
9157
|
pathDescription: cellKey
|
|
9131
9158
|
};
|
|
9132
9159
|
}
|
|
9133
|
-
const
|
|
9160
|
+
const entry = cellData.get(aggName);
|
|
9161
|
+
const value = entry?.value ?? null;
|
|
9162
|
+
let sql;
|
|
9163
|
+
let cellSQL;
|
|
9164
|
+
if (sqlQueries && entry?.queryId) {
|
|
9165
|
+
sql = sqlQueries.get(entry.queryId);
|
|
9166
|
+
if (sql) {
|
|
9167
|
+
cellSQL = buildCellSQL(sql, rowValues, colValues);
|
|
9168
|
+
}
|
|
9169
|
+
}
|
|
9134
9170
|
return {
|
|
9135
9171
|
raw: value,
|
|
9136
9172
|
formatted: formatValue(value, agg),
|
|
9137
9173
|
aggregate: aggName,
|
|
9138
|
-
pathDescription: cellKey
|
|
9174
|
+
pathDescription: cellKey,
|
|
9175
|
+
sql,
|
|
9176
|
+
cellSQL
|
|
9139
9177
|
};
|
|
9140
9178
|
}
|
|
9141
9179
|
};
|
|
@@ -9171,7 +9209,8 @@ function indexQueryResults(data, query, cellIndex, aggregates, isInverted = fals
|
|
|
9171
9209
|
// logical row groupings
|
|
9172
9210
|
query.colGroupings,
|
|
9173
9211
|
// logical col groupings
|
|
9174
|
-
primarySuffix
|
|
9212
|
+
primarySuffix,
|
|
9213
|
+
query.id
|
|
9175
9214
|
);
|
|
9176
9215
|
if (query.additionalColVariants) {
|
|
9177
9216
|
for (const variant of query.additionalColVariants) {
|
|
@@ -9195,7 +9234,8 @@ function indexQueryResults(data, query, cellIndex, aggregates, isInverted = fals
|
|
|
9195
9234
|
isInverted,
|
|
9196
9235
|
query.rowGroupings,
|
|
9197
9236
|
variant.colGroupings,
|
|
9198
|
-
nestNameSuffix
|
|
9237
|
+
nestNameSuffix,
|
|
9238
|
+
query.id
|
|
9199
9239
|
);
|
|
9200
9240
|
}
|
|
9201
9241
|
}
|
|
@@ -9230,10 +9270,10 @@ function indexFlatQueryResults(data, query, cellIndex, aggregates) {
|
|
|
9230
9270
|
colValues.set(g.dimension, value);
|
|
9231
9271
|
}
|
|
9232
9272
|
}
|
|
9233
|
-
indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex);
|
|
9273
|
+
indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex, query.id);
|
|
9234
9274
|
}
|
|
9235
9275
|
}
|
|
9236
|
-
function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggregates, baseOuterValues, baseNestedValues, cellIndex, outerDepth, isInverted = false, logicalRowGroupings, logicalColGroupings, nestNameSuffix = "") {
|
|
9276
|
+
function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggregates, baseOuterValues, baseNestedValues, cellIndex, outerDepth, isInverted = false, logicalRowGroupings, logicalColGroupings, nestNameSuffix = "", queryId) {
|
|
9237
9277
|
for (const row of data) {
|
|
9238
9278
|
const currentOuterValues = new Map(baseOuterValues);
|
|
9239
9279
|
for (let i = outerDepth; i < malloyOuterGroupings.length; i++) {
|
|
@@ -9264,7 +9304,8 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
9264
9304
|
isInverted,
|
|
9265
9305
|
logicalRowGroupings,
|
|
9266
9306
|
logicalColGroupings,
|
|
9267
|
-
nestNameSuffix
|
|
9307
|
+
nestNameSuffix,
|
|
9308
|
+
queryId
|
|
9268
9309
|
);
|
|
9269
9310
|
break;
|
|
9270
9311
|
}
|
|
@@ -9286,7 +9327,8 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
9286
9327
|
cellIndex,
|
|
9287
9328
|
0,
|
|
9288
9329
|
isInverted,
|
|
9289
|
-
nestNameSuffix
|
|
9330
|
+
nestNameSuffix,
|
|
9331
|
+
queryId
|
|
9290
9332
|
);
|
|
9291
9333
|
} else {
|
|
9292
9334
|
if (isInverted) {
|
|
@@ -9297,7 +9339,8 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
9297
9339
|
// These are actually the row values (empty when no nesting)
|
|
9298
9340
|
currentOuterValues,
|
|
9299
9341
|
// These are actually the col values
|
|
9300
|
-
cellIndex
|
|
9342
|
+
cellIndex,
|
|
9343
|
+
queryId
|
|
9301
9344
|
);
|
|
9302
9345
|
} else {
|
|
9303
9346
|
indexAggregateValues(
|
|
@@ -9305,13 +9348,14 @@ function flattenAndIndex(data, malloyOuterGroupings, malloyNestedGroupings, aggr
|
|
|
9305
9348
|
aggregates,
|
|
9306
9349
|
currentOuterValues,
|
|
9307
9350
|
baseNestedValues,
|
|
9308
|
-
cellIndex
|
|
9351
|
+
cellIndex,
|
|
9352
|
+
queryId
|
|
9309
9353
|
);
|
|
9310
9354
|
}
|
|
9311
9355
|
}
|
|
9312
9356
|
}
|
|
9313
9357
|
}
|
|
9314
|
-
function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNestedValues, cellIndex, nestedDepth, isInverted = false, nestNameSuffix = "") {
|
|
9358
|
+
function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNestedValues, cellIndex, nestedDepth, isInverted = false, nestNameSuffix = "", queryId) {
|
|
9315
9359
|
const currentDim = nestedGroupings[nestedDepth];
|
|
9316
9360
|
const remainingDims = nestedGroupings.slice(nestedDepth);
|
|
9317
9361
|
let nestedKey = nestedDepth === 0 ? `by_${currentDim.dimension}${nestNameSuffix}` : `by_${currentDim.dimension}`;
|
|
@@ -9343,7 +9387,8 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
9343
9387
|
// These are actually row values
|
|
9344
9388
|
outerValues,
|
|
9345
9389
|
// These are actually col values
|
|
9346
|
-
cellIndex
|
|
9390
|
+
cellIndex,
|
|
9391
|
+
queryId
|
|
9347
9392
|
);
|
|
9348
9393
|
} else {
|
|
9349
9394
|
indexAggregateValues(
|
|
@@ -9351,7 +9396,8 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
9351
9396
|
aggregates,
|
|
9352
9397
|
outerValues,
|
|
9353
9398
|
currentNestedValues,
|
|
9354
|
-
cellIndex
|
|
9399
|
+
cellIndex,
|
|
9400
|
+
queryId
|
|
9355
9401
|
);
|
|
9356
9402
|
}
|
|
9357
9403
|
}
|
|
@@ -9382,7 +9428,8 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
9382
9428
|
cellIndex,
|
|
9383
9429
|
nestedDepth + 1,
|
|
9384
9430
|
isInverted,
|
|
9385
|
-
nestNameSuffix
|
|
9431
|
+
nestNameSuffix,
|
|
9432
|
+
queryId
|
|
9386
9433
|
);
|
|
9387
9434
|
} else {
|
|
9388
9435
|
if (isInverted) {
|
|
@@ -9393,7 +9440,8 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
9393
9440
|
// These are actually row values
|
|
9394
9441
|
outerValues,
|
|
9395
9442
|
// These are actually col values
|
|
9396
|
-
cellIndex
|
|
9443
|
+
cellIndex,
|
|
9444
|
+
queryId
|
|
9397
9445
|
);
|
|
9398
9446
|
} else {
|
|
9399
9447
|
indexAggregateValues(
|
|
@@ -9401,13 +9449,14 @@ function indexColumnPivots(row, nestedGroupings, aggregates, outerValues, baseNe
|
|
|
9401
9449
|
aggregates,
|
|
9402
9450
|
outerValues,
|
|
9403
9451
|
currentNestedValues,
|
|
9404
|
-
cellIndex
|
|
9452
|
+
cellIndex,
|
|
9453
|
+
queryId
|
|
9405
9454
|
);
|
|
9406
9455
|
}
|
|
9407
9456
|
}
|
|
9408
9457
|
}
|
|
9409
9458
|
}
|
|
9410
|
-
function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex) {
|
|
9459
|
+
function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex, queryId) {
|
|
9411
9460
|
const cellKey = makeCellKey(rowValues, colValues);
|
|
9412
9461
|
let cellData = cellIndex.get(cellKey);
|
|
9413
9462
|
if (!cellData) {
|
|
@@ -9417,7 +9466,10 @@ function indexAggregateValues(row, aggregates, rowValues, colValues, cellIndex)
|
|
|
9417
9466
|
for (const agg of aggregates) {
|
|
9418
9467
|
const value = row[agg.name];
|
|
9419
9468
|
if (value !== void 0) {
|
|
9420
|
-
cellData.set(agg.name,
|
|
9469
|
+
cellData.set(agg.name, {
|
|
9470
|
+
value: typeof value === "number" ? value : null,
|
|
9471
|
+
queryId
|
|
9472
|
+
});
|
|
9421
9473
|
}
|
|
9422
9474
|
}
|
|
9423
9475
|
}
|
|
@@ -10582,7 +10634,9 @@ function renderDataCell(grid, rowValues, colValues, rowLeaf, colLeaf, rowRoots,
|
|
|
10582
10634
|
const humanPath = buildHumanPath(rowValues, colValues, aggregateName, grid.aggregates);
|
|
10583
10635
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : "";
|
|
10584
10636
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : "";
|
|
10585
|
-
|
|
10637
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : "";
|
|
10638
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : "";
|
|
10639
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
10586
10640
|
}
|
|
10587
10641
|
function renderDataCells(grid, rowValues, rowLeaf, lines) {
|
|
10588
10642
|
const colValues = /* @__PURE__ */ new Map();
|
|
@@ -10594,14 +10648,18 @@ function renderDataCells(grid, rowValues, rowLeaf, lines) {
|
|
|
10594
10648
|
const humanPath = buildHumanPath(rowValues, colValues, rowAgg, grid.aggregates);
|
|
10595
10649
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : "";
|
|
10596
10650
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : "";
|
|
10597
|
-
|
|
10651
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : "";
|
|
10652
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : "";
|
|
10653
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
10598
10654
|
} else {
|
|
10599
10655
|
for (const agg of grid.aggregates) {
|
|
10600
10656
|
const cell = grid.getCell(rowValues, colValues, agg.name);
|
|
10601
10657
|
const humanPath = buildHumanPath(rowValues, colValues, agg.name, grid.aggregates);
|
|
10602
10658
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : "";
|
|
10603
10659
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : "";
|
|
10604
|
-
|
|
10660
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : "";
|
|
10661
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : "";
|
|
10662
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
10605
10663
|
}
|
|
10606
10664
|
}
|
|
10607
10665
|
}
|
|
@@ -10769,7 +10827,12 @@ async function executeMalloy(malloySource, options = {}) {
|
|
|
10769
10827
|
connection: conn
|
|
10770
10828
|
});
|
|
10771
10829
|
try {
|
|
10772
|
-
const
|
|
10830
|
+
const materializer = runtime.loadQuery(malloySource);
|
|
10831
|
+
let sql;
|
|
10832
|
+
if (options.captureSQL) {
|
|
10833
|
+
sql = await materializer.getSQL();
|
|
10834
|
+
}
|
|
10835
|
+
const result = await materializer.run({ rowLimit: 1e5 });
|
|
10773
10836
|
const data = result.data.toObject();
|
|
10774
10837
|
if (options.outputPath) {
|
|
10775
10838
|
fs.writeFileSync(
|
|
@@ -10779,6 +10842,9 @@ async function executeMalloy(malloySource, options = {}) {
|
|
|
10779
10842
|
);
|
|
10780
10843
|
console.log(`Results written to: ${options.outputPath}`);
|
|
10781
10844
|
}
|
|
10845
|
+
if (options.captureSQL) {
|
|
10846
|
+
return { data: options.raw ? result : data, sql };
|
|
10847
|
+
}
|
|
10782
10848
|
return options.raw ? result : data;
|
|
10783
10849
|
} catch (error) {
|
|
10784
10850
|
console.error("Malloy execution error:", error);
|
|
@@ -10827,18 +10893,28 @@ var TPL = class {
|
|
|
10827
10893
|
orderingProvider: options.orderingProvider
|
|
10828
10894
|
});
|
|
10829
10895
|
const rawResults = /* @__PURE__ */ new Map();
|
|
10896
|
+
const sqlQueries = this.options.trackSQL ? /* @__PURE__ */ new Map() : void 0;
|
|
10830
10897
|
for (const queryInfo of queries) {
|
|
10831
10898
|
const fullMalloy = `${effectiveModel}
|
|
10832
10899
|
${queryInfo.malloy}`;
|
|
10833
|
-
|
|
10834
|
-
|
|
10900
|
+
if (this.options.trackSQL) {
|
|
10901
|
+
const result = await executeMalloy(fullMalloy, { captureSQL: true });
|
|
10902
|
+
rawResults.set(queryInfo.id, result.data);
|
|
10903
|
+
if (result.sql) {
|
|
10904
|
+
sqlQueries.set(queryInfo.id, result.sql);
|
|
10905
|
+
}
|
|
10906
|
+
} else {
|
|
10907
|
+
const data = await executeMalloy(fullMalloy);
|
|
10908
|
+
rawResults.set(queryInfo.id, data);
|
|
10909
|
+
}
|
|
10835
10910
|
}
|
|
10836
10911
|
const grid = buildGridSpec(spec, plan, rawResults, {
|
|
10837
10912
|
malloyQueries: queries,
|
|
10838
|
-
orderingProvider: options.orderingProvider
|
|
10913
|
+
orderingProvider: options.orderingProvider,
|
|
10914
|
+
sqlQueries
|
|
10839
10915
|
});
|
|
10840
10916
|
const html = renderGridToHTML(grid);
|
|
10841
|
-
return { html, grid, malloy, rawResults };
|
|
10917
|
+
return { html, grid, malloy, rawResults, sqlQueries };
|
|
10842
10918
|
}
|
|
10843
10919
|
};
|
|
10844
10920
|
function createTPL(options = {}) {
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface TPLOptions {
|
|
|
53
53
|
sourceName?: string;
|
|
54
54
|
/** @deprecated Use `sourceName` instead */
|
|
55
55
|
source?: string;
|
|
56
|
+
/**
|
|
57
|
+
* When true, captures the raw SQL generated by Malloy for each query
|
|
58
|
+
* and makes it available on each cell via `CellValue.sql` and `CellValue.cellSQL`.
|
|
59
|
+
* Useful for audit environments where consumers need to verify how each
|
|
60
|
+
* number was produced.
|
|
61
|
+
*/
|
|
62
|
+
trackSQL?: boolean;
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
65
|
* Result of compiling TPL to Malloy
|
|
@@ -115,6 +122,13 @@ export interface ExecuteResult {
|
|
|
115
122
|
malloy: string;
|
|
116
123
|
/** raw query results by query ID */
|
|
117
124
|
rawResults: Map<string, any[]>;
|
|
125
|
+
/**
|
|
126
|
+
* Raw SQL queries by query ID.
|
|
127
|
+
* Only populated when `trackSQL: true` is set.
|
|
128
|
+
* Each entry maps a query ID (e.g., 'q0') to the SQL string
|
|
129
|
+
* that was sent to the database.
|
|
130
|
+
*/
|
|
131
|
+
sqlQueries?: Map<string, string>;
|
|
118
132
|
}
|
|
119
133
|
/**
|
|
120
134
|
* High-level TPL API for parsing, compiling, executing, and rendering.
|
package/dist/index.js
CHANGED
|
@@ -86,18 +86,29 @@ export class TPL {
|
|
|
86
86
|
});
|
|
87
87
|
// execute all queries
|
|
88
88
|
const rawResults = new Map();
|
|
89
|
+
const sqlQueries = this.options.trackSQL ? new Map() : undefined;
|
|
89
90
|
for (const queryInfo of queries) {
|
|
90
91
|
const fullMalloy = `${effectiveModel}\n${queryInfo.malloy}`;
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
if (this.options.trackSQL) {
|
|
93
|
+
const result = await executeMalloy(fullMalloy, { captureSQL: true });
|
|
94
|
+
rawResults.set(queryInfo.id, result.data);
|
|
95
|
+
if (result.sql) {
|
|
96
|
+
sqlQueries.set(queryInfo.id, result.sql);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const data = await executeMalloy(fullMalloy);
|
|
101
|
+
rawResults.set(queryInfo.id, data);
|
|
102
|
+
}
|
|
93
103
|
}
|
|
94
104
|
// build grid and render
|
|
95
105
|
const grid = buildGridSpec(spec, plan, rawResults, {
|
|
96
106
|
malloyQueries: queries,
|
|
97
107
|
orderingProvider: options.orderingProvider,
|
|
108
|
+
sqlQueries,
|
|
98
109
|
});
|
|
99
110
|
const html = renderGridToHTML(grid);
|
|
100
|
-
return { html, grid, malloy, rawResults };
|
|
111
|
+
return { html, grid, malloy, rawResults, sqlQueries };
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
/**
|
|
@@ -569,7 +569,9 @@ function renderDataCell(grid, rowValues, colValues, rowLeaf, colLeaf, rowRoots,
|
|
|
569
569
|
const humanPath = buildHumanPath(rowValues, colValues, aggregateName, grid.aggregates);
|
|
570
570
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : '';
|
|
571
571
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : '';
|
|
572
|
-
|
|
572
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : '';
|
|
573
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : '';
|
|
574
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
573
575
|
}
|
|
574
576
|
/**
|
|
575
577
|
* Render data cells when there are no column pivots.
|
|
@@ -587,7 +589,9 @@ function renderDataCells(grid, rowValues, rowLeaf, lines) {
|
|
|
587
589
|
const humanPath = buildHumanPath(rowValues, colValues, rowAgg, grid.aggregates);
|
|
588
590
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : '';
|
|
589
591
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : '';
|
|
590
|
-
|
|
592
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : '';
|
|
593
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : '';
|
|
594
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
591
595
|
}
|
|
592
596
|
else {
|
|
593
597
|
// Multiple aggregate cells
|
|
@@ -596,7 +600,9 @@ function renderDataCells(grid, rowValues, rowLeaf, lines) {
|
|
|
596
600
|
const humanPath = buildHumanPath(rowValues, colValues, agg.name, grid.aggregates);
|
|
597
601
|
const titleAttr = humanPath ? ` title="${escapeHTML(humanPath)}"` : '';
|
|
598
602
|
const dataAttr = cell.pathDescription ? ` data-cell="${escapeHTML(cell.pathDescription)}"` : '';
|
|
599
|
-
|
|
603
|
+
const sqlAttr = cell.sql ? ` data-sql="${escapeHTML(cell.sql)}"` : '';
|
|
604
|
+
const cellSQLAttr = cell.cellSQL ? ` data-cell-sql="${escapeHTML(cell.cellSQL)}"` : '';
|
|
605
|
+
lines.push(`<td${classAttr}${titleAttr}${dataAttr}${sqlAttr}${cellSQLAttr}>${escapeHTML(cell.formatted)}</td>`);
|
|
600
606
|
}
|
|
601
607
|
}
|
|
602
608
|
}
|