tplm-lang 0.1.0

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.
Files changed (36) hide show
  1. package/README.md +357 -0
  2. package/dist/compiler/grid-spec-builder.d.ts +30 -0
  3. package/dist/compiler/grid-spec-builder.js +1836 -0
  4. package/dist/compiler/index.d.ts +11 -0
  5. package/dist/compiler/index.js +13 -0
  6. package/dist/compiler/malloy-generator.d.ts +36 -0
  7. package/dist/compiler/malloy-generator.js +141 -0
  8. package/dist/compiler/multi-query-utils.d.ts +42 -0
  9. package/dist/compiler/multi-query-utils.js +185 -0
  10. package/dist/compiler/query-plan-generator.d.ts +77 -0
  11. package/dist/compiler/query-plan-generator.js +1456 -0
  12. package/dist/compiler/table-spec-builder.d.ts +11 -0
  13. package/dist/compiler/table-spec-builder.js +588 -0
  14. package/dist/compiler/table-spec.d.ts +434 -0
  15. package/dist/compiler/table-spec.js +274 -0
  16. package/dist/executor/index.d.ts +71 -0
  17. package/dist/executor/index.js +232 -0
  18. package/dist/index.d.ts +214 -0
  19. package/dist/index.js +220 -0
  20. package/dist/parser/ast.d.ts +253 -0
  21. package/dist/parser/ast.js +164 -0
  22. package/dist/parser/chevrotain-parser.d.ts +118 -0
  23. package/dist/parser/chevrotain-parser.js +1266 -0
  24. package/dist/parser/index.d.ts +30 -0
  25. package/dist/parser/index.js +36 -0
  26. package/dist/parser/parser.d.ts +4 -0
  27. package/dist/parser/parser.js +4354 -0
  28. package/dist/parser/prettifier.d.ts +14 -0
  29. package/dist/parser/prettifier.js +380 -0
  30. package/dist/renderer/grid-renderer.d.ts +19 -0
  31. package/dist/renderer/grid-renderer.js +541 -0
  32. package/dist/renderer/index.d.ts +4 -0
  33. package/dist/renderer/index.js +4 -0
  34. package/package.json +67 -0
  35. package/packages/parser/tpl.pegjs +568 -0
  36. package/packages/renderer/tpl-table.css +182 -0
package/README.md ADDED
@@ -0,0 +1,357 @@
1
+ # TPLm
2
+
3
+ **One Language for Complex Tables** - Define table layout and data requirements in a single declaration.
4
+
5
+ TPL is a unified language that describes **both** the presentation structure (rows, columns, nesting, totals) **and** the data requirements (dimensions, aggregations, filters). A single TPL table statement compiles to malloy queries, fetches data, and renders the result with correctly nested table structures, even for very complex or deeply nested constructions.
6
+
7
+ ```sql
8
+ TABLE
9
+ ROWS (occupation DESC@income.sum) * income.mean:currency
10
+ COLS (education 'education level' * gender) | ALL
11
+ ;
12
+ ```
13
+
14
+ ![TPLm table example](preview.png)
15
+
16
+ ## Background
17
+
18
+ The original TPL was a language developed by the U.S. Bureau of Labor Statistics in the early 1970s for producing complex statistical tables from survey data on IBM mainframes. It was freely shared with other federal agencies and research institutions, with significant adoption. The language later influenced two commercial products: SAS's **PROC TABULATE** (1982), which adopted TPL's syntax and concepts, and **TPL Tables** by QQQ Software (1987).
19
+
20
+ **TPLm** is intended as an opinionated, lean reimplementation, with an adjusted syntax that compiles to [Malloy](https://www.malloydata.dev/) for efficient querying against DuckDB or BigQuery, and renders to well-structured HTML tables.
21
+
22
+ ## Documentation
23
+
24
+ **[Visit the full documentation site](https://jasonphillips.github.io/tplm/)** with:
25
+
26
+ - **Interactive Playground** - Try TPLm in your browser with live execution
27
+ - **Complete Syntax Reference** - Full language documentation
28
+ - **Categorized Examples** - Learn by example with editable demos
29
+ - **WASM-Powered** - Runs DuckDB entirely in your browser
30
+
31
+ **[Try the standalone playground](https://jasonphillips.github.io/tplm/playground/)** - Full-featured editor with dataset selector and multiple tabs.
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install tplm-lang
39
+ ```
40
+
41
+ ## Quick Start - Query Your Data Immediately
42
+
43
+ For basic use, no Malloy knowledge is required; just use your local data and start querying.
44
+
45
+ ```typescript
46
+ import { fromCSV } from "tplm-lang";
47
+
48
+ // Point to your CSV and query immediately
49
+ const tpl = fromCSV("data/sales.csv");
50
+
51
+ const { html } = await tpl.query(
52
+ "TABLE ROWS region[-10@revenue.sum] * revenue.sum COLS quarter | ALL;"
53
+ );
54
+
55
+ console.log(html);
56
+ ```
57
+
58
+ ### Other Data Sources
59
+
60
+ ```typescript
61
+ // Parquet files
62
+ import { fromDuckDBTable } from "tplm-lang";
63
+ const tpl = fromDuckDBTable("data/sales.parquet");
64
+
65
+ // BigQuery tables
66
+ import { fromBigQueryTable } from "tplm-lang";
67
+ const tpl = fromBigQueryTable({
68
+ table: "project.dataset.sales",
69
+ credentialsPath: "./credentials.json",
70
+ });
71
+
72
+ // Then query the same way
73
+ const { html } = await tpl.query("TABLE ROWS region * revenue.sum;");
74
+ ```
75
+
76
+ ### Adding Computed Dimensions
77
+
78
+ If you need to transform raw values (like mapping codes to labels):
79
+
80
+ ```typescript
81
+ const tpl = fromCSV("employees.csv").extend(`
82
+ dimension:
83
+ department is
84
+ pick 'Engineering' when dept_code = 1
85
+ pick 'Sales' when dept_code = 2
86
+ else 'Other'
87
+ `);
88
+
89
+ const { html } = await tpl.query("TABLE ROWS department * salary.sum;");
90
+ ```
91
+
92
+ ## Advanced Usage with Malloy Models
93
+
94
+ For complex scenarios with joins, computed measures, or multiple sources:
95
+
96
+ ```typescript
97
+ import { createTPL } from "tplm-lang";
98
+
99
+ const MODEL = `
100
+ source: sales is duckdb.table('sales.csv') extend {
101
+ dimension:
102
+ region is pick 'North' when region_code = 1 else 'South'
103
+ }
104
+ `;
105
+
106
+ const tpl = createTPL({ maxLimit: 100 });
107
+
108
+ const { html } = await tpl.execute(
109
+ "TABLE ROWS region * revenue.sum COLS quarter | ALL;",
110
+ { model: MODEL, sourceName: "sales" }
111
+ );
112
+ ```
113
+
114
+ ## Understanding the Model vs TPL
115
+
116
+ **What belongs in your Malloy model:**
117
+
118
+ - Computed dimensions (mapping codes to labels)
119
+ - Joins between tables
120
+ - Complex calculated measures TPL can't express
121
+
122
+ **What TPL computes at query time:**
123
+
124
+ - Simple aggregations: `revenue.sum`, `revenue.mean`, `n` (count)
125
+ - Percentages: `(revenue.sum ACROSS COLS)`
126
+ - You don't need to pre-define `total_revenue is revenue.sum()`
127
+
128
+ ## TPL Syntax
129
+
130
+ TPL statements describe table structure declaratively:
131
+
132
+ ```sql
133
+ TABLE [FROM source] [WHERE condition] ROWS <row-axis> [COLS <column-axis>];
134
+ ```
135
+
136
+ ### Dimensions and Measures
137
+
138
+ ```sql
139
+ -- simple dimensions
140
+ ROWS region * product
141
+
142
+ -- with limits (top N by value)
143
+ ROWS region[-10@revenue.sum] * product[-5]
144
+
145
+ -- with aggregates (computed at query time)
146
+ ROWS region * product * revenue.sum
147
+
148
+ -- multiple aggregates
149
+ ROWS region * revenue.(sum | mean | max)
150
+
151
+ -- count of records
152
+ ROWS region * n
153
+ ```
154
+
155
+ ### Crossing and Concatenation
156
+
157
+ ```sql
158
+ -- crossing (*) creates hierarchy
159
+ ROWS region * product * revenue.sum
160
+
161
+ -- concatenation (|) creates sections
162
+ ROWS region | product
163
+
164
+ -- totals with ALL
165
+ ROWS (region | ALL) * revenue.sum COLS quarter | ALL "Total"
166
+ ```
167
+
168
+ ### Formatting
169
+
170
+ ```sql
171
+ -- built-in formats
172
+ revenue.sum:currency -- $1,234.56
173
+ rate.mean:percent -- 45.6%
174
+ n:integer -- 1,235
175
+
176
+ -- custom formats
177
+ revenue:'$ #.2 M' -- $ 1.23 M
178
+ ```
179
+
180
+ ### Percentages (ACROSS)
181
+
182
+ ```sql
183
+ -- cell percentage of grand total
184
+ ROWS occupation * (n ACROSS) COLS education
185
+
186
+ -- row percentages (each row sums to 100%)
187
+ ROWS occupation * (revenue.sum ACROSS COLS) COLS education
188
+
189
+ -- column percentages (each column sums to 100%)
190
+ ROWS occupation * (revenue.sum ACROSS ROWS) COLS education
191
+
192
+ -- combine percentage with regular measure
193
+ COLS education * (revenue.sum ACROSS COLS | revenue.mean)
194
+ ```
195
+
196
+ ## API
197
+
198
+ ### Easy Connectors (Recommended)
199
+
200
+ ```typescript
201
+ import { fromCSV, fromDuckDBTable, fromBigQueryTable } from "tplm-lang";
202
+
203
+ // CSV or Parquet files
204
+ const tpl = fromCSV("data/sales.csv");
205
+ const tpl = fromDuckDBTable("data/sales.parquet");
206
+
207
+ // BigQuery
208
+ const tpl = fromBigQueryTable({
209
+ table: "project.dataset.sales",
210
+ credentialsPath: "./credentials.json",
211
+ });
212
+
213
+ // Query and render
214
+ const { html } = await tpl.query("TABLE ROWS region * revenue.sum;");
215
+
216
+ // Add computed dimensions
217
+ const tplWithDims = tpl.extend(`
218
+ dimension: region is pick 'North' when region_code = 1 else 'South'
219
+ `);
220
+ ```
221
+
222
+ ### Full API (with Malloy Models)
223
+
224
+ ```typescript
225
+ import { TPL, createTPL, createBigQueryTPL } from "tplm-lang";
226
+
227
+ // DuckDB (default)
228
+ const tpl = createTPL({ maxLimit: 100 });
229
+
230
+ // BigQuery
231
+ const tpl = createBigQueryTPL({
232
+ maxLimit: 100,
233
+ credentialsPath: "./credentials.json",
234
+ });
235
+
236
+ // parse only
237
+ const ast = tpl.parse("TABLE ROWS region * revenue.sum;");
238
+
239
+ // compile only (get malloy output)
240
+ const { malloy, queries, plan, spec } = tpl.compile(
241
+ "TABLE ROWS region[-10] * revenue.sum COLS quarter;"
242
+ );
243
+
244
+ // full pipeline: parse → compile → execute → render
245
+ const { html, grid, malloy, rawResults } = await tpl.execute(tplSource, {
246
+ model: MODEL, // Malloy model (source definitions, computed dimensions)
247
+ sourceName: "sales", // Which source to query from the model
248
+ });
249
+ ```
250
+
251
+ ### Low-Level API
252
+
253
+ ```typescript
254
+ import {
255
+ parse,
256
+ formatTPL,
257
+ buildTableSpec,
258
+ generateQueryPlan,
259
+ generateMalloyQueries,
260
+ buildGridSpec,
261
+ renderGridToHTML,
262
+ executeMalloy,
263
+ } from "tplm-lang";
264
+
265
+ // parse TPL to AST
266
+ const ast = parse(tplSource);
267
+
268
+ // format AST back to pretty TPL
269
+ const formatted = formatTPL(ast);
270
+
271
+ // step by step compilation
272
+ const spec = buildTableSpec(ast);
273
+ const plan = generateQueryPlan(spec);
274
+ const queries = generateMalloyQueries(plan, "source_name");
275
+ // execute queries...
276
+ const grid = buildGridSpec(spec, plan, results, queries);
277
+ const html = renderGridToHTML(grid);
278
+ ```
279
+
280
+ ## Options
281
+
282
+ ### maxLimit
283
+
284
+ Caps all grouping dimensions to prevent runaway queries:
285
+
286
+ ```typescript
287
+ const tpl = createTPL({ maxLimit: 100 });
288
+
289
+ // this query:
290
+ // TABLE ROWS state[-500] * revenue.sum
291
+ // is capped to:
292
+ // TABLE ROWS state[-100] * revenue.sum
293
+ ```
294
+
295
+ ### sourceName
296
+
297
+ Default Malloy source name when the statement doesn't include FROM clause:
298
+
299
+ ```typescript
300
+ const tpl = createTPL({ sourceName: "my_table" });
301
+ ```
302
+
303
+ ## Formatting
304
+
305
+ TPLm includes a built-in prettifier that formats TPL code with consistent style:
306
+
307
+ ```typescript
308
+ import { parse, formatTPL } from "tplm-lang";
309
+
310
+ const messy = "TABLE ROWS (region|ALL)*gender*revenue.sum COLS quarter|ALL;";
311
+ const ast = parse(messy);
312
+ const pretty = formatTPL(ast);
313
+
314
+ console.log(pretty);
315
+ // TABLE
316
+ // ROWS (region | ALL) * gender * revenue.sum
317
+ // COLS quarter | ALL
318
+ // ;
319
+ ```
320
+
321
+ **Formatting rules:**
322
+
323
+ - Very short statements (< 60 chars) stay on one line
324
+ - Clauses (ROWS, COLS, WHERE, FROM) on separate lines
325
+ - Top-level alternatives (`|` operators) break to new lines
326
+ - Crossing (`*` operators) stay compact
327
+ - Consistent spacing and indentation
328
+
329
+ ## Output
330
+
331
+ The renderer produces HTML tables with:
332
+
333
+ - Multi-level row dimensions with `rowspan`
334
+ - Column pivots with `colspan` for multiple aggregates
335
+ - Row and column totals
336
+ - Number formatting
337
+ - CSS classes for styling (`tpl-table`, `tpl-cell`, `tpl-total-cell`, `tpl-corner`, etc.)
338
+
339
+ ## Development
340
+
341
+ ```bash
342
+ # install dependencies
343
+ npm install
344
+
345
+ # build (parser + typescript)
346
+ npm run build
347
+
348
+ # run tests
349
+ npm run test:run
350
+
351
+ # interactive playground
352
+ npm run playground
353
+ ```
354
+
355
+ ## License
356
+
357
+ MIT
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Grid Spec Builder
3
+ *
4
+ * Transforms TableSpec + query results into a GridSpec for rendering.
5
+ *
6
+ * Key responsibilities:
7
+ * 1. Build row header hierarchy from axis tree + actual dimension values
8
+ * 2. Build column header hierarchy from axis tree + actual dimension values
9
+ * 3. Create cell lookup function mapping (rowPath, colPath) → cell value
10
+ * 4. Handle totals, nested data, and cross-dimensional aggregation (ACROSS)
11
+ */
12
+ import { TableSpec, QueryPlan, GridSpec } from './table-spec.js';
13
+ import { MalloyQuerySpec } from './query-plan-generator.js';
14
+ /**
15
+ * Query results indexed by query ID.
16
+ */
17
+ export type QueryResults = Map<string, any[]>;
18
+ /**
19
+ * Build a GridSpec from a TableSpec and query results.
20
+ *
21
+ * @param spec The table specification
22
+ * @param plan The query plan
23
+ * @param results Query results indexed by query ID
24
+ * @param malloyQueries Optional Malloy query specs (for axis inversion info)
25
+ */
26
+ export declare function buildGridSpec(spec: TableSpec, plan: QueryPlan, results: QueryResults, malloyQueries?: MalloyQuerySpec[]): GridSpec;
27
+ /**
28
+ * Print a GridSpec for debugging.
29
+ */
30
+ export declare function printGridSpec(grid: GridSpec): string;