Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
following clauses:
- `LABEL` overrides the display of column labels
- `SPAN` groups columns under a shared header cell (spanner).
- `FORMAT` configures how a column's cells are displayed.


### Changed
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ For details — module layout, traits, where extension points live — see [`src

## Building

**Prerequisite: `tree-sitter-cli`.** Any Rust build regenerates the parser from `grammar.js` via `tree-sitter-ggsql`'s build script, which runs `tree-sitter generate` and **fails if `tree-sitter-cli` is not on `PATH`**. Install it once with `npm install -g tree-sitter-cli`. To build against the committed `tree-sitter-ggsql/src/parser.c` without the CLI (e.g. if you're not touching the grammar), set `GGSQL_SKIP_GENERATE=1`.
**Prerequisite: `tree-sitter-cli`.** Any Rust build regenerates the parser from `grammar.js` via `tree-sitter-ggsql`'s build script, which runs `tree-sitter generate` and **fails if `tree-sitter-cli` is not on `PATH`**. Install it once with `npm install -g tree-sitter-cli`. To build against a pre-generated `tree-sitter-ggsql/src/parser.c` without the CLI (e.g. if you're not touching the grammar), set `GGSQL_SKIP_GENERATE=1`.

```sh
# Rust workspace (default members: tree-sitter-ggsql, src, ggsql-cli, ggsql-jupyter)
Expand Down
43 changes: 43 additions & 0 deletions doc/ggsql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
<item>VISUALIZE</item>
<item>TABULATE</item>
<item>SPAN</item>
<item>FORMAT</item>
</list>

<!-- ggsql Sub-keywords -->
Expand Down Expand Up @@ -665,6 +666,7 @@
<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="FormatClause" String="FORMAT" 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 @@ -699,6 +701,7 @@
<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="FormatClause" String="FORMAT" 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 @@ -727,6 +730,7 @@
<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="FormatClause" String="FORMAT" 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 @@ -751,6 +755,45 @@
<DetectChar char="," attribute="Symbol" context="#stay"/>
</context>

<!-- FORMAT clause context -->
<context name="FormatClause" 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="FormatClause" String="FORMAT" 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"/>

<!-- SETTING and RENAMING (only these two supported here) -->
<WordDetect attribute="Keyword" context="#stay" String="SETTING" insensitive="true"/>
<WordDetect attribute="Keyword" context="#stay" String="RENAMING" insensitive="true"/>

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

<!-- Wildcard for RENAMING -->
<DetectChar char="*" attribute="Operator" context="#stay"/>

<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 Down
53 changes: 51 additions & 2 deletions doc/syntax/clause/tabulate.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ TABULATE FROM <data-source>
LABEL <column> => <string>, ...
SPAN <string> ACROSS <column>, ...
SETTING <parameter> => <value>, ...
FORMAT <column>, ...
SETTING <parameter> => <value>, ...
RENAMING <value> => <string>, ...
```

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

### `FROM`
```ggsql
Expand Down Expand Up @@ -83,6 +86,41 @@ TABULATE FROM sales

`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.

### `FORMAT`
```ggsql
FORMAT <column>, ...
SETTING <parameter> => <value>, ...
RENAMING <value> => <string>, ...
```

Configures cell display for one or more columns. Give several `FORMAT` clauses for different column groups; if two `FORMAT` clauses name the same column, the later one wins for it.

```ggsql
TABULATE FROM sales
FORMAT price SETTING hjust => 'right'
```

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

* `hjust` (default: right for numeric columns, left otherwise): horizontal alignment for the column's cells. One of `'left'`, `'right'`, `'center'`/`'centre'`, or a number between 0 and 1 (0 is left, 1 is right, 0.5 is centered).

#### `RENAMING`
```ggsql
RENAMING <value> => <string>, ...
```

Works the same way as [`SCALE`'s `RENAMING`](scale.qmd#renaming), just applied to the columns' own cell values instead of a scale's breaks: give a value on the left and its display text on the right, or `* => '...'` to format every value with [a template](scale.qmd#break-formatting). `NULL` on the right suppresses a cell's text.

`null` (unquoted) matches an actual missing value, which makes `RENAMING` a convenient way to blank out missing data or zeroes:

```ggsql
TABULATE FROM sales
FORMAT price RENAMING null => '-', 0 => '-'
```

### Examples

```{ggsql}
Expand All @@ -98,7 +136,18 @@ SELECT * FROM ggsql:penguins LIMIT 5

TABULATE
SPAN 'Bill' ACROSS bill_len, bill_dep
LABEL
LABEL
bill_len => 'Length',
bill_dep => 'Depth'
```



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

TABULATE
FORMAT body_mass
SETTING hjust => 'right'
RENAMING null => '-', * => '{:num %.0f}g'
```
4 changes: 2 additions & 2 deletions ggsql-cli/src/writers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,8 +768,8 @@ mod tests {
panic!("expected Output::Text from the html writer");
};
assert!(html.starts_with("<table>"), "{html}");
assert!(html.contains("<th>id</th>"), "{html}");
assert!(html.contains("<td>a</td>"), "{html}");
assert!(html.contains(">id</th>"), "{html}");
assert!(html.contains(">a</td>"), "{html}");
}

#[cfg(all(feature = "html", feature = "duckdb"))]
Expand Down
5 changes: 3 additions & 2 deletions ggsql-jupyter/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ impl QueryExecutor {
tracing::warn!("{}", warning.message);
}

let html = HtmlWriter::new().write_table(table.cells())?;
let html =
HtmlWriter::new().write_table(table.cells(), table.columns(), table.rows())?;
tracing::debug!("Generated HTML table: {} chars", html.len());

Ok(ExecutionResult::Table { html })
Expand Down Expand Up @@ -344,7 +345,7 @@ mod tests {
match result {
ExecutionResult::Table { html } => {
assert!(html.contains("<table>"));
assert!(html.contains("<th>x</th>"));
assert!(html.contains(">x</th>"));
}
other => panic!("expected Table, got {other:?}"),
}
Expand Down
2 changes: 1 addition & 1 deletion src/doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This document provides a comprehensive reference for the ggsql public API.
fn execute(&self, query: &str) -> Result<ResolvedSpec>
```

Execute a ggsql query for visualization or tabulation. This is the main entry point - a required method on the Reader trait. `ResolvedSpec` is `Plot(Box<ResolvedPlot>)` or `Table(ResolvedTable)`, depending on whether the query used `VISUALISE` or `TABULATE`; `.as_plot()` / `.as_table()` (or the consuming `.into_plot()` / `.into_table()`) narrow it.
Execute a ggsql query for visualization or tabulation. This is the main entry point - a required method on the Reader trait. `ResolvedSpec` is `Plot(Box<ResolvedPlot>)` or `Table(Box<ResolvedTable>)`, depending on whether the query used `VISUALISE` or `TABULATE`; `.as_plot()` / `.as_table()` (or the consuming `.into_plot()` / `.into_table()`) narrow it.

**What happens during execution:**

Expand Down
5 changes: 4 additions & 1 deletion src/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
//! - `scale`: Scale creation, resolution, type coercion, and OOB handling
//! - `table`: Table (TABULATE) resolution
//! - `table_spanner`: `TABULATE SPAN` resolution, called from `table`
//! - `table_format`: `TABULATE FORMAT` resolution (replaces a column's
//! values with its resolved display text), called from `table`

mod casting;
mod cte;
Expand All @@ -19,13 +21,14 @@ mod position;
mod scale;
mod schema;
mod table;
mod table_format;
mod table_spanner;

// Re-export public API
pub use casting::TypeRequirement;
pub use cte::CteDefinition;
pub use schema::TypeInfo;
pub use table::{resolve_table_with_reader, TableCell, TableCellKind};
pub use table::{resolve_table_with_reader, TableCell, TableCellKind, TableColumn, TableRow};

use crate::naming;
use crate::parser;
Expand Down
Loading