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.
- package/README.md +357 -0
- package/dist/compiler/grid-spec-builder.d.ts +30 -0
- package/dist/compiler/grid-spec-builder.js +1836 -0
- package/dist/compiler/index.d.ts +11 -0
- package/dist/compiler/index.js +13 -0
- package/dist/compiler/malloy-generator.d.ts +36 -0
- package/dist/compiler/malloy-generator.js +141 -0
- package/dist/compiler/multi-query-utils.d.ts +42 -0
- package/dist/compiler/multi-query-utils.js +185 -0
- package/dist/compiler/query-plan-generator.d.ts +77 -0
- package/dist/compiler/query-plan-generator.js +1456 -0
- package/dist/compiler/table-spec-builder.d.ts +11 -0
- package/dist/compiler/table-spec-builder.js +588 -0
- package/dist/compiler/table-spec.d.ts +434 -0
- package/dist/compiler/table-spec.js +274 -0
- package/dist/executor/index.d.ts +71 -0
- package/dist/executor/index.js +232 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +220 -0
- package/dist/parser/ast.d.ts +253 -0
- package/dist/parser/ast.js +164 -0
- package/dist/parser/chevrotain-parser.d.ts +118 -0
- package/dist/parser/chevrotain-parser.js +1266 -0
- package/dist/parser/index.d.ts +30 -0
- package/dist/parser/index.js +36 -0
- package/dist/parser/parser.d.ts +4 -0
- package/dist/parser/parser.js +4354 -0
- package/dist/parser/prettifier.d.ts +14 -0
- package/dist/parser/prettifier.js +380 -0
- package/dist/renderer/grid-renderer.d.ts +19 -0
- package/dist/renderer/grid-renderer.js +541 -0
- package/dist/renderer/index.d.ts +4 -0
- package/dist/renderer/index.js +4 -0
- package/package.json +67 -0
- package/packages/parser/tpl.pegjs +568 -0
- 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
|
+

|
|
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;
|