Skip to content

v1.16.0: Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep

Latest

Choose a tag to compare

@github-actions github-actions released this 22 Oct 11:38
· 3 commits to main since this release
e9c5db9

Release 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.0

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 Start skips
    comments
    : the tokenizer counts a tab as one column (it was four), so
    a\tb puts b at 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/End are unchanged. (#826, fixes #797)
  • || is logical OR under MySQL and MariaDB: with the mysql or mariadb
    dialect (API, linter, CLI --dialect, LSP), a = 1 || b now parses as
    (a = 1) OR b, a BinaryExpression{Operator: "OR", PipesOr: true}, instead
    of a = (1 || b). SQL() and the formatter write || back. The linter and
    the security scanner treat it as OR. Pass parser.WithPipesAsConcat() /
    gosqlx.WithPipesAsConcat() to keep concatenation (PIPES_AS_CONCAT).
    Transpiling a || concatenation to MySQL/MariaDB now emits CONCAT(...),
    and a MySQL || OR becomes OR for other targets. Other dialects are
    unchanged. (#790, fixes #781)
  • Composite field access is dialect-gated: (expr).field parses under
    PostgreSQL, BigQuery, generic and 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 a ParenthesizedExpression in the subscript's Array field. This
    preserves arithmetic precedence and lets (items)[1].name round-trip without
    becoming the invalid items[1].name. (#834, fixes #833)
  • gosqlx-mcp HTTP hardening: rate limiting now keys on the TCP peer
    address and no longer trusts X-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, as render.yaml
    does) or GOSQLX_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 with Retry-After and 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
    Bearer scheme is case-insensitive. Tool input problems (missing, empty or
    non-string sql, parse/format failures, wrongly typed options) are now
    isError tool results naming the argument instead of JSON-RPC -32603
    internal errors, and indent_size must be an integer from 0 to 16.
    With neither trusted-proxy setting, gosqlx-mcp logs 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).
    New pkg/mcp API: NewRateLimitMiddleware(cfg, next),
    Config.TrustedProxyHeader, Config.TrustedProxyHops,
    Config.TrustedProxySecretHeader, Config.TrustedProxySecret. (#831, #841,
    fixes #838)
  • MCP format_sql uppercases more keywords: uppercase_keywords: true now
    also uppercases operator and function keywords (AND, OR, COUNT);
    indent_size affects nested blocks only, as now documented. (#823)
  • Comments now count toward MaxTokens: tokens plus collected comments may
    not exceed MaxTokens (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 by MaxInputSize; 10 MB of
    --\n collected 3.4M comments and peaked at about 960 MB RSS. Pooled
    tokenizers drop a Comments slice whose capacity exceeds 1024 entries on
    Reset, and
    ReleaseAST does the same for AST.Comments. (#597, part of #566)
  • MaxRecursionDepth (E2007) now covers unary +/-, NOT and derived
    tables
    : these three paths recursed outside parseExpression, so their
    nesting was limited only by input size. They now share its depth counter and
    RecursionDepthLimitError (limit 100): 99 nested unary operators or NOTs
    in a select list or WHERE parse and 100 return E2007.
    (#597, part of #566)
  • $name placeholders 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 @name and 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/
    EXCEPT chain. It now writes the chain once. Output is byte-identical; only
    time and memory change (80,000 UNIONs: 4.1 s and 46 GB allocated -> 6 ms and
    101 MB). Format, Fingerprint, Normalize and Transpile benefit 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.Clone them. Tokenize([]byte) copies the input once, so tokens never
    alias the caller's mutable buffer. ExtractTables/ExtractColumns/
    ExtractFunctions/ExtractMetadata (and the Qualified variants) and
    schema.LoadFromDDL/LoadCatalogFromDDL clone their result strings so they
    don't pin the input. Tokenizer.Reset and Parser.Reset now release the
    previous input instead of keeping it reachable from the pool. (#595, fixes
    #546 and #548; #586, fixes #552)
  • Oracle :1 / :name are bind variables: under DialectOracle, :
    followed by digits or an identifier is one TokenTypePlaceholder (value
    includes the colon). As a consequence, Oracle-mode arr[1:n] (not valid
    Oracle) now fails with "expected ], got PLACEHOLDER". Other dialects are
    unchanged. (#595, fixes #520)
  • INSERT ... WITH ... SELECT is accepted, and gosqlx format keeps 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 without AS now reports "expected AS, got LPAREN" instead of
    "expected column name". The CLI formatter used to silently drop a leading
    WITH on 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.Table as a dotted string (for example Table: "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 and SQL() match the flat query.
    Shapes that flattening would get wrong (a comma list after or inside a group),
    and an alias or ClickHouse FINAL after a group, are rejected with E4001.
    ExtractColumnsQualified splits 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(), Walk and Inspect
    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 in SQL() and the
    formatter. These pointers become invalid after ReleaseAST. (#588, fixes
    #557)
  • ReleaseAST only 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 by ReleaseAST/ReleaseStatements; they become garbage. The typed
    Put* functions keep full semantics for hand-built trees. Nodes returned by
    Get* are now fully zeroed, and typed Put* wrappers release nested subtrees
    (for example FunctionCall.Over/Filter) instead of dropping them. (#581,
    fixes #555 and #556)
  • Schema loaders: the PostgreSQL loader reads pg_constraint and 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 JSON null. (#582,
    fixes #579; #570, fixes #565)
  • LSP Document.Lines is shared: DocumentManager.Get returns a fresh
    *Document header, but its Lines now aliases the manager's immutable slice.
    Treat it as read-only. (#573, fixes #564)
  • gosqlx format renders through pkg/formatter and refuses output that does
    not round-trip
    : the CLI's private renderer is gone;
    cmd/gosqlx/cmd/sql_formatter.go is now a thin adapter over
    formatter.FormatStatement. SQLFormatter, NewSQLFormatter and Format
    keep their signatures, FormatterOptions gains a Dialect field and
    AlignColumns is 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 -i leaves the file untouched. It also refuses output that would drop
    meaningful identifier quotes or change a string literal. With
    --dialect mysql/mariadb the output must keep exactly the input's
    double-quoted tokens, because MySQL reads "..." as a string:
    WHERE name = "bob" is refused rather than rewritten as name = bob, and
    `select` rather than rewritten as "select". ANSI_QUOTES-style input
    ("users"."id", FROM "users") is formatted as before. (#599, #624)
  • Extract* and ExtractMetadata return sorted results; L006/L009 are
    deterministic
    : ExtractTables, ExtractTablesQualified, ExtractColumns,
    ExtractColumnsQualified, ExtractFunctions and ExtractMetadata (and the
    MCP extract_metadata tool, the cbinding JSON and integrations built on them)
    came out in random map order. Strings now sort in ascending byte order and
    QualifiedName values 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() of CreateTable, FunctionDesc, TriggerExecBody and
    TriggerReferencing returns pointers
    : CreateTable.Children() returns
    &c.Name, &c.Columns[i] and &c.Constraints[i] instead of value copies, so
    type switches on *ObjectName, *ColumnDef, *TableConstraint and
    *FunctionDesc match and edits made inside Inspect or Walk reach the
    parent AST. Visit order is unchanged. (#617, #621)
  • Quoted identifiers that need quotes keep them on render: ast.Identifier
    gains Quoted, set for ANSI double-quoted names. SQL(), FormatAST,
    Normalize, Fingerprint and Transpile output now re-quotes such
    identifiers, and CTE names and column lists, CREATE [MATERIALIZED] VIEW names
    and column lists and FROM/JOIN table names that are reserved words or contain
    non-identifier characters. New helpers ast.QuoteName and
    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" and id
    are unchanged and still equal; only names that need quotes (such as
    "select") now normalize with them. SQL Server #temp, Snowflake
    @stage/path and 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, but WITH (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 accepts CAST(x AS "type"). A non-ASCII spelling such as
    lıke is stored as LIKE; the formatter keeps WITH in
    WITH CHECK OPTION; and a hand-built MINUS set operator renders as the
    equivalent EXCEPT. (#604, #632)
  • Readable-style formatting emits one trailing ;: with AddSemicolon
    (readable style, FormatString default) only the outermost statement ends with
    ;. CTE bodies, IN/EXISTS/scalar subqueries, set-operation operands and
    the query of INSERT ... SELECT / CREATE VIEW no longer get an inner ; or
    ;;, and a set-operation chain ends with a single ;. Compact style is
    unchanged. (#602, #631)
  • ClickHouse SELECT ... SETTINGS ... INTO OUTFILE is rejected: the
    SETTINGS clause is now parsed rather than skipped, so the trailing
    INTO OUTFILE that used to be swallowed returns a syntax error, matching
    SELECT ... INTO OUTFILE elsewhere. Malformed SETTINGS lists and a bare
    FORMAT after SETTINGS also return syntax errors; a trailing
    FORMAT <name> after SETTINGS is still accepted. (#600, #631)
  • A bare returning alias inside an INSERT source or UPDATE ... FROM is
    treated as RETURNING
    : while parsing an INSERT source query or an
    UPDATE ... FROM list, an unquoted returning is no longer taken as an
    implicit table or select alias. Quoted returning and aliases inside nested
    derived tables, IN/EXISTS, CTE bodies and scalar subqueries are
    unaffected. RETURNING is 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
    permissive CREATE STAGE/SEQUENCE/FILE FORMAT path now rejects such names
    instead of keeping a truncated RawSQL. Names of 1 to 5 parts render
    byte-identically. (#613, #627)
  • Error context windows are bounded on long lines: (*Error).Error(),
    FormatMultiLineContext and FormatContextWindow clip 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.Reset keeps
    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.Columns is in index key order and excludes INCLUDE columns, which move
    to the new Index.IncludeColumns. Expression keys appear as
    pg_get_indexdef text instead of being dropped, and indexes on partitioned
    parents are listed. PK/UNIQUE column flags come from pg_constraint, so each
    column appears once, IsUnique is set only for a single-column UNIQUE and a
    SELECT-only role sees the same schema as the owner. ForeignKey gains
    RefSchema (empty for MySQL and SQLite). (#620, #622)
  • Named windows round-trip: OVER w now renders as OVER w instead of
    OVER (w), and WINDOW w AS (...) no longer repeats its name inside the
    parentheses. The non-standard WINDOW w AS w2 is normalized to
    WINDOW w AS (w2). (#608, #629)
  • LSP semantic token legend changed: the non-standard identifier token
    type is replaced by the standard variable, which now covers table, column
    and alias names and quoted identifiers (double-quoted identifiers were
    string before). Function names are function and type names are type.
    Comments and placeholders ($1, ?, :name, @var) are now emitted, as
    comment and parameter. AND, OR and NOT stay keyword. Positions are
    UTF-16 code units. Clients with custom rules keyed on the old identifier
    type (for example Neovim @lsp.type.identifier highlight links) must switch
    to variable. (#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.py is a shim
    that tags platform wheels). (#851)

Added

  • Claude Code plugin and marketplace: /plugin marketplace add ajitpratap0/GoSQLX then /plugin install gosqlx@gosqlx installs the
    gosqlx plugin (plugins/gosqlx/): the hosted MCP server, five SQL skills
    (analysis, formatting, security review, linting, dialects), four slash
    commands, a read-only sql-reviewer agent and an opt-out PostToolUse hook
    that validates edited .sql files when the gosqlx CLI is installed.
    (#823)

  • ChatGPT app support in gosqlx-mcp (OpenAI Apps SDK and MCP Apps):
    analyze_sql links an embedded report widget
    (ui://gosqlx/analysis-v1.html, text/html;profile=mcp-app); all tools now
    return structuredContent alongside the unchanged text JSON, carry
    securitySchemes/invocation-status metadata, and the server sends usage
    instructions. The server reports gosqlx.Version; /health reports the real tool count; the
    optional GOSQLX_MCP_OPENAI_APPS_CHALLENGE env var serves
    /.well-known/openai-apps-challenge for 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, and Server.Start
    sets read-header and idle timeouts. The analyze_sql widget 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 via WithDialect, never fails
    (input after the first lexical error becomes one error span), 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) and CSS, with dark, light and mono themes.
    Spans marshal to JSON. Built on top of it:

    • gosqlx.Highlight, gosqlx.HighlightANSI and gosqlx.HighlightHTML.
    • gosqlx highlight [file|SQL] (alias hl, reads stdin) with
      --format ansi|html|json|css, --theme, --dialect,
      --color auto|always|never (honors TTY, NO_COLOR and COLORTERM) and
      --standalone for a full HTML page.
    • LSP textDocument/semanticTokens/full uses pkg/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, 10 is valid under mysql, ILIKE is reported under mysql);
    completion and hover add the dialect's own keywords. The server also handles
    workspace/didChangeConfiguration with {"gosqlx": {"dialect": "..."}}:
    it switches dialect without a restart, re-publishes diagnostics for open
    documents and, when the client advertises refreshSupport, sends
    workspace/semanticTokens/refresh. Invalid values fall back to generic, as
    in initialize. The default generic dialect keeps the previous behaviour
    for every feature. See docs/LSP_GUIDE.md.
    (#828)

  • --dialect flag for the parse, format, analyze, and lint CLI
    commands
    : dialect-specific SQL (e.g. MySQL LIMIT offset, count) can now be
    parsed by these commands, matching the existing validate --dialect. Unknown
    dialect names are rejected up front. The default remains PostgreSQL.
    pkg/linter.Linter gains a SetDialect(keywords.SQLDialect) method so the
    linter can tokenize/parse dialect-specific syntax, and lint --security now
    parses with the selected dialect. (#526)

  • ? positional placeholders for MySQL, MariaDB and SQLite: under these
    dialects a bare ? is now a TokenTypePlaceholder (the bind parameter used
    by their database/sql drivers) 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, :name and :new_a parse under
    DialectOracle and round-trip byte-for-byte through SQL() and the
    formatter. Previously WHERE id = :1 failed with "unexpected token: COLON".
    (#595, fixes #520)

  • Tokenizer.TokenizeString(s string) and
    Tokenizer.TokenizeStringContext(ctx, s string)
    : tokenize a string in place
    without the []byte round trip. Both return []models.TokenWithSpan; token
    values reference s. (#595, fixes #546)

  • ClickHouse SAMPLE and ORDER BY ... WITH FILL round-trip: SAMPLE now
    renders, and WITH FILL [FROM x] [TO y] [STEP z] is stored in the new
    WithFillClause instead 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 new JoinClause.Using
    (one *ast.Identifier per column, Condition left nil) and rendered as
    USING (c1, c2) by SQL(), pkg/formatter and gosqlx format, instead of
    ON cols. Children(), Walk and ReleaseAST cover 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 WITH query or a doubly parenthesized query,
    and a statement or set-operation operand can be written as (query), e.g.
    (SELECT 1) UNION (SELECT 2). New TableReference.SetSubquery and
    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 GROUPS unit, EXCLUDE clause, named-window refinement and
    INTERVAL qualifiers
    : GROUPS frames and EXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS (new WindowFrame.Exclude) parse and render;
    OVER (w ORDER BY z) refines an existing window via the new
    WindowSpec.BaseWindow; INTERVAL '1' DAY and INTERVAL '1-2' YEAR TO MONTH
    parse into the new IntervalExpression.Qualifier, which fixes
    d + INTERVAL '1' DAY rendering as INTERVAL '1' AS DAY. GROUPS and
    EXCLUDE are not gated by dialect. (#608, #609, #629)

  • INSERT forms: RETURNING after INSERT ... SELECT, INSERT ... DEFAULT VALUES (new InsertStatement.DefaultValues), MySQL/MariaDB INSERT ... SET
    (new InsertStatement.SetSyntax), parenthesized query sources
    (INSERT INTO t (SELECT ...)) and UPDATE ... FROM with a comma-separated
    table list. SQL() and FormatAST render DEFAULT VALUES and the SET form;
    UPDATE ... FROM ... JOIN returns E4001. (#610, #623)

  • ClickHouse table functions and parametric aggregates: numbers(10),
    remote(...), file, s3, cluster, generateRandom and the
    keyword-named url, merge, values and null parse in FROM as a
    TableReference with TableFunc. 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 ... SETTINGS is kept: parsed into the new
    SelectStatement.Settings ([]ast.QuerySetting, stored on the rightmost
    SELECT of a set operation) and rendered by SQL() and pkg/formatter.
    (#600, #631)

  • Rendering of IGNORE NULLS, named arguments, ON DUPLICATE KEY UPDATE and
    INSERT ... OUTPUT
    : SQL() and pkg/formatter (compact and readable) used
    to drop these; they now render, with name => value via the new
    (*ast.NamedArgument).SQL(). (#600, #631)

  • SQL Server table hints and PIVOT rendering: SQL() renders
    WITH (NOLOCK)-style hints and PIVOT/UNPIVOT with the alias as
    AS alias (FROM s PIVOT (...) AS p used to render as FROM s p), and
    pkg/formatter renders hints. (#604, #632)

  • ALTER TABLE rendering in pkg/formatter: FormatStatement and
    FormatAST render ALTER TABLE operations (ADD/DROP COLUMN, RENAME,
    ADD/DROP CONSTRAINT, ALTER COLUMN ...) instead of the bare token ALTER.
    (#599, #624)

  • New schema fields: db.Index.IncludeColumns and db.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 accept AS g(a, b), and PostgreSQL also accepts
    WITH ORDINALITY with 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)

  • WithPipesAsConcat options, ast.BinaryExpression.PipesOr and
    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 return ErrUnsupportedDialect. (#780)

  • pygosqlx 1.16.0: dialects, lint, security scan and transpile: parse,
    validate, validate_detailed, format and the extract_* functions take
    an optional dialect= argument (for example dialect="sqlserver"), and
    pygosqlx.dialects() lists the supported names. An unknown dialect raises
    UnsupportedDialectError, which is also a ValueError. New
    pygosqlx.lint() runs the gosqlx lint rule set (L001-L010, optional
    dialect= and max_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 the gosqlx_*_dialect variants and
    gosqlx_lint, gosqlx_security_scan, gosqlx_transpile and
    gosqlx_dialects; each returns a JSON string the caller frees with
    gosqlx_free. The existing exports are unchanged. transpile() raises the
    new TranspileError when SQL cannot be parsed or rewritten, and results
    come back as the LintResult/LintViolation and
    SecurityScanResult/SecurityFinding dataclasses. (#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; set PYGOSQLX_ALLOW_MISSING_LIB=1
    to install anyway and supply GOSQLX_LIB_PATH at 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 MCP lint_sql tool, the C binding's
    gosqlx_lint and gosqlx.Lint. Each keeps its existing settings and
    output; CoreConfig covers the L003, L004 and L005 limits. (#853)

Changed

  • tokenizer.NewWithKeywords no 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
    explicit keywords.New calls, because parsing no longer calls it. (#595,
    fixes #550)

Fixed

  • PostgreSQL composite-returning function queries parse correctly:
    (get_item(payload)).name and (get_item(payload)).* retain their base
    expression in a new ast.FieldAccessExpression, including quoted field names
    and chained field/subscript access. SQL rendering, AST traversal, extraction,
    security scanning and expression release preserve the nested expression.
    Direct JOIN LATERAL get_item(t.payload) AS f references 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 + 2 failed 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 matching NewParser(). (#735, fixes #590;
    #789, fixes #784)
  • Oracle CONNECT BY accepts general expressions: PRIOR is a prefix
    operator, so CONNECT BY PRIOR id = parent_id + 1 parses. A column named
    prior is still an identifier. (#791, fixes #783)
  • Snowflake VariantPath renders: WHERE src:a = 1 came out as
    WHERE : = 1 from SQL() and the formatter. Keys that are not bare words
    are double-quoted. (#786, fixes #782)
  • Lower- and mixed-case CROSS JOIN, FULL JOIN and GROUPING 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.Version and gosqlx --version report 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 second pygosqlx.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 with gosqlx_free.
    pygosqlx.__version__ reports the packaged version, and editable installs
    work with current setuptools. (#847)
  • LSP serverInfo.version and release binaries report the real version:
    the LSP server reported a hard-coded 1.0.0 in serverInfo.version and now
    reports the library version. The gosqlx binary bundled with the VS Code
    extension prints the release tag from gosqlx --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.version flag that the example
    binaries ignored. (#846) An unstamped CLI now reports gosqlx.Version, so
    the CLI and library versions cannot drift, and release.yml fails a tag
    that does not match gosqlx.Version. (#853)
  • Tokenizer pool no longer leaks dialect state: a tokenizer whose dialect was
    changed via SetDialect (e.g. by validate --dialect mysql) is now restored to
    the default (PostgreSQL) dialect when returned to the pool via PutTokenizer.
    Previously Reset did not clear the dialect/keywords, so a later
    GetTokenizer caller could receive a tokenizer configured for the wrong
    dialect and silently mis-parse. The restore lives in PutTokenizer (the pool
    boundary), not Reset, because Reset also runs at the start of every
    Tokenize call 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 as a = 1 OR b = 2 AND c = 3, which changed the predicate.
    The parser now keeps grouping in a new ast.ParenthesizedExpression node.
    (#532, fixes #519)
  • IS NOT NULL no longer renders as IS NULL, and ClickHouse parametric
    aggregates keep their parameter group in SQL()
    (quantileTDigest(0.95)(value) stayed quantileTDigest(value)). (#529,
    fixes #530; AST half of #531)
  • gosqlx format keeps ClickHouse parametric aggregate parameters: the CLI
    renderer also dropped the (params) group. (#583, fixes #531)
  • gosqlx format no longer panics on NOT EXISTS (subquery). (#591, fixes
    #587)
  • Numeric window-frame offsets are kept: ROWS BETWEEN 1 PRECEDING AND CURRENT ROW rendered as ROWS BETWEEN PRECEDING AND CURRENT ROW in SQL(),
    pkg/formatter and the CLI. The CLI also stopped adding BETWEEN to 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"). A WITH
    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` and FROM ((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.DB with
    MaxOpenConns=1 used 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: PutSelectStatement no longer releases a
    derived-table subquery twice through Joins[i].Left, which could make two
    later GetSelectStatement calls 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 as NOT (EXISTS (...) NOT ), which failed
    to re-parse with E2002. SQL(), FormatAST and FormatString now render
    NOT EXISTS (...); the AST shape is unchanged. (#601, #632)
  • Table-function arguments in FROM were dropped on render:
    generate_series(1,10) AS g came out as generate_series g. SQL() and
    pkg/formatter now render the call for PostgreSQL, Snowflake, BigQuery and
    ClickHouse table functions, and transform.ReplaceTable renames
    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's Hint; 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 as Cause. (#616, #627)
  • gosqlx format dropped or mangled constructs (see Behavior changes):
    JOIN ... USING printed ON LIST, and WINDOW, INTERVAL, UPDATE ... FROM and
    DEFAULT VALUES were 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) and StringLiteralReader
    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 --dialect help on parse,
    format, analyze, lint and validate, the CLI's unknown-dialect error,
    config-file validation, the MCP validate_sql schema, the playground WASM
    bridge, the GitHub Action and the VS Code gosqlx.dialect setting 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 omitted generic and
    clickhouse, pkg/config rejected generic, bigquery, redshift and
    clickhouse, and the VS Code setting offered 6 (and suggested mysql for
    mariadb). The Go lists derive from the new keywords.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/--to keep 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_100KB 20.6 ms -> 1.96 ms, VeryLargeSQL_1MB 1755 ms ->
    21.8 ms, LargeInClause 4.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/ComplexSQL 120 -> 14, MediumSQL_10KB 2823 -> 203,
    LargeSQL_100KB 28139 -> 1988) and gosqlx parse allocs/op drop 37-66%
    (Parse_ComplexQuery 193 -> 74). B/op rises 1-6% on the tokenizer, and
    Tokenize_SimpleQuery on the []byte path 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/ComplexSQL 13 -> 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 as ParseBytes),
    ParseTree with a dialect 22.0 us -> 1.25 us, SetDialect 35.6 us / 76
    allocs -> 0 allocs, transpiler.Transpile 20.2 us -> 1.69 us and
    transform.ParseSQLWithDialect 35.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_Columns 2020 -> 1018, SELECT_Complex 198 -> 114), and
    qualified identifiers and clause parsing lose an allocation each. (#578, fixes
    #551, #553, #554)
  • Parser-owned preprocess buffer: ParserSimpleSelect 9 -> 7 allocs/op and
    1696 -> 816 B/op, ParserComplexSelect 22 -> 20 allocs/op (-64% B/op).
    (#586, fixes #552)
  • Writer-based SQL() serialization: a statement renders into one pooled
    buffer. BenchmarkSQL/7line 57 -> 1 allocs/op and 15line 122 -> 1, about
    3x faster; output is byte-identical. (#575, fixes #558)
  • Children() stops heap-copying nodes: Inspect 47 -> 26 allocs/op and
    3216 -> 928 B/op, Lint/Realistic 728 -> 580 allocs/op, Extract* 20-45%
    fewer allocations. (#588, fixes #557)
  • Cheap PutExpression after large lists: the work queue is cleared only to
    its high-water mark. PutLeafAfterBigList 2995 ns -> 19 ns. (#581, fixes
    #555)
  • ExtractMetadata in one AST pass: 114 -> 24 allocs/op, 8264 -> 2208 B/op,
    about 2.4-2.8x faster. (#572, fixes #561)
  • SplitStatements slices 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.Lint builds its rule set once: Lint/Tiny 169 -> 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, Transpile and the transform parse
    helpers return ASTs and parsers to their pools, so pool Get/Put counts balance.
    (#571, fixes #560)
  • ScanSQL literal prefilters: regexes whose required literals are absent
    are skipped. ScanSQL 26.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: BuildDiagnostics on 10,000
    lines 789 MiB / 512 ms -> 1.2 MiB / 6.8 ms, Hover 5296 B / 9 allocs -> 368 B
    / 5 allocs, SignatureHelp 3535 B / 50 allocs -> 224 B / 4 allocs, and
    sendMessage writes each frame once (11 -> 7 allocs). (#573, fixes #564)
  • Schema loaders batch their queries: Load runs 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.json re-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: parseQualifiedName collects up to 5
    parts in a stack array and joins them once instead of concatenating per part.
    A 64 KB a.a.a... chain went from 66 ms and 1.17 GB (and parsed) to 8 ms and
    10.5 MB (E2004); allocs/op on 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.Identifier grows from
    48 to 64 bytes for the Quoted flag, raising parser B/op by about 4-6%, and
    rendering with name quoting makes SQL() about 8% slower with unchanged
    allocations (#611, #632). TableReference grows from 128 to 136 bytes for
    parenthesized set operations: BenchmarkParserSimpleSelect +4.8% and
    +16 B/op (#612, #630). Reallocating lineStarts per 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 t came back from gosqlx.Format,
    SQL(), Normalize or Transpile as 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, view WITH option, 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, NOT and 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/edwards25519 v1.1.1 and golang.org/x/sys v0.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 a FROM|JOIN <fn>( pattern), and the scanner descends into array
    subscripts and slices so pg_sleep(5)[1] is not a bypass. The check is
    dialect-agnostic by design. (#605, #607, #628)
  • Scanner walks nested set-operation trees: checkUnionInjection handles
    right-nested and parenthesized UNION/EXCEPT trees, 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-mcp no 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); .gitattributes enforcing 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 apply RateLimitMiddleware, and
    docs/CHATGPT_APP.md explains 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.yml pins the
    GoReleaser current and previous tags to the v* series so pygosqlx-v*
    tags cannot truncate the Go release changelog (#851), and the previous-tag
    lookup skips the floating Marketplace v1 tag (#852); vscode-publish.yml
    stamps the tag version into the bundled binary and its smoke test fails if
    --version does 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 highlight package API reference;
    docs/api/lsp.md documents semantic tokens and runtime dialect changes via
    workspace/didChangeConfiguration; docs/api/linter.md documents
    Linter.SetDialect and gosqlx.LintWithDialect; MCP_GUIDE.md states that
    Server.Start/Server.Handler apply no rate limiting unless wrapped in
    NewRateLimitMiddleware and that lint_sql runs only L001-L010; the
    migration guide and VS Code extension changelog gain their missing v1.15.0
    sections; models.Location documents byte columns and UTF-16 LSP
    conversion. (#848) The README describes bigquery and redshift
    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