Repository navigation
v1.16.0: Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep
LatestRelease notes below are the CHANGELOG [1.16.0] section (website version). Upgrading from v1.15.0? See the migration guide.
Also released with this tag: VS Code extension 1.16.0 on the Marketplace, and pygosqlx 1.16.0, now on PyPI (pip install pygosqlx), versioned in lockstep with GoSQLX from this release.
go get github.com/ajitpratap0/GoSQLX@v1.16.0
go install github.com/ajitpratap0/GoSQLX/cmd/gosqlx@v1.16.0Covers everything merged since v1.15.0: the October 2026 performance audit
(#566) with large allocation cuts across tokenizer, parser, AST and LSP; the
batch-2/batch-3 correctness sweep (round-tripping SQL(), formatter and
gosqlx format, render-time injection hardening); new pkg/highlight SQL
syntax highlighting across API, CLI, LSP, WASM and playground; a fully
dialect-aware LSP; a hardened gosqlx-mcp HTTP server with ChatGPT app support
and a Claude Code plugin; editor-accurate positions (a tab is one column); and
pygosqlx 1.16.0 with dialect support, lint, security scan, transpile and
prebuilt wheels.
The release is source-compatible: exported signatures are unchanged apart from
additions. Runtime behaviour does change (rendered SQL, token positions, error
codes and text, parse acceptance including MySQL ||, CLI refusals, MCP HTTP
limits), so read Behavior changes before upgrading.
Behavior changes
- Token positions match editors: a tab is one column and
Startskips
comments: the tokenizer counts a tab as one column (it was four), so
a\tbputsbat column 3. The first token after one or more comments, and
the EOF token after a trailing comment, now start at the token itself rather
than at the comment. Columns stay 1-based byte offsets. Parse errors, LSP
diagnostics, semantic tokens, CLI output and the playground move
accordingly, and error carets repeat the source line's tabs. Consumers that
mapped columns back to text with tab width 4 must treat them as plain byte
offsets.Comment.Start/Endare unchanged. (#826, fixes #797) ||is logical OR under MySQL and MariaDB: with themysqlormariadb
dialect (API, linter, CLI--dialect, LSP),a = 1 || bnow parses as
(a = 1) OR b, aBinaryExpression{Operator: "OR", PipesOr: true}, instead
ofa = (1 || b).SQL()and the formatter write||back. The linter and
the security scanner treat it as OR. Passparser.WithPipesAsConcat()/
gosqlx.WithPipesAsConcat()to keep concatenation (PIPES_AS_CONCAT).
Transpiling a||concatenation to MySQL/MariaDB now emitsCONCAT(...),
and a MySQL||OR becomesORfor other targets. Other dialects are
unchanged. (#790, fixes #781)- Composite field access is dialect-gated:
(expr).fieldparses under
PostgreSQL, BigQuery,genericand the default dialect. MySQL, MariaDB, SQL
Server, SQLite, Oracle, Snowflake, ClickHouse and Redshift return E4001
("composite field access ... is not supported in "). Array
subscripts on parenthesized values are not gated. (#843, fixes #839) - L006 reports a byte column: the column-alignment rule reports the 1-based
byte column of a misaligned column's first character (a tab is one column),
consistent with token and error positions. Tabs still count as 4 spaces when
comparing alignment, so which lines are flagged is unchanged. (#840, fixes
#830) - Parenthesized array operands keep their AST grouping:
(items)[1]now
retains aParenthesizedExpressionin the subscript'sArrayfield. This
preserves arithmetic precedence and lets(items)[1].nameround-trip without
becoming the invaliditems[1].name. (#834, fixes #833) gosqlx-mcpHTTP hardening: rate limiting now keys on the TCP peer
address and no longer trustsX-Forwarded-For(which let any client evade
limits and grow limiter memory without bound). Behind a proxy, set
GOSQLX_MCP_TRUSTED_PROXY_HEADER(e.g.CF-Connecting-IP, asrender.yaml
does) orGOSQLX_MCP_TRUSTED_PROXY_HOPS. Limiter memory is capped with
least-recently-seen eviction; GET (SSE) streams are capped at 4 per client,
and a GET refused by that cap costs no rate-limit budget; JSON-RPC batches
are charged per tool call, and a malformed non-empty body or empty batch
costs 1. Denials are HTTP 429 withRetry-Afterand the request id (a denied
initialize, alone or in a batch, keeps HTTP 200 for mcp-go clients). Oversized bodies (over
64 KB) get 413, bodies not received within 30s get 408 or a closed
connection on any route, idle connections close after 120s and headers are
capped at 32 KB. Bearer tokens are compared in constant time and the
Bearerscheme is case-insensitive. Tool input problems (missing, empty or
non-stringsql, parse/format failures, wrongly typed options) are now
isErrortool results naming the argument instead of JSON-RPC -32603
internal errors, andindent_sizemust be an integer from 0 to 16.
With neither trusted-proxy setting,gosqlx-mcplogs a startup warning
that all clients behind a proxy share one bucket. The opt-in
GOSQLX_MCP_TRUSTED_PROXY_SECRET_HEADER/GOSQLX_MCP_TRUSTED_PROXY_SECRET
pair makes the trusted header and hops apply only to requests that carry the
shared secret (constant-time compare); other requests are keyed by the peer
address (see docs/MCP_GUIDE.md).
Newpkg/mcpAPI:NewRateLimitMiddleware(cfg, next),
Config.TrustedProxyHeader,Config.TrustedProxyHops,
Config.TrustedProxySecretHeader,Config.TrustedProxySecret. (#831, #841,
fixes #838)- MCP
format_sqluppercases more keywords:uppercase_keywords: truenow
also uppercases operator and function keywords (AND,OR,COUNT);
indent_sizeaffects nested blocks only, as now documented. (#823) - Comments now count toward
MaxTokens: tokens plus collected comments may
not exceedMaxTokens(1,000,000). Past that the tokenizer returns the
existing E1007 token-limit error at the comment or token that crosses the
limit. Before, comment-only input was bounded only byMaxInputSize; 10 MB of
--\ncollected 3.4M comments and peaked at about 960 MB RSS. Pooled
tokenizers drop aCommentsslice whose capacity exceeds 1024 entries on
Reset, and
ReleaseASTdoes the same forAST.Comments. (#597, part of #566) MaxRecursionDepth(E2007) now covers unary+/-,NOTand derived
tables: these three paths recursed outsideparseExpression, so their
nesting was limited only by input size. They now share its depth counter and
RecursionDepthLimitError(limit 100): 99 nested unary operators orNOTs
in a select list orWHEREparse and 100 return E2007.
(#597, part of #566)$nameplaceholders keep their text:$followed by a name with no
closing$used to produce a bare"$"placeholder and drop the name. It is
now one placeholder token whose value is"$name", matching@nameand Oracle
:name. Token counts, types and spans are unchanged, but formatter output and
fingerprints of such queries change.$1,$$...$$and$tag$...$tag$are
unaffected. (#597, part of #566)- Set-operation chains render in linear time: the formatter used to copy
the rendered left side at every level of a left-deepUNION/INTERSECT/
EXCEPTchain. It now writes the chain once. Output is byte-identical; only
time and memory change (80,000UNIONs: 4.1 s and 46 GB allocated -> 6 ms and
101 MB).Format,Fingerprint,NormalizeandTranspilebenefit too.
(#597, part of #566) - Token values reference the input (zero-copy): identifiers, numbers,
comments, placeholders, bracket identifiers, dollar-quoted bodies and clean
quoted literals/identifiers are now substrings of the input rather than
per-token copies, and so are the AST strings built from them. Keeping any of
them keeps the whole input string alive, so callers that cache values should
strings.Clonethem.Tokenize([]byte)copies the input once, so tokens never
alias the caller's mutable buffer.ExtractTables/ExtractColumns/
ExtractFunctions/ExtractMetadata(and theQualifiedvariants) and
schema.LoadFromDDL/LoadCatalogFromDDLclone their result strings so they
don't pin the input.Tokenizer.ResetandParser.Resetnow release the
previous input instead of keeping it reachable from the pool. (#595, fixes
#546 and #548; #586, fixes #552) - Oracle
:1/:nameare bind variables: underDialectOracle,:
followed by digits or an identifier is oneTokenTypePlaceholder(value
includes the colon). As a consequence, Oracle-modearr[1:n](not valid
Oracle) now fails with "expected ], got PLACEHOLDER". Other dialects are
unchanged. (#595, fixes #520) INSERT ... WITH ... SELECTis accepted, andgosqlx formatkeeps leading
CTEs: the INSERT error for a missing source now reads "expected VALUES,
SELECT, or WITH" instead of "expected VALUES or SELECT" (still E2002), and a
CTE written withoutASnow reports "expected AS, got LPAREN" instead of
"expected column name". The CLI formatter used to silently drop a leading
WITHon INSERT, UPDATE and DELETE, which changed the statement's meaning;
it now emits it. (#584, fixes #537)- Multi-part names and parenthesized join groups: column references with
three or more parts now parse in every dialect. The extra qualifiers are folded
intoIdentifier.Tableas a dotted string (for exampleTable: "db.t"), and
Identifier.SQL()quotes each dotted part separately. More than five parts is
rejected with E2004. A leading parenthesized join group in FROM is flattened
into the statement's join chain, so its AST andSQL()match the flat query.
Shapes that flattening would get wrong (a comma list after or inside a group),
and an alias or ClickHouseFINALafter a group, are rejected with E4001.
ExtractColumnsQualifiedsplits a dotted qualifier at its last dot
(db.t.col->{Schema: db, Table: t, Name: col}). (#585, fixes #521) - AST visitors see the tree's own nodes:
Children(),WalkandInspect
now return pointers into the parent's slices for value-typed children (table
references, joins, ORDER BY items, WHEN clauses, update assignments, frame
bounds, MERGE source/target and similar) instead of heap copies. A visitor
that mutates them now changes the AST, and the change shows inSQL()and the
formatter. These pointers become invalid afterReleaseAST. (#588, fixes
#557) ReleaseASTonly pools node types the parser draws from pools:
Identifier,LiteralValue,BinaryExpression,FunctionCall, most other
expressions and the SELECT/INSERT/UPDATE/DELETE statements are no longer put
back byReleaseAST/ReleaseStatements; they become garbage. The typed
Put*functions keep full semantics for hand-built trees. Nodes returned by
Get*are now fully zeroed, and typedPut*wrappers release nested subtrees
(for exampleFunctionCall.Over/Filter) instead of dropping them. (#581,
fixes #555 and #556)- Schema loaders: the PostgreSQL loader reads
pg_constraintand now
requires PostgreSQL 11 or later. Read-only roles now see foreign keys, and
foreign keys into another schema are reported with the referenced table's bare
name. Tables with no foreign keys (and MySQL tables with no indexes) carry a
nil slice instead of an empty one, which serializes as JSONnull. (#582,
fixes #579; #570, fixes #565) - LSP
Document.Linesis shared:DocumentManager.Getreturns a fresh
*Documentheader, but itsLinesnow aliases the manager's immutable slice.
Treat it as read-only. (#573, fixes #564) gosqlx formatrenders throughpkg/formatterand refuses output that does
not round-trip: the CLI's private renderer is gone;
cmd/gosqlx/cmd/sql_formatter.gois now a thin adapter over
formatter.FormatStatement.SQLFormatter,NewSQLFormatterandFormat
keep their signatures,FormatterOptionsgains aDialectfield and
AlignColumnsis now ignored. CAST,::, EXTRACT, INTERVAL, ANY/ALL, tuples,
subscripts, ARRAY, ROLLUP/CUBE/GROUPING SETS, WITHIN GROUP, set-operation
subqueries, ON CONFLICT, RETURNING, PIVOT and ALTER TABLE used to be
corrupted with exit 0 and now format correctly. Output layout follows
pkg/formatter(CLI separators;\n\n/;, no trailing semicolon). If the
formatted text does not re-parse, or re-parses to a different query, the
command exits non-zero with "formatted output does not re-parse" and
format -ileaves the file untouched. It also refuses output that would drop
meaningful identifier quotes or change a string literal. With
--dialect mysql/mariadbthe output must keep exactly the input's
double-quoted tokens, because MySQL reads"..."as a string:
WHERE name = "bob"is refused rather than rewritten asname = bob, and
`select`rather than rewritten as"select". ANSI_QUOTES-style input
("users"."id",FROM "users") is formatted as before. (#599, #624)Extract*andExtractMetadatareturn sorted results; L006/L009 are
deterministic:ExtractTables,ExtractTablesQualified,ExtractColumns,
ExtractColumnsQualified,ExtractFunctionsandExtractMetadata(and the
MCPextract_metadatatool, the cbinding JSON and integrations built on them)
came out in random map order. Strings now sort in ascending byte order and
QualifiedNamevalues by Schema, then Table, then Name. Costs about 3-15%
ns/op on the Extract benchmarks; allocs/op are unchanged. L006 now picks the
earliest continuation-column indent when counts tie, and the text-fallback
path of L009 lists violations in first-seen table order. Counts, messages and
lines are unchanged. (#618, #619, #625)Children()ofCreateTable,FunctionDesc,TriggerExecBodyand
TriggerReferencingreturns pointers:CreateTable.Children()returns
&c.Name,&c.Columns[i]and&c.Constraints[i]instead of value copies, so
type switches on*ObjectName,*ColumnDef,*TableConstraintand
*FunctionDescmatch and edits made insideInspectorWalkreach the
parent AST. Visit order is unchanged. (#617, #621)- Quoted identifiers that need quotes keep them on render:
ast.Identifier
gainsQuoted, set for ANSI double-quoted names.SQL(),FormatAST,
Normalize,FingerprintandTranspileoutput now re-quotes such
identifiers, and CTE names and column lists,CREATE [MATERIALIZED] VIEWnames
and column lists and FROM/JOIN table names that are reserved words or contain
non-identifier characters. New helpersast.QuoteNameand
ast.QuoteQualifiedName.SELECT "select"used to render as
SELECT select. A name quoted only for its case ("Total","MixedCase")
renders bare, so it keeps matching the alias or column list it refers to;
the AST does not record quoting for those. Fingerprints of"id"andid
are unchanged and still equal; only names that need quotes (such as
"select") now normalize with them. SQL Server#temp, Snowflake
@stage/pathand names with Unicode combining marks stay bare. (#611, #632) - Smart quotes inside ASCII-quoted tokens are content: a string or
identifier opened with'or"now ends only at the same ASCII quote.
“ ” ‘ ’ « »inside it are kept as written instead of being read as the
closing quote. Tokens opened with a smart quote keep their previous rules.
(#632) - SQL Server table hints must be bare words:
WITH (NOLOCK, UPDLOCK)is
unchanged, butWITH (INDEX = ix)and quoted or bracketed hints are now
parse errors; they used to be accepted and the hint silently dropped. SQL
Server now accepts double-quoted constraint names (CONSTRAINT "x y"), and
every dialect acceptsCAST(x AS "type"). A non-ASCII spelling such as
lıkeis stored asLIKE; the formatter keepsWITHin
WITH CHECK OPTION; and a hand-builtMINUSset operator renders as the
equivalentEXCEPT. (#604, #632) - Readable-style formatting emits one trailing
;: withAddSemicolon
(readable style,FormatStringdefault) only the outermost statement ends with
;. CTE bodies,IN/EXISTS/scalar subqueries, set-operation operands and
the query ofINSERT ... SELECT/CREATE VIEWno longer get an inner;or
;;, and a set-operation chain ends with a single;. Compact style is
unchanged. (#602, #631) - ClickHouse
SELECT ... SETTINGS ... INTO OUTFILEis rejected: the
SETTINGS clause is now parsed rather than skipped, so the trailing
INTO OUTFILEthat used to be swallowed returns a syntax error, matching
SELECT ... INTO OUTFILEelsewhere. Malformed SETTINGS lists and a bare
FORMATafter SETTINGS also return syntax errors; a trailing
FORMAT <name>after SETTINGS is still accepted. (#600, #631) - A bare
returningalias inside an INSERT source orUPDATE ... FROMis
treated as RETURNING: while parsing anINSERTsource query or an
UPDATE ... FROMlist, an unquotedreturningis no longer taken as an
implicit table or select alias. Quotedreturningand aliases inside nested
derived tables,IN/EXISTS, CTE bodies and scalar subqueries are
unaffected.RETURNINGis also matched case-insensitively. (#610, #623) - Qualified names with more than 5 parts return E2004: dotted object names
in FROM, DML targets and DDL (TABLE, VIEW, INDEX, USE and MySQL statements)
are capped at 5 parts, the limit already applied to column references.
Every caller (FROM, JOIN, INSERT/UPDATE/DELETE, CREATE, DROP, TRUNCATE,
REFRESH, SHOW/DESCRIBE/REPLACE, USE, ClickHouse MVTO) reports this E2004
rather than rewrapping it as E2002 "expected table name", and Snowflake's
permissiveCREATE STAGE/SEQUENCE/FILE FORMATpath now rejects such names
instead of keeping a truncatedRawSQL. Names of 1 to 5 parts render
byte-identically. (#613, #627) - Error context windows are bounded on long lines:
(*Error).Error(),
FormatMultiLineContextandFormatContextWindowclip source lines over 120
bytes to about 60 bytes either side of the error column (UTF-8 safe, marked
with...) and cap caret padding. A 100 KB single-line query used to produce
a 205 KB E1002 string and now produces about 400 bytes. Lines of 120 bytes or
less render byte-identically. (#615, #626) - Pooled tokenizers release oversized
lineStarts:Tokenizer.Resetkeeps
the backing array only up to 8192 entries (64 KiB) and otherwise replaces it
with a 16-entry slice, so a pooled tokenizer no longer holds about 72 MB after
an input with millions of lines. Token output, positions and errors are
unchanged. (#614, #626) - Postgres schema loader reports index columns and key flags correctly:
Index.Columnsis in index key order and excludes INCLUDE columns, which move
to the newIndex.IncludeColumns. Expression keys appear as
pg_get_indexdeftext instead of being dropped, and indexes on partitioned
parents are listed. PK/UNIQUE column flags come frompg_constraint, so each
column appears once,IsUniqueis set only for a single-column UNIQUE and a
SELECT-only role sees the same schema as the owner.ForeignKeygains
RefSchema(empty for MySQL and SQLite). (#620, #622) - Named windows round-trip:
OVER wnow renders asOVER winstead of
OVER (w), andWINDOW w AS (...)no longer repeats its name inside the
parentheses. The non-standardWINDOW w AS w2is normalized to
WINDOW w AS (w2). (#608, #629) - LSP semantic token legend changed: the non-standard
identifiertoken
type is replaced by the standardvariable, which now covers table, column
and alias names and quoted identifiers (double-quoted identifiers were
stringbefore). Function names arefunctionand type names aretype.
Comments and placeholders ($1,?,:name,@var) are now emitted, as
commentandparameter. AND, OR and NOT staykeyword. Positions are
UTF-16 code units. Clients with custom rules keyed on the oldidentifier
type (for example Neovim@lsp.type.identifierhighlight links) must switch
tovariable. (#825) - pygosqlx requires Python 3.10 or later: 3.8 and 3.9 are dropped, and
the package metadata lives only inpyproject.toml(setup.pyis a shim
that tags platform wheels). (#851)
Added
-
Claude Code plugin and marketplace:
/plugin marketplace add ajitpratap0/GoSQLXthen/plugin install gosqlx@gosqlxinstalls the
gosqlxplugin (plugins/gosqlx/): the hosted MCP server, five SQL skills
(analysis, formatting, security review, linting, dialects), four slash
commands, a read-onlysql-revieweragent and an opt-out PostToolUse hook
that validates edited.sqlfiles when thegosqlxCLI is installed.
(#823) -
ChatGPT app support in
gosqlx-mcp(OpenAI Apps SDK and MCP Apps):
analyze_sqllinks an embedded report widget
(ui://gosqlx/analysis-v1.html,text/html;profile=mcp-app); all tools now
returnstructuredContentalongside the unchanged text JSON, carry
securitySchemes/invocation-status metadata, and the server sends usage
instructions. The server reportsgosqlx.Version;/healthreports the real tool count; the
optionalGOSQLX_MCP_OPENAI_APPS_CHALLENGEenv var serves
/.well-known/openai-apps-challengefor app verification; idle MCP sessions
have their transport state swept 30–45 minutes after their last request
(SessionIdleTTL, sweep every TTL/2) instead of living until restart, and
session IDs are generated statelessly and redacted from transport error logs
(#835).Server.Shutdown(ctx)stops the transport, andServer.Start
sets read-header and idle timeouts. Theanalyze_sqlwidget caps long
lists, shows failed calls as errors and avoids blocked clipboard calls. Setup:
integrations/chatgpt/README.md, details:docs/CHATGPT_APP.md. (#823;
#831, #835) -
SQL syntax highlighting (
pkg/highlight):highlight.Spans(sql, opts...)
classifies SQL into sorted, non-overlapping byte-offset spans tagged with one
of 13 stable categories (keyword, function, type, identifier,
quoted-identifier, string, number, constant, operator, punctuation, comment,
placeholder, error). It is dialect-aware viaWithDialect, never fails
(input after the first lexical error becomes oneerrorspan), and detects
functions in call position and types in type position. Renderers:ANSI/
WriteANSI(256-color, true color or attributes only),HTML/WriteHTML
(classes or inline styles) andCSS, withdark,lightandmonothemes.
Spans marshal to JSON. Built on top of it:gosqlx.Highlight,gosqlx.HighlightANSIandgosqlx.HighlightHTML.gosqlx highlight [file|SQL](aliashl, reads stdin) with
--format ansi|html|json|css,--theme,--dialect,
--color auto|always|never(honors TTY,NO_COLORandCOLORTERM) and
--standalonefor a full HTML page.- LSP
textDocument/semanticTokens/fullusespkg/highlight(see Behavior
changes), with per-line splitting of multi-line tokens and coverage of
every dialect keyword and all comments. - WASM
gosqlxHighlight(sql[, dialect])returns spans with UTF-16 offsets;
the gosqlx.dev playground uses it for editor highlighting.
See docs/HIGHLIGHTING.md. (#825; #836)
-
Dialect-aware LSP server: the dialect from
initializationOptions.dialect
now drives every feature, not only semantic tokens. Diagnostics, formatting
and document symbols tokenize and parse with the dialect's rules (MySQL
LIMIT 5, 10is valid undermysql,ILIKEis reported undermysql);
completion and hover add the dialect's own keywords. The server also handles
workspace/didChangeConfigurationwith{"gosqlx": {"dialect": "..."}}:
it switches dialect without a restart, re-publishes diagnostics for open
documents and, when the client advertisesrefreshSupport, sends
workspace/semanticTokens/refresh. Invalid values fall back togeneric, as
ininitialize. The defaultgenericdialect keeps the previous behaviour
for every feature. See docs/LSP_GUIDE.md.
(#828) -
--dialectflag for theparse,format,analyze, andlintCLI
commands: dialect-specific SQL (e.g. MySQLLIMIT offset, count) can now be
parsed by these commands, matching the existingvalidate --dialect. Unknown
dialect names are rejected up front. The default remains PostgreSQL.
pkg/linter.Lintergains aSetDialect(keywords.SQLDialect)method so the
linter can tokenize/parse dialect-specific syntax, andlint --securitynow
parses with the selected dialect. (#526) -
?positional placeholders for MySQL, MariaDB and SQLite: under these
dialects a bare?is now aTokenTypePlaceholder(the bind parameter used
by theirdatabase/sqldrivers) instead of the PostgreSQL JSON key-existence
operator. PostgreSQL and the other dialects keep?/?|/?&as JSON
operators, and the default dialect is unchanged. (#527, fixes #522) -
Oracle bind variables:
:1,:10,:nameand:new_aparse under
DialectOracleand round-trip byte-for-byte throughSQL()and the
formatter. PreviouslyWHERE id = :1failed with "unexpected token: COLON".
(#595, fixes #520) -
Tokenizer.TokenizeString(s string)and
Tokenizer.TokenizeStringContext(ctx, s string): tokenize a string in place
without the[]byteround trip. Both return[]models.TokenWithSpan; token
values references. (#595, fixes #546) -
ClickHouse
SAMPLEandORDER BY ... WITH FILLround-trip:SAMPLEnow
renders, andWITH FILL [FROM x] [TO y] [STEP z]is stored in the new
WithFillClauseinstead of being skipped. (#534) -
ClickHouse tuple/array element access with a dot (
d.1,tuple.2).
(#535) -
JOIN ... USING (cols)is kept: stored in the newJoinClause.Using
(one*ast.Identifierper column,Conditionleft nil) and rendered as
USING (c1, c2)bySQL(),pkg/formatterandgosqlx format, instead of
ON cols.Children(),WalkandReleaseASTcover the columns, and the
schema validator checks each one. (#603, #630) -
Parenthesized set operations: a derived table in FROM, JOIN or a comma
list can be a set operation, aWITHquery or a doubly parenthesized query,
and a statement or set-operation operand can be written as(query), e.g.
(SELECT 1) UNION (SELECT 2). NewTableReference.SetSubqueryand
SetOperation.LeftParenthesized/RightParenthesized; parentheses that
change meaning (A EXCEPT (B UNION C)) are kept on render. ORDER BY/LIMIT the
AST cannot represent returns E4001. (#612, #630) -
Window frame
GROUPSunit,EXCLUDEclause, named-window refinement and
INTERVALqualifiers:GROUPSframes andEXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS(newWindowFrame.Exclude) parse and render;
OVER (w ORDER BY z)refines an existing window via the new
WindowSpec.BaseWindow;INTERVAL '1' DAYandINTERVAL '1-2' YEAR TO MONTH
parse into the newIntervalExpression.Qualifier, which fixes
d + INTERVAL '1' DAYrendering asINTERVAL '1' AS DAY.GROUPSand
EXCLUDEare not gated by dialect. (#608, #609, #629) -
INSERT forms:
RETURNINGafterINSERT ... SELECT,INSERT ... DEFAULT VALUES(newInsertStatement.DefaultValues), MySQL/MariaDBINSERT ... SET
(newInsertStatement.SetSyntax), parenthesized query sources
(INSERT INTO t (SELECT ...)) andUPDATE ... FROMwith a comma-separated
table list.SQL()andFormatASTrenderDEFAULT VALUESand the SET form;
UPDATE ... FROM ... JOINreturns E4001. (#610, #623) -
ClickHouse table functions and parametric aggregates:
numbers(10),
remote(...),file,s3,cluster,generateRandomand the
keyword-namedurl,merge,valuesandnullparse in FROM as a
TableReferencewithTableFunc.quantile(0.5)(DISTINCT x)keeps DISTINCT
with the argument group; DISTINCT or ORDER BY in the parameter group and a
third paren group return E2004. A[...]subscript or slice directly after a
function call (groupArray(x)[1],array_agg(x)[1]) parses as
ArraySubscriptExpression/ArraySliceExpression; SQL Server keeps
count(*) [cnt]as an alias, and SQLite behaves as before. (#605, #606,
#628) -
ClickHouse
SELECT ... SETTINGSis kept: parsed into the new
SelectStatement.Settings([]ast.QuerySetting, stored on the rightmost
SELECT of a set operation) and rendered bySQL()andpkg/formatter.
(#600, #631) -
Rendering of
IGNORE NULLS, named arguments,ON DUPLICATE KEY UPDATEand
INSERT ... OUTPUT:SQL()andpkg/formatter(compact and readable) used
to drop these; they now render, withname => valuevia the new
(*ast.NamedArgument).SQL(). (#600, #631) -
SQL Server table hints and PIVOT rendering:
SQL()renders
WITH (NOLOCK)-style hints andPIVOT/UNPIVOTwith the alias as
AS alias(FROM s PIVOT (...) AS pused to render asFROM s p), and
pkg/formatterrenders hints. (#604, #632) -
ALTER TABLErendering inpkg/formatter:FormatStatementand
FormatASTrender ALTER TABLE operations (ADD/DROP COLUMN, RENAME,
ADD/DROP CONSTRAINT, ALTER COLUMN ...) instead of the bare tokenALTER.
(#599, #624) -
New schema fields:
db.Index.IncludeColumnsanddb.ForeignKey.RefSchema
(see Behavior changes). (#620, #622) -
ast.FieldAccessExpression: represents composite-field selection with a
traversable base expression, field name, quote flag and source position.
(#834, fixes #833) -
Table-function column alias lists and
WITH ORDINALITY: FROM and JOIN
table functions acceptAS g(a, b), and PostgreSQL also accepts
WITH ORDINALITYwith or without an alias list
(unnest(arr) WITH ORDINALITY AS t(elem, n)). They are exposed as
ast.TableReference.ColumnAliases(*ast.ColumnAliasList) and
TableReference.WithOrdinality, plus the new
dialect.Capabilities.SupportsCompositeFieldAccess.SQL()and the
formatter round-trip them. (#843, fixes #839) -
WithPipesAsConcatoptions,ast.BinaryExpression.PipesOrand
dialect.Capabilities.PipesAsLogicalOr(see Behavior changes). (#790) -
gosqlx.LintWithDialect(sql, dialect): runs the full built-in rule set
with a per-dialect cached linter, so AST rules also run on SQL that parses
only in that dialect. Unknown dialects returnErrUnsupportedDialect. (#780) -
pygosqlx 1.16.0: dialects, lint, security scan and transpile:
parse,
validate,validate_detailed,formatand theextract_*functions take
an optionaldialect=argument (for exampledialect="sqlserver"), and
pygosqlx.dialects()lists the supported names. An unknown dialect raises
UnsupportedDialectError, which is also aValueError. New
pygosqlx.lint()runs thegosqlx lintrule set (L001-L010, optional
dialect=andmax_line_length=),pygosqlx.security_scan()returns typed
SQL injection findings with severity counts, and
pygosqlx.transpile(sql, from_dialect, to_dialect)converts SQL between
dialects. libgosqlx gains thegosqlx_*_dialectvariants and
gosqlx_lint,gosqlx_security_scan,gosqlx_transpileand
gosqlx_dialects; each returns a JSON string the caller frees with
gosqlx_free. The existing exports are unchanged.transpile()raises the
newTranspileErrorwhen SQL cannot be parsed or rewritten, and results
come back as theLintResult/LintViolationand
SecurityScanResult/SecurityFindingdataclasses. (#850) -
pygosqlx prebuilt wheels and PyPI release pipeline: wheels bundle the
native library for Linux x86_64/aarch64 (manylinux_2_17), macOS
x86_64/arm64 (13+) and Windows AMD64, and are tested on Python 3.10-3.14.
pygosqlx is now versioned in lockstep with GoSQLX (it was 0.2.0), and each
GoSQLX release tag publishes the matching version to PyPI through Trusted
Publishing (.github/workflows/pygosqlx-release.yml), starting with
pip install pygosqlx==1.16.0.
On a platform with no prebuilt wheel, installing from the sdist now fails
with an error unless the native library has been built first, instead of
installing a package that cannot load; setPYGOSQLX_ALLOW_MISSING_LIB=1
to install anyway and supplyGOSQLX_LIB_PATHat runtime. (#851, #852) -
rules.CoreRules/rules.DefaultCoreConfig(new package
pkg/linter/rules): the core L001-L010 rule set, defined once and shared by
gosqlx lint,gosqlx action, the MCPlint_sqltool, the C binding's
gosqlx_lintandgosqlx.Lint. Each keeps its existing settings and
output;CoreConfigcovers the L003, L004 and L005 limits. (#853)
Changed
tokenizer.NewWithKeywordsno longer consults its keyword table: the
tokenizer's keyword field was never read, so it was removed.NewWithKeywords
keeps its signature and nil check.keywords.Conflicts()now reflects only
explicitkeywords.Newcalls, because parsing no longer calls it. (#595,
fixes #550)
Fixed
- PostgreSQL composite-returning function queries parse correctly:
(get_item(payload)).nameand(get_item(payload)).*retain their base
expression in a newast.FieldAccessExpression, including quoted field names
and chained field/subscript access. SQL rendering, AST traversal, extraction,
security scanning and expression release preserve the nested expression.
DirectJOIN LATERAL get_item(t.payload) AS freferences now parse their
arguments under the same dialect gate as FROM table functions. Any keyword,
reserved ones included, can follow the dot ((a).order,(f(x)).from), as
in PostgreSQL, and renders unquoted. Composite access is dialect-gated (see
Behavior changes). (#834, fixes #833; #843, fixes #839) - Arithmetic and
||on the right of a comparison or LIKE/ILIKE/REGEXP
pattern parse:WHERE a = 1 + 2failed with "expected statement, got
PLUS". Comparisons still do not chain. (#734, fixes #596) - Pooled and zero-value parsers report the default capabilities:
GetParser().Capabilities()and a literal&parser.Parser{}returned
all-false capabilities instead of matchingNewParser(). (#735, fixes #590;
#789, fixes #784) - Oracle
CONNECT BYaccepts general expressions:PRIORis a prefix
operator, soCONNECT BY PRIOR id = parent_id + 1parses. A column named
prioris still an identifier. (#791, fixes #783) - Snowflake
VariantPathrenders:WHERE src:a = 1came out as
WHERE : = 1fromSQL()and the formatter. Keys that are not bare words
are double-quoted. (#786, fixes #782) - Lower- and mixed-case
CROSS JOIN,FULL JOINandGROUPING SETS
parse, so formatter output with lower-case keywords round-trips. (#844,
fixes #842) - Playground Lint and Analyze: Lint runs all 30 rules (it ran 3, and the
tab always reported clean SQL), Analyze reports a parse error instead of
scoring unparseable SQL, security findings and optimization suggestions are
listed, and the editor marks parse errors. (#780, fixes #737, #738, #739,
#758; #796) gosqlx.Versionandgosqlx --versionreport the release version:
v1.15.0 still reported 1.14.0, and so did the MCP server info and/health.gosqlx_version()double free in the C binding: it returned a cached,
process-wide pointer although the binding documents every result as
caller-owned, so the secondpygosqlx.version()call in a process freed it
again and aborted with "pointer being freed was not allocated". It now
returns a fresh string on every call; free it withgosqlx_free.
pygosqlx.__version__reports the packaged version, and editable installs
work with current setuptools. (#847)- LSP
serverInfo.versionand release binaries report the real version:
the LSP server reported a hard-coded1.0.0inserverInfo.versionand now
reports the library version. Thegosqlxbinary bundled with the VS Code
extension prints the release tag fromgosqlx --version; set the CLI
version at build time with
-ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=<version>".
GoReleaser no longer passes a-X main.versionflag that the example
binaries ignored. (#846) An unstamped CLI now reportsgosqlx.Version, so
the CLI and library versions cannot drift, andrelease.ymlfails a tag
that does not matchgosqlx.Version. (#853) - Tokenizer pool no longer leaks dialect state: a tokenizer whose dialect was
changed viaSetDialect(e.g. byvalidate --dialect mysql) is now restored to
the default (PostgreSQL) dialect when returned to the pool viaPutTokenizer.
PreviouslyResetdid not clear the dialect/keywords, so a later
GetTokenizercaller could receive a tokenizer configured for the wrong
dialect and silently mis-parse. The restore lives inPutTokenizer(the pool
boundary), notReset, becauseResetalso runs at the start of every
Tokenizecall where the configured dialect must persist. The keyword table is
rebuilt only when a non-default dialect was configured, so the common path is
unaffected. (#525) - Parentheses in expressions are preserved:
(a = 1 OR b = 2) AND c = 3
used to re-render asa = 1 OR b = 2 AND c = 3, which changed the predicate.
The parser now keeps grouping in a newast.ParenthesizedExpressionnode.
(#532, fixes #519) IS NOT NULLno longer renders asIS NULL, and ClickHouse parametric
aggregates keep their parameter group inSQL()
(quantileTDigest(0.95)(value)stayedquantileTDigest(value)). (#529,
fixes #530; AST half of #531)gosqlx formatkeeps ClickHouse parametric aggregate parameters: the CLI
renderer also dropped the(params)group. (#583, fixes #531)gosqlx formatno longer panics onNOT EXISTS (subquery). (#591, fixes
#587)- Numeric window-frame offsets are kept:
ROWS BETWEEN 1 PRECEDING AND CURRENT ROWrendered asROWS BETWEEN PRECEDING AND CURRENT ROWinSQL(),
pkg/formatterand the CLI. The CLI also stopped addingBETWEENto a
single-bound frame. (#592, fixes #589) INSERT INTO t (cols) WITH c AS (...) SELECT ...parses in every
dialect (it failed with E2002 "expected VALUES or SELECT, got WITH"). AWITH
is also no longer dropped from a CTE query whose body is a chain of two or more
set operations. (#584, fixes #537)- MySQL three-part identifiers and parenthesized join groups parse: SHOW
CREATE VIEW output such as`sakila`.`city`.`city`andFROM ((a JOIN b ON ...) JOIN c ON ...)is accepted. (#585, fixes #521) - Schema loaders: PostgreSQL composite foreign keys are no longer
cross-multiplied, same-named constraints on different tables stay separate,
partition clone constraints are skipped, and quoted index column names decode
exactly. The MySQL foreign-key join also matches on table name. (#582, fixes
#579) - Schema loaders no longer deadlock or return empty tables: a
*sql.DBwith
MaxOpenConns=1used to deadlock and a pooled SQLite:memory:database
returned tables with no columns, because cursors were nested. SQLite table
names are passed as bound parameters, so names containing quotes or
backslashes load, and PostgreSQL PK/UNIQUE flags no longer leak between
tables that share a column name. (#570, fixes #565) - AST release-path defects:
PutSelectStatementno longer releases a
derived-table subquery twice throughJoins[i].Left, which could make two
laterGetSelectStatementcalls return the same object. Pooled expression
nodes no longer carry stale fields. (#581, fixes #555) - Long comment runs no longer grow the stack: the tokenizer skips
consecutive comments in a loop instead of recursing once per comment. (#597) NOT EXISTS (subquery)rendered asNOT (EXISTS (...) NOT ), which failed
to re-parse with E2002.SQL(),FormatASTandFormatStringnow render
NOT EXISTS (...); the AST shape is unchanged. (#601, #632)- Table-function arguments in FROM were dropped on render:
generate_series(1,10) AS gcame out asgenerate_series g.SQL()and
pkg/formatternow render the call for PostgreSQL, Snowflake, BigQuery and
ClickHouse table functions, andtransform.ReplaceTablerenames
table-function references. (#607, #628) - Bogus context in 15 parser errors: nine E2002 and six E2004 sites passed
an explanatory sentence as the SQL source, soError()printed it as line 1
of the user's query. The context is now empty and the sentence is the
error'sHint; code, message and position are unchanged. The BETWEEN, LIKE,
REGEXP and IN-subquery operand wrappers likewise stop printing a fake
1 | )snippet and keep the inner error asCause. (#616, #627) gosqlx formatdropped or mangled constructs (see Behavior changes):
JOIN ... USINGprintedON LIST, and WINDOW, INTERVAL,UPDATE ... FROMand
DEFAULT VALUESwere lost. (#599, #603, #608, #609, #610, #624)- Readable-style output that did not re-parse: inner
;and;;removed
(#602, #631); readable-style re-parse failures over the testdata corpus fell
from 36 to 6. - Tokenizer error locations point at the offending construct: an
unterminated dollar-quoted string ($$/$tag$) and an unterminated SQL
Server[identifier]now report their opening delimiter instead of the end of
input, and a double-quoted identifier broken by a newline now reports its
opening quote on the correct column. These errors, invalid escape sequences
(which point at the character after the backslash) andStringLiteralReader
errors now use the same rule as token spans: 1-based line, 1-based byte
column, a tab counting as one column. Before, they used internal rune counts,
so the column drifted after multibyte characters and could be 0, and a dollar
quote could report the wrong line. The two errors with no position
(keywords cannot be nil, input too large) report column 1 instead of 0.
Error codes and messages are unchanged. (#827, follows #826) - Dialect lists now agree everywhere: the
--dialecthelp onparse,
format,analyze,lintandvalidate, the CLI's unknown-dialect error,
config-file validation, the MCPvalidate_sqlschema, the playground WASM
bridge, the GitHub Action and the VS Codegosqlx.dialectsetting all list or
accept the same 11 names:generic,postgresql,mysql,mariadb,
sqlserver,oracle,sqlite,snowflake,bigquery,redshift,
clickhouse. Before, help listed 7, the error omittedgenericand
clickhouse,pkg/configrejectedgeneric,bigquery,redshiftand
clickhouse, and the VS Code setting offered 6 (and suggestedmysqlfor
mariadb). The Go lists derive from the newkeywords.DialectNames(), and a
test checks the VS Code extension and the example configs and READMEs against
it. Names stay case-sensitive in the CLI.transpile --from/--tokeep their
own list of transpilable dialects. (#829)
Performance
Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the
#597 figures are single before/after measurements).
- Linear tokenizer position tracking: line/column lookup no longer rescans
from the start of the line for every token, which made long single-line input
quadratic.LargeSQL_100KB20.6 ms -> 1.96 ms,VeryLargeSQL_1MB1755 ms ->
21.8 ms,LargeInClause4.87 ms -> 136 us; allocs/op unchanged. Also adds
ASCII fast paths for keyword and identifier scanning and a counted token-slice
capacity. (#577, fixes #544, #545, #547, #549) - Zero-copy token values: tokenizer allocs/op drop 88-93%
(Tokenizer/ComplexSQL120 -> 14,MediumSQL_10KB2823 -> 203,
LargeSQL_100KB28139 -> 1988) and gosqlx parse allocs/op drop 37-66%
(Parse_ComplexQuery193 -> 74). B/op rises 1-6% on the tokenizer, and
Tokenize_SimpleQueryon the[]bytepath is about 3% (11 ns) slower. (#595,
fixes #546) - Escape-free quoted literals are substrings: quote-heavy inputs go from
400+ to 2 allocs/op and run 31-56% faster;TokenizeString/ComplexSQL13 -> 5
allocs/op. The unused global buffer pool is gone. (#595, fixes #548) - Dialect-aware entry points use the pools:
ParseBytesWithDialect
21.8 us / 70 allocs -> 4.06 us / 28 allocs (the same asParseBytes),
ParseTreewith a dialect 22.0 us -> 1.25 us,SetDialect35.6 us / 76
allocs -> 0 allocs,transpiler.Transpile20.2 us -> 1.69 us and
transform.ParseSQLWithDialect35.9 us -> 0.84 us. (#595, fixes #550) - Allocation-free parser preprocessing: keyword classification and
compound-token expansion no longer allocate. Parser allocs/op drop 20-50%
(SimpleSelect_1000_Columns2020 -> 1018,SELECT_Complex198 -> 114), and
qualified identifiers and clause parsing lose an allocation each. (#578, fixes
#551, #553, #554) - Parser-owned preprocess buffer:
ParserSimpleSelect9 -> 7 allocs/op and
1696 -> 816 B/op,ParserComplexSelect22 -> 20 allocs/op (-64% B/op).
(#586, fixes #552) - Writer-based
SQL()serialization: a statement renders into one pooled
buffer.BenchmarkSQL/7line57 -> 1 allocs/op and15line122 -> 1, about
3x faster; output is byte-identical. (#575, fixes #558) Children()stops heap-copying nodes:Inspect47 -> 26 allocs/op and
3216 -> 928 B/op,Lint/Realistic728 -> 580 allocs/op,Extract*20-45%
fewer allocations. (#588, fixes #557)- Cheap
PutExpressionafter large lists: the work queue is cleared only to
its high-water mark.PutLeafAfterBigList2995 ns -> 19 ns. (#581, fixes
#555) ExtractMetadatain one AST pass: 114 -> 24 allocs/op, 8264 -> 2208 B/op,
about 2.4-2.8x faster. (#572, fixes #561)SplitStatementsslices the source: 855 -> 1 allocs/op, 49.3 -> 6.4 KiB/op
and 32-41% faster on a 200-statement script. (#569, fixes #559)gosqlx.Lintbuilds its rule set once:Lint/Tiny169 -> 137 allocs/op.
L015 no longer upper-cases the whole input (1 -> 0 allocs/op, 96 KiB -> 0 B on
~100 KB of SQL). (#568, fixes #562)- AST and parser release in batch helpers:
ValidateMultiple,
ParseMultiple(error paths),Analyze,Transpileand thetransformparse
helpers return ASTs and parsers to their pools, so pool Get/Put counts balance.
(#571, fixes #560) ScanSQLliteral prefilters: regexes whose required literals are absent
are skipped.ScanSQL26.9 us -> 4.7 us (-82%) and malicious-input benchmarks
56-90% faster, with identical findings. (#574, fixes #563)- LSP splits each document once per request:
BuildDiagnosticson 10,000
lines 789 MiB / 512 ms -> 1.2 MiB / 6.8 ms,Hover5296 B / 9 allocs -> 368 B
/ 5 allocs,SignatureHelp3535 B / 50 allocs -> 224 B / 4 allocs, and
sendMessagewrites each frame once (11 -> 7 allocs). (#573, fixes #564) - Schema loaders batch their queries:
Loadruns 4 statements instead of
3N+1. SQLite at 100 tables: 16.7K -> 9.9K allocs/op, 679 -> 250 KiB/op,
3.59 -> 1.92 ms. (#570, fixes #565) - Linear inline-comment detection: 1 MiB of spaces followed by 20,000
comments tokenizes in 9.8 ms instead of 10.3 s. (#597) - Performance baselines refreshed:
performance_baselines.jsonre-measured
on 2026-10-13. Baselines are now about 2.5x the Apple M4 median, because
GitHub's ubuntu-latest runners measure about 1.5-3.2x slower than M4 on these
queries (up to ~3.2x), so a 1.5x baseline would fail CI's slowest runs. (#566) - Qualified-name parsing is linear:
parseQualifiedNamecollects up to 5
parts in a stack array and joins them once instead of concatenating per part.
A 64 KBa.a.a...chain went from 66 ms and 1.17 GB (and parsed) to 8 ms and
10.5 MB (E2004);allocs/opon 3- and 5-part names fell 10% and 25%. (#613,
#627) - Long error lines are cheap to render: error context uses a bounded line
scan. Long-lineError()went from 49 us / 937 KiB to 1.4 us / 1.3 KiB per
op, and the context-format benchmarks are about 20% faster. (#615, #626) - Measured costs of batch-3 correctness fixes:
ast.Identifiergrows from
48 to 64 bytes for theQuotedflag, raising parser B/op by about 4-6%, and
rendering with name quoting makesSQL()about 8% slower with unchanged
allocations (#611, #632).TableReferencegrows from 128 to 136 bytes for
parenthesized set operations:BenchmarkParserSimpleSelect+4.8% and
+16 B/op (#612, #630). ReallocatinglineStartsper call on very large
inputs adds about 2.4% B/op above roughly 410 KB (#614, #626). Sorted
Extract*results cost 3-15% ns/op on the Extract benchmarks (#618, #625).
Security
- Render-time SQL injection closed: quoted names and some literal and
keyword values were written into rendered SQL verbatim, so
SELECT 1 AS "a; DROP TABLE x; --" FROM tcame back fromgosqlx.Format,
SQL(),NormalizeorTranspileas two statements. Every name position
(SELECT, table, derived-table, JOIN, PIVOT, UPDATE/DELETE and MERGE aliases,
window names, DML targets and column lists, DDL names, constraint and
function names, data types) is now quoted when it is not a plain
identifier or is reserved. INTERVAL, PRAGMA, table-hint and PIVOT values are
escaped, and the formatter's-- UNSUPPORTED:comment covers every line.
Keyword fields (join type, set operator, DROP object type, interval unit,
constraint, partition, ALTER, MERGE, frame and lock types, FK actions,
FETCH, SAMPLE, table options, viewWITHoption, SHOW kind) are checked
against exact allow-lists; a hand-built value outside its list is dropped
or rendered so that the output fails to parse, never as an extra statement.
Found by the batch-3 adversarial review and validated with hostile-name and
AST-mutation fuzzing across all dialects. (#632) - Bounded input-driven work: comment counts, nesting depth for unary
operators,NOTand derived tables, comment runs and set-operation rendering
are now bounded or linear (see Behavior changes). These were found by the
pre-merge security review. (#597) - Tokenizer CPU-DoS on long single lines fixed by linear position tracking.
(#577, fixes #544) - Code-scanning and Dependabot alerts: every GitHub Actions
uses:is pinned
to a commit SHA, workflows default to read-only permissions, install commands
use pinned versions or lockfiles, blog slugs are validated (3 XSS alerts),
Docker base images are pinned by digest, and OpenTelemetry v1.47.0,
filippo.io/edwards25519v1.1.1 andgolang.org/x/sysv0.49.0 are picked up.
(#567) - Scanner descends into table-function arguments:
url(...),file(...)
and similar ClickHouse table functions are detected as out-of-band access
(also via aFROM|JOIN <fn>(pattern), and the scanner descends into array
subscripts and slices sopg_sleep(5)[1]is not a bypass. The check is
dialect-agnostic by design. (#605, #607, #628) - Scanner walks nested set-operation trees:
checkUnionInjectionhandles
right-nested and parenthesizedUNION/EXCEPTtrees, so
x UNION (SELECT ... FROM pg_user UNION ...)is no longer missed after
parenthesized set operations became parseable. (#612, #630) - ANSI highlighting neutralizes terminal control characters: an ESC or
other control character inside a string literal or comment could inject
OSC/CSI sequences when highlighting untrusted SQL. C0 controls other than
tab/LF/CR are shown as Unicode control pictures (ESC becomes U+241B), DEL as
U+2421, and C1 controls and invalid UTF-8 as U+FFFD. HTML output was already
escaped. (#836) gosqlx-mcpno longer logs session IDs: transport error paths reachable
by an unauthenticated response-shaped POST logged the client-supplied
Mcp-Session-Id. Non-allowlisted error arguments are now redacted and
generated IDs scrubbed, and the stateless session-ID manager is pinned so
terminated IDs are never retained. (#835)- Dependency patches: certifi 2026.10.19 (CI Sentry script) and markdown-it
14.3.3 (VS Code extension). (#837)
Internal
- CI: claude-code-action v1, fork-PR review skip, Scorecard v2.4.4 and a valid
glama-sync workflow (#536, #538, #539, #540);.gitattributesenforcing LF
line endings for golden fixtures (#594). - Website and playground: the playground WASM is built during deployment,
revalidated by browsers and no longer committed (#543, #580). - Docs: the CHANGELOG restored after the #526 merge emptied it (#593).
- Website and docs: gosqlx.dev redesign (design tokens, light-mode parity,
type scale, home, docs IA and navigation, compare and benchmarks pages),
SEO/a11y/performance passes, /changelog with one page per release, llms.txt
and raw-markdown alternates, and a reproducible competitor benchmark harness
(benchmarks/compare, #719) feeding the site. (#687-#832) - CI: VS Code extension publishes with Entra ID OIDC (#685, #686), website CI
and Lighthouse budgets, weekly claims-drift check, Vercel preview opt-out
with[vercel skip](#821, #832), and internal review/plan files untracked
(#822). Testcontainer startup in the schema loader tests is bounded by the
test deadline, so a slow image pull skips instead of failing the package
(#837). - Docs: README rewrite with a compile-checked quickstart, refreshed pkg.go.dev
docs for ParseTree (#713), all 30 lint rules documented (#785), the CHANGELOG
1.0.0 upgrade guide and 1.9.1 entry (#824);Server.Start/Server.Handler
document that they do not applyRateLimitMiddleware, and
docs/CHATGPT_APP.mdexplains that ChatGPT users share OpenAI's per-IP limit
(#841). - CI: a Python Bindings workflow tests pygosqlx and the raw ctypes ABI on
Linux and macOS with Python 3.10 and 3.13 (#847);release.ymlpins the
GoReleaser current and previous tags to thev*series sopygosqlx-v*
tags cannot truncate the Go release changelog (#851), and the previous-tag
lookup skips the floating Marketplacev1tag (#852);vscode-publish.yml
stamps the tag version into the bundled binary and its smoke test fails if
--versiondoes not match, and the README quickstart job pins the latest
tagged release explicitly (#846). - Docs: new gosqlx.dev pages for Syntax Highlighting, the ChatGPT App,
Upgrading GoSQLX (migration guide) and thehighlightpackage API reference;
docs/api/lsp.mddocuments semantic tokens and runtime dialect changes via
workspace/didChangeConfiguration;docs/api/linter.mddocuments
Linter.SetDialectandgosqlx.LintWithDialect;MCP_GUIDE.mdstates that
Server.Start/Server.Handlerapply no rate limiting unless wrapped in
NewRateLimitMiddlewareand thatlint_sqlruns only L001-L010; the
migration guide and VS Code extension changelog gain their missing v1.15.0
sections;models.Locationdocuments byte columns and UTF-16 LSP
conversion. (#848) The README describesbigqueryandredshift
accurately: generic keyword set plus some dialect-gated grammar. (#853)
All commits since v1.15.0
New Features
- a055354 feat(bench): reproducible competitor benchmark harness feeding the website (#719)
- 83b3041 feat(cli): add --dialect flag to parse, format, analyze, lint (#526)
- e6f39fc feat(highlight): dialect-aware SQL syntax highlighting (API, CLI, LSP, WASM, playground) (#825)
- a2ad545 feat(lsp): apply the configured dialect to every language feature (#828)
- 274d4b9 feat(parser): ClickHouse array/tuple element access via dot (d.1) (#535)
- 18630b9 feat(playground): mark parse errors in the SQL editor (#796)
- 2cfe06d feat(tokenizer): support ? placeholder for MySQL/MariaDB/SQLite (#527)
- 208c0b6 feat(website): light-mode parity via tokens, remove html.light override CSS (#718)
- 23acca7 feat(website): metrics single source of truth and drift check (#696)
- ae29ce8 feat(website): move /blog to /changelog with permanent redirects (#724)
- 3d89b53 feat(website): raw-markdown alternates for docs and complete llms-full.txt (#729)
- 0e565a8 feat: GoSQLX Claude Code plugin and ChatGPT app (#823)
Bug Fixes
- 4644caf fix(ast): preserve IS NOT NULL and ClickHouse parametric aggregate params in output (#529)
- ab57f96 fix(ast): render Snowflake VariantPath in SQL() and the formatter (#786)
- e1d54df fix(ast): return CreateTable, FunctionDesc and trigger children as pointers (#617) (#621)
- 073a085 fix(cbinding): return caller-owned string from gosqlx_version; add Python CI (#847)
- 404b63f fix(ci): make glama-sync workflow valid YAML (#540)
- 1f5bfb4 fix(cli): derive every dialect list from keywords.DialectNames (#829)
- 3734251 fix(cli): format NOT EXISTS without a nil dereference (#591)
- 88f18d0 fix(cli): render ClickHouse parametric aggregate params in gosqlx format (#531) (#583)
- baf528e fix(cli): render gosqlx format through pkg/formatter and refuse non-round-tripping output (#599) (#624)
- 36a964d fix(docs): relative doc links, valid anchors, local-path link and compilable jsqlparser example (#778)
- 4b61d62 fix(highlight): neutralize terminal control characters in ANSI output (#836)
- 0be81ae fix(linter): report L006 column as a byte offset; tighten comment span test (#840)
- 59f6022 fix(mcp): harden gosqlx-mcp HTTP server (rate limiting, timeouts, auth, tool errors) (#831)
- aa4f7da fix(mcp): pin stateless session IDs and stop logging session IDs in transport errors (#835)
- ee539c9 fix(mcp): rate-limit follow-ups from #831 review (#841)
- 3baad9d fix(parser): INSERT ... SELECT RETURNING, DEFAULT VALUES, MySQL INSERT ... SET and parenthesized sources (#610) (#623)
- f430473 fix(parser): accept WITH ... SELECT as the source of INSERT and keep leading CTEs in gosqlx format (#537) (#584)
- 8c1c377 fix(parser): accept general expressions in CONNECT BY (PRIOR as unary operator) (#791)
- 38b6c39 fix(parser): composite field access and table function follow-ups (#839) (#843)
- 95978a4 fix(parser): linear qualified-name joining and correct E2002 error context (#613, #616) (#627)
- 10f08d3 fix(parser): make pooled parser capabilities match NewParser (#590) (#735)
- d546323 fix(parser): make zero-value Parser{} capabilities match NewParser (#784) (#789)
- 0b78b9a fix(parser): match CROSS JOIN, FULL JOIN and GROUPING SETS in any case (#844)
- ae28600 fix(parser): parse arithmetic on the right-hand side of comparisons (#734)
- 4e49e43 fix(parser): parse composite fields and joined table functions (#834)
- 96e6c56 fix(parser): parse || as logical OR in MySQL and MariaDB (#781) (#790)
- 2304418 fix(parser): support three-part identifiers and parenthesized join groups (#521) (#585)
- 8538fe0 fix(playground): correct Analyze and Lint results from the WASM bridge (#780)
- 0715bb9 fix(schema): correct foreign-key join keys in the Postgres and MySQL loaders (#579) (#582)
- d1dfe22 fix(security): resolve code-scanning and Dependabot alerts (#567)
- e19e9cf fix(tokenizer): one column per tab and token Start after trivia (#797) (#826)
- b3c8594 fix(tokenizer): point unterminated-literal errors at the opener (#827)
- acc3d23 fix(tokenizer): restore default dialect on pool return (#525)
- 5e54542 fix(website): always ship and serve a current playground WASM (#543)
- 92542b8 fix(website): changelog timeline contrast, Changelog label, strip heading emoji (#819)
- 62ed408 fix(website): docs TOC max-height, anchor scroll offset, skip-link stacking (#774)
- e81c1ff fix(website): docs chrome surfaces, muted text, TOC scroll-spy, hover (#798 #802 #803) (#818)
- 0134f2a fix(website): highlighted, copyable, theme-aware code blocks and markdown line breaks (#788)
- 3f3ede1 fix(website): light-mode prose links, table dividers, code ligatures, benchmarks table (#817)
- 7d5087f fix(website): privacy page light-mode contrast and OG/Twitter meta (#775)
- 79f5cef fix(website): remove unsourced competitor numbers, re-measure benchmarks (#639) (#692)
- 380c322 fix(website): render content visible without JS; CSS scroll-driven reveal (#691)
- efd1c35 fix(website): technical SEO polish (theme-color, titles, JSON-LD, RSS, manifest) (#792)
- e3b6aeb fix(website): use decorative tokens for non-text fills in light mode (#816)
- 59ce1b6 fix: single-source core lint rules, derive CLI version, fix README dialect note (#853)
Performance
- bb8d255 chore(website/perf): monthly production Lighthouse and RUM budgets (#728)
- 0465ae7 perf(ast): fix release-path defects and stop ReleaseAST pooling node types the parser never draws (#555, #556) (#581)
- 65ac450 perf(ast): make SQL() serialization allocation-light (drop fmt/ToUpper, fix builderPool, writer-based rendering) (#558) (#575)
- 2d3ea2d perf(ast): stop heap-copying nodes in Children() so Walk/Inspect allocate less (#557) (#588)
- c5b870f perf(gosqlx): extract metadata in one AST pass and key qualified collectors by struct (#561) (#572)
- 755f2fe perf(gosqlx): slice source in SplitStatements instead of copying (#559) (#569)
- 2f49d2a perf(linter): build default rule set once in gosqlx.Lint; alloc-free L015 scan (#562) (#568)
- 0d6f875 perf(lsp): split document once per request, share Lines snapshot, hoist static maps, single-write sendMessage (#564) (#573)
- 0ef0966 perf(parser): allocation-free preprocess, qualified identifiers and clause parsing (#551, #553, #554) (#578)
- 4c50994 perf(parser): reuse a parser-owned preprocess buffer and the pooled Statements slice (#552) (#586)
- 809cc75 perf(schema): remove N+1 queries and nested open cursors from the Postgres/MySQL/SQLite schema loaders (#565) (#570)
- 4598431 perf(security): add derived literal prefilters to ScanSQL and hoist lookup tables (#563) (#574)
- afa67f8 perf(tokenizer): Oracle bind variables, substring token values, zero-copy quoted literals, dialect pooling (#520, #546, #548, #550) (#595)
- 498aeca perf(tokenizer): linear position tracking, ASCII fast paths, counted token-slice capacity (#544, #545, #547, #549) (#577)
- edcdeeb perf(website): drop scroll bar, gradient hero glows, lazy-load search (#706)
- 4edb971 perf(website): server components, MotionConfig reducedMotion, static counters (#704)
Other
- de484f1 chore(ci): add weekly claims-drift check and GitHub signals log (#730)
- 1c161fa chore(deps,test): patch certifi and markdown-it; bound testcontainer startup (#837)
- 9ca2dac chore(docs): generate README metrics block and check version banners in CI (#710)
- aa22785 chore(repo): untrack .playwright-mcp and ignore root screenshots (#712)
- ab25aee chore(seo): add Google Search Console verification meta tag (#732)
- 142bff2 chore(wasm): stop tracking generated playground binary (#580)
- 5586fe0 chore(website): add typecheck and lint scripts and a website CI workflow (#635) (#687)
- a0eab01 chore(website/ci): Lighthouse route coverage, error-level assertions and resource budgets (#695)
- b8aee5b ci(claude): upgrade claude-code-action from @beta to @v1 (#539)
- 32960cb ci(claude-review): skip automatic review on fork PRs (#538)
- 7442049 ci(python): build pygosqlx platform wheels and publish to PyPI (#851)
- 85ffb1d ci(vscode): document the immutable OIDC subject used by the publish job (#686)
- e8c02f1 ci(vscode): publish with Entra ID (OIDC) instead of a PAT, with PAT fallback (#685)
- 4721b9c ci(website): opt out of Vercel preview builds with [vercel skip] (#821)
- 142812f ci: update OpenSSF Scorecard action to v2.4.4 (#536)
- 0a2b8fb docs(changelog): restore CHANGELOG.md emptied by the merge in #526 (#593)
- 92b4fa2 docs(changelog): show the 1.0.0 upgrade guide and add the 1.9.1 entry (#787) (#824)
- fce8bc2 docs(changelog): v1.15.0 release notes (back-dated to 2026-07-30) (#633)
- 9c6cfd5 docs(getting-started): lead with Step 1, move feature examples to Feature Tour (#653) (#694)
- 6d87bd3 docs(gosqlx): refresh pkg.go.dev docs for ParseTree and extend examples (#713)
- ae5053a docs(readme): align with released v1.15.0, compile-check the quickstart, drop unsupported 1.5M+ claims (#640) (#690)
- a911327 docs(readme): cut vanity sections, replace emoji feature table, fix doc links (#725)
- 25c1cdf docs(readme): dark-mode logo/format image, canonical links, badges, social preview (#776)
- 1c5eb7a docs(readme): demo GIF, VHS tape, before/after PNG, architecture SVG and logos (#726)
- ae2daca docs(readme): header with logo picture, value line and six badges (#720)
- 3e1b12c docs(readme): stacked install sections and one compile-checked quickstart (#722)
- e9c5db9 docs(release): prepare v1.16.0 (#845)
- 98268fd docs(website): document all 30 lint rules, drop stale version labels, polish guides (#785)
- aeca3f1 docs(website): task-first guides and docs IA restructure with redirects (#727)
- a624dc9 feat(ast,parser): round-trip ClickHouse SAMPLE and ORDER BY WITH FILL (#534)
- a08d7b7 feat(cbinding,python): dialect-aware exports, lint, security scan, transpile (pygosqlx 0.3.0) (#850)
- 50fb716 feat(parser,ast): preserve parenthesized expressions (fix #519) (#532)
- 23df02f feat(website/benchmarks): methodology and reproduce block (#721)
- 39e09f4 feat(website/blog): launch /blog with six evergreen articles (#731)
- 091747e feat(website/compare): per-competitor alternative pages (#723)
- a22990f feat(website/design): semantic design-token system, collapse legacy color tokens (#714)
- 5ff4815 feat(website/design): two-family type system and fluid type scale (#715)
- b118766 feat(website/docs): docs index redesign with Start-here row and real item links (#711)
- efc8d2a feat(website/docs): docs navigation, edit link, last updated, heading search (#709)
- c95aa7c feat(website/home): hero redesign with solid headline, formatted-SQL demo and proof line (#717)
- ba87236 feat(website/home): home sections redesign (proof strip, integrations, dialect cards, performance) (#716)
- 5d9e2f8 feat(website/seo): add pageMeta() helper, single-brand title template, keyword home title (#699)
- a9cd3b4 feat(website/seo): hand-written meta descriptions for docs, blog and playground (#705)
- c079ccb feat(website/seo): per-page Open Graph images via next/og (#703)
- ff3fe0b feat(website/seo): serve llms.txt/llms-full.txt, RSS feed and robots.ts (#708)
- 0fdec4a feat(website/seo): structured data on home, docs, compare and blog (#702)
- 728c544 fix(ast,formatter): NOT EXISTS, quoted identifiers, table hints and PIVOT render correctly (#601, #611, #604) (#632)
- 6188686 fix(ast,formatter): keep numeric window-frame bound offsets (#589) (#592)
- 77ab49a fix(ci,python): skip floating v1 tag in release changelog; fail sdist wheel builds without libgosqlx (#852)
- 1be0808 fix(formatter,ast,parser): no inner semicolons; keep IGNORE NULLS, named args, ON DUPLICATE KEY, OUTPUT, SETTINGS (#602, #600) (#631)
- c35349f fix(linter,gosqlx): deterministic L006/L009 and Extract* result order (#618, #619) (#625)
- f0ebf86 fix(lsp,cli,ci): report the real version from LSP and release binaries (#846)
- 1cd8f78 fix(parser,ast): ClickHouse parametric aggregates, FROM table functions and their rendering (#605, #606, #607) (#628)
- 458b3c3 fix(parser,ast,formatter): keep JOIN ... USING and accept parenthesized set operations (#603, #612) (#630)
- 82982e5 fix(parser,ast,formatter): named window round-trip, GROUPS/EXCLUDE frames and INTERVAL bounds (#608, #609) (#629)
- 41a5035 fix(schema/postgres): report index key order, INCLUDE and expression keys, partitioned-parent indexes, FK referenced schema and catalog-based key flags (#620) (#622)
- c1f69d5 fix(tokenizer,errors): release oversized lineStarts and bound error context on long lines (#614, #615) (#626)
- 61ca6c8 fix(tokenizer,parser,formatter): bound input-driven recursion, comment counts and set-operation rendering (#597)
- 7904ffc fix(website/a11y): contrast tokens, focus-visible, tap targets, theme toggle, search combobox (#707)
- 5480a73 fix(website/a11y): single main landmark and server-rendered playground h1 (#700)
- f4b99b4 fix(website/changelog): one page per release, re-level headings, link #N refs (#777)
- c71fca1 fix(website/ci): build when the previous Vercel commit is unavailable (#832)
- cd5c4cb fix(website/docs): inline code backticks, TOC headings and reading time (#693)
- 5479d2b fix(website/home): correct API samples, ClickHouse 83/83, a11y and layout polish (#779)
- 6748866 fix(website/home): keep MCP card within viewport so Copy buttons stay visible (#766) (#795)
- 5670ff3 fix(website/home): show real server-fetched GitHub star count (#638) (#689)
- a6ba01c fix(website/pages): vscode dialects/copy, benchmarks command, 404 button, compare table, facts link (#793)
- b0a1c25 fix(website/playground): raised input/output pane surface in light mode (#815)
- 5825a56 fix(website/playground): structured parse errors, editor focus, AST a11y, dark surface, tab underline, layout (#794)
- dcea358 fix(website/product): AST block layout, compare polarity, VS Code rule count, editor theme (#698)
- fb56afc fix(website/seo): self-referencing canonicals and sitemap fixes (#636) (#688)
- 5024c6a fix(website/theme): give light surfaces real elevation and let utilities override .glass (#814)
- 465f2a3 perf(gosqlx,transform,transpiler): release ASTs and use the parser pool in batch helpers (#560) (#571)
- 20cd6ac perf(website/home): defer WASM load until user intent; SW only on /playground (#701)
- 7651995 perf(website/sentry): drop Session Replay, logs and PII from client bundle (#697)
Full Changelog: v1.15.0...v1.16.0