[Unreleased]
0.5.2 - 2026-09-11
Changed
- Update GitHub Actions to deal with issues publishing Windows installers.
0.5.1 - 2026-09-11
Changed
- Update GitHub Actions to deal with issues publishing Windows installers and cargo packages.
0.5.0 - 2026-09-10
Added
Seven new writers render a plot directly, through hephaestus:
svg,pdfandhepas default features, andpng,jpeg,tiffandwebpoff by default and needing a GPU adapter at render time. Each takes the canvas settingswidth,height,units(px,in,cm,mm,pt),dpiandbackground, plus what its own format offers:compressionforpngandtiff,qualityforjpeg,text,embed-fontsandid-prefixforsvg,compressandlinksforpdf,lossyandembed-fontsforhep.webphas none — it is lossless with no rate control.LABEL captionis honored by the new writers. It has no Vega-Lite equivalent and is ignored there.New
ggsql viewsubcommand shows a query’s plot in a native window, blocking until it is closed. Resizing re-lays-out the plot rather than stretching it.-D(--viewer-option) takeswidth,height,backgroundandtitle;unitsanddpiare refused, since a window is sized in logical pixels and its resolution belongs to the display. Behind a new off-by-defaultwindowfeature, and needs a GPU adapter; the subcommand exists either way and says what would enable it.New
equal_earthmap projection (+proj=eqearth).New caching layer that wraps any
Readerwith an in-memory, writeable cache backend (currently duckdb or sqlite), making write-constrained databases usable and avoiding repeated remote reads during interactive iteration. Memoized reads are bounded by a TTL and an LRU byte budget, configurable per connection. Selected by the composite connection scheme<cache>+<primary>://…(e.g.duckdb+odbc://…) or, in the CLI, by--cache <duckdb|sqlite>onexec,runandview. The cache can be cleared mid-session with the-- @uncachemeta-command.Writers can be configured from key–value options:
Writer::from_optionstakes aWriterOptionsset, and the CLI collects them from a repeatable--writer-option key=valueflag onexecandrun(short-D, also spellable--writer-options). Several settings can be collapsed into one flag separated by;—-D 'width=1600;dpi=150', quoted because shells read;themselves — and the two forms mix. An unknown key or unusable value is an error naming the option, not a silently ignored setting.--reader,--writer, and--outputgained the short forms-r,-w, and-oonexecandrun;validate --readeralso takes-r.--output’s extension picks the writer when--writeris omitted:svg,pdf,hep,png,jpg/jpeg,tif/tiff,webpandjson/vl.jsoneach name their own. An explicit--writerstill wins, warning on stderr if it disagrees with the extension. An unrecognised extension falls back to Vega-Lite; an extension naming a writer the build lacks is an error.New
ggsql::fonts::{register_font, registered_font_families, set_generic_family}let a host register font faces itself, for a platform with no font database to enumerate. A browser is the case that needs it: it enumerates nothing, so the wasm package ships four Roboto faces and registers them before drawing — without them a plot has no text at all, and, since text is what sets the layout, the wrong margins with it. A page wanting its own typography callsregisterFontFromUrl(url, { genericFor })instead, which registers the face and points a generic at whatever family name the file turned out to carry — the one place that name exists.New off-by-default
webfontsfeature:fonts::register_fontalso accepts the WOFF and WOFF2 containers a font CDN serves a browser, unwrapping them to the sfnt inside. That is how a font arrives at a web page, soggsql-wasmturns it on. Without the feature such a container is refused by name rather than reaching the shaper and registering nothing, which would draw a plot with no text and no indication why.Text is rendered as rich text (markdown) by the new writers. A text layer’s
labelis parsed for**bold**,*italic*,_underline_,~~strike~~,`code`and marquee-style{selector body}spans that set a colour or size ({.red hot},{#0072B2 blue},{.20 big}), and so are the plot title, subtitle, caption, axis titles, legend titles and break labels. The newparsesetting on the text layer turns it off for that layer (SETTING parse => false), drawing the label exactly as given; it defaults totrue. Chrome text has no switch yet. The Vega-Lite writer has no rich-text equivalent and ignoresparse, always drawing text literally.New
minor_breakssetting on continuous scales, controlling the unlabelled subdivisions between breaks: a whole number of minor breaks per interval between two breaks (0removes them), an array of exact positions, or — for temporal scales — an interval such as'week'. Defaults to a value chosen by the transformation. This has no Vega-Lite equivalent and is ignored by that writer.The VS Code / Positron extension now ships the
ggsql-jupyterkernel, so installing the extension is all that is needed to run queries. It is offered alongside every ggsql kernel found on the machine — a Jupyter kernelspec, a native install, one onPATH, or the path inggsql.kernelPath— each named for the version it reports, so the New Console Session picker shows which is which. A kernel too old to report one is still offered, named without a version. The bundled kernel is the default.ggsql-jupyteraccepts--version.
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 the playground and the live examples on the docs site look like every other ggsql output rather than like a second implementation, and
vega,vega-liteandvega-embedare gone from the page — about 1.8 MB less JavaScript. A plot re-solves its layout when its box changes size, so a wider pane gets more tick labels rather than stretched ones. Breaking:GgsqlContext.executereturns aGgsqlPlotto draw rather than a Vega-Lite JSON string; the npm package is entered through a new TypeScript client that addsPlotViewandregisterDefaultFontsbeside it.init()— and a newinitSync()— wire the extension loader and the CSV/Parquet converters themselves, soinitExtensionLoaderis gone and entering through the package rather than the generated glue is now required. - Plots in a Positron console now open a
positron.plotcomm, so the Plots pane renders them at its own size, re-renders sharp when resized, and its save, copy and zoom affordances work on them. A new--max-plots(default 32) caps the retained history, closing the oldest first. Once the pane has reported a size, a new plot arrives already rendered at it. - Plots in notebooks and documents are now rendered by the kernel and no longer need network access. A
VISUALISEquery in JupyterLab, a Positron notebook or a Quarto render previously emitted HTML that fetched vega, vega-lite and vega-embed from a CDN on every render; it now emits a rendered image. - Quarto’s figure settings are honoured:
QUARTO_FIG_FORMATselects the writer (png,jpeg,svg,pdf) andQUARTO_FIG_WIDTH/_HEIGHTare read as inches atQUARTO_FIG_DPI, sofig-width: 6means six inches and a PDF document gets a vector figure. - Kernel plots render as SVG wherever raster output is unavailable — no GPU adapter, or a build with the new
raster-plotsfeature turned off — so a GPU is needed for raster output, not to see a plot. - An unknown writer, a writer whose feature is off, and an unusable writer setting are now reported before the query runs rather than after.
--writerand-Dlist every writer and its settings in their long help, marking the ones this build does not have and naming the feature that would add each. - A
FROMon theVISUALISEclause now takes exactly one bare source. It previously reused the SQLFROMgrammar while only ever reading the first source, soVISUALISE FROM a, bsilently plottedaalone andVISUALISE FROM a JOIN b ON …silently dropped the join. Both are now parse errors, as is an alias (VISUALISE FROM tbl AS t), which parsed before but had no effect. Join two tables in aSELECTand visualise its result. - Dodging now only takes effect where groups actually meet on a position. A layer whose grouping gives every group a position of its own —
colourmapped to the same column as the discrete axis, say — is drawn at its full width instead of being squeezed into1/nof the band and shifted off its own category, which made a coloured ridgeline plot (DRAW violin SETTING side => 'top') land its violins between the axis ticks or outside the panel altogether. Groups in different facet panels don’t meet either. Where any position does hold several groups the whole layer still dodges, so an element keeps the same slot in every position. Jitter, which dodges before jittering, follows the same rule. - Categorical
yaxes now run bottom-up, so the first level sits at the bottom of the panel as it does in ggplot2. This affects every plot with a discrete or ordinaly— horizontal bars, boxplots and violins by category, points and 2D jitter — and brings the Vega-Lite writer in line with the new writers, which already read this way. - Banded marks now measure against the full step in the Vega-Lite writer. A band fraction (a bar’s
width, a dodge displacement, a jitter spread, a violin or boxplot half-width, a discrete tile’s extent) is a fraction of the whole category step, sowidth => 0.9leaves a 10% gap — ggplot2’s convention. Vega-Lite previously subtracted its own default band padding first, and a further 20% of every step for dodged, jittered and half-sided layers, so their marks were narrower, their displacements smaller and their category ticks pulled toward the middle of the panel. - Position scales like
SCALE lonandSCALE lattransfer their limits to map projections, and transfer theirbreakssetting to the graticule (#492).
Fixed
- Positron no longer offers a ggsql runtime on a machine that has no kernel.
- A dodged violin or half-boxplot on a categorical
yaxis is no longer flipped in the Vega-Lite writer. Both took their band displacement from an encoding of their own that read a ggsql offset as pointing down the screen, so their groups came out in the opposite order to every other mark — a violin put the first group above the second where a boxplot of the same data put it below, and a half-boxplot’s box parted company with its own whiskers once dodged. Violins are also clipped to the panel now, as every other mark is. - An identity-scaled column is now read exactly like the equivalent literal.
SCALE IDENTITY <aes>hands its values straight to the aesthetic, so they mean what the same value written withSETTINGmeans, but several were passed to the renderer unconverted: asizecolumn was read as a symbol area in pixels² rather than the radius in pointsSETTING size => 3gives (markers far too small), ashapecolumn of names such as'star'made Vega-Lite fail to render at all, and alinetypecolumn of names such as'dashed'drew a solid line in both writers.size,linewidth,fontsize,shapeandlinetypeidentity columns now convert per row, so an identity column and a setting produce the same drawing. A value the aesthetic already understands still passes through untouched. DRAW bar MAPPING <category> AS yproduced a single bar against a synthetic axis instead of horizontal bars. A layer whose geom synthesises its primary position (bar, boxplot) now transposes when the user maps a discretey, and stays put when they map a continuous one — that being the value axis, where a loneDRAW boxplot MAPPING <value> AS yalready belongs.RENAMINGwas ignored on a discrete or ordinal scale over a non-string domain (SCALE ORDINAL color RENAMING 6 => 'June'on a numeric month), because the break label was formatted as6.0while the rename was keyed on6.- A temporal axis given a calendar interval (
SETTING breaks => '2 months') no longer draws ticks outside its own domain. The generator steps a whole interval past each end, and the filter that trims them back compared only plain numbers, so a date break was never constrained at all. - Minor breaks are no longer extrapolated beyond the outermost major break when the majors are unevenly spaced, as they are when set by hand (
SETTING breaks => (37, 42, 55)). Their spacing was taken from the first interval alone, so they matched no part of the axis. Evenly spaced majors still extend to the edge of the range. Scale::break_labels()— what a writer reads to label an axis, colorbar or legend tick — labels a temporal break with its own date (1973-04-23) instead of the epoch number its position projects to (1208), and keysRENAMINGoverrides by that same string, so a rename on a temporal scale is found rather than missed. Numeric and categorical labels are unchanged.- A scale with an explicit input range that no layer trains —
SCALE x FROM (0, 10)alongside a diagonalrule, whose position is deliberately kept out of scale training — takes its type from that range (numeric or temporal → continuous, string or boolean → discrete) instead of staying untyped, so consumers get a fully resolved scale. - The VS Code / Positron extension now offers its “Source Current File” button and code cells in plain
.sqlfiles, so existing SQL can be run against a ggsql kernel without renaming it..sqlfiles keep their usual SQL syntax highlighting; to get ggsql highlighting as well, map them to theggsqllanguage type withfiles.associations, which the extension points out the first time a.sqlfile is opened. The newggsql.enableSqlFilessetting turns the whole behaviour off. - The VS Code / Positron extension contributes a “ggsql File” entry to the New File dialog.
- The VS Code / Positron extension now ships a language icon that renders in the session picker, editor tabs and the Explorer. It previously pointed at a file that did not exist, which left the icon blank.
- In plain VS Code, the extension no longer offers run buttons, keybindings or Command Palette entries for commands that need the Positron runtime and so had no handler there.
- ggsql interpreter sessions in Positron now come back after an extension host restart as well as after a window reload. A session the user renamed also keeps its name across the restore, and ggsql runtimes are rediscovered on every window open rather than risking a stale cache hit.
0.4.1 - 2026-06-22
Changed
- Pinned the Windows image for GitHub Actions runners to Windows Server 2022.
0.4.0 - 2026-06-22
Added
- New
AdbcReader<D: Driver>for connecting to data sources via ADBC (Arrow Database Connectivity), behind a new off-by-defaultadbcfeature flag. Generic over any concreteadbc_core::sync::Driver, so concrete drivers (Flight SQL, Snowflake, etc.) compose at the call site. Tested againstadbc_datafusionfor in-process unit coverage. - New
aggregateSETTING on Identity-stat layers (point, line, area, bar, ribbon, range, segment, arrow, rule, text). By default it collapses each group to a single row by replacing every numeric mapping in place with its aggregated value. See theDRAWdocumentation for details (#384). - Added panel decorations (grid lines, axes, background) for polar coordinates (#156).
- Added
radarsetting to polar coordinates for making radar plots (#418). - New
sideSETTING on theboxplotlayer and thejitterposition, mirroring the existingviolinsetting (#439). - New
hingeSETTING on theboxplotlayer, mirroring the existingrangesetting (#438) - New
DRAW spatiallayer for rendering simple features (WKT/WKB) for drawing maps and choropleths (#370). - New builtin dataset
ggsql:worldfor showcasing spatial examples. Data is a subset of columns from the Natural Earth country data at 1:110m resolution (#370). - New
PROJECT TO <map>family of spatial map projections. For general projections, one can usePROJECT TO crs SETTING target => '+proj=...'. Several named projections have explicit support using e.g.PROJECT TO mollweide. Works for a subset of layers, notablyspatial,point,text,path,polygonandtile. Requires a spatial backend like PostGIS, SpatiaLite, or DuckDB spatial extension (#455).
Fixed
- Quoted SQL identifiers (e.g.
"variable.dotted") inVISUALISEcolumn references are now unquoted at parse time, so they correctly match the underlying Arrow schema during validation. - Dodging of horizontal violin plots were broken due to a bad orientation assumption in the VegaLite writer. We now correctly use the orientation to dodge in the correct dimension (#439).
- Fixed misbehaviour of numeric scale’s
RENAMINGclause due to pre-formatting issues (#461)
Changed
boxplot,violin, andrangenow support omitting the categorical aesthetic, matchingbar.pointnow treats both position aesthetics as optional.- Upgraded dependencies: duckdb-rs v1.10502, arrow v58 (#447).
- Renamed the
widthsetting in therangelayer tohinge. This prevents it from clashing withwidthneeded byposition => 'dodge'(#437). - Pinned the minimum supported Rust version to 1.86 (the maximum Rust version CRAN ships) so the crate keeps building for the R bindings.
0.3.3 - 2026-05-27
Fixed
- Add CASE expression support to tree-sitter grammar (#432)
- Fix Vega-Lite spec emitted for boxplot (#449)
- Support predicates in function arguments (#457)
0.3.2 - 2026-05-05
Fixed
- Side effects like
CREATE TEMP TABLEbefore theVISUALISEstatement are now separated from directly feeding into the visualisation data (#415) - Fixed bug where panel axes were unintentionally anchored to zero when using
FACET ... SETTING free => 'x'/'y'(#410). - Fixed bug where faceted data were matched to the incorrect panels (#409)
Changed
- Restructured how ggsql integrates with ODBC drivers to use the system ODBC, rather than bundling unixodbc as part of binary releases. This fixes several issues on Linux and macOS caused by relative paths to dynamic libraries.
0.3.1 - 2026-04-30
Fixed
- Fixed stacking in faceted plots (#403)
0.3.0 - 2026-04-29
Added
- Add cell delimiters and code lens actions to the Positron extension (#366)
- ODBC is now turned on for the CLI as well (#344)
FROMcan now come beforeVISUALIZE, mirroring the DuckDB style. This means thatFROM table VISUALIZE x, yandVISUALIZE x, y FROM tableare equivalent queries (#369)- CLI now has built-in documentation through the
docscommand as well as a skill for llms through theskillcommand (#361) - The ggsql wasm package is now published on GitHub Releases and NPM (#367)
Fixed
- Rendering of inline plots in Positron had a bad interaction with how we handled auto-resizing in the plot pane. We now have a per-output-location path in the Jupyter kernel (#360)
- Passing the shape aesthetic via
SETTINGnow correctly translates named shapes (#368) - Asterisk shape now has lines 60 degrees apart, giving an even shape
validate()now reports an actionable error when a SQL expression (e.g.CAST(...)or a function call) appears inside aVISUALISEmapping, instead of silently treating the entire query as SQL (#389)- Error messages no longer leak internal aesthetic names. Validation, scale, and writer errors now report user-facing aesthetic names (
x,y,panel,row, …) instead of internal forms (pos1,pos2,facet1, …), translated based on the active coordinate system and facet layout (#388). - Fixed opacity calculation in point layers with Vega-Lite (#393)
- Fixed an issue with case-sensitive column references in mappings (#374)
- Fixed SQL function set quantifiers in the ggsql grammar (#395)
- Fixed loading of dynamic libraries in PyPI build of
ggsql-jupyter(#355, #392) - Fixed an issue with OOB null-filtering, leading to missing median lines in boxplots (#394)
Changed
- Reverted an earlier decision to materialize CTEs and the global query in Rust before registering them back to the backend. We now keep the data purely on the backend until the layer query as was always intended (#363)
- Relieved some grammatical constraints on the SQL-portion before the VISUALISE portion (#364).
- Simplified internal approach to DataFrame with DuckDB reader (#365)
- Moved the CLI to its own module rather than be part of the main crate (#379)
- Restructured CLAUDE.md to better deal with the rising complexity of the project (#382)
- Renamed the
errorbarlayer torange. The geom was never error-specific and is generally useful for displaying intervals (min/max ranges, candlestick wicks, percentile bands, etc.). - The
segmentlayer now requires bothxendandyend(rather than auto-filling a missing endpoint from the start position). For axis-aligned 1D intervals — lollipops, candlestick wicks, etc. — use therangelayer instead.
Removed
- Removed polars from dependency list along with all its transient dependencies. Rewrote DataFrame struct on top of arrow (#350)
- Moved ggsql-python to its own repo (posit-dev/ggsql-python) and cleaned up any additional references to it
- Moved ggsql-r to its own repo (posit-dev/ggsql-r)
[2.7.0] - 2026-04-20
- First alpha release. No changes tracked before this