Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
07a248b
fix(parser): model EXPLAIN <stmt> as ExplainStatement
Oct 22, 2026
a109ed9
test(realworld): add 2.7k-statement multi-dialect real-world corpus w…
Oct 22, 2026
0c00506
fix(parser): operators and predicates (IS forms, ANY/ALL arrays, AT T…
Oct 22, 2026
0c3e6a0
fix(parser): complete DML grammar for UPDATE, DELETE, INSERT, REPLACE…
Oct 22, 2026
eb4c736
fix(parser): parse utility statements and keep identifier quoting
Oct 22, 2026
2e052b4
fix(tokenizer): dialect-correct string escapes and missing lexical forms
Oct 22, 2026
109d352
fix(parser): accept keywords used as column names
Oct 22, 2026
263f657
fix(parser): parse and render SELECT-body dialect clauses without dro…
Oct 22, 2026
c1caf6d
fix(parser): reject utility keywords without arguments
Oct 22, 2026
b1891d8
fix(parser): parse keyword-named, qualified and special-form function…
Oct 22, 2026
5839845
fix(formatter): map LIMIT expressions to TOP/FETCH and allow APPLY ta…
Oct 22, 2026
2e28e81
fix(parser,ast): keep dotted quoted collations and index COLLATE; wal…
Oct 22, 2026
ed30f40
fix(parser,ast): tighten LIMIT BY, ASOF and star-modifier edge cases
Oct 22, 2026
5028943
fix(parser): reject bare CALL, SET = and savepoint statements without…
Oct 22, 2026
573bb2a
fix(parser,ast,security): release and scan new primary-expression nodes
Oct 22, 2026
ea03678
Merge branch 'fix/dialect-pkg-h' into test/dialect-audit-2026-10
Oct 22, 2026
6a2d602
Merge branch 'fix/dialect-pkg-c2' into test/dialect-audit-2026-10
Oct 22, 2026
9fce72d
Merge branch 'fix/dialect-pkg-d1' into test/dialect-audit-2026-10
Oct 22, 2026
c3df487
Merge branch 'fix/dialect-pkg-e' into test/dialect-audit-2026-10
Oct 22, 2026
42d361c
Merge branch 'fix/dialect-pkg-c1' into test/dialect-audit-2026-10
Oct 22, 2026
2b98e05
Merge branch 'fix/dialect-pkg-b' into test/dialect-audit-2026-10
Oct 22, 2026
c4d2f34
test(bench): add real-world corpus parse and parse+format benchmarks
Oct 22, 2026
087e4dc
fix(gosqlx): keep procedural bodies and honour GO/DELIMITER in SplitS…
Oct 22, 2026
53bfdb5
fix(parser): end T-SQL utility statements at the next statement and r…
Oct 22, 2026
12a3af1
fix(parser,ast): MERGE OUTPUT INTO, MySQL DELETE rejection, MERGE/REP…
Oct 22, 2026
2410f92
fix(transform): ReplaceTable and AddTableAlias handle multi-table UPD…
Oct 22, 2026
4e2eefd
fix(tokenizer,ast,parser): never change a string literal's value on f…
Oct 22, 2026
b536299
fix(transpiler): re-render string literals for the target dialect
Oct 22, 2026
6e21bfc
feat(parser): parse ALTER TABLE across dialects and ALTER <object> st…
Oct 22, 2026
02f9245
fix(parser,ast,formatter): set-op precedence, CTE and table-factor mo…
Oct 22, 2026
ded32a0
feat(parser,ast,formatter): column definitions and table constraints …
Oct 22, 2026
577571d
fix(formatter): render ALTER ROLE, POLICY and CONNECTOR instead of a …
Oct 22, 2026
d5515c1
feat(parser,ast,formatter): model CREATE TABLE AS, table options, eng…
Oct 22, 2026
975d359
fix(gosqlx,linter): report syntax errors instead of "no violations"
Oct 22, 2026
a774147
fix(formatter,gosqlx): keep comments and optimizer hints when formatting
Oct 22, 2026
956503a
fix(gosqlx): ParseReaderMultiple skips comment-only segments
Oct 22, 2026
145483e
fix(cli): exit non-zero on syntax errors and fix flag, dialect and I/…
Oct 22, 2026
dd9bf6a
docs: remove APIs and settings that do not exist; document CLI changes
Oct 22, 2026
d2c7f72
fix(parser,ast,formatter): CTE keyword names, SEARCH/CYCLE, PIVOT forms
Oct 22, 2026
989ae70
fix(ast,parser,formatter,transpiler): keep needed identifier quotes a…
Oct 22, 2026
4c1fffd
fix(gosqlx,lsp,cbinding): keep comments in dialect formatting; splitt…
Oct 22, 2026
f1b319c
fix(parser,ast): qualified positional columns, db..t, OPENJSON schema
Oct 22, 2026
759210e
perf(ast): keep TableReference compact by grouping rare modifiers
Oct 22, 2026
56868e6
fix(ast,parser,formatter): keep needed quotes on DDL object names
Oct 22, 2026
16804ff
fix(ast,parser,formatter): keep needed quotes on window names
Oct 22, 2026
af2f539
perf(parser,ast): keep name-quote recording off the hot path
Oct 22, 2026
283285e
fix(parser): keep ClickHouse projection/index columns and quoted lite…
Oct 22, 2026
15e13c6
fix(gosqlx,formatter): do not merge statements after a begin column; …
Oct 22, 2026
7cd6403
fix(tokenizer): decode ClickHouse \/ and \= escapes; refuse \e and \N
Oct 22, 2026
f9dd1a1
fix(parser,formatter): end ALTER TABLE at the next statement; allow-l…
Oct 22, 2026
0b788eb
fix(formatter,parser): keep ';' after verbatim statements; reject bar…
Oct 22, 2026
8863acd
fix(parser,ast): query tail after nested paren operand, looser left o…
Oct 22, 2026
30a04df
fix(parser,transform): keep cursor set operations whole, scope-aware …
Oct 22, 2026
9ec87a4
fix(transpiler): render output for the target dialect
Oct 22, 2026
2be588e
Merge branch 'fix/dialect-f-obj' into test/dialect-audit-2026-10
Oct 22, 2026
fb317db
Merge branch 'fix/dialect-d2' into test/dialect-audit-2026-10
Oct 22, 2026
085a948
Merge branch 'fix/dialect-g' into test/dialect-audit-2026-10
Oct 22, 2026
58738e6
Merge branch 'fix/dialect-f-col' into test/dialect-audit-2026-10
Oct 22, 2026
09da766
Merge branch 'fix/dialect-fu-quoting' into test/dialect-audit-2026-10
Oct 22, 2026
44e61dd
Merge branch 'fix/dialect-fu-strings' into test/dialect-audit-2026-10
Oct 22, 2026
89ef1bb
Merge branch 'fix/dialect-fu-batch' into test/dialect-audit-2026-10
Oct 22, 2026
059b52b
fix(transform): drop recorded quotes of names a table transform replaces
Oct 22, 2026
8494795
Merge branch 'fix/dialect-i' into test/dialect-audit-2026-10
Oct 22, 2026
6c81d7c
chore(lint): remove dead helpers left by the dialect merges; scope er…
Oct 22, 2026
82976e5
fix(ast,parser,formatter): AlterStatement.SQL, keep quote kinds in ty…
Oct 22, 2026
4fd2a9a
fix(formatter,parser,cli): dialect-aware Format, hoist WITH over set …
Oct 22, 2026
f237faa
perf(tokenizer,parser,gosqlx): tokenize internal parse paths into a r…
Oct 22, 2026
efeef37
fix(parser): statement boundaries in scripts without semicolons; whol…
Oct 22, 2026
aa8b1a8
fix(gosqlx): keep T-SQL "stmt; ELSE" in one SplitStatements piece; ch…
Oct 22, 2026
8cb9a41
test(realworld): T-SQL batches without semicolons and PL/SQL blocks
Oct 22, 2026
1717b3a
perf(ast)!: move the rare dialect SELECT clauses behind SelectStateme…
Oct 22, 2026
a5d6bea
fix(parser,ast): parse VALUES, ^, BINARY, PG prefix operators, string…
Oct 22, 2026
e459379
perf(tokenizer): build a compound keyword's joined Value only when ne…
Oct 22, 2026
2a9772a
fix(parser): bound wasted block scans; END/ABORT/RELEASE end a txn sc…
Oct 22, 2026
d6761db
perf(formatter,gosqlx): presize the SELECT render buffer; skip the op…
Oct 22, 2026
f6d8075
fix(parser,ast,formatter): close the real-world corpus failures (join…
Oct 22, 2026
8771e1e
perf(parser): keep the new syntax checks off the common parse path
Oct 22, 2026
a4a3d5c
Merge branch 'fix/dialect-w3-render' into test/dialect-audit-2026-10
Oct 22, 2026
b609a11
Merge branch 'fix/dialect-w3-boundaries' into test/dialect-audit-2026-10
Oct 22, 2026
78362f5
Merge branch 'fix/dialect-w3-perf' into test/dialect-audit-2026-10
Oct 22, 2026
9192563
fix(dialect): preserve GO batches and finish audit validation
Oct 22, 2026
0b5172b
chore(test): remove obsolete splitter tiling wrapper
Oct 22, 2026
73c0ae0
test(formatter): measure UNION allocation growth
Oct 22, 2026
0db001a
chore(sync): integrate main PIVOT updates
Oct 22, 2026
2acf38d
fix(render): keep PostgreSQL @ apart from its operand
Oct 25, 2026
0061884
fix(render): hoist WITH over nested parenthesized set operands
Oct 25, 2026
6025360
fix(formatter): keep nested optimizer hints with their SELECT
Oct 25, 2026
db952d2
fix(parser): treat a GO line as a hard batch boundary
Oct 25, 2026
c7587d7
fix(lsp): leave GO separators out of the document outline
Oct 25, 2026
c9fd57f
fix(analyze): exclude GO separators from statement counts
Oct 25, 2026
1c61555
fix(cli): re-enable the statement-count guard for SQL Server
Oct 25, 2026
25af930
fix(ast): release standalone VALUES and top-level set operations
Oct 25, 2026
5d60ce6
fix(transpiler): rewrite or refuse dialect-specific operators
Oct 25, 2026
861eb08
refactor(test): share the dropped-word detector
Oct 25, 2026
b14f14c
docs(changelog): note dialect audit review fixes
Oct 25, 2026
297b3c7
perf(parser): skip operator and clause probes at operand and clause ends
Oct 25, 2026
cd49a3c
perf(parser): screen dialect probes inline and parse DML targets in p…
Oct 25, 2026
3cf9323
perf(parser,ast): remove the dialect-audit parse regression
Oct 25, 2026
40af323
perf(parser): skip keyword probes for literals and trim DML statement…
Oct 25, 2026
8de2981
perf(parser): inline the function-call and DML clause screens
Oct 25, 2026
2a6b534
Merge origin/main into test/dialect-audit-2026-10
Oct 25, 2026
1cd9c76
perf(ast,parser): move UPDATE/DELETE dialect fields behind Extras
Oct 26, 2026
c8bd83b
Merge origin/test/dialect-audit-2026-10 (3cf93234) into the audit branch
Oct 26, 2026
3e964a2
test(realworld): add multi-dialect real-world corpus with ratchet
Oct 26, 2026
e3a7095
Merge origin/test/realworld-corpus into fix/dialect-audit-fixes
Oct 26, 2026
680f5a5
docs(changelog): describe the dialect fixes against main
Oct 26, 2026
5065e71
refactor(parser): leave the GO-inside-a-statement rule to its own PR
Oct 26, 2026
58670e4
fix(parser)!: treat a GO line inside a SQL Server statement as an error
Oct 26, 2026
fb1ffcb
test(realworld): flag malformed known-failure entries and truncate by…
Oct 26, 2026
81eee98
fix(gosqlx): make SplitStatements linear on unterminated q-quotes and…
Oct 26, 2026
c6834da
fix(formatter): linear comment placement; attach comments by parser s…
Oct 26, 2026
32d7634
fix(ast): render MERGE source column aliases, walk DDL/TOP/ARRAY JOIN…
Oct 26, 2026
19b36aa
fix(parser): stop verbatim utility statements from swallowing the nex…
Oct 26, 2026
f08a4df
Merge branch 'fix/rv-splitter-dos' into fix/dialect-audit-fixes
Oct 26, 2026
0c0ad03
Merge branch 'fix/rv-fmt-linear' into fix/dialect-audit-fixes
Oct 26, 2026
8e3d9cd
Merge branch 'fix/rv-ast-render' into fix/dialect-audit-fixes
Oct 26, 2026
5dcaf7c
Merge branch 'fix/rv-parser-implicit-end' into fix/dialect-audit-fixes
Oct 26, 2026
11ec38c
Merge branch 'test/realworld-corpus' into fix/dialect-audit-fixes
Oct 26, 2026
29f8c3e
fix(cli): exclude GO separators from parse's statement count; documen…
Oct 26, 2026
839214b
Merge branch 'fix/dialect-audit-fixes' into fix/tsql-go-inside-statement
Oct 26, 2026
86d8032
fix(parser): rename GO-line start index after merging statement starts
Oct 26, 2026
d008b6f
Merge origin/main (#866) into fix/dialect-audit-fixes
Oct 26, 2026
9e927bc
test(gosqlx): check splitter scaling by ratio instead of wall-clock b…
Oct 26, 2026
b9a9a78
Merge remote-tracking branch 'origin/fix/dialect-audit-fixes' into fi…
Oct 26, 2026
782d654
Merge origin/main (#867) into fix/tsql-go-inside-statement
Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
exist; the limits are constants.

### Behavior changes
- SQL Server: a line holding only `GO [n]` inside a statement is now a parse
error ("GO batch separator inside a statement"), matching sqlcmd, instead of
a column or alias named `go`. `GO 0` / `GO 1.5` alone on a line report
"invalid GO repeat count".
- A raw NUL or Ctrl-Z byte inside a string literal is an error in dialects
whose literals cannot spell it back (NUL without backslash escapes; both
without a dialect; Ctrl-Z in ClickHouse; both in Snowflake). Formatting used
Expand Down
39 changes: 39 additions & 0 deletions pkg/gosqlx/batch_separator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,42 @@ func TestBatchSeparatorInvalidCounts(t *testing.T) {
})
}
}

// TestBatchSeparatorInvalidCountMessage checks that GO with an invalid repeat
// count alone on its line is reported as such, not as a FROM / end of
// statement error from the statement before it.
func TestBatchSeparatorInvalidCountMessage(t *testing.T) {
for _, count := range []string{"0", "1.5", "999999999999999999999999999999"} {
t.Run(count, func(t *testing.T) {
tree, err := ParseWithDialect("SELECT 1\nGO "+count+"\nSELECT 2", keywords.DialectSQLServer)
if err == nil {
ast.ReleaseAST(tree)
t.Fatal("accepted invalid GO repeat count")
}
if !strings.Contains(err.Error(), "invalid GO repeat count") {
t.Fatalf("error = %v, want an invalid GO repeat count error", err)
}
})
}
}

// TestBatchSeparatorInsideStatementIsAnError checks that formatting refuses
// a statement that runs across a GO line (sqlcmd would split it there)
// instead of writing go as a column name, with or without comments.
func TestBatchSeparatorInsideStatementIsAnError(t *testing.T) {
for _, sql := range []string{
"SELECT a,\ngo\nFROM t",
"SELECT a, -- c\ngo\nFROM t",
"SELECT a /* c */,\nGO 2\nFROM t",
} {
t.Run(sql, func(t *testing.T) {
out, err := FormatWithDialect(sql, DefaultFormatOptions(), keywords.DialectSQLServer)
if err == nil {
t.Fatalf("formatted as %q, want an error", out)
}
if !strings.Contains(err.Error(), "GO batch separator inside a statement") {
t.Fatalf("error = %v", err)
}
})
}
}
8 changes: 8 additions & 0 deletions pkg/sql/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,15 @@ func (p *Parser) parseTokens(tokens []models.TokenWithSpan) (*ast.AST, error) {
continue
}

startPos := p.currentPos
stmt, err := p.parseStatement()
if err == nil {
result.Statements = append(result.Statements, stmt)
result.StatementStarts = append(result.StatementStarts, start)
}
if p.IsSQLServer() { // batchLineError returns err unchanged elsewhere
_, err = p.batchLineError(startPos, err)
}
if err != nil {
// Clean up the AST on error
ast.ReleaseAST(result)
Expand Down Expand Up @@ -664,11 +668,15 @@ func (p *Parser) parseContextTokens(ctx context.Context, tokens []models.TokenWi
continue
}

startPos := p.currentPos
stmt, err := p.parseStatement()
if err == nil {
result.Statements = append(result.Statements, stmt)
result.StatementStarts = append(result.StatementStarts, start)
}
if p.IsSQLServer() { // batchLineError returns err unchanged elsewhere
_, err = p.batchLineError(startPos, err)
}
if err != nil {
// Clean up the AST on error
ast.ReleaseAST(result)
Expand Down
9 changes: 9 additions & 0 deletions pkg/sql/parser/recovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ func (p *Parser) parseWithRecovery(tokens []models.TokenWithSpan) ([]ast.Stateme

stmtStartPos := p.currentPos
stmt, err := p.parseStatement()
if err == nil {
// A statement that ran across a GO line is reported, and parsing
// resumes at that line so the batch boundary is kept.
if at, gerr := p.batchLineError(stmtStartPos, nil); gerr != nil {
errors = append(errors, gerr)
p.seek(at)
continue
}
}
if err != nil {
// Create a ParseError with position info, preserving original error
loc := p.currentLocation()
Expand Down
34 changes: 34 additions & 0 deletions pkg/sql/parser/statement_boundary.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ package parser

import (
"github.com/ajitpratap0/GoSQLX/internal/batch"
goerrors "github.com/ajitpratap0/GoSQLX/pkg/errors"
"github.com/ajitpratap0/GoSQLX/pkg/models"
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
)
Expand Down Expand Up @@ -62,6 +63,39 @@ func (p *Parser) parseBatchSeparatorAt() (*ast.BatchSeparator, bool) {
return sep, true
}

// batchLineError checks, in SQL Server, the tokens a statement starting at
// token index start ran over (up to the current token when parseErr is set,
// since the statement parser stopped there) for a line holding only GO [n].
// sqlcmd ends the batch at such a line wherever it appears, so a statement
// that runs across it is an error; GO with an invalid repeat count (GO 0,
// GO 1.5) gets an error of its own. It returns parseErr when there is no
// such line, otherwise the index of the GO token and the GO error.
func (p *Parser) batchLineError(start int, parseErr error) (int, error) {
if !p.IsSQLServer() {
return -1, parseErr
}
last := p.currentPos - 1
if parseErr != nil {
last = p.currentPos
}
if last >= len(p.tokens) {
last = len(p.tokens) - 1
}
for i := start; i <= last; i++ {
if t := &p.tokens[i].Token; t.Type != models.TokenTypeIdentifier || len(t.Value) != 2 {
continue
}
if _, valid, ok := batch.Line(p.tokens, i); ok {
loc := p.tokens[i].Start
if !valid {
return i, goerrors.InvalidSyntaxError("invalid GO repeat count: GO takes a positive whole number", loc, "")
}
return i, goerrors.InvalidSyntaxError("GO batch separator inside a statement: a line holding only GO ends the batch", loc, "")
}
}
return -1, parseErr
}

// noteStatementEnd is called by the statement loops after a statement was
// parsed. When no ';' follows and the next token unambiguously starts a new
// statement, that token is marked as a statement boundary so that utility
Expand Down
75 changes: 75 additions & 0 deletions pkg/sql/parser/statement_boundary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,78 @@ func TestParser_UnclosedBlocksBoundedWork(t *testing.T) {
})
}
}

// TestParser_GOLineInsideStatement checks that a line holding only GO [n]
// ends the batch wherever it appears, as sqlcmd splits the script there: a
// statement that would run across it is an error, not a statement with a
// column or alias named go. GO with an invalid repeat count gets its own
// error instead of a confusing one from the statement parser.
func TestParser_GOLineInsideStatement(t *testing.T) {
tests := []struct {
name string
sql string
want string // substring of the error
}{
{"select list", "SELECT a,\ngo\nFROM t", "GO batch separator inside a statement"},
// GO ends the first batch before it; the second one starts with FROM.
{"alias position", "SELECT a\nGO 2\nFROM t", "FROM"},
{"where clause", "UPDATE t SET a = 1 WHERE\nGO\nb = 2", "GO batch separator inside a statement"},
{"lower case with count", "SELECT a,\n go 3\nFROM t", "GO batch separator inside a statement"},
{"zero count", "SELECT 1\nGO 0\nSELECT 2", "invalid GO repeat count"},
{"fractional count", "SELECT 1\nGO 1.5\nSELECT 2", "invalid GO repeat count"},
{"zero count first", "GO 0\nSELECT 2", "invalid GO repeat count"},
{"zero count last", "SELECT 1\nGO 0", "invalid GO repeat count"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tree, err := ParseWithDialect(tc.sql, keywords.DialectSQLServer)
if err == nil {
defer ast.ReleaseAST(tree)
t.Fatalf("parsed as %v, want an error", stmtShapes(tree))
}
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want it to mention %q", err, tc.want)
}
})
}

// Other dialects have no GO directive: go is an ordinary name there.
tree, err := ParseWithDialect("SELECT a,\ngo\nFROM t", keywords.DialectPostgreSQL)
if err != nil {
t.Fatalf("postgresql: %v", err)
}
ast.ReleaseAST(tree)

// GO on the same line as other text is not a separator.
for _, sql := range []string{"SELECT a, go FROM t", "SELECT a,\ngo, b\nFROM t"} {
tree, err := ParseWithDialect(sql, keywords.DialectSQLServer)
if err != nil {
t.Fatalf("%q: %v", sql, err)
}
ast.ReleaseAST(tree)
}
}

// TestParser_RecoveryStopsAtGOLineInsideStatement checks that the recovering
// parser reports a statement that ran across a GO line and keeps the batch
// boundary.
func TestParser_RecoveryStopsAtGOLineInsideStatement(t *testing.T) {
tkz := tokenizer.GetTokenizer()
defer tokenizer.PutTokenizer(tkz)
tkz.SetDialect(keywords.DialectSQLServer)
tokens, err := tkz.TokenizeString("SELECT a,\nGO\nFROM t\nGO\nSELECT 2")
if err != nil {
t.Fatal(err)
}
p := NewParser(WithDialect("sqlserver"))
defer p.Release()
stmts, errs := p.ParseWithRecoveryFromModelTokens(tokens)
tree := &ast.AST{Statements: stmts}
defer ast.ReleaseAST(tree)
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "GO batch separator inside a statement") {
t.Fatalf("errors = %v, want a GO separator error first", errs)
}
if got := fmt.Sprint(stmtShapes(tree)); !strings.HasPrefix(got, "[BatchSeparator") || !strings.HasSuffix(got, "BatchSeparator Select]") {
t.Fatalf("recovery shapes = %s", got)
}
}
Loading