From 984ded84278f4b9f23daad6800a2796cba25969b Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:00:09 +0530 Subject: [PATCH 01/12] chore(parser): remove stray parser.go-e backup with conflict markers [vercel skip] The sed backup file was committed by accident in #791. It is not compiled but ships in the module zip. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- pkg/sql/parser/parser.go-e | 1457 ------------------------------------ 1 file changed, 1457 deletions(-) delete mode 100644 pkg/sql/parser/parser.go-e diff --git a/pkg/sql/parser/parser.go-e b/pkg/sql/parser/parser.go-e deleted file mode 100644 index 3635aeca..00000000 --- a/pkg/sql/parser/parser.go-e +++ /dev/null @@ -1,1457 +0,0 @@ -// Copyright 2026 GoSQLX Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package parser - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - - goerrors "github.com/ajitpratap0/GoSQLX/pkg/errors" - "github.com/ajitpratap0/GoSQLX/pkg/metrics" - "github.com/ajitpratap0/GoSQLX/pkg/models" - "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" - "github.com/ajitpratap0/GoSQLX/pkg/sql/dialect" - "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" - "github.com/ajitpratap0/GoSQLX/pkg/sql/token" -) - -// ConversionResult holds a preprocessed token stream and optional source-position -// mappings. Callers typically obtain this via ParseFromModelTokensWithPositions and -// pass it to ParseWithPositions. -// -// After the token-type unification (#322) the Tokens field holds -// []models.TokenWithSpan directly; span information is no longer stripped. -type ConversionResult struct { - Tokens []models.TokenWithSpan - // Deprecated: PositionMapping is always nil. Position information is now embedded - // directly in models.TokenWithSpan.Start and .End fields. - // Scheduled for removal in v2.0. - PositionMapping []TokenPosition -} - -// TokenPosition maps a parser token back to its original source position. -type TokenPosition struct { - OriginalIndex int - Start models.Location - End models.Location - SourceToken *models.TokenWithSpan -} - -// parserPool provides object pooling for Parser instances to reduce allocations. -// This significantly improves performance in high-throughput scenarios. -// -// Pool statistics (v1.6.0 production workloads): -// - Hit Rate: 95%+ in concurrent environments -// - Memory Savings: 60-80% reduction vs non-pooled allocation -// - Allocation Rate: <100 bytes/op for pooled parsing -// -// Usage pattern (MANDATORY): -// -// parser := parser.GetParser() -// defer parser.PutParser(parser) // MUST return to pool -// ast, err := parser.Parse(tokens) -var parserPool = sync.Pool{ - New: func() interface{} { - p := &Parser{} - p.resetDialectState() - return p - }, -} - -// GetParser returns a Parser instance from the pool. -// The caller MUST call PutParser when done to return it to the pool. -// -// This function is thread-safe and designed for concurrent use. Each goroutine -// should get its own parser instance from the pool. -// -// Performance: O(1) amortized, <50ns typical latency -// -// Usage: -// -// parser := parser.GetParser() -// defer parser.PutParser(parser) // MANDATORY - prevents resource leaks -// ast, err := parser.Parse(tokens) -// -// Thread Safety: Safe for concurrent calls - each goroutine gets its own instance. -func GetParser() *Parser { - metrics.RecordNamedPoolGet("parser") - return parserPool.Get().(*Parser) -} - -// PutParser returns a Parser instance to the pool after resetting it. -// This MUST be called after parsing is complete to enable reuse and prevent memory leaks. -// -// The parser is automatically reset before being returned to the pool, clearing all -// internal state (tokens, position, depth, context, position mappings). -// -// Performance: O(1), <30ns typical latency -// -// Usage: -// -// parser := parser.GetParser() -// defer parser.PutParser(parser) // Use defer to ensure cleanup on error paths -// -// Thread Safety: Safe for concurrent calls - operates on independent parser instances. -func PutParser(p *Parser) { - if p != nil { - p.Reset() - parserPool.Put(p) - metrics.RecordNamedPoolPut("parser") - } -} - -// resetDialectState puts the typed dialect and its capability cache into the -// default (dialect.Unknown) state. It is the single initialiser shared by -// NewParser, the pool's New func and Reset, so a pooled default parser and a -// freshly constructed one are state-identical. Allocation-free: -// dialect.Unknown.Capabilities() returns a small struct by value. -func (p *Parser) resetDialectState() { - p.dialectTyped = dialect.Unknown - p.capabilitiesCache = dialect.Unknown.Capabilities() - p.refreshPipesMode() -} - -// refreshPipesMode recomputes pipesAsOr from the dialect capabilities and the -// PIPES_AS_CONCAT opt-in. It must run after any change to either input, so -// the expression parser reads a single precomputed bool on its hot path. -func (p *Parser) refreshPipesMode() { - p.pipesAsOr = p.capabilitiesCache.PipesAsLogicalOr && !p.pipesAsConcat -} - -// maxPooledTokenBuf is the largest Parser.tokenBuf capacity (in tokens, about -// 288 KiB at 72 B per token) that Reset keeps for reuse. Larger buffers are -// dropped so one huge query does not pin memory in the parser pool. -const maxPooledTokenBuf = 4096 - -// Reset clears the parser state for reuse from the pool. -func (p *Parser) Reset() { - // p.tokens may alias a caller-owned slice (ParseWithPositions) or - // tokenBuf; drop the reference rather than keeping either alive. Token - // values are substrings of the caller's input, so any retained token - // would keep the whole previous SQL string alive while the parser sits - // in the pool. - p.tokens = nil - if cap(p.tokenBuf) > maxPooledTokenBuf { - p.tokenBuf = nil - } else { - // Drop the string/Word references into the previous query text. - // Only [0:len) needs clearing: tokenBuf[len:cap] is always zero - // (see preprocess). - clear(p.tokenBuf) - p.tokenBuf = p.tokenBuf[:0] - } - p.currentPos = 0 - p.currentToken = models.TokenWithSpan{} - p.depth = 0 - p.ctx = nil - p.strict = false - p.pipesAsConcat = false - p.dialect = "" - // INVARIANT: p.dialectTyped must always equal dialect.Parse(p.dialect). - // Reset clears both fields in lockstep; see WithDialect for the active path. - p.resetDialectState() - p.dmlReturning = false - p.inConnectBy = false -} - -// currentLocation returns the source location of the current token. -// Span information is embedded directly in models.TokenWithSpan so no -// separate position-mapping slice is required. -func (p *Parser) currentLocation() models.Location { - if p.currentPos < len(p.tokens) { - return p.tokens[p.currentPos].Start - } - return models.Location{} -} - -// MaxRecursionDepth defines the maximum allowed recursion depth for parsing operations. -// This prevents stack overflow from deeply nested expressions, CTEs, or other recursive structures. -// -// DoS Protection: This limit protects against denial-of-service attacks via malicious SQL -// with deeply nested expressions like: (((((...((value))...))))) -// -// Typical Values: -// - MaxRecursionDepth = 100: Protects against stack exhaustion -// - Legitimate queries rarely exceed depth of 10-15 -// - Malicious queries can reach thousands without this limit -// -// What counts: every nested level of parentheses, subqueries (including -// derived tables in FROM and JOIN), CTEs, and each prefix operator (unary + -// and -, NOT) adds one level. Chains such as NOT NOT NOT x or - - - 1 are -// therefore limited like nested parentheses. -// -// Error: Exceeding this depth returns goerrors.RecursionDepthLimitError -const MaxRecursionDepth = 100 - -// Used for fast path checks: tokens with Type set use O(1) switch dispatch. - -// Parser represents a SQL parser that converts a stream of tokens into an Abstract Syntax Tree (AST). -// -// The parser implements a recursive descent algorithm with one-token lookahead, supporting -// comprehensive SQL features across multiple database dialects. -// -// Architecture: -// - Recursive Descent: Top-down parsing with predictive lookahead -// - Statement Routing: O(1) Type-based dispatch for statement types -// - Expression Precedence: Handles operator precedence via recursive descent levels -// - Error Recovery: Provides detailed syntax error messages with position information -// -// Internal State: -// - tokens: Token stream from the tokenizer (converted to parser tokens) -// - tokenBuf: Parser-owned scratch buffer holding the preprocessed stream, -// reused across parses and scrubbed (or dropped when large) by Reset -// - currentPos: Current position in token stream -// - currentToken: Current token being examined -// - depth: Recursion depth counter (DoS protection via MaxRecursionDepth) -// - ctx: Optional context for cancellation support -// - positions: Source position mapping for enhanced error reporting -// -// Thread Safety: -// - NOT thread-safe - each goroutine must use its own parser instance -// - Use GetParser()/PutParser() to obtain thread-local instances from pool -// - Parser instances maintain no shared state between calls -// -// Memory Management: -// - Use GetParser() to obtain from pool -// - Use defer PutParser() to return to pool (MANDATORY) -// - Reset() is called automatically by PutParser() -// -// Performance Characteristics: -// - Throughput: 1.38M+ operations/second sustained -// - Latency: 347ns average for complex queries -// - Token Processing: 8M tokens/second -// - Allocation: <100 bytes/op with object pooling -// -// ParserOption configures optional parser behavior. -type ParserOption func(*Parser) - -// WithStrictMode enables strict parsing mode. In strict mode, the parser rejects -// empty statements (e.g., lone semicolons like ";;; SELECT 1 ;;;" will error -// instead of silently discarding empty statements between semicolons). -// -// By default, the parser operates in lenient mode where empty statements are -// silently ignored for backward compatibility. -func WithStrictMode() ParserOption { - return func(p *Parser) { - p.strict = true - } -} - -// WithDialect sets the SQL dialect for dialect-aware parsing. -// Supported values: "postgresql", "mysql", "sqlserver", "oracle", "sqlite", etc. -// If not set, defaults to "postgresql" for backward compatibility. -// -// INVARIANT: p.dialectTyped must always equal dialect.Parse(p.dialect). -// This option is the sole supported mutator of the parser's active -// dialect; it assigns both fields in lockstep so Parser.DialectTyped() -// and Parser.Capabilities() remain O(1) for the parser's lifetime. -func WithDialect(newDialect string) ParserOption { - return func(p *Parser) { - p.dialect = newDialect - p.dialectTyped = dialect.Parse(newDialect) - p.capabilitiesCache = p.dialectTyped.Capabilities() - p.refreshPipesMode() - } -} - -// WithPipesAsConcat mirrors the MySQL / MariaDB PIPES_AS_CONCAT SQL mode: -// || is parsed as string concatenation instead of logical OR. -// -// By default the MySQL and MariaDB dialects parse || as logical OR, with OR -// precedence, and produce the same AST as the OR keyword -// (BinaryExpression{Operator: "OR"}), with PipesOr set so SQL() writes || -// back. Every other dialect, including the -// default (no dialect), already parses || as concatenation, so this option -// has no effect there. It may be passed before or after WithDialect. -func WithPipesAsConcat() ParserOption { - return func(p *Parser) { - p.pipesAsConcat = true - p.refreshPipesMode() - } -} - -// Dialect returns the SQL dialect configured for this parser. -// Returns "postgresql" if no dialect was explicitly set. -func (p *Parser) Dialect() string { - if p.dialect == "" { - return "postgresql" - } - return p.dialect -} - -// Parser is a recursive-descent SQL parser that converts a token stream into an -// Abstract Syntax Tree (AST). -// -// Parser instances are not thread-safe. Each goroutine must use its own instance, -// obtained from the pool via GetParser and returned with PutParser: -// -// p := parser.GetParser() -// defer parser.PutParser(p) -// tree, err := p.ParseFromModelTokens(tokens) -// -// For dialect-aware parsing or strict mode, use NewParser with options, or call -// ApplyOptions on a pooled instance before parsing. -type Parser struct { - tokens []models.TokenWithSpan - // tokenBuf is parser-owned scratch holding the preprocessed token stream - // (see preprocess). It is reused across parses and never exposed to - // callers. INVARIANT: tokenBuf[len:cap] is always zeroed. - tokenBuf []models.TokenWithSpan - currentPos int - currentToken models.TokenWithSpan - depth int // Current recursion depth - ctx context.Context // Optional context for cancellation support - strict bool // Strict mode rejects empty statements - // dialect holds the SQL dialect for dialect-aware parsing as a raw - // string (default: "" which the string-returning Dialect() method - // reports as "postgresql" for v1.x backward compatibility). - // - // Deprecated: prefer Parser.DialectTyped() / Parser.Capabilities() for - // new parser code. The string field is retained for v1.x backward - // compatibility and will be removed in v2.0 in favour of a typed - // dialect.Dialect field. - dialect string - // dialectTyped is the typed mirror of dialect, parsed once at - // configuration time so that DialectTyped() and Capabilities() are - // O(1) on the hot path rather than re-running dialect.Parse on every - // invocation. - // - // INVARIANT: dialectTyped must always equal dialect.Parse(dialect). - // Maintained by WithDialect (the sole mutator) and Reset. Do not set - // dialect directly; funnel all changes through WithDialect. - dialectTyped dialect.Dialect - // capabilitiesCache is the pre-computed capability matrix for - // dialectTyped. It is refreshed alongside dialectTyped in WithDialect - // and Reset so that Capabilities() is a single struct-field read. - capabilitiesCache dialect.Capabilities - // dmlReturning is true while parsing an INSERT source query or an - // UPDATE ... FROM list, where an unquoted RETURNING starts the RETURNING - // clause and must not be taken as an implicit alias (#610). - dmlReturning bool -<<<<<<< HEAD - // inConnectBy is true while parsing a CONNECT BY condition, where PRIOR - // is a prefix operator binding to its operand (#783). - inConnectBy bool -======= - // pipesAsConcat is the PIPES_AS_CONCAT opt-in set by WithPipesAsConcat. - pipesAsConcat bool - // pipesAsOr is true when || is logical OR: the dialect has - // Capabilities.PipesAsLogicalOr (MySQL, MariaDB) and pipesAsConcat is - // unset. Derived by refreshPipesMode; never set it directly. - pipesAsOr bool ->>>>>>> origin/main -} - -// Deprecated: Parse is provided for backward compatibility only and is scheduled for -// removal in v2.0. Use ParseFromModelTokens with a []models.TokenWithSpan slice from -// the tokenizer instead. This shim wraps each token.Token into a zero-span -// models.TokenWithSpan and has no position information. -// -// Parse parses a slice of token.Token into an AST. -// -// This API is preserved for backward compatibility. Prefer ParseFromModelTokens -// which accepts []models.TokenWithSpan directly and preserves span information. -// -// Internally the tokens are wrapped into models.TokenWithSpan (with empty spans) -// and the preprocessing step is applied before parsing. -// -// Thread Safety: NOT thread-safe - use separate parser instances per goroutine. -func (p *Parser) Parse(tokens []token.Token) (*ast.AST, error) { - // Wrap legacy token.Token into models.TokenWithSpan (spans are zero). - wrapped := make([]models.TokenWithSpan, len(tokens)) - for i, t := range tokens { - wrapped[i] = models.WrapToken(models.Token{Type: t.Type, Value: t.Literal}) - } - // Preprocessing still normalises compound tokens and keyword types. - return p.parseTokens(p.preprocess(wrapped)) -} - -// preprocess normalises tokens into the parser-owned tokenBuf and returns it. -// The result is only valid until the next preprocess or Reset on p, so it must -// never be handed to a caller; parse methods are not re-entrant on one Parser. -func (p *Parser) preprocess(tokens []models.TokenWithSpan) []models.TokenWithSpan { - // Keep tokenBuf[len:cap] zeroed. After Reset len == 0, so this is a no-op - // on the pooled path; on a parser reused without Reset it scrubs the - // previous stream so a shorter one cannot leave stale strings in the tail. - clear(p.tokenBuf) - p.tokenBuf = preprocessTokensInto(p.tokenBuf, tokens) - return p.tokenBuf -} - -// parseTokens is the core parsing routine. It takes a preprocessed -// []models.TokenWithSpan (already normalised by preprocessTokens or p.preprocess) and returns -// the parsed AST. -func (p *Parser) parseTokens(tokens []models.TokenWithSpan) (*ast.AST, error) { - p.tokens = tokens - p.currentPos = 0 - if len(tokens) > 0 { - p.currentToken = tokens[0] - } - - // Get a pre-allocated AST from the pool - result := ast.NewAST() - - // Pre-allocate statements slice based on a reasonable estimate - estimatedStmts := 1 // Most SQL queries have just one statement - if len(tokens) > 100 { - estimatedStmts = 2 // For larger inputs, allocate more - } - if cap(result.Statements) < estimatedStmts { - result.Statements = make([]ast.Statement, 0, estimatedStmts) - } else { - result.Statements = result.Statements[:0] - } - - // Parse statements using Type (int) comparisons for speed - for p.currentPos < len(tokens) && !p.isType(models.TokenTypeEOF) { - // Skip semicolons between statements - if p.isType(models.TokenTypeSemicolon) { - if err := p.checkStrictEmptySemicolon(); err != nil { - ast.ReleaseAST(result) - return nil, err - } - p.advance() - continue - } - - stmt, err := p.parseStatement() - if err != nil { - // Clean up the AST on error - ast.ReleaseAST(result) - return nil, err - } - result.Statements = append(result.Statements, stmt) - - // Optionally consume semicolon after statement - if p.isType(models.TokenTypeSemicolon) { - p.advance() - } - } - - // Check if we got any statements - if len(result.Statements) == 0 { - ast.ReleaseAST(result) - if err := p.checkStrictEmpty(); err != nil { - return nil, err - } - return nil, goerrors.IncompleteStatementError(models.Location{}, "") - } - - return result, nil -} - -// ParseFromModelTokens parses tokenizer output ([]models.TokenWithSpan) directly into an AST. -// -// This is the preferred entry point for parsing SQL. It accepts the output of the -// tokenizer directly without any conversion step. Span information is preserved -// throughout parsing and is available for error reporting. -// -// Issue #322: token_conversion.go has been removed; preprocessing is now a -// lightweight normalisation step that works entirely with models.TokenWithSpan. -func (p *Parser) ParseFromModelTokens(tokens []models.TokenWithSpan) (*ast.AST, error) { - return p.parseTokens(p.preprocess(tokens)) -} - -// ParseFromModelTokensWithPositions is identical to ParseFromModelTokens. -// Position information is embedded in every models.TokenWithSpan. -// -// Deprecated: Use ParseFromModelTokens directly. Scheduled for removal in v2.0. -func (p *Parser) ParseFromModelTokensWithPositions(tokens []models.TokenWithSpan) (*ast.AST, error) { - return p.ParseFromModelTokens(tokens) -} - -// ParseContextFromModelTokens parses tokenizer output with context support for cancellation. -func (p *Parser) ParseContextFromModelTokens(ctx context.Context, tokens []models.TokenWithSpan) (*ast.AST, error) { - return p.parseContextTokens(ctx, p.preprocess(tokens)) -} - -// ParseWithPositions parses tokens with position tracking for enhanced error reporting. -// -// ParseWithPositions parses a ConversionResult into an AST. -// Since models.TokenWithSpan already embeds span/position information, -// this is now a thin wrapper around parseTokens - no separate conversion step needed. -// -// Thread Safety: NOT thread-safe - use separate parser instances per goroutine. -func (p *Parser) ParseWithPositions(result *ConversionResult) (*ast.AST, error) { - return p.parseTokens(result.Tokens) -} - -// ParseContext parses tokens into an AST with context support for cancellation and timeouts. -// -// This method enables graceful cancellation of long-running parsing operations by checking -// the context at strategic points (statement boundaries and expression starts). The parser -// checks context.Err() approximately every 10-20 operations, balancing responsiveness with overhead. -// -// Parameters: -// - ctx: Context for cancellation and timeout control -// - tokens: Slice of parser tokens to parse -// -// Returns: -// - *ast.AST: Parsed Abstract Syntax Tree if successful -// - error: Parsing error, context.Canceled, or context.DeadlineExceeded -// -// Context Checking Strategy: -// - Checked before each statement parsing -// - Checked at the start of parseExpression (recursive) -// - Overhead: ~2% vs non-context parsing -// - Cancellation latency: <100μs typical -// -// Use Cases: -// - Long-running parsing operations that need to be cancellable -// - Implementing timeouts for parsing (prevent hanging on malicious input) -// - Graceful shutdown scenarios in server applications -// - User-initiated cancellation in interactive tools -// -// Error Handling: -// - Returns context.Canceled when ctx.Done() is closed -// - Returns context.DeadlineExceeded when timeout expires -// - Cleans up partial AST on cancellation (no memory leaks) -// -// Usage with Timeout: -// -// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) -// defer cancel() -// -// parser := parser.GetParser() -// defer parser.PutParser(parser) -// -// ast, err := parser.ParseContext(ctx, tokens) -// if err != nil { -// if errors.Is(err, context.DeadlineExceeded) { -// log.Println("Parsing timeout exceeded") -// } else if errors.Is(err, context.Canceled) { -// log.Println("Parsing was cancelled") -// } else { -// log.Printf("Parse error: %v", err) -// } -// return -// } -// defer ast.ReleaseAST(ast) -// -// Usage with Cancellation: -// -// ctx, cancel := context.WithCancel(context.Background()) -// defer cancel() -// -// // Cancel from another goroutine based on user action -// go func() { -// <-userCancelSignal -// cancel() -// }() -// -// ast, err := parser.ParseContext(ctx, tokens) -// // Check for context.Canceled error -// -// Performance Impact: -// - Adds ~2% overhead vs Parse() due to context checking -// - Average: ~354ns for complex queries (vs 347ns for Parse) -// - Negligible impact on modern CPUs with branch prediction -// -// Thread Safety: NOT thread-safe - use separate parser instances per goroutine. -// ParseContext parses a slice of token.Token with context support (backward compat shim). -// For new code prefer ParseContextFromModelTokens. -func (p *Parser) ParseContext(ctx context.Context, tokens []token.Token) (*ast.AST, error) { - // Wrap legacy token.Token into models.TokenWithSpan. - wrapped := make([]models.TokenWithSpan, len(tokens)) - for i, t := range tokens { - wrapped[i] = models.WrapToken(models.Token{Type: t.Type, Value: t.Literal}) - } - return p.parseContextTokens(ctx, p.preprocess(wrapped)) -} - -// parseContextTokens is the core context-aware parsing routine. It takes -// a preprocessed []models.TokenWithSpan and respects ctx for cancellation. -func (p *Parser) parseContextTokens(ctx context.Context, tokens []models.TokenWithSpan) (*ast.AST, error) { - // Check context before starting - if err := ctx.Err(); err != nil { - return nil, err - } - - // Store context for use during parsing - p.ctx = ctx - defer func() { p.ctx = nil }() // Clear context when done - - p.tokens = tokens - p.currentPos = 0 - if len(tokens) > 0 { - p.currentToken = tokens[0] - } - - // Get a pre-allocated AST from the pool - result := ast.NewAST() - - // Pre-allocate statements slice based on a reasonable estimate - estimatedStmts := 1 // Most SQL queries have just one statement - if len(tokens) > 100 { - estimatedStmts = 2 // For larger inputs, allocate more - } - if cap(result.Statements) < estimatedStmts { - result.Statements = make([]ast.Statement, 0, estimatedStmts) - } else { - result.Statements = result.Statements[:0] - } - - // Parse statements using Type (int) comparisons for speed - for p.currentPos < len(tokens) && !p.isType(models.TokenTypeEOF) { - // Check context before each statement - if err := ctx.Err(); err != nil { - // Clean up the AST on error - ast.ReleaseAST(result) - // Context cancellation is not a parsing error; return the context error - // directly so callers can use errors.Is(err, context.Canceled/DeadlineExceeded). - return nil, err - } - - // Skip semicolons between statements - if p.isType(models.TokenTypeSemicolon) { - p.advance() - continue - } - - stmt, err := p.parseStatement() - if err != nil { - // Clean up the AST on error - ast.ReleaseAST(result) - return nil, err - } - result.Statements = append(result.Statements, stmt) - - // Optionally consume semicolon after statement - if p.isType(models.TokenTypeSemicolon) { - p.advance() - } - } - - // Check if we got any statements - if len(result.Statements) == 0 { - ast.ReleaseAST(result) - return nil, goerrors.IncompleteStatementError(p.currentLocation(), "") - } - - return result, nil -} - -// Release releases any resources held by the parser -func (p *Parser) Release() { - // Reset internal state to avoid memory leaks - p.tokens = nil - p.tokenBuf = nil - p.currentPos = 0 - p.currentToken = models.TokenWithSpan{} - p.depth = 0 - p.ctx = nil -} - -// parseStatement parses a single SQL statement using O(1) Type-based dispatch. -// -// This is the statement routing function that examines the current token and dispatches -// to the appropriate specialized parser based on the statement type. It uses O(1) switch -// dispatch on Type (integer enum) which compiles to a jump table for optimal performance. -// -// Performance Optimization: -// - Fast Path: O(1) Type switch (~0.24ns per comparison) -// - Fallback: String-based matching for tokens without Type (~3.4ns) -// - Jump Table: Compiler generates jump table for switch on integers -// - 14x Faster: Type vs string comparison on hot paths -// -// Supported Statement Types: -// -// DML (Data Manipulation): -// - SELECT: Query with joins, subqueries, window functions, CTEs -// - INSERT: Insert with VALUES, column list, RETURNING -// - UPDATE: Update with SET, WHERE, RETURNING -// - DELETE: Delete with WHERE, RETURNING -// - MERGE: SQL:2003 MERGE with MATCHED/NOT MATCHED -// -// DDL (Data Definition): -// - CREATE: TABLE, VIEW, MATERIALIZED VIEW, INDEX -// - ALTER: ALTER TABLE for column and constraint modifications -// - DROP: Drop objects with CASCADE/RESTRICT -// - TRUNCATE: TRUNCATE TABLE with identity options -// - REFRESH: REFRESH MATERIALIZED VIEW -// -// Advanced: -// - WITH: Common Table Expressions (CTEs) with recursive support -// - Set Operations: UNION, EXCEPT, INTERSECT (via parseSelectWithSetOperations) -// -// Returns: -// - ast.Statement: Parsed statement node (specific type depends on SQL) -// - error: Syntax error if statement is invalid or unsupported -// -// Error Handling: -// - Returns expectedError("statement") if token is not a statement keyword -// - Returns specific parse errors from statement-specific parsers -// - Checks context for cancellation if ctx is set -// -// Context Checking: -// - Checks p.ctx.Err() before parsing to enable cancellation -// - Fast path: nil check + atomic read -// - Overhead: <5ns when context is set -// -// Thread Safety: NOT thread-safe - operates on parser instance state. -func (p *Parser) parseStatement() (ast.Statement, error) { - // Check context if available - if p.ctx != nil { - if err := p.ctx.Err(); err != nil { - // Context cancellation is not a parsing error; return the context error - // directly so callers can use errors.Is(err, context.Canceled/DeadlineExceeded). - return nil, err - } - } - - // O(1) switch dispatch on Token.Type (compiles to jump table). - // All tokens are normalized at parse entry so Type is always set. - switch p.currentToken.Token.Type { - case models.TokenTypeWith: - return p.parseWithStatement() - case models.TokenTypeSelect: - stmtPos := p.currentLocation() - p.advance() - stmt, err := p.parseSelectWithSetOperations() - if err != nil { - return nil, err - } - if ss, ok := stmt.(*ast.SelectStatement); ok { - if ss.Pos.IsZero() { - ss.Pos = stmtPos - } - } - // ClickHouse trailing SETTINGS k = v [, k = v]... on SELECT; stored on - // the (rightmost) SelectStatement (#600). - if p.dialect == string(keywords.DialectClickHouse) && p.isTokenMatch("SETTINGS") { - if err := p.parseTrailingSelectSettings(stmt); err != nil { - return nil, err - } - } - return stmt, nil - case models.TokenTypeLParen: - // Parenthesized query expression: (SELECT 1) UNION (SELECT 2), - // (SELECT 1) ORDER BY 1. Any other "(" keeps the generic error. - switch p.peekToken().Token.Type { - case models.TokenTypeSelect, models.TokenTypeWith, models.TokenTypeLParen: - default: - return nil, p.expectedError("statement") - } - stmtPos := p.currentLocation() - q, err := p.parseParenthesizedQuery("SELECT in parenthesized query") - if err != nil { - return nil, err - } - stmt, err := p.parseSetOperationChain(q, true) - if err != nil { - return nil, err - } - if ss, ok := stmt.(*ast.SelectStatement); ok && ss.Pos.IsZero() { - ss.Pos = stmtPos - } - return stmt, nil - case models.TokenTypeInsert: - stmtPos := p.currentLocation() - p.advance() - stmt, err := p.parseInsertStatement() - if err != nil { - return nil, err - } - if is, ok := stmt.(*ast.InsertStatement); ok { - if is.Pos.IsZero() { - is.Pos = stmtPos - } - } - return stmt, nil - case models.TokenTypeUpdate: - stmtPos := p.currentLocation() - p.advance() - stmt, err := p.parseUpdateStatement() - if err != nil { - return nil, err - } - if us, ok := stmt.(*ast.UpdateStatement); ok { - if us.Pos.IsZero() { - us.Pos = stmtPos - } - } - return stmt, nil - case models.TokenTypeDelete: - stmtPos := p.currentLocation() - p.advance() - stmt, err := p.parseDeleteStatement() - if err != nil { - return nil, err - } - if ds, ok := stmt.(*ast.DeleteStatement); ok { - if ds.Pos.IsZero() { - ds.Pos = stmtPos - } - } - return stmt, nil - case models.TokenTypeAlter: - stmtPos := p.currentLocation() - p.advance() - // MariaDB: ALTER SEQUENCE [IF EXISTS] name [options...] - if p.isMariaDB() && p.isTokenMatch("SEQUENCE") { - p.advance() // Consume SEQUENCE - stmt, err := p.parseAlterSequenceStatement() - if err != nil { - return nil, err - } - if stmt.Pos.IsZero() { - stmt.Pos = stmtPos - } - return stmt, nil - } - return p.parseAlterTableStmt() - case models.TokenTypeMerge: - p.advance() - return p.parseMergeStatement() - case models.TokenTypeCreate: - p.advance() - return p.parseCreateStatement() - case models.TokenTypeDrop: - stmtPos := p.currentLocation() - p.advance() - // MariaDB: DROP SEQUENCE [IF EXISTS | IF NOT EXISTS] name - if p.isMariaDB() && p.isTokenMatch("SEQUENCE") { - p.advance() // Consume SEQUENCE - stmt, err := p.parseDropSequenceStatement() - if err != nil { - return nil, err - } - if stmt.Pos.IsZero() { - stmt.Pos = stmtPos - } - return stmt, nil - } - return p.parseDropStatement() - case models.TokenTypeRefresh: - p.advance() - return p.parseRefreshStatement() - case models.TokenTypeTruncate: - p.advance() - return p.parseTruncateStatement() - case models.TokenTypeShow: - p.advance() - return p.parseShowStatement() - case models.TokenTypeDescribe, models.TokenTypeExplain, models.TokenTypeDesc: - // DESC is the ORDER-BY sort-direction token but also a synonym for - // DESCRIBE at statement position (Oracle, Snowflake, MySQL). - p.advance() - return p.parseDescribeStatement() - case models.TokenTypeReplace: - p.advance() - return p.parseReplaceStatement() - case models.TokenTypeKeyword: - // Handle keyword-type tokens that have dedicated parsers. - // PRAGMA is a SQLite statement keyword tokenized as TokenTypeKeyword - // when the SQLite dialect keyword set is active. - if strings.EqualFold(p.currentToken.Token.Value, "PRAGMA") { - p.advance() - return p.parsePragmaStatement() - } - // Snowflake stage operations may arrive as keyword tokens depending on - // the active keyword table (LIST, COPY, etc. can be registered). - if p.dialect == string(keywords.DialectSnowflake) { - upper := strings.ToUpper(p.currentToken.Token.Value) - switch upper { - case "COPY", "PUT", "GET", "LIST", "REMOVE", "LS": - return p.parseSnowflakeStageStatement(upper) - } - } - case models.TokenTypeIdentifier: - // PRAGMA may be tokenized as IDENTIFIER when no dialect-specific keyword - // set is active (e.g. when using the default PostgreSQL tokenizer dialect). - // Support it as a statement keyword regardless of tokenizer dialect. - if strings.EqualFold(p.currentToken.Token.Value, "PRAGMA") { - p.advance() - return p.parsePragmaStatement() - } - // Snowflake session-context switches: USE [WAREHOUSE|DATABASE|SCHEMA|ROLE] . - // USE is not tokenized as a keyword; dispatch by value in the Snowflake dialect. - if p.dialect == string(keywords.DialectSnowflake) && - strings.EqualFold(p.currentToken.Token.Value, "USE") { - return p.parseSnowflakeUseStatement() - } - // Snowflake stage operations: COPY INTO, PUT, GET, LIST, REMOVE. - // All tokenize as identifiers; parse-only stubs that consume the - // rest of the statement body. - if p.dialect == string(keywords.DialectSnowflake) { - upper := strings.ToUpper(p.currentToken.Token.Value) - switch upper { - case "COPY", "PUT", "GET", "LIST", "REMOVE", "LS": - return p.parseSnowflakeStageStatement(upper) - } - } - } - return nil, p.expectedError("statement") -} - -// parseSnowflakeUseStatement parses: -// -// USE [WAREHOUSE | DATABASE | SCHEMA | ROLE] -// -// The object-kind keyword is optional (plain "USE " switches the current -// database). Returned as an UnsupportedStatement until a dedicated UseStatement -// node is introduced. -func (p *Parser) parseSnowflakeUseStatement() (ast.Statement, error) { - p.advance() // Consume USE - var rawParts []string - rawParts = append(rawParts, "USE") - // Optional object kind. - switch strings.ToUpper(p.currentToken.Token.Value) { - case "WAREHOUSE", "DATABASE", "SCHEMA", "ROLE": - rawParts = append(rawParts, strings.ToUpper(p.currentToken.Token.Value)) - p.advance() - } - name, err := p.parseQualifiedName() - if err != nil { - return nil, p.qualifiedNameError(err, "name after USE") - } - rawParts = append(rawParts, name) - stmt := ast.GetUnsupportedStatement() - stmt.Kind = "USE" - stmt.RawSQL = strings.Join(rawParts, " ") - return stmt, nil -} - -// parseSnowflakeStageStatement parses Snowflake stage operations as stubs: -// -// COPY INTO FROM [options] -// PUT file:// @ -// GET @ file:// -// LIST @ (or LS) -// REMOVE @/ -// -// The statement is consumed token-by-token (tracking balanced parens) until -// ';' or EOF and returned as an UnsupportedStatement tagged with the -// operation kind. No full AST modeling yet; follow-up work. -func (p *Parser) parseSnowflakeStageStatement(kind string) (ast.Statement, error) { - p.advance() // Consume leading kind token - - var rawParts []string - rawParts = append(rawParts, kind) - - // COPY INTO: consume the INTO keyword if present. - if kind == "COPY" && p.isType(models.TokenTypeInto) { - rawParts = append(rawParts, "INTO") - p.advance() - } - - // Consume the rest of the statement body, capturing tokens for RawSQL. - depth := 0 - for { - t := p.currentToken.Token.Type - if t == models.TokenTypeEOF { - break - } - if t == models.TokenTypeSemicolon && depth == 0 { - break - } - if p.currentToken.Token.Value != "" { - rawParts = append(rawParts, p.currentToken.Token.Value) - } - if t == models.TokenTypeLParen { - depth++ - } else if t == models.TokenTypeRParen { - depth-- - } - p.advance() - } - stub := ast.GetUnsupportedStatement() - stub.Kind = kind - stub.RawSQL = strings.Join(rawParts, " ") - return stub, nil -} - -// NewParser creates a new parser with optional configuration. -func NewParser(opts ...ParserOption) *Parser { - p := &Parser{} - p.resetDialectState() - for _, opt := range opts { - opt(p) - } - return p -} - -// ApplyOptions applies parser options to configure behavior. -func (p *Parser) ApplyOptions(opts ...ParserOption) { - for _, opt := range opts { - opt(p) - } -} - -// checkStrictEmpty returns an error if strict mode is enabled and no statements were parsed. -// This consolidates the repeated strict empty-statement check pattern. -func (p *Parser) checkStrictEmpty() error { - if p.strict { - return goerrors.InvalidSyntaxError( - "empty statement not allowed in strict mode", - p.currentLocation(), - "", - ).WithHint("provide at least one SQL statement") - } - return nil -} - -// checkStrictEmptySemicolon returns an error if strict mode is enabled and a bare semicolon is encountered. -func (p *Parser) checkStrictEmptySemicolon() error { - if p.strict { - return goerrors.InvalidSyntaxError( - "empty statement not allowed in strict mode", - p.currentLocation(), - "", - ).WithHint("remove extra semicolons or disable strict mode") - } - return nil -} - -// advance moves to the next token -func (p *Parser) advance() { - p.currentPos++ - if p.currentPos < len(p.tokens) { - p.currentToken = p.tokens[p.currentPos] - } else { - p.currentToken = models.TokenWithSpan{} // EOF sentinel - } -} - -// peekToken returns the next token without advancing the parser position. -// Returns an empty TokenWithSpan if at the end of input. -func (p *Parser) peekToken() models.TokenWithSpan { - nextPos := p.currentPos + 1 - if nextPos < len(p.tokens) { - return p.tokens[nextPos] - } - return models.TokenWithSpan{} -} - -// ============================================================================= -// Type-based Helper Methods (Fast Int Comparisons) -// ============================================================================= -// These methods use int-based Type comparisons which are significantly -// faster than string comparisons (~0.24ns vs ~3.4ns). Use these for hot paths. -// They include fallback to string-based Type comparison for backward compatibility -// with tests that create tokens directly without setting Type. - -// isType checks if the current token's Type matches the expected type. -// Pure integer comparison - no string fallback. -func (p *Parser) isType(expected models.TokenType) bool { - return p.currentToken.Token.Type == expected -} - -// matchType checks if the current token's Type matches the expected type and advances if so. -func (p *Parser) matchType(expected models.TokenType) bool { - if p.currentToken.Token.Type == expected { - p.advance() - return true - } - return false -} - -// isAnyType checks if the current token's Type matches any of the given types. -// More efficient than multiple isType calls when checking many alternatives. -func (p *Parser) isAnyType(types ...models.TokenType) bool { - for _, t := range types { - if p.isType(t) { - return true - } - } - return false -} - -// isIdentifier checks if the current token is an identifier. -// Includes both regular identifiers and double-quoted identifiers. -// In SQL, double-quoted strings are treated as identifiers (e.g., "column_name"). -func (p *Parser) isIdentifier() bool { - return p.isType(models.TokenTypeIdentifier) || p.isType(models.TokenTypeDoubleQuotedString) -} - -// isStringLiteral checks if the current token is a string literal. -// Handles all string token subtypes (single-quoted, dollar-quoted, etc.) -func (p *Parser) isStringLiteral() bool { - switch p.currentToken.Token.Type { - case models.TokenTypeString, models.TokenTypeSingleQuotedString, models.TokenTypeDollarQuotedString: - return true - } - return false -} - -// isComparisonOperator checks if the current token is a comparison operator using O(1) switch. -func (p *Parser) isComparisonOperator() bool { - switch p.currentToken.Token.Type { - case models.TokenTypeEq, models.TokenTypeLt, models.TokenTypeGt, - models.TokenTypeNeq, models.TokenTypeLtEq, models.TokenTypeGtEq, - models.TokenTypeTilde, models.TokenTypeTildeAsterisk, - models.TokenTypeExclamationMarkTilde, models.TokenTypeExclamationMarkTildeAsterisk: - return true - } - return false -} - -// isQuantifier checks if the current token is ANY or ALL using O(1) switch. -func (p *Parser) isQuantifier() bool { - switch p.currentToken.Token.Type { - case models.TokenTypeAny, models.TokenTypeAll: - return true - } - return false -} - -// isBooleanLiteral checks if the current token is TRUE or FALSE using O(1) switch. -func (p *Parser) isBooleanLiteral() bool { - switch p.currentToken.Token.Type { - case models.TokenTypeTrue, models.TokenTypeFalse: - return true - } - return false -} - -// ============================================================================= - -// expectedError returns an error for unexpected token -func (p *Parser) expectedError(expected string) error { - return goerrors.ExpectedTokenError(expected, p.currentToken.Token.Type.String(), p.currentLocation(), "") -} - -// parseIdent parses an identifier -func (p *Parser) parseIdent() *ast.Identifier { - // Accept both regular identifiers and double-quoted identifiers - if !p.isType(models.TokenTypeIdentifier) && !p.isType(models.TokenTypeDoubleQuotedString) { - return nil - } - pos := p.currentLocation() - ident := &ast.Identifier{Name: p.currentToken.Token.Value, Pos: pos, Quoted: p.isType(models.TokenTypeDoubleQuotedString)} - p.advance() - return ident -} - -// parseIdentAsString parses an identifier and returns its name as a string -func (p *Parser) parseIdentAsString() string { - ident := p.parseIdent() - if ident == nil { - return "" - } - return ident.Name -} - -// parseBareWordAsString parses any word-like token (identifier or keyword) and -// returns its value as a string. This is used in contexts where arbitrary -// user-defined names may collide with SQL keywords (e.g. DCPROPERTIES keys). -// It advances the parser position and returns "" if no word-like token is found. -func (p *Parser) parseBareWordAsString() string { - typ := p.currentToken.Token.Type - // Reject punctuation / operator / structural tokens. - switch typ { - case models.TokenTypeEOF, models.TokenTypeEq, models.TokenTypeComma, - models.TokenTypeLParen, models.TokenTypeRParen, - models.TokenTypeLBracket, models.TokenTypeRBracket, - models.TokenTypeLBrace, models.TokenTypeRBrace, - models.TokenTypeSemicolon, models.TokenTypePeriod, - models.TokenTypeUnknown: - return "" - } - if p.currentToken.Token.Value == "" { - return "" - } - val := p.currentToken.Token.Value - p.advance() - return val -} - -// parseObjectName parses an object name (possibly qualified) -func (p *Parser) parseObjectName() ast.ObjectName { - ident := p.parseIdent() - if ident == nil { - return ast.ObjectName{} - } - return ast.ObjectName{Name: ident.Name} -} - -// parseStringLiteral parses a string literal -func (p *Parser) parseStringLiteral() string { - if !p.isStringLiteral() { - return "" - } - value := p.currentToken.Token.Value - p.advance() - return value -} - -// maxQualifiedNameParts caps the number of dot-separated parts in a qualified -// object (table, view, index, schema) name. Four parts cover the longest form -// any supported dialect accepts (SQL Server server.database.schema.table); the -// limit matches maxColumnRefParts so object and column references agree. -const maxQualifiedNameParts = maxColumnRefParts - -// errQualifiedNameTooLong is the Cause of the E2004 that parseQualifiedName -// returns for a name with more than maxQualifiedNameParts parts. -var errQualifiedNameTooLong = errors.New("qualified name has too many parts") - -// isQualifiedNameTooLong reports whether err is the part-limit error from -// parseQualifiedName. Callers that re-report a bad name in their own words -// ("expected table name") must pass this error through unchanged: it is raised -// after the extra '.' is consumed, so a generic message would point at a -// valid identifier. -func isQualifiedNameTooLong(err error) bool { - return errors.Is(err, errQualifiedNameTooLong) -} - -// qualifiedNameError returns err when it is the part-limit error from -// parseQualifiedName and p.expectedError(expected) otherwise. -func (p *Parser) qualifiedNameError(err error, expected string) error { - if isQualifiedNameTooLong(err) { - return err - } - return p.expectedError(expected) -} - -// parseQualifiedName parses a possibly qualified object name (table, -// schema.table, db.schema.table, server.db.schema.table) and returns it as a -// dotted string. The parts are collected and joined once, so the cost is -// linear in the input, and names with more than maxQualifiedNameParts parts -// are rejected with E2004. -func (p *Parser) parseQualifiedName() (string, error) { - if !p.isIdentifier() && !p.isNonReservedKeyword() { - return "", p.expectedError("identifier") - } - var parts [maxQualifiedNameParts]string - parts[0] = p.currentToken.Token.Value - n := 1 - p.advance() - - // Check for schema.table or db.schema.table - for p.isType(models.TokenTypePeriod) { - p.advance() // Consume . - if n == maxQualifiedNameParts { - return "", goerrors.InvalidSyntaxError( - fmt.Sprintf("qualified name has more than %d parts", maxQualifiedNameParts), - p.currentLocation(), - "", - ).WithCause(errQualifiedNameTooLong) - } - if !p.isIdentifier() && !p.isNonReservedKeyword() { - return "", p.expectedError("identifier after .") - } - parts[n] = p.currentToken.Token.Value - n++ - p.advance() - } - - return strings.Join(parts[:n], "."), nil -} - -// Accepts IDENT or non-reserved keywords that can be used as table names -func (p *Parser) parseTableReference() (*ast.TableReference, error) { - name, err := p.parseQualifiedName() - if err != nil { - return nil, err - } - return &ast.TableReference{Name: name}, nil -} - -// isNonReservedKeyword checks if current token is a non-reserved keyword -// that can be used as a table or column name -func (p *Parser) isNonReservedKeyword() bool { - // These keywords can be used as table/column names in most SQL dialects. - // Use Type where possible, with value fallback for tokens that have - // the generic TokenTypeKeyword. - switch p.currentToken.Token.Type { - case models.TokenTypeTarget, models.TokenTypeSource, models.TokenTypeMatched: - return true - case models.TokenTypeTable, models.TokenTypeIndex, models.TokenTypeView, - models.TokenTypeKey, models.TokenTypeColumn, models.TokenTypeDatabase, - models.TokenTypePartition, models.TokenTypeRows, models.TokenTypeDefault: - // DDL keywords that are commonly used as quoted identifiers in MySQL (backtick) - // and SQL Server (bracket) dialects, and as plain column names in ClickHouse - // system tables (system.parts.partition, system.replicas.table, - // system.tables.rows, etc). - return true - case models.TokenTypeKeyword: - // Token may have generic Type; check value for specific keywords - switch strings.ToUpper(p.currentToken.Token.Value) { - case "TARGET", "SOURCE", "MATCHED", "VALUE", "NAME", "TYPE", "STATUS", - "TABLES", "DATABASES": - return true - } - } - return false -} - -// isOraclePseudoColumn returns true when the current keyword token is an -// Oracle/MariaDB pseudo-column that should be parsed as an expression. -func (p *Parser) isOraclePseudoColumn() bool { - return p.isOraclePseudoColumn2(p.currentToken.Token.Value) -} - -// isOraclePseudoColumn2 checks if a given name is an Oracle pseudo-column. -func (p *Parser) isOraclePseudoColumn2(name string) bool { - switch strings.ToUpper(name) { - case "ROWNUM", "ROWID", "LEVEL", "SYSDATE", "SYSTIMESTAMP": - return true - } - return false -} - -// isWindowClauseKeyword returns true if the current token is the WINDOW -// keyword that starts a named window clause (SQL:2003). Must not be consumed -// as a table alias. -func (p *Parser) isWindowClauseKeyword() bool { - return p.isIdentifier() && strings.EqualFold(p.currentToken.Token.Value, "WINDOW") -} - -// isSettingsKeyword returns true if the current token is the ClickHouse -// SETTINGS keyword. Must not be consumed as a table alias. -func (p *Parser) isSettingsKeyword() bool { - return p.dialect == string(keywords.DialectClickHouse) && - p.isTokenMatch("SETTINGS") -} - -// isReturningKeyword reports whether the current token is an unquoted -// RETURNING keyword (any case). Quoted identifiers named returning are names, -// not the keyword: quoting is the only way to use a keyword as an identifier. -func (p *Parser) isReturningKeyword() bool { - if p.isType(models.TokenTypeReturning) { - return true - } - return p.isType(models.TokenTypeIdentifier) && - strings.EqualFold(p.currentToken.Token.Value, "RETURNING") && - !p.isQuotedIdentifierToken() -} - -// isQuotedIdentifierToken reports whether the current identifier token was -// written with delimiters. Bracket identifiers record Token.Quote = '['; the -// tokenizer leaves Quote zero for backtick identifiers, so a source span wider -// than the value also counts as quoted. Tokens without spans (Start == End) -// are treated as unquoted. -func (p *Parser) isQuotedIdentifierToken() bool { - tok := p.currentToken - if tok.Token.Quote != 0 { - return true - } - return tok.Start.Line == tok.End.Line && tok.End.Column-tok.Start.Column > len(tok.Token.Value) -} - -// isDMLReturningKeyword reports whether the current token is RETURNING inside -// an INSERT source query or UPDATE ... FROM list, where it starts the -// statement's RETURNING clause rather than naming an alias (#610). -// -// A RETURNING list cannot begin with a join keyword (including ON and USING), -// a clause keyword, a comma, a dot, a closing parenthesis or the end of the -// statement, nor with any clause that may follow a table reference in the -// current dialect, so when one of those follows, the token is an ordinary -// alias (FROM s JOIN r returning ON ..., FROM s returning LEFT JOIN ...), as -// it is in dialects that do not reserve the word. -func (p *Parser) isDMLReturningKeyword() bool { - if !p.dmlReturning || !p.isReturningKeyword() { - return false - } - next := p.peekToken().Token.Type - if next.IsJoinKeyword() { - return false - } - switch next { - case models.TokenTypeComma, models.TokenTypePeriod, models.TokenTypeRParen, - models.TokenTypeEOF, models.TokenTypeSemicolon, - models.TokenTypeWhere, models.TokenTypeGroup, models.TokenTypeHaving, - models.TokenTypeOrder, models.TokenTypeLimit, models.TokenTypeOffset, - models.TokenTypeFetch, models.TokenTypeFor, models.TokenTypeWith, - models.TokenTypeUnion, models.TokenTypeExcept, models.TokenTypeIntersect: - return false - } - return !p.nextStartsTableClause() -} - -// nextStartsTableClause reports whether the token after the current one -// starts a dialect-specific clause that may follow a table reference (WINDOW, -// QUALIFY, PIVOT, START WITH / CONNECT BY, SAMPLE, MATCH_RECOGNIZE, time -// travel, ClickHouse FINAL / PREWHERE / ARRAY JOIN / SETTINGS, MINUS). It -// evaluates the same dialect-gated predicates that guard FROM aliases, one -// token ahead, and restores the parser position before returning. -func (p *Parser) nextStartsTableClause() bool { - savedPos, savedTok := p.currentPos, p.currentToken - p.advance() - defer func() { p.currentPos, p.currentToken = savedPos, savedTok }() - if p.isWindowClauseKeyword() || p.isQualifyKeyword() || p.isMinusSetOp() || - p.isPivotKeyword() || p.isUnpivotKeyword() || p.isMariaDBClauseStart() || - p.isSampleKeyword() || p.isMatchRecognizeKeyword() || - p.isSnowflakeTimeTravelStart() || p.isSettingsKeyword() { - return true - } - return p.dialect == string(keywords.DialectClickHouse) && - (p.isTokenMatch("FINAL") || p.isTokenMatch("PREWHERE") || - (p.isType(models.TokenTypeArray) && p.peekToken().Token.Type == models.TokenTypeJoin)) -} - -// canBeAlias checks if current token can be used as an alias. -// Aliases can be IDENT, double-quoted identifiers, or certain non-reserved keywords, -// but NOT contextual clause keywords that would be consumed as aliases by mistake -// (e.g. MINUS in Snowflake/Oracle, QUALIFY in Snowflake/BigQuery, WINDOW). -func (p *Parser) canBeAlias() bool { - if p.isMinusSetOp() || p.isQualifyKeyword() || p.isWindowClauseKeyword() || p.isSettingsKeyword() || p.isDMLReturningKeyword() { - return false - } - return p.isIdentifier() || p.isNonReservedKeyword() -} - -// parseAlterTableStmt is a simplified version for the parser implementation -// It delegates to the more comprehensive parseAlterStatement in alter.go -func (p *Parser) parseAlterTableStmt() (ast.Statement, error) { - // We've already consumed the ALTER token in matchType - // This is just a placeholder that delegates to the main implementation - return p.parseAlterStatement() -} - -// isJoinKeyword checks if current token is a JOIN-related keyword -func (p *Parser) isJoinKeyword() bool { - // ClickHouse: LEFT ARRAY JOIN is not a regular JOIN — exclude it so - // the ARRAY JOIN parser handles it separately. - if p.dialect == string(keywords.DialectClickHouse) && - p.isType(models.TokenTypeLeft) && - p.peekToken().Token.Type == models.TokenTypeArray { - return false - } - if p.isAnyType( - models.TokenTypeJoin, models.TokenTypeInner, models.TokenTypeLeft, - models.TokenTypeRight, models.TokenTypeFull, models.TokenTypeCross, - models.TokenTypeNatural, - ) { - return true - } - // SQL Server: OUTER APPLY starts with OUTER - if p.dialect == string(keywords.DialectSQLServer) && p.isType(models.TokenTypeOuter) { - return true - } - // ClickHouse: GLOBAL JOIN — GLOBAL is TokenTypeKeyword, not a dedicated join token - if p.dialect == string(keywords.DialectClickHouse) && p.isTokenMatch("GLOBAL") { - return true - } - // ClickHouse: ANY / ALL as join strictness prefix (ANY LEFT JOIN, ALL INNER JOIN) - if p.dialect == string(keywords.DialectClickHouse) && - (p.isType(models.TokenTypeAny) || p.isType(models.TokenTypeAll)) { - return true - } - return false -} - -// parseWithStatement parses a WITH statement (Common Table Expression). -// It supports both simple and recursive CTEs, multiple CTE definitions, and column specifications. -// -// Examples: -// -// WITH sales_summary AS (SELECT region, total FROM sales) SELECT * FROM sales_summary -// WITH RECURSIVE emp_tree AS (SELECT emp_id FROM employees) SELECT * FROM emp_tree -// WITH first AS (SELECT * FROM t1), second AS (SELECT * FROM first) SELECT * FROM second -// WITH summary(region, total) AS (SELECT region, SUM(amount) FROM sales GROUP BY region) SELECT * FROM summary From 76a6c311bfb1493fb4664573cc5612136ac36bf1 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:00:09 +0530 Subject: [PATCH 02/12] chore(release): bump version to 1.16.0 [vercel skip] gosqlx.Version and the CLI version were still 1.14.0 (v1.15.0 shipped with them), so gosqlx --version, the MCP server info and /health reported 1.14.0. Also bumps the VS Code extension, Claude Code plugin and marketplace, glama, performance baseline label, llms.txt and the site's release badge. pygosqlx __version__ now matches pyproject (0.2.0). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- .claude-plugin/marketplace.json | 2 +- CLAUDE.md | 2 +- cmd/gosqlx/cmd/doc.go | 2 +- cmd/gosqlx/cmd/root.go | 17 +++++++++++++++-- cmd/gosqlx/doc.go | 2 +- examples/README.md | 2 +- glama.json | 2 +- llms.txt | 2 +- performance_baselines.json | 2 +- pkg/gosqlx/gosqlx.go | 2 +- plugins/gosqlx/.claude-plugin/plugin.json | 2 +- python/pygosqlx/__init__.py | 2 +- vscode-extension/package-lock.json | 4 ++-- vscode-extension/package.json | 2 +- website/src/data/metrics.ts | 2 +- 15 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index dd5c9f2a..cffbe09c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "gosqlx", "source": "./plugins/gosqlx", "description": "Validate, format, lint, analyze and security-scan SQL with GoSQLX (MCP tools, skills, slash commands, a SQL reviewer agent and a .sql validation hook).", - "version": "1.15.0", + "version": "1.16.0", "category": "development", "tags": ["sql", "database", "linting", "formatting", "security", "mcp"], "homepage": "https://gosqlx.dev", diff --git a/CLAUDE.md b/CLAUDE.md index 0252732d..33efc143 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ GoSQLX is a **production-ready**, **race-free**, high-performance SQL parsing SD **Requirements**: Go 1.26+ (upgraded from 1.23 to fix stdlib vulnerabilities; `mark3labs/mcp-go` requires 1.23) -**Production Status**: ✅ Validated for production deployment (v1.6.0+, current: v1.14.0) +**Production Status**: ✅ Validated for production deployment (v1.6.0+, current: v1.16.0) - Thread-safe with zero race conditions (20,000+ concurrent operations tested) - 1.38M+ ops/sec sustained, 1.5M peak with memory-efficient object pooling - ~80-85% SQL-99 compliance (window functions, CTEs, set operations, MERGE, etc.) diff --git a/cmd/gosqlx/cmd/doc.go b/cmd/gosqlx/cmd/doc.go index d3e18257..97aa606e 100644 --- a/cmd/gosqlx/cmd/doc.go +++ b/cmd/gosqlx/cmd/doc.go @@ -348,7 +348,7 @@ // // Version information: // -// Version = "1.14.0" - Current CLI version +// Version = "1.16.0" - Current CLI version // // # Dependencies // diff --git a/cmd/gosqlx/cmd/root.go b/cmd/gosqlx/cmd/root.go index 33d317db..4bb6311a 100644 --- a/cmd/gosqlx/cmd/root.go +++ b/cmd/gosqlx/cmd/root.go @@ -28,6 +28,19 @@ import ( // This version tracks feature releases and compatibility. // Format: MAJOR.MINOR.PATCH (Semantic Versioning 2.0.0) // +// Version 1.16.0 includes: +// - SQL syntax highlighting (pkg/highlight) for API, CLI (gosqlx highlight), LSP, WASM and playground +// - Dialect-aware LSP (diagnostics, formatting, completion, didChangeConfiguration) +// - --dialect on parse, format, analyze and lint; one list of 11 dialect names everywhere +// - Hardened gosqlx-mcp HTTP server, ChatGPT app support and a Claude Code plugin +// - Editor-accurate positions (a tab is one column) and MySQL/MariaDB || as logical OR +// - October 2026 performance audit (zero-copy tokens, fewer allocations) and correctness sweep +// +// Version 1.15.0 includes: +// - ParseTree opaque Tree API with typed walkers +// - ClickHouse dialect: 83/83 QA pass +// - Oracle gaps and architect-review correctness sweeps +// // Version 1.14.0 includes: // - Dialect-aware SQL formatting (TOP/FETCH FIRST/LIMIT per dialect) // - Snowflake dialect: 87/87 QA pass (MATCH_RECOGNIZE, @stage, SAMPLE, QUALIFY, VARIANT, time-travel, MINUS, LATERAL FLATTEN, TRY_CAST) @@ -43,7 +56,7 @@ import ( // - ClickHouse SQL dialect support (PREWHERE, FINAL, GLOBAL IN) // - LSP semantic tokens + diagnostic debouncing // - Parser API consolidation (ParseFromModelTokens canonical) -var Version = "1.14.0" +var Version = "1.16.0" var ( // verbose enables detailed output for debugging and troubleshooting. @@ -131,7 +144,7 @@ Key features: • CI/CD integration with proper exit codes Performance: 1.5M+ operations/second sustained, 1.97M peak. 100-1000x faster than competitors.`, - Version: "1.14.0", + Version: "1.16.0", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/gosqlx/doc.go b/cmd/gosqlx/doc.go index ce5f50c2..a14a13b2 100644 --- a/cmd/gosqlx/doc.go +++ b/cmd/gosqlx/doc.go @@ -24,7 +24,7 @@ // // # Version // -// Current version: 1.14.0 +// Current version: 1.16.0 // // # Architecture // diff --git a/examples/README.md b/examples/README.md index fe87ef01..a13cb528 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # GoSQLX Examples -**Version**: v1.14.0 +**Version**: v1.16.0 This directory contains various examples demonstrating how to use the GoSQLX SQL parsing SDK. diff --git a/glama.json b/glama.json index 8c020b43..0c573fbb 100644 --- a/glama.json +++ b/glama.json @@ -6,7 +6,7 @@ "description": "Production-ready SQL parsing, formatting, linting, security scanning, and metadata extraction for Go. 7 MCP tools covering the full SQL analysis pipeline: validate_sql, format_sql, parse_sql, extract_metadata, security_scan, lint_sql, and analyze_sql. Supports PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, and ClickHouse dialects. 1.38M+ ops/sec, zero-copy, race-free.", "repository": "https://github.com/ajitpratap0/GoSQLX", "homepage": "https://gosqlx.dev", - "version": "1.14.0", + "version": "1.16.0", "license": "Apache-2.0", "author": { "name": "Ajit Pratap Singh", diff --git a/llms.txt b/llms.txt index 2a0cb425..313ac442 100644 --- a/llms.txt +++ b/llms.txt @@ -4,7 +4,7 @@ GoSQLX is a production-ready SQL parsing library for Go with zero-copy tokenization, recursive-descent parsing and AST generation backed by object pooling. It supports 8 dialects (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse) and ships a CLI (`gosqlx`) for validation, formatting, linting, transpilation and security analysis. Apache-2.0 licensed. Requires Go 1.26+. -Current stable version: v1.15.0 +Current stable version: v1.16.0 Website: https://gosqlx.dev Interactive Playground: https://gosqlx.dev/playground/ diff --git a/performance_baselines.json b/performance_baselines.json index 2ba19c52..358eae5f 100644 --- a/performance_baselines.json +++ b/performance_baselines.json @@ -1,5 +1,5 @@ { - "version": "1.14.0", + "version": "1.16.0", "updated": "2026-10-13", "baselines": { "SimpleSelect": { diff --git a/pkg/gosqlx/gosqlx.go b/pkg/gosqlx/gosqlx.go index 9eddf3a1..73b1e268 100644 --- a/pkg/gosqlx/gosqlx.go +++ b/pkg/gosqlx/gosqlx.go @@ -30,7 +30,7 @@ import ( ) // Version is the current GoSQLX library version. -const Version = "1.14.0" +const Version = "1.16.0" // Parse tokenizes and parses SQL in one call, returning an Abstract Syntax Tree (AST). // diff --git a/plugins/gosqlx/.claude-plugin/plugin.json b/plugins/gosqlx/.claude-plugin/plugin.json index f3db6b0f..a658fbce 100644 --- a/plugins/gosqlx/.claude-plugin/plugin.json +++ b/plugins/gosqlx/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gosqlx", "displayName": "GoSQLX", - "version": "1.15.0", + "version": "1.16.0", "description": "SQL validation, formatting, linting, injection scanning and analysis for Claude Code, powered by the GoSQLX parser (multi-dialect).", "author": { "name": "Ajit Pratap Singh", diff --git a/python/pygosqlx/__init__.py b/python/pygosqlx/__init__.py index 960872b1..a6836436 100644 --- a/python/pygosqlx/__init__.py +++ b/python/pygosqlx/__init__.py @@ -63,7 +63,7 @@ ValidationError, ) -__version__ = "0.1.0" +__version__ = "0.2.0" __all__ = [ # Functions "parse", diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json index 2b87d74d..494d55e2 100644 --- a/vscode-extension/package-lock.json +++ b/vscode-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "gosqlx", - "version": "1.14.0", + "version": "1.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gosqlx", - "version": "1.14.0", + "version": "1.16.0", "license": "Apache-2.0", "dependencies": { "vscode-languageclient": "^9.0.1" diff --git a/vscode-extension/package.json b/vscode-extension/package.json index d2cd97f2..c9219263 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -2,7 +2,7 @@ "name": "gosqlx", "displayName": "GoSQLX", "description": "High-performance SQL parsing, validation, formatting, and analysis powered by GoSQLX", - "version": "1.14.0", + "version": "1.16.0", "publisher": "ajitpratap0", "license": "Apache-2.0", "repository": { diff --git a/website/src/data/metrics.ts b/website/src/data/metrics.ts index dd0c6df9..b0e4eb20 100644 --- a/website/src/data/metrics.ts +++ b/website/src/data/metrics.ts @@ -35,7 +35,7 @@ export const DIALECT_NAMES = [ /** Project facts (not performance). */ export const PROJECT = { /** Latest released tag. */ - version: 'v1.15.0', + version: 'v1.16.0', /** Dialects supported (PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse). */ dialectCount: DIALECT_NAMES.length, /** Built-in linter rules L001-L030. */ From b57d0063aa48f64b1ec08d84457f88e661b78390 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:00:38 +0530 Subject: [PATCH 03/12] docs(changelog): finalize 1.16.0 release notes [vercel skip] Turns [Unreleased] into [1.16.0] - 2026-10-21, adds the missing entries for #734, #735, #780, #786, #789, #790, #791, #826, #835, #836, #837, #840, #841, #843 and #844, updates the MCP and composite-field entries amended by #841 and #843, adds PR refs, and adds the compare links. The VS Code extension gets a 1.16.0 entry, and split-changelog.js generates the /changelog page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- CHANGELOG.md | 175 +++++-- vscode-extension/CHANGELOG.md | 27 + website/src/content/blog/v1-16-0.md | 738 ++++++++++++++++++++++++++++ 3 files changed, 911 insertions(+), 29 deletions(-) create mode 100644 website/src/content/blog/v1-16-0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 74f5aff0..e5cf04a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,27 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.16.0] - 2026-10-21 — 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 -(tracking issue #566), the batch-2 and batch-3 bug fixes and new dialect -coverage. The release is source-compatible: exported signatures are unchanged -apart from additions. -Runtime behaviour does change (rendered SQL, error codes and text, parse -acceptance, CLI refusals), so read **Behavior changes** before upgrading. +(#566) with 2-90% fewer allocations 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; and editor-accurate positions (a tab is one column). +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`. (#833) + 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 IP and - no longer tighten limits for everyone; JSON-RPC batches are charged per tool - call. Denials are HTTP 429 with `Retry-After` and the request id (a denied - `initialize` keeps HTTP 200 for mcp-go clients). Oversized bodies (over + 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 @@ -35,8 +71,19 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. 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](docs/MCP_GUIDE.md)). New `pkg/mcp` API: `NewRateLimitMiddleware(cfg, next)`, - `Config.TrustedProxyHeader`, `Config.TrustedProxyHops`. + `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 @@ -240,7 +287,7 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. `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`. + to `variable`. (#825) ### Added - **Claude Code plugin and marketplace**: `/plugin marketplace add @@ -249,24 +296,23 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. (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. `validate_sql` accepts `mariadb` and `clickhouse`; the server - reports `gosqlx.Version`; `/health` reports the real tool count; the + 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 not logged. `uppercase_keywords: true` on - `format_sql` now also uppercases operator and function keywords (`AND`, - `OR`, `COUNT`); `indent_size` affects nested blocks only, as now - documented. `Server.Shutdown(ctx)` stops the transport, and `Server.Start` + 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`. + `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, @@ -282,14 +328,13 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. `--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` now uses `pkg/highlight` with the - standard token types (keyword, function, type, variable, string, number, - operator, comment, parameter), UTF-16 positions, per-line splitting of - multi-line tokens, and coverage of every dialect keyword and all comments. + - 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](docs/HIGHLIGHTING.md). + See [docs/HIGHLIGHTING.md](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 @@ -301,6 +346,7 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. `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](docs/LSP_GUIDE.md#sql-dialect). + (#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 @@ -381,7 +427,21 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. - **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. (#833) + 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) ### Changed - **`tokenizer.NewWithKeywords` no longer consults its keyword table**: the @@ -397,7 +457,33 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. 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. (#833) + 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`. - **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`. @@ -479,7 +565,7 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. 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. + 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 @@ -492,7 +578,7 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. `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. + own list of transpilable dialects. (#829) ### Performance Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the @@ -616,6 +702,19 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the 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 @@ -624,6 +723,23 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the - 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). ## [1.15.0] - 2026-07-30 — Tree API, ClickHouse 100%, Oracle Gaps, Correctness Sweep @@ -2541,7 +2657,8 @@ For questions about upgrading or changelog entries: - Open an issue: https://github.com/ajitpratap0/GoSQLX/issues - Join discussions: https://github.com/ajitpratap0/GoSQLX/discussions -[Unreleased]: https://github.com/ajitpratap0/GoSQLX/compare/v1.15.0...HEAD +[Unreleased]: https://github.com/ajitpratap0/GoSQLX/compare/v1.16.0...HEAD +[1.16.0]: https://github.com/ajitpratap0/GoSQLX/compare/v1.15.0...v1.16.0 [1.15.0]: https://github.com/ajitpratap0/GoSQLX/compare/v1.14.0...v1.15.0 [1.14.0]: https://github.com/ajitpratap0/GoSQLX/compare/v1.13.0...v1.14.0 [1.13.0]: https://github.com/ajitpratap0/GoSQLX/compare/v1.12.1...v1.13.0 diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index 3e347f97..fa8184ca 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to the "GoSQLX" extension will be documented in this file. +## [1.16.0] - 2026-10-21 + +Bundles the GoSQLX v1.16.0 language server. See the +[main CHANGELOG](https://github.com/ajitpratap0/GoSQLX/blob/main/CHANGELOG.md) +for the full list. + +### Added +- **The `gosqlx.dialect` setting drives every feature**: diagnostics, + formatting, document symbols, completion and hover use the selected dialect, + and changing the setting restarts the language server automatically. +- **`gosqlx.dialect` offers all 11 dialects**: `generic`, `postgresql`, `mysql`, + `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, + `redshift` and `clickhouse` (it offered 6). + +### Changed +- **Semantic highlighting uses standard token types**: the non-standard + `identifier` type is replaced by `variable` (table, column and alias names + and quoted identifiers); comments and placeholders are now highlighted. + Custom `editor.semanticTokenColorCustomizations` rules keyed on `identifier` + must switch to `variable`. +- **Positions match the editor**: a tab counts as one column, and diagnostics + for a token after a comment point at the token, not the comment. +- Under the `mysql` and `mariadb` dialects, `||` is parsed as logical OR. + +### Security +- markdown-it 14.3.3 in the extension's lockfile (build tooling). + ## [1.14.0] - 2026-04-12 ### Added diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md new file mode 100644 index 00000000..71d4878f --- /dev/null +++ b/website/src/content/blog/v1-16-0.md @@ -0,0 +1,738 @@ +--- +title: "v1.16.0 — Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep" +date: "2026-10-21" +version: "1.16.0" +description: "Covers everything merged since v1.15.0: the October 2026 performance audit (#566) with 2-90% fewer allocations across tokenizer, parser, AST and LSP; the…" +--- + +Covers everything merged since v1.15.0: the October 2026 performance audit +(#566) with 2-90% fewer allocations 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; and editor-accurate positions (a tab is one column). +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](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 `NOT`s + 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 `UNION`s: 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 ` 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) + +### 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](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](docs/LSP_GUIDE.md#sql-dialect). + (#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) + +### 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`. +- **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 (` 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). From d4761f28518a58f199f1baade2e9f2d40be90e33 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:00:38 +0530 Subject: [PATCH 04/12] docs: v1.15 to v1.16 migration guide and release doc fixes [vercel skip] MIGRATION gains a v1.15.0 -> v1.16.0 section with a behaviour-change checklist. ERROR_CODES documents E4001 dialect gating and the 11 dialect names. CLI_GUIDE lists --dialect on format, parse, analyze and lint and all 11 dialects. SQL_COMPATIBILITY marks Oracle CONNECT BY/PRIOR and SQL Server PIVOT as parsed. api/mcp.md matches the current Config and middleware API, api/models.md drops the nonexistent Location.Offset and documents byte columns, and HIGHLIGHTING notes ANSI control-character neutralization. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- docs/CLI_GUIDE.md | 25 +++++++++++++++--- docs/ERROR_CODES.md | 25 ++++++++++++++---- docs/HIGHLIGHTING.md | 7 +++++ docs/MIGRATION.md | 54 +++++++++++++++++++++++++++++++++++++++ docs/SQL_COMPATIBILITY.md | 21 +++++++-------- docs/api/mcp.md | 38 ++++++++++++++++++++++----- docs/api/models.md | 5 ++-- 7 files changed, 147 insertions(+), 28 deletions(-) diff --git a/docs/CLI_GUIDE.md b/docs/CLI_GUIDE.md index f0b4eeb2..5ae118d4 100644 --- a/docs/CLI_GUIDE.md +++ b/docs/CLI_GUIDE.md @@ -196,6 +196,7 @@ gosqlx format [file...] [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| | `-i, --in-place` | bool | false | Edit files in place (not supported with stdin) | +| `--dialect` | string | postgresql | SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse); unknown names are rejected | | `--indent` | int | 2 | Indentation size in spaces (0-8) | | `--uppercase` | bool | true | Uppercase SQL keywords | | `--no-uppercase` | bool | false | Keep original keyword case | @@ -293,6 +294,7 @@ gosqlx parse [file|query] [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| +| `--dialect` | string | postgresql | SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse); unknown names are rejected | | `--ast` | bool | false | Show detailed AST structure | | `--tokens` | bool | false | Show tokenization output | | `--tree` | bool | false | Show tree visualization | @@ -367,6 +369,7 @@ gosqlx analyze [file|query] [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| +| `--dialect` | string | postgresql | SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse); unknown names are rejected | | `--security` | bool | false | Focus on security vulnerability analysis | | `--performance` | bool | false | Focus on performance optimization analysis | | `--complexity` | bool | false | Focus on complexity metrics | @@ -460,6 +463,7 @@ gosqlx lint [file...] [flags] |------|------|---------|-------------| | `-r, --recursive` | bool | false | Recursively process directories | | `-p, --pattern` | string | `*.sql` | File pattern for recursive processing | +| `--dialect` | string | postgresql | SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse); unknown names are rejected | | `--auto-fix` | bool | false | Automatically fix violations where possible | | `--max-length` | int | 100 | Maximum line length for L005 rule | | `--fail-on-warn` | bool | false | Exit with error code on warnings | @@ -1298,6 +1302,12 @@ gosqlx validate --dialect mysql mysql_query.sql # SQL Server validation gosqlx validate --dialect sqlserver tsql_query.sql + +# parse, format, analyze and lint take the same flag +gosqlx format --dialect mysql "SELECT * FROM t LIMIT 5, 10" +gosqlx parse --dialect clickhouse query.sql +gosqlx analyze --dialect sqlserver tsql_query.sql +gosqlx lint --dialect oracle oracle_query.sql ``` ### Pipeline Processing @@ -1428,12 +1438,19 @@ GoSQLX supports multiple SQL dialects with dialect-specific features: | Dialect | Identifier | Special Features | |---------|-----------|------------------| | PostgreSQL | `postgresql` | JSONB, arrays, LATERAL JOIN, DISTINCT ON, FILTER clause | -| MySQL | `mysql` | Backtick identifiers, MySQL-specific functions | -| SQL Server | `sqlserver` | Bracket identifiers, T-SQL syntax | -| Oracle | `oracle` | Oracle-specific syntax and functions | -| SQLite | `sqlite` | SQLite-specific features | +| MySQL | `mysql` | Backtick identifiers, `LIMIT offset, count`, `\|\|` as logical OR | +| MariaDB | `mariadb` | MySQL syntax plus SEQUENCE DDL, temporal tables, CONNECT BY | +| SQL Server | `sqlserver` | Bracket identifiers, T-SQL syntax, TOP, PIVOT/UNPIVOT | +| Oracle | `oracle` | `:1`/`:name` bind variables, CONNECT BY, PRIOR | +| SQLite | `sqlite` | SQLite-specific features, `?` placeholders | +| Snowflake | `snowflake` | VARIANT paths, `@stage`, QUALIFY, MATCH_RECOGNIZE | +| BigQuery | `bigquery` | Keyword set and composite field access | +| Redshift | `redshift` | Keyword set | +| ClickHouse | `clickhouse` | PREWHERE, FINAL, SETTINGS, table functions, parametric aggregates | | Generic | `generic` | Standard SQL only | +Dialect names are case-sensitive. The same 11 names are accepted by every command's `--dialect` flag, the config file, the LSP server and the MCP `validate_sql` tool (see `keywords.DialectNames()`). + ### Advanced SQL Features Supported - **Window Functions**: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, FIRST_VALUE, LAST_VALUE, NTILE diff --git a/docs/ERROR_CODES.md b/docs/ERROR_CODES.md index a8a648ec..7e64c2e8 100644 --- a/docs/ERROR_CODES.md +++ b/docs/ERROR_CODES.md @@ -551,9 +551,24 @@ SELECT users.id FROM users, orders WHERE users.id > 10 ### E4001 - Unsupported Feature -SQL feature not yet implemented. +The SQL uses a feature that GoSQLX does not implement, or that the selected dialect does not support. -**Note:** GoSQLX is under active development. Check documentation for currently supported features. +Common causes: +- Composite field access `(expr).field` outside PostgreSQL, BigQuery and `generic` (and the default dialect). +- `TOP` outside SQL Server, or `LIMIT` under Oracle. +- A comma list, alias or ClickHouse `FINAL` after a parenthesized join group. +- `JOIN` in `UPDATE ... FROM`, or `ORDER BY`/`LIMIT` after a parenthesized query that the AST cannot represent. + +```sql +-- Wrong (with --dialect mysql): composite field access +SELECT (t).name FROM t +-- Error E4001: composite field access (expr).field is not supported in mysql + +-- Right: parse it as PostgreSQL, BigQuery or generic, or select the column directly +SELECT t.name FROM t +``` + +**Fix:** pick the dialect the SQL was written for (`--dialect`, `ParseWithDialect`, LSP `dialect`), or rewrite the construct. --- @@ -561,7 +576,7 @@ SQL feature not yet implemented. SQL dialect-specific syntax not supported. -**Note:** GoSQLX supports standard SQL with extensions for PostgreSQL, MySQL, SQL Server, Oracle, and SQLite. Some dialect-specific features may not be available. +**Note:** GoSQLX accepts 11 dialect names: `generic`, `postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift` and `clickhouse` (see `keywords.DialectNames()`). Some dialect-specific features may not be available. --- @@ -867,7 +882,7 @@ Error codes follow a hierarchical structure: - **E1xxx**: Lexical/tokenization errors (invalid characters, literals) - **E2xxx**: Syntax/parsing errors (missing clauses, unexpected tokens) - **E3xxx**: Semantic errors (undefined references, type mismatches) -- **E4xxx**: Unsupported features (not yet implemented) +- **E4xxx**: Unsupported features (not implemented, or not supported in the selected dialect) ## Performance Tips @@ -927,7 +942,7 @@ This comprehensive error code reference covers all 27 error codes in GoSQLX: - **9 Tokenizer Errors (E1001-E1009)**: Lexical analysis and DoS protection - **12 Parser Errors (E2001-E2012)**: SQL syntax and parsing failures - **4 Semantic Errors (E3001-E3004)**: Logical and type validation -- **2 Unsupported Feature Errors (E4001-E4002)**: Features not yet implemented +- **2 Unsupported Feature Errors (E4001-E4002)**: Features not implemented or not supported in the selected dialect Additionally, GoSQLX provides: - **30 Linter Rules (L001-L030)**: Style, safety, performance and naming checks diff --git a/docs/HIGHLIGHTING.md b/docs/HIGHLIGHTING.md index 54b22aca..953c3ceb 100644 --- a/docs/HIGHLIGHTING.md +++ b/docs/HIGHLIGHTING.md @@ -136,6 +136,13 @@ if err != nil { bold, italic and underline only. Styles are reset before each newline and re-applied after it, so output stays correct through `head`, `grep` or a pager. +**Untrusted input.** `ANSI` and `WriteANSI` never copy terminal control +characters from the source, so a string literal or comment cannot inject +escape sequences. C0 controls other than tab, newline and carriage return are +shown as Unicode control pictures (ESC becomes U+241B), DEL as U+2421, and C1 +controls and invalid UTF-8 as U+FFFD. The rendered text therefore differs from +the input only where it contained such characters. HTML output is escaped. + ### HTML and CSS ```go diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index b5f4d6ba..f48db55a 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,5 +1,59 @@ # Migration Guide +## v1.15.0 → v1.16.0 (2026-10-21) + +**Source-compatible upgrade**: exported signatures are unchanged apart from additions, so code that compiled against v1.15.0 compiles against v1.16.0. Runtime behaviour does change in several places (token positions, rendered SQL, parse acceptance, CLI refusals, MCP HTTP limits). Read the checklist below before upgrading. The [CHANGELOG](../CHANGELOG.md) has the full list. + +### Why upgrade + +- **SQL syntax highlighting**: new `pkg/highlight`, `gosqlx.Highlight*`, the `gosqlx highlight` command, LSP semantic tokens and the playground editor all share one dialect-aware classifier. See [HIGHLIGHTING.md](HIGHLIGHTING.md). +- **Dialect-aware LSP**: the configured dialect drives diagnostics, formatting, symbols, completion and hover, and can be changed at runtime with `workspace/didChangeConfiguration`. +- **`gosqlx-mcp` hardening and ChatGPT app support**, plus a Claude Code plugin (`/plugin marketplace add ajitpratap0/GoSQLX`). +- **Performance audit (#566)**: zero-copy token values, linear position tracking and fewer allocations across the tokenizer, parser, AST, linter and LSP. +- **Correctness sweep**: `SQL()`, the formatter and `gosqlx format` round-trip far more constructs, and render-time SQL injection through quoted names is closed. + +### New APIs to adopt + +- `gosqlx.Highlight`, `gosqlx.HighlightANSI`, `gosqlx.HighlightHTML` and `pkg/highlight` (`Spans`, `ANSI`, `HTML`, `CSS`, `WithDialect`). +- `gosqlx.LintWithDialect(sql, dialect)` and `(*linter.Linter).SetDialect`. +- `parser.WithPipesAsConcat()` / `gosqlx.WithPipesAsConcat()` for MySQL/MariaDB servers running with `PIPES_AS_CONCAT`. +- `Tokenizer.TokenizeString` / `TokenizeStringContext` to tokenize a string without a `[]byte` copy. +- `keywords.DialectNames()`: the 11 dialect names every surface accepts. +- `pkg/mcp`: `NewRateLimitMiddleware`, `Config.TrustedProxyHeader`, `TrustedProxyHops`, `TrustedProxySecretHeader`, `TrustedProxySecret`, `Server.Shutdown`. +- AST additions: `FieldAccessExpression`, `ParenthesizedExpression`, `JoinClause.Using`, `TableReference.SetSubquery`, `TableReference.ColumnAliases`, `TableReference.WithOrdinality`, `BinaryExpression.PipesOr`, `Identifier.Quoted`, `WindowFrame.Exclude`, `WindowSpec.BaseWindow`, `SelectStatement.Settings` and others listed in the CHANGELOG. +- CLI: `gosqlx highlight`, and `--dialect` on `parse`, `format`, `analyze` and `lint`. + +### Behavior changes to act on + +- [ ] **Token columns**: a tab is now one column (it was four), and a token's `Start` after comments is the token itself. Columns are 1-based byte offsets. If you map `models.Location.Column` back to text, treat it as a byte offset. L006 reports byte columns too. +- [ ] **MySQL/MariaDB `||`**: under `mysql`/`mariadb`, `||` is logical OR. Pass `WithPipesAsConcat()` if your server uses `PIPES_AS_CONCAT`. +- [ ] **Composite field access** `(expr).field` returns E4001 under MySQL, MariaDB, SQL Server, SQLite, Oracle, Snowflake, ClickHouse and Redshift. +- [ ] **Zero-copy token values**: token and AST strings reference the input. `strings.Clone` any value you cache long-term. +- [ ] **Oracle binds**: under the Oracle dialect `:1` and `:name` are placeholders. +- [ ] **Sorted extraction**: `Extract*` and `ExtractMetadata` results are sorted; drop any code that sorted them itself or relied on map order. +- [ ] **`gosqlx format`** renders through `pkg/formatter` and exits non-zero if the result does not re-parse to the same query; `format -i` then leaves the file untouched. +- [ ] **Rendering**: quoted names that need quotes keep them, `NOT EXISTS`, named windows and window frame offsets render correctly, and readable style emits a single trailing `;`. Update golden files that captured the old output. +- [ ] **AST pooling**: `ReleaseAST` no longer pools most expression and statement nodes, and `Children()`/`Walk` return pointers into the parent, so visitor edits now change the tree. Do not use those pointers after `ReleaseAST`. +- [ ] **Limits**: comments count toward `MaxTokens`; unary operators, `NOT` and derived tables count toward `MaxRecursionDepth`; qualified names over 5 parts return E2004. +- [ ] **LSP semantic tokens**: the `identifier` token type is now the standard `variable`. Update editor rules such as Neovim `@lsp.type.identifier`. +- [ ] **Schema loaders**: the PostgreSQL loader needs PostgreSQL 11+. `Index.Columns` excludes INCLUDE columns (now `Index.IncludeColumns`), and tables with no foreign keys carry a nil slice. +- [ ] **`gosqlx-mcp` behind a proxy**: rate limiting keys on the TCP peer. Set `GOSQLX_MCP_TRUSTED_PROXY_HEADER` (for example `CF-Connecting-IP`) or `GOSQLX_MCP_TRUSTED_PROXY_HOPS`, optionally with the proxy-secret pair, or every client shares one bucket. Tool input errors are now `isError` results, and `indent_size` must be 0 to 16. See [MCP_GUIDE.md](MCP_GUIDE.md). + +### Companion release versions + +| Component | v1.15.0 | v1.16.0 | +|-----------|---------|---------| +| Library (`github.com/ajitpratap0/GoSQLX`) | 1.15.0 | **1.16.0** | +| CLI (`cmd/gosqlx`, `gosqlx --version`) | 1.14.0 (stale constant) | **1.16.0** | +| MCP server (`cmd/gosqlx-mcp`) | 1.14.0 (stale constant) | **1.16.0** | +| VS Code extension (`ajitpratap0.gosqlx`) | 1.15.0 | **1.16.0** | +| Claude Code plugin (`gosqlx@gosqlx`) | 1.15.0 | **1.16.0** | +| OpenTelemetry integration (`integrations/opentelemetry`) | requires v1.14.0 | updated after the tag | +| GORM integration (`integrations/gorm`) | requires v1.14.0 | updated after the tag | +| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | 0.2.0 (alpha), independent track | + +--- + ## v1.13.0 → v1.14.0 (2026-04-12) **Drop-in upgrade** — no breaking API changes. Anything that compiled against v1.13.0 will still compile and run identically against v1.14.0. diff --git a/docs/SQL_COMPATIBILITY.md b/docs/SQL_COMPATIBILITY.md index eaa09ab9..269c7579 100644 --- a/docs/SQL_COMPATIBILITY.md +++ b/docs/SQL_COMPATIBILITY.md @@ -379,7 +379,7 @@ tree, err := gosqlx.ParseTree(ctx, sql, gosqlx.WithDialect("mysql"), gosqlx.WithPipesAsConcat()) ``` -Before #781, `||` was concatenation in every dialect, including MySQL and MariaDB. +Before v1.16.0 (#781), `||` was concatenation in every dialect, including MySQL and MariaDB. ### MariaDB-Specific Features @@ -407,7 +407,7 @@ MariaDB inherits all MySQL features (SHOW, DESCRIBE, REPLACE INTO, ON DUPLICATE | Feature | Support Level | GoSQLX Parser | Test Coverage | Notes | |---------|---------------|---------------|---------------|-------| | **MERGE** | ✅ Full | ✅ Full | 95% | MERGE statements with WHEN clauses | -| **PIVOT/UNPIVOT** | ✅ Full | 🔧 Syntax | 10% | Keywords reserved, no parsing logic | +| **PIVOT/UNPIVOT** | ✅ Full | ✅ Full | — | Parsed and rendered since v1.14.0 (`FROM s PIVOT (...) AS p`) | | **CROSS/OUTER APPLY** | ✅ Full | 🔧 Syntax | 10% | Keywords reserved, no parsing logic | | **HierarchyID** | ✅ Full | 🔧 Syntax | 20% | Data type recognition | | **T-SQL functions** | ✅ Full | ⚠️ Partial | 40% | Subset of T-SQL functions | @@ -416,8 +416,8 @@ MariaDB inherits all MySQL features (SHOW, DESCRIBE, REPLACE INTO, ON DUPLICATE | Feature | Support Level | GoSQLX Parser | Test Coverage | Notes | |---------|---------------|---------------|---------------|-------| -| **CONNECT BY** | ✅ Full | 🔧 Syntax | 10% | Keywords reserved, no parsing logic | -| **PRIOR operator** | ✅ Full | 🔧 Syntax | 10% | Keyword reserved, no parsing logic | +| **CONNECT BY** | ✅ Full | ✅ Full | — | `START WITH`, `CONNECT BY [NOCYCLE]` with general expressions | +| **PRIOR operator** | ✅ Full | ✅ Full | — | Prefix operator (`CONNECT BY PRIOR id = parent_id + 1`); a column named `prior` stays an identifier | | **DECODE function** | ✅ Full | ⚠️ Partial | 40% | Recognized as function, no special handling | | **NVL/NVL2** | ✅ Full | ⚠️ Partial | 40% | Recognized as function, no special handling | | **Dual table** | ✅ Full | ✅ Full | 100% | Standard table reference | @@ -629,13 +629,16 @@ GoSQLX provides a first-class dialect mode engine that threads the SQL dialect t | Dialect | Dialect String | Keyword Set | Dialect-Specific Parsing | Status | |---------|---------------|-------------|--------------------------|--------| | **PostgreSQL** | `"postgresql"` | Full PG keywords | `::`, `ON CONFLICT`, `$$`, JSONB ops, LATERAL, DISTINCT ON | ✅ Default dialect | -| **MySQL** | `"mysql"` | MySQL keywords | SHOW, DESCRIBE, REPLACE INTO, ON DUPLICATE KEY, LIMIT n,m, GROUP_CONCAT, MATCH AGAINST, REGEXP | ✅ Full support | +| **MySQL** | `"mysql"` | MySQL keywords | SHOW, DESCRIBE, REPLACE INTO, ON DUPLICATE KEY, LIMIT n,m, GROUP_CONCAT, MATCH AGAINST, REGEXP, `\|\|` as logical OR | ✅ Full support | | **SQL Server** | `"sqlserver"` | T-SQL keywords | MERGE, bracket identifiers `[col]`, PIVOT/UNPIVOT | ✅ PIVOT/UNPIVOT (since v1.14.0) | -| **Oracle** | `"oracle"` | Oracle keywords | DUAL table, basic PL/SQL keywords, MINUS as EXCEPT | ⚠️ Keywords + basic parsing | +| **Oracle** | `"oracle"` | Oracle keywords | DUAL table, basic PL/SQL keywords, MINUS as EXCEPT, `:1`/`:name` binds, CONNECT BY / START WITH / PRIOR | ⚠️ Keywords + basic parsing | | **SQLite** | `"sqlite"` | SQLite keywords | Flexible typing, simplified syntax | ⚠️ Keywords + basic parsing | | **Snowflake** | `"snowflake"` | Snowflake keywords | MATCH_RECOGNIZE, @stage, SAMPLE, QUALIFY, VARIANT paths, time-travel, MINUS, LATERAL FLATTEN, TRY_CAST, IGNORE/RESPECT NULLS, LIKE ANY/ALL, CREATE STAGE/STREAM/TASK/PIPE stubs | ✅ 87/87 QA pass | | **ClickHouse** | `"clickhouse"` | ClickHouse keywords | PREWHERE, FINAL, GLOBAL IN/NOT IN, nested types, parametric aggregates, bare-bracket arrays, ORDER BY WITH FILL, CODEC, WITH TOTALS, LIMIT BY, ANY/ALL JOIN, SETTINGS, TTL, INSERT FORMAT | ✅ 83/83 QA pass (was 69/83 in v1.14.0) | | **MariaDB** | `"mariadb"` | MariaDB keywords (superset of MySQL) | All MySQL features + SEQUENCE DDL, FOR SYSTEM_TIME, WITH SYSTEM VERSIONING, PERIOD FOR, CONNECT BY | ✅ | +| **BigQuery** | `"bigquery"` | BigQuery keywords | Composite field access `(expr).field`, table functions | ⚠️ Keywords + basic parsing | +| **Redshift** | `"redshift"` | Redshift keywords | Keyword recognition | ⚠️ Keywords + basic parsing | +| **Generic** | `"generic"` | Standard SQL keywords | No dialect-specific syntax; composite field access allowed | ✅ | ### Usage @@ -669,7 +672,7 @@ gosqlx format --dialect mysql query.sql - OPENROWSET / OPENQUERY not supported #### Oracle -- CONNECT BY / START WITH / PRIOR not parsed (keywords reserved) +- CONNECT BY / START WITH / PRIOR parsed (PRIOR is a prefix operator, since v1.16.0) - PL/SQL blocks not supported - DECODE recognized as generic function only - Pipelined table functions not supported @@ -852,7 +855,7 @@ gosqlx format --dialect mysql query.sql ### Suitable with Considerations -- **Advanced dialect-specific features** (keyword recognition only for: PIVOT/UNPIVOT, CONNECT BY, PRAGMA, ATTACH/DETACH) +- **Advanced dialect-specific features** (keyword recognition only for: CROSS/OUTER APPLY, ATTACH/DETACH) - **Complex XML operations** (syntax recognition only) - **Dialect-specific functions** (DECODE, NVL, recognized as generic functions) - **Newest SQL standard features (SQL-2011+)** @@ -860,8 +863,6 @@ gosqlx format --dialect mysql query.sql ### Development Needed -- **PIVOT/UNPIVOT parsing logic** (keywords reserved) -- **CONNECT BY hierarchical queries** (keywords reserved) - **Full XML function support** - **Row pattern recognition (MATCH_RECOGNIZE)** - **Complete temporal table support** diff --git a/docs/api/mcp.md b/docs/api/mcp.md index cd09235b..cd68e033 100644 --- a/docs/api/mcp.md +++ b/docs/api/mcp.md @@ -13,28 +13,52 @@ MCP server exposing all GoSQLX capabilities as Model Context Protocol tools over ### `Config` ```go type Config struct { - Host string // GOSQLX_MCP_HOST (default "127.0.0.1") - Port int // GOSQLX_MCP_PORT (default 8080, range 1–65535) - AuthToken string // GOSQLX_MCP_AUTH_TOKEN (default "" = auth disabled) + Host string // GOSQLX_MCP_HOST (default "127.0.0.1") + Port int // GOSQLX_MCP_PORT (default 8080, range 1–65535) + AuthToken string // GOSQLX_MCP_AUTH_TOKEN (default "" = auth disabled) + OpenAIAppsChallenge string // GOSQLX_MCP_OPENAI_APPS_CHALLENGE (served at /.well-known/openai-apps-challenge) + TrustedProxyHeader string // GOSQLX_MCP_TRUSTED_PROXY_HEADER, e.g. "CF-Connecting-IP" + TrustedProxyHops int // GOSQLX_MCP_TRUSTED_PROXY_HOPS (X-Forwarded-For hops; 0 = ignored) + TrustedProxySecretHeader string // GOSQLX_MCP_TRUSTED_PROXY_SECRET_HEADER + TrustedProxySecret string // GOSQLX_MCP_TRUSTED_PROXY_SECRET (min 16 bytes) } ``` +Without a trusted-proxy setting, rate limits key on the TCP peer address, so every client behind a reverse proxy shares one bucket. When both secret fields are set, the trusted header and hops apply only to requests that carry the shared secret (constant-time compare). See [MCP_GUIDE.md](../MCP_GUIDE.md) for the proxy setup. + ## Functions ### `LoadConfig() (*Config, error)` -Load from env vars. Returns error only if `GOSQLX_MCP_PORT` is non-integer or out of range. +Load from env vars. Returns an error if `GOSQLX_MCP_PORT` is non-integer or out of range, or if the trusted-proxy settings are invalid (bad header name, negative hops, or a proxy secret shorter than 16 bytes). ### `DefaultConfig() *Config` -Returns `Config{Host: "127.0.0.1", Port: 8080}` with auth disabled. +Returns `Config{Host: "127.0.0.1", Port: 8080}` with auth disabled and no trusted proxy. ### `New(cfg *Config) *Server` Create server with all 7 tools registered. Registers tools once at construction - no dynamic registration. +### `(s *Server) Handler() http.Handler` +Returns the streamable HTTP handler wrapped in bearer auth. Use it to embed the server in your own `http.Server`. + +> **Warning:** `Handler` and `Start` do not apply rate limiting, the GET stream cap, the body read deadline or the 64 KB body cap. Wrap `Handler()` with `NewRateLimitMiddleware(cfg, h)` (as `cmd/gosqlx-mcp` does) before exposing it to untrusted clients. + ### `(s *Server) Start(ctx context.Context) error` -Bind to `cfg.Addr()`, serve streamable HTTP. Blocks until ctx cancelled or fatal error. Graceful shutdown on context cancellation. +Bind to `cfg.Addr()`, serve streamable HTTP with read-header and idle timeouts. Blocks until ctx is cancelled or a fatal error occurs, then shuts down the HTTP server and the session sweeper. Applies bearer auth only (see the warning above). + +### `(s *Server) Shutdown(ctx context.Context) error` +Stops the shared transport and its idle-session sweeper. Call it when you serve `Handler()` yourself. + +### `NewRateLimitMiddleware(cfg *Config, next http.Handler) http.Handler` +Per-client rate limiting keyed by peer address or the configured trusted proxy. Denials are HTTP 429 with `Retry-After` (a denied `initialize` keeps HTTP 200 for mcp-go clients). Also enforces the 64 KB body cap, the body read deadline and 4 GET (SSE) streams per client. + +### `RateLimitMiddleware(next http.Handler) http.Handler` +`NewRateLimitMiddleware(nil, next)`: keys on the TCP peer address only. ### `BearerAuthMiddleware(cfg *Config, next http.Handler) http.Handler` -Returns `next` unchanged when `cfg.AuthEnabled()` is false. When enabled, enforces `Authorization: Bearer ` on all requests; returns HTTP 401 on failure. +Returns `next` unchanged when `cfg.AuthEnabled()` is false. When enabled, enforces `Authorization: Bearer ` (scheme case-insensitive, token compared in constant time) on all requests; returns HTTP 401 on failure. + +### `SessionIdleTTL` +How long an idle MCP session's transport state is kept before the sweeper frees it. ## Registered Tools diff --git a/docs/api/models.md b/docs/api/models.md index c39fccb1..00b24ee0 100644 --- a/docs/api/models.md +++ b/docs/api/models.md @@ -41,11 +41,12 @@ type Span struct { ```go type Location struct { Line int // 1-based line number - Column int // 1-based column number - Offset int // 0-based byte offset + Column int // 1-based byte column within the line } ``` +`Column` counts bytes, not runes or UTF-16 units: a multi-byte character counts its bytes, and a tab is one column. A token's `Start` is the token itself, after any leading whitespace and comments. Convert to UTF-16 before sending positions to an LSP client. + ## Functions ### `NewTokenWithSpan(tokenType TokenType, value string, span Span) TokenWithSpan` From cc96bece46d50ac5a04264c3bd375686f36a5041 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:00:38 +0530 Subject: [PATCH 05/12] chore(website): regenerate sample AST and changelog pages The website build rewrote both files on main: sample-ast.json gains the WithOrdinality and ColumnAliases fields from #843, and v1-9-1.md drops the emoji the changelog splitter strips. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- website/src/content/blog/v1-9-1.md | 4 ++-- website/src/generated/sample-ast.json | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/website/src/content/blog/v1-9-1.md b/website/src/content/blog/v1-9-1.md index e97ba1f7..3508cca9 100644 --- a/website/src/content/blog/v1-9-1.md +++ b/website/src/content/blog/v1-9-1.md @@ -8,11 +8,11 @@ description: "Documentation and community-health release; no runtime behavior ch Documentation and community-health release; no runtime behavior changes. (These changes were also announced in the 1.9.2 notes.) -### 📚 Documentation (PR #351) +### Documentation (PR #351) - Fix pkg.go.dev license detection and add comprehensive doc comments across `pkg/sql/ast`, `pkg/transform`, `pkg/sql/parser`, `pkg/errors`, `pkg/models`, `pkg/formatter`, `pkg/gosqlx` and the tokenizer packages -### 🌍 Community (PR #350) +### Community (PR #350) - Open-source health files (issue and discussion templates, SUPPORT.md, etc.) - LLM discovery metadata and SEO improvements for project discoverability diff --git a/website/src/generated/sample-ast.json b/website/src/generated/sample-ast.json index 53b9c6da..802a3279 100644 --- a/website/src/generated/sample-ast.json +++ b/website/src/generated/sample-ast.json @@ -90,9 +90,11 @@ "Alias": "u", "Subquery": null, "Lateral": false, - "TableHints": null, "Final": false, + "WithOrdinality": false, + "TableHints": null, "TableFunc": null, + "ColumnAliases": null, "TimeTravel": null, "ForSystemTime": null, "Pivot": null, @@ -110,9 +112,11 @@ "Alias": "u", "Subquery": null, "Lateral": false, - "TableHints": null, "Final": false, + "WithOrdinality": false, + "TableHints": null, "TableFunc": null, + "ColumnAliases": null, "TimeTravel": null, "ForSystemTime": null, "Pivot": null, @@ -125,9 +129,11 @@ "Alias": "o", "Subquery": null, "Lateral": false, - "TableHints": null, "Final": false, + "WithOrdinality": false, + "TableHints": null, "TableFunc": null, + "ColumnAliases": null, "TimeTravel": null, "ForSystemTime": null, "Pivot": null, From 1a37f23989d7fd5ac7ac08dc215a1e7b30cfcb18 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:12:41 +0530 Subject: [PATCH 06/12] docs(release): correct allocation claim, plugin history, E4001 and LoadConfig errors Address review notes on #845: the lead paragraph understated the perf audit's allocation cuts, the MIGRATION table listed a v1.15.0 plugin that never shipped, E4001 also covers LIMIT under SQL Server, and LoadConfig rejects more trusted-proxy misconfigurations than the API doc listed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- CHANGELOG.md | 2 +- docs/ERROR_CODES.md | 2 +- docs/MIGRATION.md | 2 +- docs/api/mcp.md | 2 +- website/src/content/blog/v1-16-0.md | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5cf04a6..61d2a37f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.16.0] - 2026-10-21 — 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 2-90% fewer allocations across tokenizer, parser, AST and LSP; the +(#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 diff --git a/docs/ERROR_CODES.md b/docs/ERROR_CODES.md index 7e64c2e8..1f858cd1 100644 --- a/docs/ERROR_CODES.md +++ b/docs/ERROR_CODES.md @@ -555,7 +555,7 @@ The SQL uses a feature that GoSQLX does not implement, or that the selected dial Common causes: - Composite field access `(expr).field` outside PostgreSQL, BigQuery and `generic` (and the default dialect). -- `TOP` outside SQL Server, or `LIMIT` under Oracle. +- `TOP` outside SQL Server, or `LIMIT` under SQL Server or Oracle. - A comma list, alias or ClickHouse `FINAL` after a parenthesized join group. - `JOIN` in `UPDATE ... FROM`, or `ORDER BY`/`LIMIT` after a parenthesized query that the AST cannot represent. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index f48db55a..e3ebac4a 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -47,7 +47,7 @@ | CLI (`cmd/gosqlx`, `gosqlx --version`) | 1.14.0 (stale constant) | **1.16.0** | | MCP server (`cmd/gosqlx-mcp`) | 1.14.0 (stale constant) | **1.16.0** | | VS Code extension (`ajitpratap0.gosqlx`) | 1.15.0 | **1.16.0** | -| Claude Code plugin (`gosqlx@gosqlx`) | 1.15.0 | **1.16.0** | +| Claude Code plugin (`gosqlx@gosqlx`) | — (new) | **1.16.0** | | OpenTelemetry integration (`integrations/opentelemetry`) | requires v1.14.0 | updated after the tag | | GORM integration (`integrations/gorm`) | requires v1.14.0 | updated after the tag | | Python bindings (`pygosqlx`) | 0.2.0 (alpha) | 0.2.0 (alpha), independent track | diff --git a/docs/api/mcp.md b/docs/api/mcp.md index cd68e033..2d70461e 100644 --- a/docs/api/mcp.md +++ b/docs/api/mcp.md @@ -29,7 +29,7 @@ Without a trusted-proxy setting, rate limits key on the TCP peer address, so eve ## Functions ### `LoadConfig() (*Config, error)` -Load from env vars. Returns an error if `GOSQLX_MCP_PORT` is non-integer or out of range, or if the trusted-proxy settings are invalid (bad header name, negative hops, or a proxy secret shorter than 16 bytes). +Load from env vars. Returns an error if `GOSQLX_MCP_PORT` is non-integer or out of range, or if the trusted-proxy settings are invalid (bad header name, `X-Forwarded-For` as the trusted header, hops outside 0-16, a proxy secret shorter than 16 bytes, a secret header or secret set without the other, a secret header that reuses `Authorization`, `X-Forwarded-For` or the trusted header, or a secret with no trusted header or hops configured). See [MCP_GUIDE.md](../MCP_GUIDE.md). ### `DefaultConfig() *Config` Returns `Config{Host: "127.0.0.1", Port: 8080}` with auth disabled and no trusted proxy. diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md index 71d4878f..4d803bb0 100644 --- a/website/src/content/blog/v1-16-0.md +++ b/website/src/content/blog/v1-16-0.md @@ -2,11 +2,11 @@ title: "v1.16.0 — Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep" date: "2026-10-21" version: "1.16.0" -description: "Covers everything merged since v1.15.0: the October 2026 performance audit (#566) with 2-90% fewer allocations across tokenizer, parser, AST and LSP; the…" +description: "Covers everything merged since v1.15.0: the October 2026 performance audit (#566) with large allocation cuts across tokenizer, parser, AST and LSP; the…" --- Covers everything merged since v1.15.0: the October 2026 performance audit -(#566) with 2-90% fewer allocations across tokenizer, parser, AST and LSP; the +(#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 From deeaa3f4c87a3c2a506d06c56056e08efc64b500 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Wed, 21 Oct 2026 23:15:29 +0530 Subject: [PATCH 07/12] docs(release): make glama.json match the MCP server List all 11 validate_sql dialects, describe lint_sql as the 10 rules it runs (L001-L010; the CLI and SDK have 30), sync tool descriptions with pkg/mcp/server.go, and drop the unsourced 1.38M ops/sec claim in favour of a link to the benchmarks page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- glama.json | 59 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/glama.json b/glama.json index 0c573fbb..d4960934 100644 --- a/glama.json +++ b/glama.json @@ -1,9 +1,11 @@ { "$schema": "https://glama.ai/mcp/schemas/server.json", - "maintainers": ["ajitpratap0"], + "maintainers": [ + "ajitpratap0" + ], "name": "gosqlx-mcp", "display_name": "GoSQLX — SQL Intelligence MCP Server", - "description": "Production-ready SQL parsing, formatting, linting, security scanning, and metadata extraction for Go. 7 MCP tools covering the full SQL analysis pipeline: validate_sql, format_sql, parse_sql, extract_metadata, security_scan, lint_sql, and analyze_sql. Supports PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, and ClickHouse dialects. 1.38M+ ops/sec, zero-copy, race-free.", + "description": "SQL parsing, formatting, linting, security scanning and metadata extraction, built on the GoSQLX Go SDK. 7 MCP tools: validate_sql, format_sql, parse_sql, extract_metadata, security_scan, lint_sql and analyze_sql. Dialect-aware validation for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, BigQuery, Redshift and ClickHouse. Zero-copy tokenizer, race-free, with object pooling.", "repository": "https://github.com/ajitpratap0/GoSQLX", "homepage": "https://gosqlx.dev", "version": "1.16.0", @@ -38,6 +40,8 @@ "sql-server", "oracle", "snowflake", + "bigquery", + "redshift", "clickhouse", "mcp", "code-analysis" @@ -45,15 +49,15 @@ "tools": [ { "name": "validate_sql", - "description": "Validate SQL syntax against generic or dialect-specific rules. Accepts optional dialect parameter (generic, mysql, postgresql, sqlite, sqlserver, oracle, snowflake). Returns {valid: bool, error?: string, dialect?: string}." + "description": "Check whether SQL is syntactically valid, optionally for a specific dialect (dialect parameter: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse). Returns {valid: bool, error?: string, dialect?: string}." }, { "name": "format_sql", - "description": "Format SQL with configurable indentation size, keyword casing (uppercase/lowercase), and optional trailing semicolon. Returns the formatted SQL string and the options applied." + "description": "Pretty-print SQL: one clause per line, configurable indent size, optional upper-case keywords and trailing semicolon. Comments are not kept. Returns {formatted_sql, options}." }, { "name": "parse_sql", - "description": "Parse SQL into an AST and return a summary: total statement count and fully-qualified AST node types for each statement." + "description": "Parse SQL and return an AST summary: the statement count and the type of each statement." }, { "name": "extract_metadata", @@ -65,11 +69,11 @@ }, { "name": "lint_sql", - "description": "Lint SQL against all 30 GoSQLX style rules (L001–L030): whitespace, keywords, safety, performance, and naming. Returns violations with line/column positions and fix suggestions." + "description": "Lint SQL with the same 10 rules as `gosqlx lint` (L001–L010): trailing whitespace, mixed indentation, consecutive blank lines, indentation depth, line length (>100), column alignment, keyword case (upper), comma placement (trailing), aliasing consistency and redundant whitespace. Returns {violation_count, violations} with line/column positions." }, { "name": "analyze_sql", - "description": "Run all 6 analysis tools concurrently (validate_sql, parse_sql, extract_metadata, security_scan, lint_sql, format_sql) and return a single composite report. The most efficient way to get full SQL intelligence in one call." + "description": "Run validate, parse, metadata, security, lint and format concurrently and return one composite report. The most efficient way to get a full review of a query in one call." } ], "capabilities": { @@ -80,28 +84,53 @@ "features": { "dialects": [ "generic", + "postgresql", "mysql", "mariadb", - "postgresql", - "sqlite", "sqlserver", "oracle", + "sqlite", "snowflake", + "bigquery", + "redshift", "clickhouse" ], "security": { "injection_detection": true, - "severity_levels": ["critical", "high", "medium", "low"], - "patterns": ["tautology", "union-attack", "stacked-queries", "comment-bypass", "blind-injection"] + "severity_levels": [ + "critical", + "high", + "medium", + "low" + ], + "patterns": [ + "tautology", + "union-attack", + "stacked-queries", + "comment-bypass", + "blind-injection" + ] }, "linting": { - "rules": 30, - "rule_ids": ["L001", "L002", "L003", "L004", "L005", "L006", "L007", "L008", "L009", "L010", "L011", "L012", "L013", "L014", "L015", "L016", "L017", "L018", "L019", "L020", "L021", "L022", "L023", "L024", "L025", "L026", "L027", "L028", "L029", "L030"] + "rules": 10, + "rule_ids": [ + "L001", + "L002", + "L003", + "L004", + "L005", + "L006", + "L007", + "L008", + "L009", + "L010" + ], + "note": "The GoSQLX CLI and Go SDK provide 30 rules (L001–L030); the MCP lint_sql tool applies L001–L010." }, "performance": { - "ops_per_sec": "1380000+", "concurrency": "race-free", - "memory": "zero-copy object pooling" + "memory": "zero-copy tokenizer with object pooling", + "benchmarks": "https://gosqlx.dev/benchmarks" } }, "related_servers": [ From a774ee75e00e7f914a66bca67c84f93f28362efa Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 14:56:56 +0530 Subject: [PATCH 08/12] docs(release): add #846-#851 to the v1.16.0 CHANGELOG and migration guide Record the pygosqlx 0.3.0 API and wheels, the gosqlx_version double free, LSP serverInfo/CLI version reporting, the Python CI and release workflows and the #848 docs. MIGRATION companion table lists pygosqlx 0.3.0, libgosqlx exports and the LSP serverInfo version, with matching behaviour-change notes. Regenerate the v1.16.0 blog page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++- docs/MIGRATION.md | 8 +++- website/src/content/blog/v1-16-0.md | 59 ++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d2a37f..6a597ea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,9 @@ 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; and editor-accurate positions (a tab is one column). +and a Claude Code plugin; editor-accurate positions (a tab is one column); and +pygosqlx 0.3.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 @@ -289,6 +291,10 @@ limits), so read **Behavior changes** before upgrading. 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 @@ -442,6 +448,25 @@ limits), so read **Behavior changes** before upgrading. - **`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 0.3.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. (#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. + `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published + through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). + (#851) ### Changed - **`tokenizer.NewWithKeywords` no longer consults its keyword table**: the @@ -484,6 +509,21 @@ limits), so read **Behavior changes** before upgrading. #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="`. + GoReleaser no longer passes a `-X main.version` flag that the example + binaries ignored. (#846) - **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`. @@ -740,6 +780,23 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the 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); `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) ## [1.15.0] - 2026-07-30 — Tree API, ClickHouse 100%, Oracle Gaps, Correctness Sweep diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 4b86dedb..e5bf1efe 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -37,6 +37,10 @@ - [ ] **Limits**: comments count toward `MaxTokens`; unary operators, `NOT` and derived tables count toward `MaxRecursionDepth`; qualified names over 5 parts return E2004. - [ ] **LSP semantic tokens**: the `identifier` token type is now the standard `variable`. Update editor rules such as Neovim `@lsp.type.identifier`. - [ ] **Schema loaders**: the PostgreSQL loader needs PostgreSQL 11+. `Index.Columns` excludes INCLUDE columns (now `Index.IncludeColumns`), and tables with no foreign keys carry a nil slice. +- [ ] **LSP `serverInfo.version`** now reports the GoSQLX library version (1.16.0) instead of a hard-coded `1.0.0`. Update any client check that matched `1.0.0`. +- [ ] **CLI version stamping**: release builds can set the `gosqlx --version` string with `-ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version="`; a plain `go build` keeps the built-in 1.16.0. +- [ ] **C binding `gosqlx_version()`** now returns a newly allocated string on every call. Free each result once with `gosqlx_free` (earlier releases returned a shared pointer, so freeing it twice crashed). Callers that never freed it now leak a few bytes per call. +- [ ] **pygosqlx 0.3.0** requires Python 3.10 or later (3.8 and 3.9 are dropped). An unknown `dialect=` raises `UnsupportedDialectError`, a subclass of both `GoSQLXError` and `ValueError`. - [ ] **`gosqlx-mcp` behind a proxy**: rate limiting keys on the TCP peer. Set `GOSQLX_MCP_TRUSTED_PROXY_HEADER` (for example `CF-Connecting-IP`) or `GOSQLX_MCP_TRUSTED_PROXY_HOPS`, optionally with the proxy-secret pair, or every client shares one bucket. Tool input errors are now `isError` results, and `indent_size` must be 0 to 16. See [MCP_GUIDE.md](MCP_GUIDE.md). ### Companion release versions @@ -50,7 +54,9 @@ | Claude Code plugin (`gosqlx@gosqlx`) | — (new) | **1.16.0** | | OpenTelemetry integration (`integrations/opentelemetry`) | requires v1.14.0 | updated after the tag | | GORM integration (`integrations/gorm`) | requires v1.14.0 | updated after the tag | -| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | 0.2.0 (alpha), independent track | +| LSP `serverInfo.version` (`gosqlx lsp`) | 1.0.0 (hard-coded) | **1.16.0** (library version) | +| C binding (`libgosqlx`) | 1.15.0 exports | **1.16.0**: adds `gosqlx_*_dialect`, `gosqlx_lint`, `gosqlx_security_scan`, `gosqlx_transpile`, `gosqlx_dialects` | +| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | **0.3.0** (alpha), independent track; Python 3.10+; PyPI wheels from the `pygosqlx-v0.3.0` tag | --- diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md index 4d803bb0..d2775225 100644 --- a/website/src/content/blog/v1-16-0.md +++ b/website/src/content/blog/v1-16-0.md @@ -11,7 +11,9 @@ 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; and editor-accurate positions (a tab is one column). +and a Claude Code plugin; editor-accurate positions (a tab is one column); and +pygosqlx 0.3.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 @@ -285,6 +287,10 @@ limits), so read **Behavior changes** before upgrading. 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 @@ -438,6 +444,25 @@ limits), so read **Behavior changes** before upgrading. - **`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 0.3.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. (#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. + `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published + through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). + (#851) ### Changed - **`tokenizer.NewWithKeywords` no longer consults its keyword table**: the @@ -480,6 +505,21 @@ limits), so read **Behavior changes** before upgrading. #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="`. + GoReleaser no longer passes a `-X main.version` flag that the example + binaries ignored. (#846) - **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`. @@ -736,3 +776,20 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the 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); `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) From 967b9415d5135c14c0cfd3ca7608639565985293 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 15:10:02 +0530 Subject: [PATCH 09/12] docs(release): address v1.16.0 release review; add #852 - Release date 2026-10-22 in CHANGELOG, MIGRATION, the extension changelog and the generated blog post (re-stamp at tag time if it slips). - vscode-extension CHANGELOG: Fixed entry for the #846 version reporting. - CHANGELOG: keep the Behavior changes list tight; name TranspileError and the LintResult/SecurityScanResult types in the pygosqlx 0.3.0 entry. - MIGRATION: PyPI wheels row is conditional on the pygosqlx-v0.3.0 tag. - Add #852 (sdist builds fail without libgosqlx; release tag lookup skips v1). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- CHANGELOG.md | 16 +++++++++++----- docs/MIGRATION.md | 4 ++-- vscode-extension/CHANGELOG.md | 7 ++++++- website/src/content/blog/v1-16-0.md | 16 +++++++++++----- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a597ea7..de1d1991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.16.0] - 2026-10-21 — Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep +## [1.16.0] - 2026-10-22 — 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 @@ -290,7 +290,6 @@ limits), so read **Behavior changes** before upgrading. 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) @@ -460,13 +459,19 @@ limits), so read **Behavior changes** before upgrading. 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. (#850) + `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. `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). - (#851) + 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) ### Changed - **`tokenizer.NewWithKeywords` no longer consults its keyword table**: the @@ -783,7 +788,8 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the - 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); `vscode-publish.yml` + 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). diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index e5bf1efe..9cc5d900 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,6 +1,6 @@ # Migration Guide -## v1.15.0 → v1.16.0 (2026-10-21) +## v1.15.0 → v1.16.0 (2026-10-22) **Source-compatible upgrade**: exported signatures are unchanged apart from additions, so code that compiled against v1.15.0 compiles against v1.16.0. Runtime behaviour does change in several places (token positions, rendered SQL, parse acceptance, CLI refusals, MCP HTTP limits). Read the checklist below before upgrading. The [CHANGELOG](../CHANGELOG.md) has the full list. @@ -56,7 +56,7 @@ | GORM integration (`integrations/gorm`) | requires v1.14.0 | updated after the tag | | LSP `serverInfo.version` (`gosqlx lsp`) | 1.0.0 (hard-coded) | **1.16.0** (library version) | | C binding (`libgosqlx`) | 1.15.0 exports | **1.16.0**: adds `gosqlx_*_dialect`, `gosqlx_lint`, `gosqlx_security_scan`, `gosqlx_transpile`, `gosqlx_dialects` | -| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | **0.3.0** (alpha), independent track; Python 3.10+; PyPI wheels from the `pygosqlx-v0.3.0` tag | +| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | **0.3.0** (alpha), independent track; Python 3.10+; PyPI wheels once `pygosqlx-v0.3.0` is tagged and published | --- diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index 2e2712eb..ae332258 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to the "GoSQLX" extension will be documented in this file. -## [1.16.0] - 2026-10-21 +## [1.16.0] - 2026-10-22 Bundles the GoSQLX v1.16.0 language server. See the [main CHANGELOG](https://github.com/ajitpratap0/GoSQLX/blob/main/CHANGELOG.md) @@ -26,6 +26,11 @@ for the full list. for a token after a comment point at the token, not the comment. - Under the `mysql` and `mariadb` dialects, `||` is parsed as logical OR. +### Fixed +- **Version reporting**: the bundled `gosqlx --version` and the language + server's `serverInfo.version` now report the release version (1.16.0) + instead of 1.14.0 and a hard-coded 1.0.0. (#846) + ### Security - markdown-it 14.3.3 in the extension's lockfile (build tooling). diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md index d2775225..858d312e 100644 --- a/website/src/content/blog/v1-16-0.md +++ b/website/src/content/blog/v1-16-0.md @@ -1,6 +1,6 @@ --- title: "v1.16.0 — Performance Audit, SQL Highlighting, Dialect-Aware LSP, Claude Code Plugin & ChatGPT App, Correctness Sweep" -date: "2026-10-21" +date: "2026-10-22" version: "1.16.0" description: "Covers everything merged since v1.15.0: the October 2026 performance audit (#566) with large allocation cuts across tokenizer, parser, AST and LSP; the…" --- @@ -286,7 +286,6 @@ limits), so read **Behavior changes** before upgrading. 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) @@ -456,13 +455,19 @@ limits), so read **Behavior changes** before upgrading. 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. (#850) + `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. `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). - (#851) + 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) ### Changed - **`tokenizer.NewWithKeywords` no longer consults its keyword table**: the @@ -779,7 +784,8 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the - 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); `vscode-publish.yml` + 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). From e2479cd1560939bbcb810854c03bfae3d4b8c031 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 15:43:23 +0530 Subject: [PATCH 10/12] build(release): replace deprecated goreleaser keys (formats, version_template) goreleaser check (v2.12.7) flagged archives.format, format_overrides.format and snapshot.name_template as deprecated; release.yml runs goreleaser 'latest'. Snapshot release dry-run builds all six archives. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- .goreleaser.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 6d5a83ce..6d7a38cb 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -43,10 +43,10 @@ builds: archives: - id: gosqlx-tools - format: tar.gz + formats: [ tar.gz ] format_overrides: - goos: windows - format: zip + formats: [ zip ] name_template: >- {{ .ProjectName }}_tools_ {{- .Version }}_ @@ -64,7 +64,7 @@ checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: sort: asc From df1362b9b8a9499d283d6e2b5963a4d472345144 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 15:47:05 +0530 Subject: [PATCH 11/12] docs(release): add #853 to the v1.16.0 CHANGELOG Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- CHANGELOG.md | 12 ++++++++++-- website/src/content/blog/v1-16-0.md | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de1d1991..d697dcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -472,6 +472,11 @@ limits), so read **Behavior changes** before upgrading. 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 @@ -528,7 +533,9 @@ limits), so read **Behavior changes** before upgrading. version at build time with `-ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version="`. GoReleaser no longer passes a `-X main.version` flag that the example - binaries ignored. (#846) + 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`. @@ -802,7 +809,8 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the `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) + conversion. (#848) The README describes `bigquery` and `redshift` + accurately: generic keyword set plus some dialect-gated grammar. (#853) ## [1.15.0] - 2026-07-30 — Tree API, ClickHouse 100%, Oracle Gaps, Correctness Sweep diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md index 858d312e..213a00ad 100644 --- a/website/src/content/blog/v1-16-0.md +++ b/website/src/content/blog/v1-16-0.md @@ -468,6 +468,11 @@ limits), so read **Behavior changes** before upgrading. 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 @@ -524,7 +529,9 @@ limits), so read **Behavior changes** before upgrading. version at build time with `-ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version="`. GoReleaser no longer passes a `-X main.version` flag that the example - binaries ignored. (#846) + 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`. @@ -798,4 +805,5 @@ Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the `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) + conversion. (#848) The README describes `bigquery` and `redshift` + accurately: generic keyword set plus some dialect-gated grammar. (#853) From 7d862307b4427f5ac8e77b9deb3694b6fe91ce28 Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Thu, 22 Oct 2026 16:35:44 +0530 Subject: [PATCH 12/12] feat(python): version pygosqlx in lockstep with GoSQLX (1.16.0) pygosqlx wheels bundle libgosqlx built from the same commit, so its version now matches the GoSQLX release. The first PyPI release is 1.16.0 (it was 0.2.0, never published); no older versions are backfilled. - pygosqlx-release.yml also publishes on GoSQLX 'vX.Y.Z' tags (the floating 'v1' tag does not match); 'pygosqlx-v*' stays for Python-only .postN releases. Both must match pyproject.toml and be on main. - pyproject.toml / __version__ 1.16.0; the version test now asserts __version__ equals the bundled library's version(). - CHANGELOG, MIGRATION and python/README describe the lockstep scheme. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RkzVu3JY9Y6BRWfjesWts9 --- .github/workflows/pygosqlx-release.yml | 44 +++++++++++++---------- CHANGELOG.md | 10 +++--- docs/MIGRATION.md | 4 +-- pkg/cbinding/cbinding.go | 2 +- python/README.md | 16 +++++---- python/pygosqlx/__init__.py | 2 +- python/pygosqlx/core.py | 2 +- python/pyproject.toml | 2 +- python/tests/test_dialect_and_analysis.py | 10 +++--- website/src/content/blog/v1-16-0.md | 10 +++--- 10 files changed, 59 insertions(+), 43 deletions(-) diff --git a/.github/workflows/pygosqlx-release.yml b/.github/workflows/pygosqlx-release.yml index de93e3d6..036c4148 100644 --- a/.github/workflows/pygosqlx-release.yml +++ b/.github/workflows/pygosqlx-release.yml @@ -4,20 +4,24 @@ # pull_request (python/**, pkg/**, go.mod, go.sum, this file) # -> build + test wheels and sdist only. Nothing is published. # pkg/** because pkg/cbinding links the whole parser via pkg/gosqlx. -# push of a tag matching 'pygosqlx-v*' (e.g. pygosqlx-v0.3.0) -# -> build + test, then publish to PyPI. The tag must match the version -# in python/pyproject.toml and must point at a commit on main. -# This is the ONLY path to PyPI, so every published version has a tag. +# push of a GoSQLX release tag 'vX.Y.Z' (e.g. v1.16.0) +# -> build + test, then publish to PyPI. pygosqlx is versioned in +# lockstep with GoSQLX (its wheels bundle libgosqlx from the same +# commit), so the tag must equal the version in python/pyproject.toml. +# push of a tag 'pygosqlx-v*' (e.g. pygosqlx-v1.16.0.post1) +# -> same, for Python-only follow-up releases between GoSQLX releases. +# Either tag must point at a commit on main. Tag pushes are the ONLY path +# to PyPI, so every published version has a tag. # workflow_dispatch # -> build + test, then publish to the index chosen by the 'target' # input: 'testpypi' (default) or 'none' (build only). Publishing # from a dispatch is only allowed on main. TestPyPI skips files that # already exist, so repeated dry runs of one version do not fail. # -# Tag isolation: the main GoSQLX 'v*' release tags never trigger this -# workflow, and pygosqlx-v* tags must not disturb the Go release: -# release.yml pins GoReleaser's current/previous tag to the 'v*' series, and -# release.yml, vscode-publish.yml and bench-compare.yml filter on 'v*'. +# Tag isolation: pygosqlx-v* tags must not disturb the Go release: +# release.yml pins GoReleaser's current/previous tag to the 'vX.Y.Z' series, +# and release.yml, vscode-publish.yml and bench-compare.yml filter on 'v*'. +# The floating Marketplace tag 'v1' does not match 'v[0-9]+.[0-9]+.[0-9]+'. # # Wheels: one py3-none- wheel per OS/arch (the bundled libgosqlx is # loaded through ctypes and does not depend on the CPython ABI), each tested @@ -34,12 +38,13 @@ # repository 'GoSQLX', workflow 'pygosqlx-release.yml', environment # 'pypi'. # 2. In GitHub repo Settings -> Environments create an environment named -# 'pypi' (recommended: deployment policy limited to tags 'pygosqlx-v*' -# and a required reviewer). Also recommended: a tag ruleset restricting -# who may create 'pygosqlx-v*' tags. +# 'pypi' (deployment policy limited to tags 'v[0-9]*.[0-9]*.[0-9]*' +# and 'pygosqlx-v*'; optionally a required reviewer). +# Both steps were done for the first release (v1.16.0). # 3. Optional, for dry runs: repeat 1 on https://test.pypi.org with # environment 'testpypi', and create a 'testpypi' GitHub environment. -# Then release with: git tag -a pygosqlx-vX.Y.Z && git push origin pygosqlx-vX.Y.Z +# Releases then happen with the GoSQLX tag (git push origin vX.Y.Z), or +# git tag -a pygosqlx-vX.Y.Z.postN && git push origin pygosqlx-vX.Y.Z.postN name: pygosqlx Release @@ -54,6 +59,7 @@ on: - '.github/workflows/pygosqlx-release.yml' push: tags: + - 'v[0-9]+.[0-9]+.[0-9]+' - 'pygosqlx-v*' workflow_dispatch: inputs: @@ -205,7 +211,7 @@ jobs: # Defense in depth: never publish a tag whose commit is not on main. - name: Check tagged commit is on main - if: startsWith(github.ref, 'refs/tags/pygosqlx-v') + if: startsWith(github.ref, 'refs/tags/') run: | if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then echo "::error::$GITHUB_SHA ($GITHUB_REF_NAME) is not an ancestor of origin/main" @@ -218,12 +224,14 @@ jobs: python-version: '3.13' - name: Check tag matches package version - if: startsWith(github.ref, 'refs/tags/pygosqlx-v') + if: startsWith(github.ref, 'refs/tags/') env: TAG: ${{ github.ref_name }} run: | pkg_version="$(python -c 'import tomllib; print(tomllib.load(open("python/pyproject.toml", "rb"))["project"]["version"])')" - if [ "${TAG#pygosqlx-v}" != "$pkg_version" ]; then + tag_version="${TAG#pygosqlx-v}" + tag_version="${tag_version#v}" + if [ "$tag_version" != "$pkg_version" ]; then echo "::error::tag $TAG does not match python/pyproject.toml version $pkg_version" exit 1 fi @@ -254,9 +262,9 @@ jobs: name: publish to PyPI needs: [ wheels, sdist ] # Only a tag *push* publishes to PyPI. A workflow_dispatch run on a tag - # ref also has github.ref == refs/tags/pygosqlx-v*, so the event check is - # what keeps 'target: none' / 'testpypi' dispatches off real PyPI. - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/pygosqlx-v') + # ref also has github.ref == refs/tags/..., so the event check is what + # keeps 'target: none' / 'testpypi' dispatches off real PyPI. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-24.04 timeout-minutes: 15 environment: diff --git a/CHANGELOG.md b/CHANGELOG.md index d697dcf5..a1f66f83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ batch-2/batch-3 correctness sweep (round-tripping `SQL()`, formatter and 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 0.3.0 with dialect support, lint, security scan, transpile 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 @@ -447,7 +447,7 @@ limits), so read **Behavior changes** before upgrading. - **`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 0.3.0: dialects, lint, security scan and transpile**: `parse`, +- **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 @@ -466,8 +466,10 @@ limits), so read **Behavior changes** before upgrading. - **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. - `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published - through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). + 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` diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 9cc5d900..dcbcb951 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -40,7 +40,7 @@ - [ ] **LSP `serverInfo.version`** now reports the GoSQLX library version (1.16.0) instead of a hard-coded `1.0.0`. Update any client check that matched `1.0.0`. - [ ] **CLI version stamping**: release builds can set the `gosqlx --version` string with `-ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version="`; a plain `go build` keeps the built-in 1.16.0. - [ ] **C binding `gosqlx_version()`** now returns a newly allocated string on every call. Free each result once with `gosqlx_free` (earlier releases returned a shared pointer, so freeing it twice crashed). Callers that never freed it now leak a few bytes per call. -- [ ] **pygosqlx 0.3.0** requires Python 3.10 or later (3.8 and 3.9 are dropped). An unknown `dialect=` raises `UnsupportedDialectError`, a subclass of both `GoSQLXError` and `ValueError`. +- [ ] **pygosqlx 1.16.0** (previously 0.2.0; now versioned in lockstep with GoSQLX) requires Python 3.10 or later (3.8 and 3.9 are dropped). An unknown `dialect=` raises `UnsupportedDialectError`, a subclass of both `GoSQLXError` and `ValueError`. - [ ] **`gosqlx-mcp` behind a proxy**: rate limiting keys on the TCP peer. Set `GOSQLX_MCP_TRUSTED_PROXY_HEADER` (for example `CF-Connecting-IP`) or `GOSQLX_MCP_TRUSTED_PROXY_HOPS`, optionally with the proxy-secret pair, or every client shares one bucket. Tool input errors are now `isError` results, and `indent_size` must be 0 to 16. See [MCP_GUIDE.md](MCP_GUIDE.md). ### Companion release versions @@ -56,7 +56,7 @@ | GORM integration (`integrations/gorm`) | requires v1.14.0 | updated after the tag | | LSP `serverInfo.version` (`gosqlx lsp`) | 1.0.0 (hard-coded) | **1.16.0** (library version) | | C binding (`libgosqlx`) | 1.15.0 exports | **1.16.0**: adds `gosqlx_*_dialect`, `gosqlx_lint`, `gosqlx_security_scan`, `gosqlx_transpile`, `gosqlx_dialects` | -| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | **0.3.0** (alpha), independent track; Python 3.10+; PyPI wheels once `pygosqlx-v0.3.0` is tagged and published | +| Python bindings (`pygosqlx`) | 0.2.0 (alpha) | **1.16.0** (alpha), now versioned in lockstep with GoSQLX; Python 3.10+; first PyPI release, published by the `v1.16.0` tag | --- diff --git a/pkg/cbinding/cbinding.go b/pkg/cbinding/cbinding.go index 633de085..3bd4670f 100644 --- a/pkg/cbinding/cbinding.go +++ b/pkg/cbinding/cbinding.go @@ -38,7 +38,7 @@ // gosqlx_version() - return the library version string (e.g. "1.10.0") // gosqlx_free(ptr) - free a string previously returned by any gosqlx_* function // -// Dialect-aware and analysis exports (added for pygosqlx 0.3.0; see cbinding_ext.go): +// Dialect-aware and analysis exports (added in v1.16.0; see cbinding_ext.go): // // gosqlx_parse_dialect(sql, dialect) - gosqlx_parse with a SQL dialect // gosqlx_validate_dialect(sql, dialect) - gosqlx_validate with a SQL dialect diff --git a/python/README.md b/python/README.md index 23bc93bd..0da49e25 100644 --- a/python/README.md +++ b/python/README.md @@ -11,9 +11,9 @@ pip install pygosqlx ``` Prebuilt wheels bundle the native `libgosqlx` library, so no Go toolchain is -needed. They are published to PyPI from the `pygosqlx-v*` release tags; the -command above works once the first such tag (`pygosqlx-v0.3.0`) has been -published. Until then, build from source as shown below. +needed. pygosqlx is versioned in lockstep with GoSQLX: each GoSQLX release +(starting with 1.16.0) publishes the matching pygosqlx version, whose wheels +bundle the library built from that release. Wheels are available for Python 3.10-3.14 on: @@ -181,7 +181,7 @@ re-rendered without rewrites. ### Library compatibility -The functions and the `dialect=` argument added in 0.3.0 need a shared +The functions and the `dialect=` argument added in 1.16.0 need a shared library built from a matching GoSQLX checkout. With an older library, calls without `dialect=` keep working, while `dialect=`, `lint()`, `security_scan()`, `transpile()` and `dialects()` raise `GoSQLXError` asking @@ -308,9 +308,11 @@ cd /tmp && PYGOSQLX_TEST_INSTALLED=1 python -m pytest --import-mode=importlib /p Wheels for every supported platform and an sdist are built and tested by `.github/workflows/pygosqlx-release.yml` on each pull request that touches -`python/` or `pkg/`. Pushing a tag `pygosqlx-vX.Y.Z` (matching the version in -`pyproject.toml`, on a commit already on `main`) publishes them to PyPI -through Trusted Publishing; this is the only path to PyPI. A manual dispatch +`python/` or `pkg/`. Pushing the GoSQLX release tag `vX.Y.Z` publishes them to +PyPI through Trusted Publishing, so bump `pyproject.toml` and `__version__` +in the release PR. Python-only follow-ups use a `pygosqlx-vX.Y.Z.postN` tag. +Either tag must match `pyproject.toml` and point at a commit on `main`; tag +pushes are the only path to PyPI. A manual dispatch can publish a dry run to TestPyPI. See the workflow header for the one-time PyPI setup. ### Project structure diff --git a/python/pygosqlx/__init__.py b/python/pygosqlx/__init__.py index a2fa0643..52823ea5 100644 --- a/python/pygosqlx/__init__.py +++ b/python/pygosqlx/__init__.py @@ -89,7 +89,7 @@ ValidationError, ) -__version__ = "0.3.0" +__version__ = "1.16.0" __all__ = [ # Functions "parse", diff --git a/python/pygosqlx/core.py b/python/pygosqlx/core.py index 05eeeba1..8ff91a26 100644 --- a/python/pygosqlx/core.py +++ b/python/pygosqlx/core.py @@ -258,7 +258,7 @@ def _load_library() -> ctypes.CDLL: lib, "gosqlx_extract_metadata", [ctypes.c_char_p], ctypes.c_void_p ) - # --- Dialect-aware variants and analysis exports (libgosqlx for 0.3.0) --- + # --- Dialect-aware variants and analysis exports (libgosqlx 1.16.0+) --- for name in _DIALECT_EXPORTS.values(): _bind_optional( lib, name, [ctypes.c_char_p, ctypes.c_char_p], ctypes.c_void_p diff --git a/python/pyproject.toml b/python/pyproject.toml index d6f83d83..5ef9abc5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygosqlx" -version = "0.3.0" +version = "1.16.0" description = "Python bindings for GoSQLX - High-performance SQL parser" readme = "README.md" requires-python = ">=3.10" diff --git a/python/tests/test_dialect_and_analysis.py b/python/tests/test_dialect_and_analysis.py index 0c8a1b1f..129a61cc 100644 --- a/python/tests/test_dialect_and_analysis.py +++ b/python/tests/test_dialect_and_analysis.py @@ -1,4 +1,4 @@ -"""Tests for the pygosqlx 0.3.0 API: dialect=, lint(), security_scan(), +"""Tests for the pygosqlx 1.16.0 API: dialect=, lint(), security_scan(), transpile() and dialects(). Tests in the ``TestOlderLibrary`` and ``TestArgumentValidation`` classes run @@ -97,8 +97,10 @@ def test_new_names_exported(self): assert name in pygosqlx.__all__ assert hasattr(pygosqlx, name) - def test_version_is_0_3_0(self): - assert pygosqlx.__version__ == "0.3.0" + def test_version_in_lockstep_with_library(self): + # pygosqlx is versioned in lockstep with GoSQLX; a Python-only + # follow-up release adds a .postN suffix. + assert pygosqlx.__version__.split(".post")[0] == pygosqlx.version() def test_dialect_is_keyword_only(self): with pytest.raises(TypeError): @@ -168,7 +170,7 @@ def test_transpile_non_string_dialect_rejected(self): class TestOlderLibrary: - """A library built before 0.3.0 lacks the new exports: fail cleanly.""" + """A library built before 1.16.0 lacks the new exports: fail cleanly.""" @pytest.fixture def old_library(self, monkeypatch): diff --git a/website/src/content/blog/v1-16-0.md b/website/src/content/blog/v1-16-0.md index 213a00ad..7aa29b3d 100644 --- a/website/src/content/blog/v1-16-0.md +++ b/website/src/content/blog/v1-16-0.md @@ -12,7 +12,7 @@ batch-2/batch-3 correctness sweep (round-tripping `SQL()`, formatter and 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 0.3.0 with dialect support, lint, security scan, transpile 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 @@ -443,7 +443,7 @@ limits), so read **Behavior changes** before upgrading. - **`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 0.3.0: dialects, lint, security scan and transpile**: `parse`, +- **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 @@ -462,8 +462,10 @@ limits), so read **Behavior changes** before upgrading. - **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. - `pip install pygosqlx` works once the first `pygosqlx-v*` tag is published - through PyPI Trusted Publishing (`.github/workflows/pygosqlx-release.yml`). + 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`