diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd15e31..5d22733b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Added - ODBC is now turned on for the CLI as well (#344) +- CLI now has built-in documentation through the `docs` command as well as a +skill for llms through the `skill` command (#361) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 2761357f..76519327 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -722,7 +722,7 @@ checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", - "unicode-width", + "unicode-width 0.2.2", ] [[package]] @@ -781,6 +781,24 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "coolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" +dependencies = [ + "crossterm", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -870,6 +888,73 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crokey" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" +dependencies = [ + "crokey-proc_macros", + "crossterm", + "once_cell", + "serde", + "strict", +] + +[[package]] +name = "crokey-proc_macros" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" +dependencies = [ + "crossterm", + "proc-macro2", + "quote", + "strict", + "syn 2.0.117", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -885,6 +970,33 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -988,6 +1100,28 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "digest" version = "0.10.7" @@ -1058,6 +1192,15 @@ dependencies = [ "libloading", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1480,6 +1623,7 @@ dependencies = [ "serde_json", "sprintf", "tempfile", + "termimad", "thiserror 1.0.69", "toml_edit 0.22.27", "tree-sitter", @@ -2113,6 +2257,29 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "lazy-regex" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2184,9 +2351,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libduckdb-sys" @@ -2262,6 +2429,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -2308,6 +2481,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "minimad" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" +dependencies = [ + "once_cell", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2325,6 +2507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -2853,9 +3036,9 @@ dependencies = [ [[package]] name = "pathfinder_simd" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" dependencies = [ "rustc_version", ] @@ -3690,9 +3873,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ "aws-lc-rs", "log", @@ -3947,6 +4130,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -4055,6 +4259,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" +[[package]] +name = "strict" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" + [[package]] name = "stringprep" version = "0.1.5" @@ -4190,6 +4400,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termimad" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7301d9c2c4939c97f25376b70d3c13311f8fefdee44092fc361d2a98adc2cbb6" +dependencies = [ + "coolor", + "crokey", + "crossbeam", + "lazy-regex", + "minimad", + "serde", + "thiserror 2.0.18", + "unicode-width 0.1.14", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4650,6 +4876,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.2" diff --git a/doc/vendor/SKILL.md b/doc/vendor/SKILL.md new file mode 100644 index 00000000..9397ecae --- /dev/null +++ b/doc/vendor/SKILL.md @@ -0,0 +1,497 @@ +--- +name: ggsql +description: Write ggsql queries — a grammar of graphics for SQL. Use when the user wants to create, modify, or understand a ggsql visualization query. +allowed-tools: Bash(ggsql:*) +argument-hint: "[description of desired visualization]" +metadata: + author: George Stagg (@georgestagg) + version: "1.0" +license: MIT +--- + +# ggsql Query Writer + +ggsql is a SQL extension for declarative data visualization based on Grammar of Graphics principles. It lets users combine SQL data queries with visualization specifications in a single, composable syntax. + +When the user describes a visualization they want, write a valid ggsql query. Use ONLY syntax documented below. NEVER invent clauses, settings, aesthetics, or layer types. + +## Query structure + +A ggsql query has two parts: + +1. **SQL part** (optional): Standard SQL executed on the backend. Any tables, CTEs, or SELECT results are available to the visualization. +2. **VISUALISE part** (required): Begins with `VISUALISE` (or `VISUALIZE`). Everything after this is the visualization query. + +There are two patterns for combining SQL with VISUALISE: + +### Pattern A: SELECT → VISUALISE + +The last SQL statement is a SELECT. Data flows from its result set into VISUALISE, which has no `FROM` clause. + +```ggsql +SELECT name, score_a, score_b FROM 'dataset.csv' WHERE value > 50 +VISUALISE score_a AS x, score_b AS y +[DRAW / PLACE / SCALE / FACET / PROJECT / LABEL clauses] +``` + +Works with any SQL that ends in a SELECT: bare SELECT, WITH...SELECT, UNION/INTERSECT/EXCEPT. + +### Pattern B: VISUALISE FROM + +VISUALISE provides its own data source via `FROM`. Use when referencing a table, file, CTE, or built-in dataset directly without a trailing SELECT. + +```ggsql +VISUALISE score_a AS x, score_b AS y FROM 'dataset.csv' +DRAW point +``` + +```ggsql +WITH summary AS (SELECT category, COUNT(*) AS n FROM 'dataset.csv' GROUP BY category) +VISUALISE category AS x, n AS y FROM summary +DRAW bar +``` + +## Data sources + +Data sources can appear in `VISUALISE ... FROM` or `DRAW ... MAPPING ... FROM`: + +- **Table/CTE name** (unquoted): `FROM sales`, `FROM my_cte` +- **File path** (single-quoted string): `FROM 'data.parquet'`, `FROM 'data.csv'` +- **Built-in datasets**: `FROM ggsql:penguins`, `FROM ggsql:airquality` + +## VISUALISE clause + +Marks the start of the visualization. Optionally defines global mappings inherited by all layers. + +``` +VISUALISE , ... FROM +``` + +### Mapping forms + +- **Explicit**: `column AS aesthetic` — e.g. `revenue AS y` +- **Implicit**: `column` — column name must match aesthetic name, e.g. `x` maps to `x` +- **Wildcard**: `*` — all columns with names matching aesthetics are mapped +- **Constants**: `'red' AS fill`, `42 AS size` — literal values mapped to aesthetic + +```ggsql +VISUALISE bill_len AS x, bill_dep AS y, species AS fill FROM ggsql:penguins +VISUALISE * FROM my_table +VISUALISE FROM ggsql:penguins +``` + +## DRAW clause + +Defines a layer. Multiple DRAW clauses stack layers (first = bottom, last = top). + +``` +DRAW + MAPPING , ... FROM + REMAPPING AS , ... + SETTING => , ... + FILTER + PARTITION BY , ... + ORDER BY , ... +``` + +All subclauses are optional if VISUALISE provides global mappings and data. + +### MAPPING + +Same syntax as VISUALISE mappings. Layer mappings merge with global mappings (layer takes precedence). Can include `FROM` for layer-specific data. + +- Use `null` to prevent inheriting a global mapping: `MAPPING null AS color` + +### REMAPPING + +For statistical layers (histogram, density, boxplot, violin, smooth, bar without y). Maps calculated statistics to aesthetics. Each layer documents its available stats and default remapping. + +```ggsql +DRAW histogram + MAPPING body_mass AS x + REMAPPING density AS y -- use density instead of default count +``` + +### SETTING + +Set literal aesthetic values or layer parameters. Aesthetics set here bypass scales. + +```ggsql +DRAW point + SETTING size => 5, opacity => 0.7, stroke => 'red' +``` + +**Position adjustment** is a special setting: +```ggsql +SETTING position => 'identity' -- no adjustment (default for most) +SETTING position => 'stack' -- stack (default for bar, histogram, area) +SETTING position => 'dodge' -- side by side (default for boxplot, violin) +SETTING position => 'jitter' -- random offset +``` + +### FILTER + +SQL WHERE condition applied to layer data. Content is passed to the database: +```ggsql +DRAW point + FILTER sex = 'female' AND body_mass > 4000 +``` + +### PARTITION BY + +Additional grouping columns beyond mapped discrete aesthetics: +```ggsql +DRAW line + MAPPING Day AS x, Temp AS y + PARTITION BY Month +``` + +### ORDER BY + +Controls record order (important for path layers): +```ggsql +DRAW path + ORDER BY timestamp +``` + +## PLACE clause + +Creates annotation layers with literal values only (no data mappings). Supports tuples for multiple annotations. + +``` +PLACE + SETTING => , ... +``` + +```ggsql +PLACE point SETTING x => 5, y => 10, color => 'red' +PLACE rule SETTING y => 70, linetype => 'dotted' +PLACE text SETTING x => (34, 44), y => (66, 49), label => ('Mean = 34', 'Mean = 44') +``` + +## SCALE clause + +Controls how data values are translated to aesthetic values. Sensible defaults are always provided. + +``` +SCALE FROM TO VIA + SETTING => , ... + RENAMING =>