v1.16.0 — Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep
Covers 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), soa\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 themysqlormariadbdialect (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, setGOSQLX_MCP_TRUSTED_PROXY_HEADER(e.g.CF-Connecting-IP, asrender.yamldoes) 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 deniedinitialize, 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 theBearerscheme is case-insensitive. Tool input problems (missing, empty or non-stringsql, parse/format failures, wrongly typed options) are nowisErrortool 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-inGOSQLX_MCP_TRUSTED_PROXY_SECRET_HEADER/GOSQLX_MCP_TRUSTED_PROXY_SECRETpair 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 onReset, andReleaseASTdoes 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 andRecursionDepthLimitError(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-deep
UNION/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) andschema.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 leadingWITHon 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
into
Identifier.Tableas a dotted string (for exampleTable: "db.t"), andIdentifier.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(),WalkandInspectnow 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 typedPut*functions keep full semantics for hand-built trees. Nodes returned byGet*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 overformatter.FormatStatement.SQLFormatter,NewSQLFormatterandFormatkeep their signatures,FormatterOptionsgains aDialectfield andAlignColumnsis 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 followspkg/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" andformat -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 andQualifiedNamevalues 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,TriggerExecBodyandTriggerReferencingreturns 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.IdentifiergainsQuoted, 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.QuoteNameandast.QuoteQualifiedName.SELECT "select"used to render asSELECT 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"andidare 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 aslıkeis stored asLIKE; the formatter keepsWITHinWITH 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 trailingINTO OUTFILEthat used to be swallowed returns a syntax error, matchingSELECT ... INTO OUTFILEelsewhere. Malformed SETTINGS lists and a bareFORMATafter SETTINGS also return syntax errors; a trailingFORMAT <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 anUPDATE ... 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 MV
TO) 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 aspg_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.ForeignKeygainsRefSchema(empty for MySQL and SQLite). (#620, #622) - Named windows round-trip:
OVER wnow renders asOVER winstead ofOVER (w), andWINDOW w AS (...)no longer repeats its name inside the parentheses. The non-standardWINDOW w AS w2is normalized toWINDOW 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 werestringbefore). Function names arefunctionand type names aretype. Comments and placeholders ($1,?,:name,@var) are now emitted, ascommentandparameter. AND, OR and NOT staykeyword. Positions are UTF-16 code units. Clients with custom rules keyed on the oldidentifiertype (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 in
pyproject.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 thegosqlxplugin (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, carrysecuritySchemes/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.Startsets 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.dialectnow drives every feature, not only semantic tokens. Diagnostics, formatting and document symbols tokenize and parse with the dialect's rules (MySQLLIMIT 5, 10is valid undermysql,ILIKEis reported undermysql); completion and hover add the dialect's own keywords. The server also handlesworkspace/didChangeConfigurationwith{"gosqlx": {"dialect": "..."}}: it switches dialect without a restart, re-publishes diagnostics for open documents and, when the client advertisesrefreshSupport, sendsworkspace/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 underDialectOracleand round-trip byte-for-byte throughSQL()and the formatter. PreviouslyWHERE id = :1failed with "unexpected token: COLON". (#595, fixes #520) -
Tokenizer.TokenizeString(s string)andTokenizer.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 newWithFillClauseinstead 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 asUSING (c1, c2)bySQL(),pkg/formatterandgosqlx format, instead ofON 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, a
WITHquery 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.SetSubqueryandSetOperation.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 andINTERVALqualifiers:GROUPSframes andEXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS(newWindowFrame.Exclude) parse and render;OVER (w ORDER BY z)refines an existing window via the newWindowSpec.BaseWindow;INTERVAL '1' DAYandINTERVAL '1-2' YEAR TO MONTHparse into the newIntervalExpression.Qualifier, which fixesd + INTERVAL '1' DAYrendering asINTERVAL '1' AS DAY.GROUPSandEXCLUDEare 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 aTableReferencewithTableFunc.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 asArraySubscriptExpression/ArraySliceExpression; SQL Server keepscount(*) [cnt]as an alias, and SQLite behaves as before. (#605, #606, #628) -
ClickHouse
SELECT ... SETTINGSis kept: parsed into the newSelectStatement.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 UPDATEandINSERT ... 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()rendersWITH (NOLOCK)-style hints andPIVOT/UNPIVOTwith the alias asAS alias(FROM s PIVOT (...) AS pused to render asFROM s p), andpkg/formatterrenders hints. (#604, #632) -
ALTER TABLErendering inpkg/formatter:FormatStatementandFormatASTrender 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 acceptsWITH ORDINALITYwith or without an alias list (unnest(arr) WITH ORDINALITY AS t(elem, n)). They are exposed asast.TableReference.ColumnAliases(*ast.ColumnAliasList) andTableReference.WithOrdinality, plus the newdialect.Capabilities.SupportsCompositeFieldAccess.SQL()and the formatter round-trip them. (#843, fixes #839) -
WithPipesAsConcatoptions,ast.BinaryExpression.PipesOranddialect.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"), andpygosqlx.dialects()lists the supported names. An unknown dialect raisesUnsupportedDialectError, which is also aValueError. Newpygosqlx.lint()runs thegosqlx lintrule set (L001-L010, optionaldialect=andmax_line_length=),pygosqlx.security_scan()returns typed SQL injection findings with severity counts, andpygosqlx.transpile(sql, from_dialect, to_dialect)converts SQL between dialects. libgosqlx gains thegosqlx_*_dialectvariants andgosqlx_lint,gosqlx_security_scan,gosqlx_transpileandgosqlx_dialects; each returns a JSON string the caller frees withgosqlx_free. The existing exports are unchanged.transpile()raises the newTranspileErrorwhen SQL cannot be parsed or rewritten, and results come back as theLintResult/LintViolationandSecurityScanResult/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 withpip 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=1to install anyway and supplyGOSQLX_LIB_PATHat runtime. (#851, #852) -
rules.CoreRules/rules.DefaultCoreConfig(new packagepkg/linter/rules): the core L001-L010 rule set, defined once and shared bygosqlx lint,gosqlx action, the MCPlint_sqltool, the C binding'sgosqlx_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.NewWithKeywordskeeps 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 namedprioris still an identifier. (#791, fixes #783) - Snowflake
VariantPathrenders:WHERE src:a = 1came out asWHERE : = 1fromSQL()and the formatter. Keys that are not bare words are double-quoted. (#786, fixes #782) - Lower- and mixed-case
CROSS JOIN,FULL JOINandGROUPING SETSparse, 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 via
SetDialect(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 laterGetTokenizercaller 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 everyTokenizecall 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 = 3used 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"). AWITHis 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.DBwithMaxOpenConns=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 renderNOT 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()andpkg/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, so
Error()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 fake1 | )snippet and keep the inner error asCause. (#616, #627) gosqlx formatdropped or mangled constructs (see Behavior changes):JOIN ... USINGprintedON LIST, and WINDOW, INTERVAL,UPDATE ... FROMandDEFAULT 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) andStringLiteralReadererrors 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 omittedgenericandclickhouse,pkg/configrejectedgeneric,bigquery,redshiftandclickhouse, and the VS Code setting offered 6 (and suggestedmysqlformariadb). 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, andTokenize_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:
ParseBytesWithDialect21.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 andtransform.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, andsendMessagewrites 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-line
Error()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). SortedExtract*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, sox 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-suppliedMcp-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.Handlerdocument that they do not applyRateLimitMiddleware, anddocs/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.ymlstamps 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 the
highlightpackage API reference;docs/api/lsp.mddocuments semantic tokens and runtime dialect changes viaworkspace/didChangeConfiguration;docs/api/linter.mddocumentsLinter.SetDialectandgosqlx.LintWithDialect;MCP_GUIDE.mdstates thatServer.Start/Server.Handlerapply no rate limiting unless wrapped inNewRateLimitMiddlewareand 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 describesbigqueryandredshiftaccurately: generic keyword set plus some dialect-gated grammar. (#853)