Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@

- `ggsql-jupyter` accepts `--version`.

- (WIP) New `TABULATE` clause renders a query result as a table instead of a
plot. Tables are rendered via new `html` writer. `TABULATE` supports the
following clauses:
- `LABEL` overrides the display of column labels
- `SPAN` groups columns under a shared header cell (spanner).


### Changed
- The wasm bundle draws plots with ggsql's own renderer instead of emitting
Vega-Lite. A query is executed in the browser and drawn straight to SVG, so
Expand Down
4 changes: 4 additions & 0 deletions doc/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ website:
href: syntax/clause/project.qmd
- text: "`LABEL`"
href: syntax/clause/label.qmd
- text: "`TABULATE`"
href: syntax/clause/tabulate.qmd
- text: Gallery
href: gallery/index.qmd
- href: faq.qmd
Expand Down Expand Up @@ -106,6 +108,8 @@ website:
href: syntax/clause/project.qmd
- text: "`LABEL`"
href: syntax/clause/label.qmd
- text: "`TABULATE`"
href: syntax/clause/tabulate.qmd
- section: Layers
contents:
- section: Types
Expand Down
76 changes: 76 additions & 0 deletions doc/ggsql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
<item>LABEL</item>
<item>VISUALISE</item>
<item>VISUALIZE</item>
<item>TABULATE</item>
<item>SPAN</item>
</list>

<!-- ggsql Sub-keywords -->
Expand All @@ -110,6 +112,7 @@
<item>RENAMING</item>
<item>TO</item>
<item>VIA</item>
<item>ACROSS</item>
</list>

<!-- Scale Type Modifiers (before aesthetic name in SCALE) -->
Expand Down Expand Up @@ -397,6 +400,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>

<!-- Fat arrow operator -->
<StringDetect attribute="Operator" context="#stay" String="=&gt;"/>
Expand Down Expand Up @@ -444,6 +448,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down Expand Up @@ -484,6 +489,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down Expand Up @@ -523,6 +529,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down Expand Up @@ -569,6 +576,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down Expand Up @@ -612,6 +620,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down Expand Up @@ -654,6 +663,8 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="SpanClause" String="SPAN" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand All @@ -676,6 +687,70 @@
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- TABULATE clause context -->
<context name="TabulateClause" attribute="Normal Text" lineEndContext="#stay">
<Detect2Chars char="-" char1="-" attribute="Comment" context="Comment"/>
<Detect2Chars char="/" char1="*" attribute="Comment" context="CommentMulti" beginRegion="comment"/>
<DetectChar char="'" attribute="String" context="StringSingle"/>
<DetectChar char="&quot;" attribute="String" context="StringDouble"/>
<RegExpr attribute="Number" context="#stay" String="-?[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?"/>

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="SpanClause" String="SPAN" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="#stay" String="FROM" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WHERE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WITH" insensitive="true"/>

<StringDetect attribute="Operator" context="#stay" String="=&gt;"/>
<AnyChar attribute="Operator" context="#stay" String="=!&lt;&gt;+-*/%"/>
<DetectChar char="(" attribute="Symbol" context="#stay"/>
<DetectChar char=")" attribute="Symbol" context="#stay"/>
<DetectChar char="[" attribute="Symbol" context="#stay"/>
<DetectChar char="]" attribute="Symbol" context="#stay"/>
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- SPAN clause context -->
<context name="SpanClause" attribute="Normal Text" lineEndContext="#stay">
<Detect2Chars char="-" char1="-" attribute="Comment" context="Comment"/>
<Detect2Chars char="/" char1="*" attribute="Comment" context="CommentMulti" beginRegion="comment"/>
<DetectChar char="'" attribute="String" context="StringSingle"/>
<DetectChar char="&quot;" attribute="String" context="StringDouble"/>
<RegExpr attribute="Number" context="#stay" String="-?[0-9]+\.?[0-9]*([eE][+-]?[0-9]+)?"/>

<!-- Exit to other clause contexts -->
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="SpanClause" String="SPAN" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="#stay" String="FROM" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WHERE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="WITH" insensitive="true"/>

<!-- ACROSS and SETTING (only SETTING supported here, not FILTER/PARTITION/ORDER) -->
<WordDetect attribute="Keyword" context="#stay" String="ACROSS" insensitive="true"/>
<WordDetect attribute="Keyword" context="#stay" String="SETTING" insensitive="true"/>

<!-- SPAN's own SETTING keys -->
<WordDetect attribute="Attribute" context="#stay" String="gather" insensitive="true"/>
<WordDetect attribute="Attribute" context="#stay" String="level" insensitive="true"/>

<StringDetect attribute="Operator" context="#stay" String="=&gt;"/>
<AnyChar attribute="Operator" context="#stay" String="=!&lt;&gt;+-*/%"/>
<DetectChar char="(" attribute="Symbol" context="#stay"/>
<DetectChar char=")" attribute="Symbol" context="#stay"/>
<DetectChar char="[" attribute="Symbol" context="#stay"/>
<DetectChar char="]" attribute="Symbol" context="#stay"/>
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- VISUALISE clause context -->
<context name="VisualiseClause" attribute="Normal Text" lineEndContext="#stay">
<Detect2Chars char="-" char1="-" attribute="Comment" context="Comment"/>
Expand All @@ -691,6 +766,7 @@
<WordDetect attribute="Keyword" context="ProjectClause" String="PROJECT" insensitive="true"/>
<WordDetect attribute="Keyword" context="FacetClause" String="FACET" insensitive="true"/>
<WordDetect attribute="Keyword" context="LabelClause" String="LABEL" insensitive="true"/>
<WordDetect attribute="Keyword" context="TabulateClause" String="TABULATE" insensitive="true"/>
<WordDetect attribute="Keyword" context="VisualiseClause" String="VISUALISE" insensitive="true"/>
<WordDetect attribute="Keyword" context="VisualiseClause" String="VISUALIZE" insensitive="true"/>
<WordDetect attribute="Keyword" context="Normal" String="SELECT" insensitive="true"/>
Expand Down
104 changes: 104 additions & 0 deletions doc/syntax/clause/tabulate.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: "Output a table instead of a plot with `TABULATE`"
---

<!--
This document is work in progress.
Take statements herein as malleable truths.
We will be adding more clauses, more settings and rephrasing language.
-->

The `TABULATE` clause marks the beginning of a ggsql table declaration — the plain-table counterpart to [`VISUALISE`](visualise.qmd). Instead of building a plot, it renders the query result directly as a table. As with `VISUALISE`, it demarcates where the regular SQL query ends: everything before `TABULATE` is standard SQL sent to the backend, and if it ends with a `SELECT`, that result becomes the table's data unless overridden by `FROM`.

`TABULATE` currently only produces output through the HTML writer — every other writer (Vega-Lite, SVG, PDF, `.hep`, the raster writers) supports plots only and errors if given a `TABULATE` query.

## Clause syntax
`TABULATE` doesn't require any additional clauses.

```ggsql
TABULATE FROM <data-source>
LABEL <column> => <string>, ...
SPAN <string> ACROSS <column>, ...
SETTING <parameter> => <value>, ...
```

`LABEL` and `SPAN` may each appear more than once, in any order, after the source.

### `FROM`
```ggsql
FROM <data-source>
```

Same shape as [`VISUALISE`'s `FROM`](visualise.qmd#from): a bare identifier names a CTE or table already available in the backend, and a string names a file path the backend can read directly. `FROM` is optional if the query already ends with a `SELECT`; combining both (a trailing `SELECT` *and* `TABULATE FROM`) is an error, since it would leave two candidate data sources.

### `LABEL`
```ggsql
LABEL <column> => <string>, ...
```

Overrides the display header text for one or more columns. Unlike `VISUALISE`'s `LABEL` (which is keyed by aesthetic), `TABULATE`'s `LABEL` is keyed by column name, and there is no equivalent to `VISUALISE`'s automatic labelling logic — a column with no `LABEL` entry keeps its own name as its header.

Give `NULL` instead of a string to blank a column's header while keeping the column itself:

```ggsql
TABULATE FROM sales
LABEL revenue => 'Revenue ($)', region => NULL
```

If every column ends up with a blank header this way, the header row is omitted entirely rather than rendered as a row of empty cells.

### `SPAN`
```ggsql
SPAN <string> ACROSS <column>, ...
SETTING <parameter> => <value>, ...
```

Groups columns under one spanner cell, drawn in a row above the column labels:

```ggsql
TABULATE FROM sales
SPAN 'Q1' ACROSS jan, feb, mar
```

The label is mandatory and can be a string (`''` is a present-but-blank cell) or `NULL`, which suppresses the spanner cell itself while still grouping the columns — useful when the only reason for the `SPAN` is a shared `SETTING`. Each `SPAN` clause defines exactly one spanner; give several `SPAN` clauses for several spanners, the same way multiple `DRAW` clauses give multiple layers.

Spanners whose column sets overlap (but aren't identical) are placed on separate header rows automatically, closest-to-the-columns first.

#### `SETTING`
```ggsql
SETTING <parameter> => <value>, ...
```

* `gather` (boolean, default `true`): moves the spanner's columns to sit contiguously, pulling them next to the first-listed member without disturbing any other column's relative order. Set to `false` to leave the columns exactly where they are.
* `level` (whole number, no default): pins the spanner to a specific header row (`1` is the row closest to the column labels). If omitted, a level is assigned automatically, high enough to avoid every other spanner it shares a column with.
* `id` (string, no default): names this spanner so a *later* `SPAN` clause can fold its columns into a new one.

Reference an `id` in place of a column name in a later `SPAN`'s `ACROSS` list to include everything the earlier spanner covers:

```ggsql
TABULATE FROM sales
SPAN 'Q1' ACROSS jan, feb, mar SETTING id => 'q1'
SPAN 'H1' ACROSS q1, apr, may, jun
```

`H1` ends up covering `jan, feb, mar, apr, may, jun`. Only an `id` declared by an *earlier* `SPAN` clause is recognised — referencing one declared later, or a typo, is treated as an unknown column, the same error as any other bad `ACROSS` entry. Every `id` must be unique across the whole query, and can't collide with an actual column name.

### Examples

```{ggsql}
SELECT * FROM ggsql:penguins LIMIT 5

TABULATE
```



```{ggsql}
SELECT * FROM ggsql:penguins LIMIT 5

TABULATE
SPAN 'Bill' ACROSS bill_len, bill_dep
LABEL
bill_len => 'Length',
bill_dep => 'Depth'
```
2 changes: 1 addition & 1 deletion src/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ src/
├── parser/ Tree-sitter integration → typed AST (Spec: Plot or Table)
├── plot/ AST: Plot, Layer, Geom, Scale, Facet, Projection, Mappings (see plot/CLAUDE.md)
├── table/ AST stub for TABULATE, parallel to plot/ (no fields yet)
├── table/ AST for TABULATE, parallel to plot/ (source, labels, spans)
├── reader/ Reader trait + drivers (DuckDB, SQLite, ODBC, Snowflake, …)
├── execute/ Pipeline that turns Plot + Reader → ResolvedPlot
├── writer/ Writer trait + Vega-Lite implementation (see writer/vegalite/CLAUDE.md)
Expand Down
2 changes: 2 additions & 0 deletions src/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! - `layer`: Layer query building, data transforms, and stat application
//! - `scale`: Scale creation, resolution, type coercion, and OOB handling
//! - `table`: Table (TABULATE) resolution
//! - `table_spanner`: `TABULATE SPAN` resolution, called from `table`

mod casting;
mod cte;
Expand All @@ -18,6 +19,7 @@ mod position;
mod scale;
mod schema;
mod table;
mod table_spanner;

// Re-export public API
pub use casting::TypeRequirement;
Expand Down
Loading