diff --git a/CHANGELOG.md b/CHANGELOG.md index d22c8826..cca6a057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 in dialects without them. ### Fixed +- Preserve dialect numeric and PostgreSQL Unicode-escape literals and quoted type names; correct JSON/power precedence and retain operator meaning during target-dialect formatting. Hand-built expression trees, including hand-built operands inserted into parsed trees, now render necessary parentheses. +- Formatting or transpiling to another dialect rewrites PostgreSQL `#` (XOR), + `~`, `<<`/`>>`, `&`/`|` and `||` (CONCAT in MySQL/MariaDB) into the target's + form, and refuses with an error where the target has none (for example `#` + in Oracle/SQLite, `@>`/`?`/regex `~` outside PostgreSQL), instead of + emitting them unchanged (`a # b` read as `a` plus a comment in MySQL). - Scan raw dialect statements and standalone VALUES for injection patterns, recover panics at every C export, and guard unlinked tokenizer scratch calls. - Formatter lower-case mode covers expression and built-in function keywords; @@ -192,6 +198,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 longer read as MariaDB's `ANALYZE `. ### Behavior changes +- Numeric words without whitespace no longer silently become a number plus a + column alias. Numeric spelling is retained. Differences from the previous lexer: + + | Dialect | Numeric changes | Unicode-escape changes | + | --- | --- | --- | + | PostgreSQL | `1_000`, `1_000.000_1`, `0o17`, `0b101`, and base-prefix separators such as `0x_FF` are single numbers. Malformed forms (`1x`, `0x`, `1__0`, `1_`, `1FROM`) error. | `U&'...'` with optional `UESCAPE` (comments allowed before it) is validated and preserved as one literal; `U&"..."` decodes to a quoted identifier usable wherever a name is. Malformed escapes error. | + | MySQL / MariaDB | `0b101` is a single bit literal; `1x`, `1_000`, and bare `0x` are identifiers rather than number/alias pairs. A number with a fraction still ends before a name (`1.5a` is `1.5 AS a`). | Unchanged: `U&'x'` remains separate `U`, `&`, and string tokens. | + | SQLite | `1_000` is one number; numeric trailing junk and unsupported `0b101` error. | Unchanged. | + | ClickHouse | `1_000` and `0b101` are single numbers; numeric trailing junk errors. | Unchanged. | + | SQL Server | Numeric trailing junk, including `1_000` and `0b101`, errors; a keyword written against a number (`1AS x`) is still accepted. | Unchanged. | + | Oracle | `1d`, `1.5f`, `1e5D` (BINARY_DOUBLE/BINARY_FLOAT) are single numbers. Storage sizes (`100M`, `8K`, `2G`) keep their units wherever a size appears; keywords against a number (`1FROM`) are accepted; other trailing junk, `1_000` and `0b101` error. | Unchanged. | + | Snowflake / BigQuery / Redshift | Numeric trailing junk, `0b101` and `1_000` error. | Unchanged. | + | generic | As Oracle for storage sizes and keywords; `1d`, `1x`, `1_000`, `0b101` error. | Unchanged. | + + `0x1F` remains a single hex literal in every dialect. - 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 diff --git a/cmd/gosqlx/cmd/format_errors_test.go b/cmd/gosqlx/cmd/format_errors_test.go new file mode 100644 index 00000000..271729db --- /dev/null +++ b/cmd/gosqlx/cmd/format_errors_test.go @@ -0,0 +1,33 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestSQLFormatterRejectsUnsupportedXor(t *testing.T) { + tree, err := gosqlx.ParseWithDialect("SELECT a ^ b", keywords.DialectMySQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + f := NewSQLFormatter(FormatterOptions{Dialect: "sqlite"}) + for _, comments := range []bool{false, true} { + var out string + if comments { + out, err = f.FormatWithComments(tree, nil, nil) + } else { + out, err = f.Format(tree) + } + if err == nil || !strings.Contains(err.Error(), "XOR") || out != "" { + t.Fatalf("comments=%v: got %q, %v; want XOR error", comments, out, err) + } + } +} diff --git a/cmd/gosqlx/cmd/formatter.go b/cmd/gosqlx/cmd/formatter.go index bab6052e..a352caf4 100644 --- a/cmd/gosqlx/cmd/formatter.go +++ b/cmd/gosqlx/cmd/formatter.go @@ -401,8 +401,14 @@ func verifyFormattedSQL(original *ast.AST, inputTokens []models.TokenWithSpan, f } canonical := NewSQLFormatter(FormatterOptions{Compact: true, UppercaseKw: true, Dialect: dialect}).style for i, stmt := range original.Statements { - want := pkgformatter.FormatStatement(stmt, canonical) - got := pkgformatter.FormatStatement(reparsed.Statements[i], canonical) + want, err := pkgformatter.FormatStatementChecked(stmt, canonical) + if err != nil { + return fmt.Errorf("cannot format original statement %d: %w", i+1, err) + } + got, err := pkgformatter.FormatStatementChecked(reparsed.Statements[i], canonical) + if err != nil { + return fmt.Errorf("cannot format reparsed statement %d: %w", i+1, err) + } if !strings.EqualFold(want, got) { return fmt.Errorf("formatted output changes statement %d (formatter bug; input left unchanged)", i+1) } diff --git a/cmd/gosqlx/cmd/sql_formatter.go b/cmd/gosqlx/cmd/sql_formatter.go index 7a40317f..14188b5e 100644 --- a/cmd/gosqlx/cmd/sql_formatter.go +++ b/cmd/gosqlx/cmd/sql_formatter.go @@ -16,6 +16,7 @@ package cmd import ( "errors" + "fmt" "strings" pkgformatter "github.com/ajitpratap0/GoSQLX/pkg/formatter" @@ -26,7 +27,7 @@ import ( // SQLFormatter renders a parsed AST as SQL text for the format command. // // It is a thin adapter over pkg/formatter: every statement is rendered with -// pkgformatter.FormatStatement, so the CLI and the library share a single +// pkgformatter.FormatStatementChecked, so the CLI and the library share a single // renderer and the CLI has no renderer of its own to fall out of sync (#599). // Rendering gaps in pkg/formatter are caught, where detectable, by // verifyFormattedSQL. @@ -106,7 +107,11 @@ func (f *SQLFormatter) formatStatements(astObj *ast.AST) ([]string, error) { } stmts := make([]string, len(astObj.Statements)) for i, stmt := range astObj.Statements { - stmts[i] = pkgformatter.FormatStatement(stmt, f.style) + formatted, err := pkgformatter.FormatStatementChecked(stmt, f.style) + if err != nil { + return nil, fmt.Errorf("cannot format statement %d: %w", i+1, err) + } + stmts[i] = formatted } return stmts, nil } diff --git a/pkg/cbinding/format_errors_test.go b/pkg/cbinding/format_errors_test.go new file mode 100644 index 00000000..775cf85d --- /dev/null +++ b/pkg/cbinding/format_errors_test.go @@ -0,0 +1,28 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestFormatResultReportsNumericTrailingJunk(t *testing.T) { + for _, dialect := range []string{"", "postgresql"} { + result := formatResultFor("SELECT 1x", dialect) + if result.Success || result.Formatted != "" || !strings.Contains(result.Error, "numeric literal") { + t.Fatalf("dialect=%q: %+v; want error result", dialect, result) + } + raw, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + decoded := decodeMap(t, string(raw)) + success, ok := decoded["success"].(bool) + if !ok || success || errorOf(decoded) == "" { + t.Fatalf("error JSON = %s", raw) + } + } +} diff --git a/pkg/fingerprint/fingerprint.go b/pkg/fingerprint/fingerprint.go index 1823e18c..adc30a9e 100644 --- a/pkg/fingerprint/fingerprint.go +++ b/pkg/fingerprint/fingerprint.go @@ -117,7 +117,11 @@ func Normalize(sql string) (string, error) { opts := ast.CompactStyle() var parts []string for _, stmt := range astObj.Statements { - parts = append(parts, formatter.FormatStatement(stmt, opts)) + formatted, err := formatter.FormatStatementChecked(stmt, opts) + if err != nil { + return "", fmt.Errorf("fingerprint: formatting failed: %w", err) + } + parts = append(parts, formatted) } return strings.Join(parts, "; "), nil diff --git a/pkg/fingerprint/format_errors_test.go b/pkg/fingerprint/format_errors_test.go new file mode 100644 index 00000000..ee7b5632 --- /dev/null +++ b/pkg/fingerprint/format_errors_test.go @@ -0,0 +1,18 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package fingerprint_test + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/fingerprint" +) + +func TestNormalizeRejectsNumericTrailingJunk(t *testing.T) { + out, err := fingerprint.Normalize("SELECT 1x") + if err == nil || !strings.Contains(err.Error(), "tokenization failed") || out != "" { + t.Fatalf("got %q, %v; want tokenization error", out, err) + } +} diff --git a/pkg/formatter/caret_dialect_test.go b/pkg/formatter/caret_dialect_test.go new file mode 100644 index 00000000..60369b4d --- /dev/null +++ b/pkg/formatter/caret_dialect_test.go @@ -0,0 +1,141 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package formatter_test + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/formatter" + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestCaretTargetDialect(t *testing.T) { + tests := []struct { + source, target keywords.SQLDialect + in, contains string + wantError bool + }{ + {"postgresql", "mysql", "SELECT a ^ (b + c) * d", "POWER(a, (b + c)) * d", false}, + {"postgresql", "sqlserver", "SELECT a ^ b", "POWER(a, b)", false}, + {"postgresql", "oracle", "SELECT a ^ b", "POWER(a, b)", false}, + {"postgresql", "sqlite", "SELECT a ^ b", "POWER(a, b)", false}, + {"generic", "postgresql", "SELECT a -> b ^ 2", "(a -> b) ^ 2", false}, + {"generic", "postgresql", "SELECT a ^ b -> c", "a ^ (b -> c)", false}, + {"mysql", "postgresql", "SELECT a * b ^ c", "a * (b # c)", false}, + {"mysql", "sqlserver", "SELECT a * b ^ c", "a * (b ^ c)", false}, + {"sqlserver", "mysql", "SELECT a + b ^ c", "(a + b) ^ c", false}, + {"sqlserver", "snowflake", "SELECT a ^ b", "BITXOR(a, b)", false}, + {"mysql", "clickhouse", "SELECT a ^ b", "bitXor(a, b)", false}, + {"mysql", "sqlite", "SELECT a ^ b", "", true}, + {"mysql", "generic", "SELECT a ^ b", "", true}, + {"postgresql", "mysql", `SELECT U&'d\0061t'`, "", true}, + // PostgreSQL # (XOR), ~, shifts, || and PostgreSQL-only operators. + {"postgresql", "mysql", "SELECT a # b FROM t", "SELECT a ^ b FROM t", false}, + {"postgresql", "mysql", "SELECT a # b * c", "a ^ (b * c)", false}, + {"postgresql", "mariadb", "SELECT a * (b # c)", "a * (b ^ c)", false}, + {"postgresql", "sqlserver", "SELECT a # b + c", "a ^ (b + c)", false}, + {"postgresql", "snowflake", "SELECT a # b, ~a, a << 2, a | b", "BITXOR(a, b), BITNOT(a), BITSHIFTLEFT(a, 2), BITOR(a, b)", false}, + {"postgresql", "clickhouse", "SELECT a # b, ~a, a >> 2, a & b", "bitXor(a, b), bitNot(a), bitShiftRight(a, 2), bitAnd(a, b)", false}, + {"postgresql", "sqlserver", "SELECT a << 2", "LEFT_SHIFT(a, 2)", false}, + {"postgresql", "oracle", "SELECT a & b FROM t", "BITAND(a, b)", false}, + {"postgresql", "mysql", "SELECT a || b || c", "CONCAT(a, b, c)", false}, + {"postgresql", "oracle", "SELECT a # b FROM t", "", true}, + {"postgresql", "oracle", "SELECT ~a FROM t", "", true}, + {"postgresql", "oracle", "SELECT a << 2 FROM t", "", true}, + {"postgresql", "sqlite", "SELECT a # b", "", true}, + {"postgresql", "mysql", "SELECT a @> b", "", true}, + {"postgresql", "snowflake", "SELECT a ~ 'x'", "", true}, + } + for _, tt := range tests { + t.Run(string(tt.source)+"/"+string(tt.target)+"/"+tt.in, func(t *testing.T) { + a, err := gosqlx.ParseWithDialect(tt.in, tt.source) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(a) + before := a.SQL() + opts := ast.CompactStyle() + opts.Dialect = string(tt.target) + got, err := formatter.FormatASTChecked(a, opts) + if (err != nil) != tt.wantError { + t.Fatalf("output=%q error=%v", got, err) + } + if tt.wantError { + if legacy := formatter.FormatAST(a, opts); legacy != "" { + t.Errorf("unchecked returned unsafe SQL %q", legacy) + } + if trans, err := gosqlx.Transpile(tt.in, tt.source, tt.target); err == nil { + t.Errorf("transpiler returned %q, want error", trans) + } + return + } + if !strings.Contains(got, tt.contains) { + t.Errorf("got %q want fragment %q", got, tt.contains) + } + again, err := gosqlx.ParseWithDialect(got, tt.target) + if err != nil { + t.Fatalf("target reparse %q: %v", got, err) + } + defer ast.ReleaseAST(again) + if a.SQL() != before { + t.Errorf("source mutated: %q -> %q", before, a.SQL()) + } + trans, err := gosqlx.Transpile(tt.in, tt.source, tt.target) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(trans, tt.contains) { + t.Errorf("transpiler disagrees: %q", trans) + } + }) + } +} + +func TestFormatQuotedTypesForTarget(t *testing.T) { + for _, in := range []string{`SELECT CAST(x AS "MyType"), x::"char"`, `CREATE TABLE t (a "MyDomain")`} { + a, err := gosqlx.ParseWithDialect(in, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(a) + before := a.SQL() + opts := ast.CompactStyle() + opts.Dialect = "mysql" + out, err := formatter.FormatASTChecked(a, opts) + if err != nil { + t.Fatal(err) + } + if strings.Contains(out, `"My`) || !strings.Contains(out, "`") { + t.Fatalf("target type quotes lost: %s", out) + } + b, err := gosqlx.ParseWithDialect(out, keywords.DialectMySQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(b) + if a.SQL() != before { + t.Fatal("source mutated") + } + } +} + +func TestFormatTargetDoesNotMutateMapExpressions(t *testing.T) { + a, err := gosqlx.ParseWithDialect(`SELECT * FROM t AT (TIMESTAMP => CAST('x' AS "MyType"))`, keywords.DialectSnowflake) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(a) + before := a.SQL() + opts := ast.CompactStyle() + opts.Dialect = "mysql" + if _, err := formatter.FormatASTChecked(a, opts); err != nil { + t.Fatal(err) + } + if got := a.SQL(); got != before { + t.Fatalf("source mutated: %q -> %q", before, got) + } +} diff --git a/pkg/formatter/dialect_expressions.go b/pkg/formatter/dialect_expressions.go new file mode 100644 index 00000000..04a3d3b9 --- /dev/null +++ b/pkg/formatter/dialect_expressions.go @@ -0,0 +1,207 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package formatter + +import ( + "fmt" + "reflect" + "strings" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" +) + +// FormatASTChecked renders an AST and reports operators that have no equivalent +// in the requested target dialect. It never mutates the caller's tree. +func FormatASTChecked(a *ast.AST, opts ast.FormatOptions) (string, error) { + if a == nil { + return "", nil + } + n, err := prepareDialectNode(a, opts.Dialect) + if err != nil { + return "", err + } + copy, ok := n.(*ast.AST) + if !ok { + return "", fmt.Errorf("unexpected formatted AST type %T", n) + } + return formatAST(copy, opts), nil +} + +// FormatStatementChecked is FormatStatement with target-dialect errors. +func FormatStatementChecked(s ast.Statement, opts ast.FormatOptions) (string, error) { + if s == nil { + return "", nil + } + n, err := prepareDialectNode(s, opts.Dialect) + if err != nil { + return "", err + } + copy, ok := n.(ast.Statement) + if !ok { + return "", fmt.Errorf("unexpected formatted statement type %T", n) + } + return formatStatement(copy, opts), nil +} + +// FormatExpressionChecked is FormatExpression with target-dialect errors. +func FormatExpressionChecked(e ast.Expression, opts ast.FormatOptions) (string, error) { + if e == nil { + return "", nil + } + if opts.Dialect == "" { + return formatExpression(e, opts), nil + } + // An expression occupies an ordinary statement slot during the shared + // rewrite, so nested expressions receive exactly the same semantic checks. + s := &ast.SelectStatement{Columns: []ast.Expression{e}} + n, err := prepareDialectNode(s, opts.Dialect) + if err != nil { + return "", err + } + copy, ok := n.(*ast.SelectStatement) + if !ok { + return "", fmt.Errorf("unexpected formatted expression container %T", n) + } + return formatExpression(copy.Columns[0], opts), nil +} + +func prepareDialectNode(n ast.Node, dialect string) (ast.Node, error) { + if dialect == "" || nilRenderNode(n) { + return n, nil + } + if !needsDialectRewrite(n, strings.ToLower(dialect)) { + return n, nil + } + v := copyRenderValue(reflect.ValueOf(n), make(map[uintptr]reflect.Value)) + copy, ok := v.Interface().(ast.Node) + if !ok { + return nil, fmt.Errorf("cannot render node %T", n) + } + if a, ok := copy.(*ast.AST); ok { + for _, s := range a.Statements { + if err := ast.RetargetOperators(s, "", dialect); err != nil { + return nil, err + } + } + } else if s, ok := copy.(ast.Statement); ok { + if err := ast.RetargetOperators(s, "", dialect); err != nil { + return nil, err + } + } + ast.RestyleDataTypesForDialect(copy, dialect) + return copy, nil +} + +func needsDialectRewrite(n ast.Node, target string) bool { + if nilRenderNode(n) { + return false + } + if e, ok := n.(ast.Expression); ok && ast.OperatorNeedsRetarget(e, target) { + return true + } + switch x := n.(type) { + case *ast.CastExpression: + if !x.TypeAsString && ast.DataTypeNeedsRestyleForDialect(x.Type, target) { + return true + } + case *ast.ColumnDef: + if ast.DataTypeNeedsRestyleForDialect(x.Type, target) { + return true + } + case *ast.CreateTableStatement: + for _, col := range x.Columns { + if ast.DataTypeNeedsRestyleForDialect(col.Type, target) { + return true + } + } + case *ast.CreateMaterializedViewStatement: + for _, col := range x.ColumnDefs { + if ast.DataTypeNeedsRestyleForDialect(col.Type, target) { + return true + } + } + case *ast.AlterTableOperation: + if ast.DataTypeNeedsRestyleForDialect(x.DataType, target) { + return true + } + if x.ColumnDef != nil && ast.DataTypeNeedsRestyleForDialect(x.ColumnDef.Type, target) { + return true + } + for _, col := range x.ColumnDefs { + if col != nil && ast.DataTypeNeedsRestyleForDialect(col.Type, target) { + return true + } + } + } + return childrenNeedDialectRewrite(n, target) +} + +func nilRenderNode(n ast.Node) bool { + if n == nil { + return true + } + v := reflect.ValueOf(n) + return v.Kind() == reflect.Pointer && v.IsNil() +} + +// copyRenderValue copies exported AST storage before dialect rewrites. Private +// literal metadata is read-only and can remain shared with the source tree. +func copyRenderValue(v reflect.Value, seen map[uintptr]reflect.Value) reflect.Value { + switch v.Kind() { + case reflect.Pointer: + if v.IsNil() { + return v + } + if prior, ok := seen[v.Pointer()]; ok { + return prior + } + out := reflect.New(v.Type().Elem()) + seen[v.Pointer()] = out + out.Elem().Set(copyRenderValue(v.Elem(), seen)) + return out + case reflect.Interface: + if v.IsNil() { + return v + } + out := reflect.New(v.Type()).Elem() + out.Set(copyRenderValue(v.Elem(), seen)) + return out + case reflect.Struct: + out := reflect.New(v.Type()).Elem() + out.Set(v) + for i := 0; i < v.NumField(); i++ { + if v.Type().Field(i).IsExported() { + out.Field(i).Set(copyRenderValue(v.Field(i), seen)) + } + } + return out + case reflect.Slice: + if v.IsNil() { + return v + } + out := reflect.MakeSlice(v.Type(), v.Len(), v.Len()) + for i := 0; i < v.Len(); i++ { + out.Index(i).Set(copyRenderValue(v.Index(i), seen)) + } + return out + case reflect.Map: + if v.IsNil() { + return v + } + out := reflect.MakeMapWithSize(v.Type(), v.Len()) + iter := v.MapRange() + for iter.Next() { + out.SetMapIndex(iter.Key(), copyRenderValue(iter.Value(), seen)) + } + return out + case reflect.Array: + out := reflect.New(v.Type()).Elem() + for i := 0; i < v.Len(); i++ { + out.Index(i).Set(copyRenderValue(v.Index(i), seen)) + } + return out + default: + return v + } +} diff --git a/pkg/formatter/dialect_prepare_test.go b/pkg/formatter/dialect_prepare_test.go new file mode 100644 index 00000000..ebfe4d13 --- /dev/null +++ b/pkg/formatter/dialect_prepare_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package formatter + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" +) + +func TestPrepareDialectNodeOnlyCopiesChanges(t *testing.T) { + for _, tc := range []struct { + name, target string + expr ast.Expression + }{ + {"plain cast", "mysql", &ast.CastExpression{Expr: &ast.Identifier{Name: "x"}, Type: "INT"}}, + {"quoted cast", "postgresql", &ast.CastExpression{Expr: &ast.Identifier{Name: "x"}, Type: `"MyType"`}}, + {"power", "postgresql", &ast.BinaryExpression{Operator: "^", Left: &ast.Identifier{Name: "x"}, Right: &ast.Identifier{Name: "y"}}}, + {"xor", "mysql", &ast.BinaryExpression{Operator: "^", BitwiseXor: true, Left: &ast.Identifier{Name: "x"}, Right: &ast.Identifier{Name: "y"}}}, + {"prefix", "postgresql", &ast.UnaryExpression{Operator: ast.PGAbs, Expr: &ast.Identifier{Name: "x"}}}, + {"unicode", "postgresql", &ast.UnicodeEscapeExpression{RawSQL: `U&'d\0061t'`}}, + {"object", "snowflake", &ast.ObjectLiteral{}}, + } { + t.Run(tc.name, func(t *testing.T) { + n := &ast.SelectStatement{Columns: []ast.Expression{tc.expr}} + got, err := prepareDialectNode(n, tc.target) + if err != nil { + t.Fatal(err) + } + if got != n { + t.Fatal("copied a tree that needs no target rewrite") + } + if allocs := testing.AllocsPerRun(100, func() { + _, _ = prepareDialectNode(n, tc.target) + }); allocs != 0 { + t.Errorf("unchanged expression path allocated %g times", allocs) + } + }) + } + for _, n := range []ast.Node{ + &ast.CreateTableStatement{Columns: []ast.ColumnDef{{Name: "x", Type: "INT"}}}, + &ast.CreateMaterializedViewStatement{}, + &ast.AlterStatement{}, + } { + got, err := prepareDialectNode(n, "mysql") + if err != nil { + t.Fatal(err) + } + if got != n { + t.Fatalf("copied %T without a differing type delimiter", n) + } + } +} diff --git a/pkg/formatter/dialect_scan.go b/pkg/formatter/dialect_scan.go new file mode 100644 index 00000000..a6767544 --- /dev/null +++ b/pkg/formatter/dialect_scan.go @@ -0,0 +1,143 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package formatter + +import "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + +// Scan common expression paths without materializing Children slices. Uncommon +// nodes use their authoritative Children method, including dialect extensions. +func childrenNeedDialectRewrite(n ast.Node, target string) bool { + switch x := n.(type) { + case *ast.AST: + for _, s := range x.Statements { + if needsDialectRewrite(s, target) { + return true + } + } + return false + case *ast.Identifier, *ast.Ident, *ast.LiteralValue, *ast.UnicodeEscapeExpression: + return false + case *ast.BinaryExpression: + return nodesNeedDialectRewrite(target, x.Left, x.Right, x.DialectExtras().Escape) + case *ast.UnaryExpression: + return needsDialectRewrite(x.Expr, target) + case *ast.CastExpression: + return needsDialectRewrite(x.Expr, target) + case *ast.AliasedExpression: + return needsDialectRewrite(x.Expr, target) + case *ast.ParenthesizedExpression: + return needsDialectRewrite(x.Expr, target) + case *ast.FunctionCall: + if expressionsNeedDialectRewrite(x.Arguments, target) || expressionsNeedDialectRewrite(x.Parameters, target) || + nodesNeedDialectRewrite(target, x.Filter, x.Over) { + return true + } + for i := range x.OrderBy { + if needsDialectRewrite(&x.OrderBy[i], target) { + return true + } + } + for i := range x.WithinGroup { + if needsDialectRewrite(&x.WithinGroup[i], target) { + return true + } + } + return x.Extras != nil && nodesNeedDialectRewrite(target, x.Extras.Separator, x.Extras.Keep) + case *ast.SelectStatement: + // Dialect extensions retain the shared visitor's traversal. + if x.Extras == nil && x.ArrayJoin == nil { + if nodesNeedDialectRewrite(target, x.With, x.Top, x.Sample, x.PrewhereClause, + x.Where, x.Having, x.Qualify, x.StartWith, x.ConnectBy, x.Fetch, x.For) || + expressionsNeedDialectRewrite(x.DistinctOnColumns, target) || + expressionsNeedDialectRewrite(x.Columns, target) || expressionsNeedDialectRewrite(x.GroupBy, target) { + return true + } + for i := range x.From { + if needsDialectRewrite(&x.From[i], target) { + return true + } + } + for i := range x.Joins { + if needsDialectRewrite(&x.Joins[i], target) { + return true + } + } + for i := range x.OrderBy { + if needsDialectRewrite(&x.OrderBy[i], target) { + return true + } + } + for i := range x.Windows { + if needsDialectRewrite(&x.Windows[i], target) { + return true + } + } + for _, setting := range x.Settings { + if needsDialectRewrite(setting.Value, target) { + return true + } + } + return false + } + case *ast.JoinClause: + return nodesNeedDialectRewrite(target, &x.Left, &x.Right, x.Condition, x.MatchCondition) || expressionsNeedDialectRewrite(x.Using, target) + case *ast.OrderByExpression: + return nodesNeedDialectRewrite(target, x.Expression, x.WithFill) + case *ast.WithClause: + for _, cte := range x.CTEs { + if needsDialectRewrite(cte, target) { + return true + } + } + return false + case *ast.CommonTableExpr: + return nodesNeedDialectRewrite(target, x.Statement, x.ScalarExpr, x.Cycle) + case *ast.WindowSpec: + if expressionsNeedDialectRewrite(x.PartitionBy, target) || needsDialectRewrite(x.FrameClause, target) { + return true + } + for i := range x.OrderBy { + if needsDialectRewrite(&x.OrderBy[i], target) { + return true + } + } + return false + case *ast.WindowFrame: + return needsDialectRewrite(x.Start.Value, target) || x.End != nil && needsDialectRewrite(x.End.Value, target) + case *ast.CaseExpression: + if nodesNeedDialectRewrite(target, x.Value, x.ElseClause) { + return true + } + for _, when := range x.WhenClauses { + if nodesNeedDialectRewrite(target, when.Condition, when.Result) { + return true + } + } + return false + } + for _, child := range n.Children() { + if needsDialectRewrite(child, target) { + return true + } + } + return false +} + +func nodesNeedDialectRewrite(target string, nodes ...ast.Node) bool { + for _, node := range nodes { + if needsDialectRewrite(node, target) { + return true + } + } + return false +} + +func expressionsNeedDialectRewrite(exprs []ast.Expression, target string) bool { + for _, e := range exprs { + if needsDialectRewrite(e, target) { + return true + } + } + return false +} diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 1b8dc15c..71288408 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -181,7 +181,7 @@ func (f *Formatter) Format(sql string) (string, error) { } style.Dialect = f.opts.Dialect - return FormatAST(parsedAST, style), nil + return FormatASTChecked(parsedAST, style) } // FormatString is a convenience function that formats SQL with default options. diff --git a/pkg/formatter/nested_dialect_errors_test.go b/pkg/formatter/nested_dialect_errors_test.go new file mode 100644 index 00000000..ae924d26 --- /dev/null +++ b/pkg/formatter/nested_dialect_errors_test.go @@ -0,0 +1,48 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package formatter_test + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/formatter" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" +) + +func TestCheckedFormattingRejectsNestedUnsupportedOperator(t *testing.T) { + xor := func() ast.Expression { + return &ast.BinaryExpression{Operator: "^", BitwiseXor: true, + Left: &ast.Identifier{Name: "a"}, Right: &ast.Identifier{Name: "b"}} + } + query := func(e ast.Expression) *ast.SelectStatement { return &ast.SelectStatement{Columns: []ast.Expression{e}} } + for _, tc := range []struct { + name string + stmt ast.Statement + }{ + {"where", &ast.SelectStatement{Columns: []ast.Expression{&ast.Identifier{Name: "a"}}, Where: xor()}}, + {"cast", query(&ast.CastExpression{Expr: xor(), Type: "INT"})}, + {"case result", query(&ast.CaseExpression{WhenClauses: []ast.WhenClause{{Condition: &ast.Identifier{Name: "a"}, Result: xor()}}})}, + {"function argument", query(&ast.FunctionCall{Name: "F", Arguments: []ast.Expression{xor()}})}, + {"function parameters", query(&ast.FunctionCall{Name: "F", Parameters: []ast.Expression{xor()}})}, + {"function order", query(&ast.FunctionCall{Name: "F", OrderBy: []ast.OrderByExpression{{Expression: xor()}}})}, + {"window frame", query(&ast.FunctionCall{Name: "F", Over: &ast.WindowSpec{FrameClause: &ast.WindowFrame{End: &ast.WindowFrameBound{Value: xor()}}}})}, + {"cte", &ast.SelectStatement{With: &ast.WithClause{CTEs: []*ast.CommonTableExpr{{Name: "c", Statement: query(xor())}}}}}, + {"scalar cte", &ast.SelectStatement{With: &ast.WithClause{CTEs: []*ast.CommonTableExpr{{Name: "c", ScalarExpr: xor()}}}}}, + {"named window", &ast.SelectStatement{Windows: []ast.WindowSpec{{OrderBy: []ast.OrderByExpression{{Expression: xor()}}}}}}, + {"join condition", &ast.SelectStatement{Joins: []ast.JoinClause{{Condition: xor()}}}}, + {"join match condition", &ast.SelectStatement{Joins: []ast.JoinClause{{MatchCondition: xor()}}}}, + {"map value", &ast.SelectStatement{From: []ast.TableReference{{Name: "t", TimeTravel: &ast.TimeTravelClause{Kind: "AT", Named: map[string]ast.Expression{"OFFSET": xor()}}}}}}, + {"subquery", &ast.SelectStatement{From: []ast.TableReference{{Subquery: query(xor())}}}}, + } { + t.Run(tc.name, func(t *testing.T) { + opts := ast.CompactStyle() + opts.Dialect = "sqlite" + out, err := formatter.FormatStatementChecked(tc.stmt, opts) + if err == nil || !strings.Contains(err.Error(), "XOR") || out != "" { + t.Fatalf("got %q, %v; expected unsupported nested XOR error", out, err) + } + }) + } +} diff --git a/pkg/formatter/render.go b/pkg/formatter/render.go index f5fe38a8..e9631b36 100644 --- a/pkg/formatter/render.go +++ b/pkg/formatter/render.go @@ -78,15 +78,38 @@ func (f *nodeFormatter) result() string { // ─── public rendering API ──────────────────────────────────────────────────── -// FormatAST renders a full *ast.AST with the given options. +// FormatAST renders an AST. An unsupported target operator returns an empty +// string; use FormatASTChecked to receive the diagnostic. func FormatAST(a *ast.AST, opts ast.FormatOptions) string { + out, _ := FormatASTChecked(a, opts) + return out +} + +// FormatStatement renders a statement. If a target-dialect conversion is +// unsupported, it returns an empty string and discards the error. Use +// FormatStatementChecked whenever the result can carry a target dialect. +func FormatStatement(s ast.Statement, opts ast.FormatOptions) string { + out, _ := FormatStatementChecked(s, opts) + return out +} + +// FormatExpression renders an expression. If a target-dialect conversion is +// unsupported, it returns an empty string and discards the error. Use +// FormatExpressionChecked whenever the result can carry a target dialect. +func FormatExpression(e ast.Expression, opts ast.FormatOptions) string { + out, _ := FormatExpressionChecked(e, opts) + return out +} + +// FormatAST renders a full *ast.AST with the given options. +func formatAST(a *ast.AST, opts ast.FormatOptions) string { if a == nil { return "" } parts := make([]string, 0, len(a.Statements)) hasBatch := false for _, stmt := range a.Statements { - part := FormatStatement(stmt, opts) + part := formatStatement(stmt, opts) // Renderers that keep a statement as written (UtilityStatement and // the stmtSQL fallback) do not add the ";" themselves; without it the // next statement would be read back as part of their text. @@ -150,7 +173,7 @@ func FormatAST(a *ast.AST, opts ast.FormatOptions) string { // FormatStatement renders a single AST statement with the given options. // Returns "" for nil. -func FormatStatement(s ast.Statement, opts ast.FormatOptions) string { +func formatStatement(s ast.Statement, opts ast.FormatOptions) string { if s == nil { return "" } @@ -207,7 +230,7 @@ func FormatStatement(s ast.Statement, opts ast.FormatOptions) string { // FormatExpression renders an AST expression with the given options. // Returns "" for nil. -func FormatExpression(e ast.Expression, opts ast.FormatOptions) string { +func formatExpression(e ast.Expression, opts ast.FormatOptions) string { if e == nil { return "" } @@ -316,14 +339,14 @@ func renderSelect(s *ast.SelectStatement, opts ast.FormatOptions) string { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("PREWHERE")) sb.WriteString(" ") - sb.WriteString(FormatExpression(s.PrewhereClause, opts)) + sb.WriteString(formatExpression(s.PrewhereClause, opts)) } if s.Where != nil { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("WHERE")) sb.WriteString(" ") - sb.WriteString(FormatExpression(s.Where, opts)) + sb.WriteString(formatExpression(s.Where, opts)) } if gb := groupBySQL(s, f); gb != "" { @@ -335,7 +358,7 @@ func renderSelect(s *ast.SelectStatement, opts ast.FormatOptions) string { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("HAVING")) sb.WriteString(" ") - sb.WriteString(FormatExpression(s.Having, opts)) + sb.WriteString(formatExpression(s.Having, opts)) } // QUALIFY, START WITH, CONNECT BY @@ -447,7 +470,7 @@ func renderInsert(i *ast.InsertStatement, opts ast.FormatOptions) string { sb.WriteString(strings.Join(sets, ", ")) } else if i.Query != nil { sb.WriteString(f.clauseSep()) - sb.WriteString(FormatStatement(i.Query, nestedOpts(opts))) + sb.WriteString(formatStatement(i.Query, nestedOpts(opts))) } else if len(i.Values) > 0 { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("VALUES")) @@ -539,7 +562,7 @@ func renderUpdate(u *ast.UpdateStatement, opts ast.FormatOptions) string { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("WHERE")) sb.WriteString(" ") - sb.WriteString(FormatExpression(u.Where, opts)) + sb.WriteString(formatExpression(u.Where, opts)) } writeDMLOrderLimit(sb, f, x) writeReturning(sb, f, u.Returning, x.ReturningInto) @@ -608,7 +631,7 @@ func renderDelete(d *ast.DeleteStatement, opts ast.FormatOptions) string { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("WHERE")) sb.WriteString(" ") - sb.WriteString(FormatExpression(d.Where, opts)) + sb.WriteString(formatExpression(d.Where, opts)) } writeDMLOrderLimit(sb, f, x) writeReturning(sb, f, d.Returning, x.ReturningInto) @@ -670,10 +693,10 @@ func renderSetOperationBody(s *ast.SetOperation, opts ast.FormatOptions) string if innermost := spine[len(spine)-1]; innermost.Left != nil { if ast.SetOperandNeedsParens(innermost, false, opts.Dialect) { sb.WriteString("(") - sb.WriteString(FormatStatement(innermost.Left, inner)) + sb.WriteString(formatStatement(innermost.Left, inner)) sb.WriteString(")") } else { - sb.WriteString(FormatStatement(innermost.Left, inner)) + sb.WriteString(formatStatement(innermost.Left, inner)) } } for i := len(spine) - 1; i >= 0; i-- { @@ -691,10 +714,10 @@ func renderSetOperationBody(s *ast.SetOperation, opts ast.FormatOptions) string if level.Right != nil { if ast.SetOperandNeedsParens(level, true, opts.Dialect) { sb.WriteString("(") - sb.WriteString(FormatStatement(level.Right, inner)) + sb.WriteString(formatStatement(level.Right, inner)) sb.WriteString(")") } else { - sb.WriteString(FormatStatement(level.Right, inner)) + sb.WriteString(formatStatement(level.Right, inner)) } } } @@ -1010,7 +1033,7 @@ func renderIn(i *ast.InExpression, opts ast.FormatOptions, context ...*expressio } sb.WriteString(" (") if i.Subquery != nil { - sb.WriteString(FormatStatement(i.Subquery, nestedOpts(opts))) + sb.WriteString(formatStatement(i.Subquery, nestedOpts(opts))) } else { parts := make([]string, len(i.List)) for idx, e := range i.List { @@ -1032,7 +1055,7 @@ func renderExists(e *ast.ExistsExpression, opts ast.FormatOptions) string { sb.WriteString(f.kw("EXISTS")) sb.WriteString(" (") - sb.WriteString(FormatStatement(e.Subquery, nestedOpts(opts))) + sb.WriteString(formatStatement(e.Subquery, nestedOpts(opts))) sb.WriteString(")") return f.result() @@ -1043,13 +1066,13 @@ func renderSubquery(s *ast.SubqueryExpression, opts ast.FormatOptions) string { return "" } if opts.KeywordCase == ast.KeywordLower && s.Bare { - return FormatStatement(s.Subquery, nestedOpts(opts)) + return formatStatement(s.Subquery, nestedOpts(opts)) } f := newNodeFormatter(opts) sb := f.sb sb.WriteString("(") - sb.WriteString(FormatStatement(s.Subquery, nestedOpts(opts))) + sb.WriteString(formatStatement(s.Subquery, nestedOpts(opts))) sb.WriteString(")") return f.result() diff --git a/pkg/formatter/render_case_helpers.go b/pkg/formatter/render_case_helpers.go index 97e247a9..d70aee8f 100644 --- a/pkg/formatter/render_case_helpers.go +++ b/pkg/formatter/render_case_helpers.go @@ -162,7 +162,7 @@ func (c *expressionCase) formatWith(f *nodeFormatter, e ast.Expression) string { func formatExpressionChild(e ast.Expression, opts ast.FormatOptions, context ...*expressionCase) string { if len(context) == 0 { - return FormatExpression(e, opts) + return formatExpression(e, opts) } f := *context[0].f f.opts = opts @@ -222,7 +222,7 @@ func (f *nodeFormatter) exprListSQL(exprs []ast.Expression) string { if f.expressionCase != nil { parts[i] = formatExpressionChild(e, opts, f.expressionCase) } else { - parts[i] = FormatExpression(e, opts) + parts[i] = formatExpression(e, opts) } } return strings.Join(parts, ", ") @@ -281,7 +281,7 @@ func (f *nodeFormatter) tableSubquerySQL(s ast.Statement) string { } opts := nestedOpts(f.opts) opts.NewlinePerClause = false - return FormatStatement(s, opts) + return formatStatement(s, opts) } func lowerDataType(typ string) string { diff --git a/pkg/formatter/render_ddl.go b/pkg/formatter/render_ddl.go index de3cf7cc..f096ce3b 100644 --- a/pkg/formatter/render_ddl.go +++ b/pkg/formatter/render_ddl.go @@ -141,7 +141,7 @@ func renderExplain(s *ast.ExplainStatement, opts ast.FormatOptions) string { } if s.Statement != nil { sb.WriteString(" ") - sb.WriteString(FormatStatement(s.Statement, opts)) + sb.WriteString(formatStatement(s.Statement, opts)) } return sb.String() } diff --git a/pkg/formatter/render_ddl_name_quoting_632_test.go b/pkg/formatter/render_ddl_name_quoting_632_test.go index 4c207d5f..64fc7c38 100644 --- a/pkg/formatter/render_ddl_name_quoting_632_test.go +++ b/pkg/formatter/render_ddl_name_quoting_632_test.go @@ -87,7 +87,7 @@ func ddlNameRenderCases() []aliasRenderCase { } return "" }}, - {"create_table_column_type", `CREATE TABLE t (a ` + hostileQ + `)`, hostile, func(s ast.Statement) string { + {"create_table_column_type", `CREATE TABLE t (a ` + hostileQ + `)`, hostileQ, func(s ast.Statement) string { if c := createTable(s); c != nil && len(c.Columns) > 0 { return c.Columns[0].Type } @@ -221,7 +221,7 @@ func ddlNameRenderCases() []aliasRenderCase { } return "" }}, - {"cast_type", `SELECT a::` + hostileQ + ` FROM t`, hostile, func(s ast.Statement) string { + {"cast_type", `SELECT a::` + hostileQ + ` FROM t`, hostileQ, func(s ast.Statement) string { if c, ok := firstColumnExpr(s).(*ast.CastExpression); ok { return c.Type } diff --git a/pkg/formatter/render_ddl_objects.go b/pkg/formatter/render_ddl_objects.go index 2556c272..691e7241 100644 --- a/pkg/formatter/render_ddl_objects.go +++ b/pkg/formatter/render_ddl_objects.go @@ -33,8 +33,8 @@ func (f *nodeFormatter) ddlStyle() ast.DDLStyle { opts := f.opts return ast.DDLStyle{ Keyword: f.kw, - Expr: func(e ast.Expression) string { return FormatExpression(e, opts) }, - Stmt: func(s ast.Statement) string { return FormatStatement(s, nestedOpts(opts)) }, + Expr: func(e ast.Expression) string { return formatExpression(e, opts) }, + Stmt: func(s ast.Statement) string { return formatStatement(s, nestedOpts(opts)) }, ClauseSep: f.clauseSep(), NameQuote: f.nq, } diff --git a/pkg/formatter/render_query_ext.go b/pkg/formatter/render_query_ext.go index d3ca3a35..f53a9622 100644 --- a/pkg/formatter/render_query_ext.go +++ b/pkg/formatter/render_query_ext.go @@ -32,7 +32,7 @@ func cteSQL(cte *ast.CommonTableExpr, f *nodeFormatter) string { var sb strings.Builder // ClickHouse scalar CTE: WITH AS if cte.ScalarExpr != nil { - sb.WriteString(FormatExpression(cte.ScalarExpr, f.opts)) + sb.WriteString(formatExpression(cte.ScalarExpr, f.opts)) sb.WriteString(" ") sb.WriteString(f.kw("AS")) sb.WriteString(" ") @@ -57,7 +57,7 @@ func cteSQL(cte *ast.CommonTableExpr, f *nodeFormatter) string { sb.WriteString(" ") } sb.WriteString("(") - sb.WriteString(FormatStatement(cte.Statement, nestedOpts(f.opts))) + sb.WriteString(formatStatement(cte.Statement, nestedOpts(f.opts))) sb.WriteString(")") sb.WriteString(ast.CTESearchCycleSQL(cte, f.kw)) return sb.String() diff --git a/pkg/formatter/render_select_ext.go b/pkg/formatter/render_select_ext.go index 2b8e5da8..8beb28d2 100644 --- a/pkg/formatter/render_select_ext.go +++ b/pkg/formatter/render_select_ext.go @@ -48,10 +48,10 @@ func writeSelectPrologue(sb *strings.Builder, s *ast.SelectStatement, f *nodeFor sb.WriteString(" ") if ast.TopNeedsParens(s.Top) { sb.WriteString("(") - sb.WriteString(FormatExpression(s.Top.Count, f.opts)) + sb.WriteString(formatExpression(s.Top.Count, f.opts)) sb.WriteString(")") } else { - sb.WriteString(FormatExpression(s.Top.Count, f.opts)) + sb.WriteString(formatExpression(s.Top.Count, f.opts)) } if s.Top.IsPercent { sb.WriteString(" ") @@ -180,13 +180,13 @@ func writeHierarchical(sb *strings.Builder, s *ast.SelectStatement, f *nodeForma sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("QUALIFY")) sb.WriteString(" ") - sb.WriteString(FormatExpression(s.Qualify, f.opts)) + sb.WriteString(formatExpression(s.Qualify, f.opts)) } if s.StartWith != nil { sb.WriteString(f.clauseSep()) sb.WriteString(f.kw("START WITH")) sb.WriteString(" ") - sb.WriteString(FormatExpression(s.StartWith, f.opts)) + sb.WriteString(formatExpression(s.StartWith, f.opts)) } if s.ConnectBy != nil { sb.WriteString(f.clauseSep()) @@ -196,7 +196,7 @@ func writeHierarchical(sb *strings.Builder, s *ast.SelectStatement, f *nodeForma sb.WriteString(f.kw("NOCYCLE")) sb.WriteString(" ") } - sb.WriteString(FormatExpression(s.ConnectBy.Condition, f.opts)) + sb.WriteString(formatExpression(s.ConnectBy.Condition, f.opts)) } } diff --git a/pkg/gosqlx/format_errors_test.go b/pkg/gosqlx/format_errors_test.go new file mode 100644 index 00000000..7f02c478 --- /dev/null +++ b/pkg/gosqlx/format_errors_test.go @@ -0,0 +1,28 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package gosqlx + +import ( + "errors" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestFormatRejectsNumericTrailingJunk(t *testing.T) { + for _, sql := range []string{"SELECT 1x", "SELECT 1x -- keep comment"} { + t.Run(sql, func(t *testing.T) { + for _, dialect := range []keywords.SQLDialect{"", keywords.DialectPostgreSQL} { + out, err := FormatWithDialect(sql, DefaultFormatOptions(), dialect) + if !errors.Is(err, ErrTokenize) || out != "" { + t.Fatalf("FormatWithDialect(%q, %q) = %q, %v; want tokenize error", sql, dialect, out, err) + } + } + out, err := Format(sql, DefaultFormatOptions()) + if !errors.Is(err, ErrTokenize) || out != "" { + t.Fatalf("Format = %q, %v; want tokenize error", out, err) + } + }) + } +} diff --git a/pkg/gosqlx/format_realworld_bench_test.go b/pkg/gosqlx/format_realworld_bench_test.go new file mode 100644 index 00000000..ea5b07dc --- /dev/null +++ b/pkg/gosqlx/format_realworld_bench_test.go @@ -0,0 +1,134 @@ +// 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 gosqlx + +import ( + "crypto/sha256" + "errors" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/formatter" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +type formatRealworldStatement struct { + dialect keywords.SQLDialect + sql string + commentRefusal bool +} + +// BenchmarkFormatWithDialectRealworld measures one statement per operation, +// cycling through the parseable real-world corpus in sorted path order. Corpus +// discovery and preflight are outside the timer. Statements whose executable +// comments cannot be preserved remain in the workload with their expected +// refusal checked. Compare corpus hashes as well as counts across revisions. +func BenchmarkFormatWithDialectRealworld(b *testing.B) { + statements := loadFormatRealworldStatements(b) + options := DefaultFormatOptions() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + statement := statements[i%len(statements)] + formatted, err := FormatWithDialect(statement.sql, options, statement.dialect) + if statement.commentRefusal { + if !errors.Is(err, formatter.ErrUnplaceableComment) || formatted != "" { + b.Fatalf("format %s statement %d: expected empty output and comment refusal, got %q, %v", statement.dialect, i%len(statements), formatted, err) + } + continue + } + if err != nil { + b.Fatalf("format %s statement %d: %v", statement.dialect, i%len(statements), err) + } + if formatted == "" { + b.Fatalf("format %s statement %d returned empty SQL", statement.dialect, i%len(statements)) + } + } +} + +func loadFormatRealworldStatements(b *testing.B) []formatRealworldStatement { + b.Helper() + const root = "../../testdata/realworld" + dirs, err := os.ReadDir(root) // ReadDir and Glob both return sorted paths. + if err != nil { + b.Fatalf("read real-world corpus: %v", err) + } + var statements []formatRealworldStatement + counts := make(map[keywords.SQLDialect]int) + skipped := 0 + refusals := 0 + options := DefaultFormatOptions() + digest := sha256.New() + for _, dir := range dirs { + if !dir.IsDir() { + continue + } + dialect := keywords.SQLDialect(dir.Name()) + if !keywords.IsValidDialect(string(dialect)) { + b.Fatalf("unknown corpus dialect %q", dialect) + } + files, err := filepath.Glob(filepath.Join(root, dir.Name(), "*.sql")) + if err != nil { + b.Fatalf("find %s corpus files: %v", dialect, err) + } + for _, file := range files { + data, err := os.ReadFile(file) + if err != nil { + b.Fatalf("read %s: %v", file, err) + } + for _, sql := range SplitStatements(string(data), string(dialect)) { + if onlyComments(sql) { + continue + } + if !formatRealworldParseable(sql, dialect) { + skipped++ + continue + } + formatted, err := FormatWithDialect(sql, options, dialect) + commentRefusal := errors.Is(err, formatter.ErrUnplaceableComment) + if commentRefusal { + if formatted != "" { + b.Fatalf("format preflight %s returned SQL alongside comment refusal: %q", file, formatted) + } + refusals++ + } else if err != nil || formatted == "" { + b.Fatalf("format preflight %s: output %q, error %v", file, formatted, err) + } + statements = append(statements, formatRealworldStatement{dialect: dialect, sql: sql, commentRefusal: commentRefusal}) + counts[dialect]++ + if _, err := fmt.Fprintf(digest, "%s\x00%d\x00%s\x00", dialect, len(sql), sql); err != nil { + b.Fatalf("hash corpus statement: %v", err) + } + } + } + } + if len(statements) == 0 { + b.Fatal("no parseable real-world statements") + } + b.Logf("corpus: %d statements, %d unparseable skipped, %d expected comment refusals, sha256=%x; dialect counts=%v", len(statements), skipped, refusals, digest.Sum(nil), counts) + return statements +} + +func formatRealworldParseable(sql string, dialect keywords.SQLDialect) bool { + tree, err := ParseWithDialect(sql, dialect) + if err != nil { + return false + } + defer ast.ReleaseAST(tree) + return true +} diff --git a/pkg/gosqlx/gosqlx.go b/pkg/gosqlx/gosqlx.go index 9b4f114a..68f93434 100644 --- a/pkg/gosqlx/gosqlx.go +++ b/pkg/gosqlx/gosqlx.go @@ -677,7 +677,11 @@ func formatSQL(sql string, options FormatOptions, dialect keywords.SQLDialect) ( } if len(comments) == 0 { - return formatter.FormatAST(parsedAST, astOpts), nil + out, err := formatter.FormatASTChecked(parsedAST, astOpts) + if err != nil { + return "", fmt.Errorf("cannot format SQL: %w", err) + } + return out, nil } // Comments go back around the statements they belonged to. A comment that @@ -686,7 +690,10 @@ func formatSQL(sql string, options FormatOptions, dialect keywords.SQLDialect) ( // dropped or moved: either would change what the statement does. stmts := make([]string, len(parsedAST.Statements)) for i, stmt := range parsedAST.Statements { - stmts[i] = formatter.FormatStatement(stmt, astOpts) + stmts[i], err = formatter.FormatStatementChecked(stmt, astOpts) + if err != nil { + return "", fmt.Errorf("cannot format SQL: %w", err) + } } layout := formatter.CommentLayout{Terminator: ";", Gap: "\n", Dialect: string(dialect), Batches: formatter.BatchPositions(parsedAST), Starts: formatter.StatementPositions(parsedAST)} if options.AddSemicolon { diff --git a/pkg/gosqlx/mysql_digit_names_test.go b/pkg/gosqlx/mysql_digit_names_test.go new file mode 100644 index 00000000..2e77dc54 --- /dev/null +++ b/pkg/gosqlx/mysql_digit_names_test.go @@ -0,0 +1,47 @@ +// 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 gosqlx + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +// TestMySQLDigitLeadingNamesUseBackticks checks that bare digit-leading +// MySQL names are delimited with backticks wherever they appear: "1col" would +// be a string literal in MySQL's default mode. +func TestMySQLDigitLeadingNamesUseBackticks(t *testing.T) { + for _, tc := range []struct{ sql, want string }{ + {"SELECT 1col FROM t", "SELECT `1col` FROM t"}, + {"SELECT t.1col FROM t", "SELECT t.`1col` FROM t"}, + {"SELECT db.t.1col FROM db.t", "SELECT db.t.`1col` FROM db.t"}, + {"SELECT a FROM 1tbl", "SELECT a FROM `1tbl`"}, + {"SELECT a FROM db.1tbl t", "SELECT a FROM db.`1tbl` t"}, + {"UPDATE t SET t.1col = 1", "UPDATE t SET t.`1col` = 1"}, + } { + t.Run(tc.sql, func(t *testing.T) { + tree, err := ParseWithDialect(tc.sql, keywords.DialectMySQL) + if err != nil { + t.Fatalf("parse: %v", err) + } + defer ast.ReleaseAST(tree) + if got := tree.SQL(); got != tc.want { + t.Errorf("SQL() = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/pkg/gosqlx/tree.go b/pkg/gosqlx/tree.go index 0ac2dc71..d296822d 100644 --- a/pkg/gosqlx/tree.go +++ b/pkg/gosqlx/tree.go @@ -139,12 +139,16 @@ func (t *Tree) Functions() []string { // faster and guaranteed to match the parsed structure. // // Pass zero or more FormatOption values to customize indent width and keyword -// casing; see WithIndent and WithUppercaseKeywords. +// casing; see WithIndent and WithUppercaseKeywords. It returns an empty string +// on rendering errors; use FormatChecked to handle errors. func (t *Tree) Format(opts ...FormatOption) string { - if t == nil || t.ast == nil { - return "" - } - return FormatAST(t.ast, opts...) + out, _ := t.FormatChecked(opts...) + return out +} + +// FormatChecked is Format with semantic rendering errors returned to the caller. +func (t *Tree) FormatChecked(opts ...FormatOption) (string, error) { + return FormatTreeChecked(t, opts...) } // Release returns any pooled resources associated with the tree. It is @@ -471,22 +475,36 @@ func applyFormatOptions(cfg formatOptions, opts []FormatOption) formatOptions { // FormatTree renders a Tree back to SQL text using the AST-based formatter. // It does not re-tokenize or re-parse; if you already have a Tree, prefer -// this over the top-level Format(sql, opts) function. +// this over the top-level Format(sql, opts) function. It returns an empty string +// on rendering errors; use FormatTreeChecked to handle errors. func FormatTree(t *Tree, opts ...FormatOption) string { - if t == nil || t.ast == nil { - return "" + out, _ := FormatTreeChecked(t, opts...) + return out +} + +// FormatTreeChecked is FormatTree with semantic rendering errors returned to the caller. +func FormatTreeChecked(t *Tree, opts ...FormatOption) (string, error) { + if t == nil { + return "", nil } - return FormatAST(t.ast, opts...) + return FormatASTChecked(t.ast, opts...) } // FormatAST renders a raw *ast.AST back to SQL text. This is the escape-hatch // equivalent of FormatTree for callers that hold the underlying AST directly // (e.g., from the low-level parser API). Internally it delegates to -// pkg/formatter.FormatAST — it does not re-parse. +// pkg/formatter.FormatAST — it does not re-parse. It returns an empty string +// on rendering errors; use FormatASTChecked to handle errors. func FormatAST(a *ast.AST, opts ...FormatOption) string { + out, _ := FormatASTChecked(a, opts...) + return out +} + +// FormatASTChecked is FormatAST with semantic rendering errors returned to the caller. +func FormatASTChecked(a *ast.AST, opts ...FormatOption) (string, error) { if a == nil { - return "" + return "", nil } astOpts := buildASTFormatOptions(opts) - return formatter.FormatAST(a, astOpts) + return formatter.FormatASTChecked(a, astOpts) } diff --git a/pkg/lsp/format_errors_test.go b/pkg/lsp/format_errors_test.go new file mode 100644 index 00000000..2d4576ea --- /dev/null +++ b/pkg/lsp/format_errors_test.go @@ -0,0 +1,35 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package lsp + +import ( + "errors" + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/formatter" + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestFormattingFallbackPreservesSemanticError(t *testing.T) { + // The wire API currently uses one dialect for both parsing and rendering. + // Use a real cross-dialect error to exercise its semantic error branch. + tree, err := gosqlx.ParseWithDialect("SELECT a ^ b", keywords.DialectMySQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + opts := ast.CompactStyle() + opts.Dialect = "sqlite" + _, renderErr := formatter.FormatASTChecked(tree, opts) + if renderErr == nil { + t.Fatal("expected unsupported XOR error") + } + out, err := formattingFallback("SELECT a ^ b", FormattingOptions{InsertSpaces: true, TabSize: 2}, renderErr) + if !errors.Is(err, renderErr) || !strings.Contains(err.Error(), "formatting failed") || out != "" { + t.Fatalf("got %q, %v; want wrapped render error", out, err) + } +} diff --git a/pkg/lsp/handler.go b/pkg/lsp/handler.go index bf970f8e..d14e0770 100644 --- a/pkg/lsp/handler.go +++ b/pkg/lsp/handler.go @@ -16,6 +16,7 @@ package lsp import ( "encoding/json" + stderrors "errors" "fmt" "regexp" "strconv" @@ -803,8 +804,10 @@ func (h *Handler) handleFormatting(params json.RawMessage) ([]TextEdit, error) { } formatted, err := formatForDialect(content, opts, h.currentDialect()) if err != nil { - // If parsing fails, fall back to basic formatting - formatted = formatSQL(content, p.Options) + formatted, err = formattingFallback(content, p.Options, err) + if err != nil { + return nil, err + } } if formatted == content { return []TextEdit{}, nil @@ -825,6 +828,15 @@ func (h *Handler) handleFormatting(params json.RawMessage) ([]TextEdit, error) { }, nil } +// formattingFallback tolerates incomplete SQL while preserving semantic +// rendering errors, which must reach the client rather than yield a misleading edit. +func formattingFallback(content string, opts FormattingOptions, err error) (string, error) { + if !stderrors.Is(err, gosqlx.ErrSyntax) && !stderrors.Is(err, gosqlx.ErrTokenize) { + return "", fmt.Errorf("formatting failed: %w", err) + } + return formatSQL(content, opts), nil +} + // formatSQL provides basic SQL formatting func formatSQL(sql string, opts FormattingOptions) string { // Basic SQL formatter - normalize whitespace and keyword casing diff --git a/pkg/mcp/format_errors_test.go b/pkg/mcp/format_errors_test.go new file mode 100644 index 00000000..2f9af0e2 --- /dev/null +++ b/pkg/mcp/format_errors_test.go @@ -0,0 +1,14 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package mcp + +import ( + "context" + "testing" +) + +func TestFormatToolReportsNumericTrailingJunk(t *testing.T) { + result, err := handleFormatSQL(context.Background(), makeReq(map[string]any{"sql": "SELECT 1x"})) + requireToolError(t, result, err, "format failed") +} diff --git a/pkg/sql/ast/ast_expressions.go b/pkg/sql/ast/ast_expressions.go index aabfdc15..ff373654 100644 --- a/pkg/sql/ast/ast_expressions.go +++ b/pkg/sql/ast/ast_expressions.go @@ -544,7 +544,10 @@ type BinaryExpression struct { // that emits SQL for another dialect can keep its meaning; the // transpiler rewrites it for the target. BitwiseXor bool - CustomOp *CustomBinaryOperator // For PostgreSQL custom operators + // XorAtAdditive records SQL Server's additive precedence for bitwise ^. + // MySQL and MariaDB instead bind bitwise ^ tighter than multiplication. + XorAtAdditive bool + CustomOp *CustomBinaryOperator // For PostgreSQL custom operators // Extras holds the optional pattern ESCAPE clause. Extras *BinaryExtras Pos models.Location // Source position of the operator (1-based line and column) diff --git a/pkg/sql/ast/data_type_quotes.go b/pkg/sql/ast/data_type_quotes.go new file mode 100644 index 00000000..7cd9edf9 --- /dev/null +++ b/pkg/sql/ast/data_type_quotes.go @@ -0,0 +1,202 @@ +// 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 ast + +import "strings" + +// RestyleDataTypes changes explicit delimiters in type names under node. +// Callers formatting into another dialect should apply it to a private tree. +func RestyleDataTypes(node Node, style rune) { + restyleDataTypes(node, func(typ string) string { return RequoteDataType(typ, style) }) +} + +// RestyleDataTypesForDialect retains delimiters accepted by the target dialect. +// ClickHouse accepts both backticks and double quotes; brackets are translated. +func RestyleDataTypesForDialect(node Node, dialect string) { + style := rune('"') + switch strings.ToLower(dialect) { + case "mysql", "mariadb": + style = '`' + case "sqlserver": + style = '[' + case "clickhouse": + restyleDataTypes(node, func(typ string) string { return requoteDataType(typ, style, true) }) + return + } + RestyleDataTypes(node, style) +} + +// DataTypeNeedsRestyleForDialect reports whether restyling an explicit type +// name delimiter would change typ. It skips arrays and string parameters and, +// like RequoteDataType, leaves malformed type spellings alone. It allocates no +// memory and does not modify typ. +func DataTypeNeedsRestyleForDialect(typ, target string) bool { + if !strings.ContainsAny(typ, "\"`[") { + return false + } + style := byte('"') + if strings.EqualFold(target, "mysql") || strings.EqualFold(target, "mariadb") { + style = '`' + } else if strings.EqualFold(target, "sqlserver") { + style = '[' + } + keepANSIAndBacktick := strings.EqualFold(target, "clickhouse") + changed := false + for i := 0; i < len(typ); i++ { + c := typ[i] + if c == '\'' { + for i++; i < len(typ); i++ { + if typ[i] == '\'' { + if i+1 < len(typ) && typ[i+1] == '\'' { + i++ + continue + } + break + } + } + if i >= len(typ) { + return false + } + continue + } + bracketName := c == '[' && (i == 0 || i+1 < len(typ) && typ[i+1] != ']' && (typ[i+1] < '0' || typ[i+1] > '9')) + if c != '"' && c != '`' && !bracketName { + continue + } + end, ok := skipTypeDelimited(typ, i) + if !ok { + return false + } + if c != style && !(keepANSIAndBacktick && c != '[') { + changed = true + } + i = end + } + return changed +} + +func restyleDataTypes(node Node, requote func(string) string) { + Inspect(node, func(n Node) bool { + switch v := n.(type) { + case *CastExpression: + if !v.TypeAsString { + v.Type = requote(v.Type) + } + case *CreateTableStatement: + for i := range v.Columns { + v.Columns[i].Type = requote(v.Columns[i].Type) + } + case *CreateMaterializedViewStatement: + for i := range v.ColumnDefs { + v.ColumnDefs[i].Type = requote(v.ColumnDefs[i].Type) + } + case *AlterStatement: + ops := v.Operations + if len(ops) == 0 && v.Operation != nil { + ops = []AlterOperation{v.Operation} + } + for _, op := range ops { + if col, ok := op.(*AlterTableOperation); ok && col != nil { + col.DataType = requote(col.DataType) + if col.ColumnDef != nil { + col.ColumnDef.Type = requote(col.ColumnDef.Type) + } + for _, def := range col.ColumnDefs { + if def != nil { + def.Type = requote(def.Type) + } + } + } + } + } + return true + }) +} + +// RequoteDataType changes explicit identifier delimiters in a type spelling +// while retaining bare type keywords, parameters, arrays and string arguments. +func RequoteDataType(typ string, style rune) string { + return requoteDataType(typ, style, false) +} + +func requoteDataType(typ string, style rune, keepANSIAndBacktick bool) string { + if !strings.ContainsAny(typ, "\"`[") { + return typ + } + var out strings.Builder + for i := 0; i < len(typ); i++ { + c := typ[i] + if c == '\'' { + start := i + for i++; i < len(typ); i++ { + if typ[i] == '\'' { + if i+1 < len(typ) && typ[i+1] == '\'' { + i++ + continue + } + break + } + } + if i >= len(typ) { + return typ + } + out.WriteString(typ[start : i+1]) + continue + } + bracketName := c == '[' && (i == 0 || i+1 < len(typ) && typ[i+1] != ']' && (typ[i+1] < '0' || typ[i+1] > '9')) + if c != '"' && c != '`' && !bracketName { + out.WriteByte(c) + continue + } + end, ok := skipTypeDelimited(typ, i) + if !ok { + return typ + } + if keepANSIAndBacktick && c != '[' { + out.WriteString(typ[i : end+1]) + i = end + continue + } + close := c + if close == '[' { + close = ']' + } + name := strings.ReplaceAll(typ[i+1:end], string([]byte{close, close}), string(close)) + out.WriteString(QuoteNameWith(name, QuotedName(style))) + i = end + } + return out.String() +} + +// skipTypeDelimited also handles SQL Server brackets, which otherwise mark +// array dimensions in a type spelling. +func skipTypeDelimited(s string, i int) (int, bool) { + if s[i] != '[' { + return skipDelimited(s, i) + } + for j := i + 1; j < len(s); j++ { + if s[j] == '\\' { + return 0, false + } + if s[j] == ']' { + if j+1 < len(s) && s[j+1] == ']' { + j++ + continue + } + return j, true + } + } + return 0, false +} diff --git a/pkg/sql/ast/data_type_quotes_test.go b/pkg/sql/ast/data_type_quotes_test.go new file mode 100644 index 00000000..ecb78311 --- /dev/null +++ b/pkg/sql/ast/data_type_quotes_test.go @@ -0,0 +1,70 @@ +// 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 ast_test + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" +) + +func TestRequoteDataType(t *testing.T) { + for _, tc := range []struct { + typ string + style rune + want string + }{ + {`"char"`, '`', "`char`"}, + {`"MySchema"."a]b"[]`, '[', `[MySchema].[a]]b][]`}, + {`[123]`, '"', `"123"`}, + {`[a]]b]`, '"', `"a]b"`}, + {"`a``b`", '"', "\"a`b\""}, + {`"a""b"`, '[', `[a"b]`}, + {`Tuple("a b" INT, "c" TEXT)`, '`', "Tuple(`a b` INT, `c` TEXT)"}, + {`DateTime64(3, 'a"b')`, '`', `DateTime64(3, 'a"b')`}, + {`INT[3]`, '`', `INT[3]`}, + {`"unfinished`, '`', `"unfinished`}, + } { + t.Run(tc.typ, func(t *testing.T) { + if got := ast.RequoteDataType(tc.typ, tc.style); got != tc.want { + t.Fatalf("got %s, want %s", got, tc.want) + } + }) + } +} + +func TestRestyleDataTypesForDialect(t *testing.T) { + for _, tc := range []struct { + dialect string + typ string + want string + }{ + {"clickhouse", "Tuple(`x y` UInt8, \"z\" String)", "Tuple(`x y` UInt8, \"z\" String)"}, + {"clickhouse", "[MyType][]", `"MyType"[]`}, + {"clickhouse", "Tuple([x y] UInt8, `z` String)", "Tuple(\"x y\" UInt8, `z` String)"}, + {"mysql", `"MyType"`, "`MyType`"}, + {"mariadb", `"MyType"`, "`MyType`"}, + {"sqlserver", `"MyType"`, "[MyType]"}, + {"postgresql", "`MyType`", `"MyType"`}, + } { + t.Run(tc.dialect+tc.typ, func(t *testing.T) { + cast := &ast.CastExpression{Type: tc.typ} + ast.RestyleDataTypesForDialect(cast, tc.dialect) + if cast.Type != tc.want { + t.Fatalf("got %s, want %s", cast.Type, tc.want) + } + }) + } +} diff --git a/pkg/sql/ast/format_compat.go b/pkg/sql/ast/format_compat.go index e840a166..3734d0b7 100644 --- a/pkg/sql/ast/format_compat.go +++ b/pkg/sql/ast/format_compat.go @@ -22,6 +22,10 @@ // If pkg/formatter has not been imported the methods fall back to SQL() output. // All methods are marked deprecated; callers should migrate to // pkg/formatter.FormatStatement(), FormatExpression() or FormatAST() directly. +// +// These legacy string-returning methods cannot report target-dialect rendering +// errors and return an empty string on failure. Error-aware callers must use +// pkg/formatter.FormatStatementChecked, FormatExpressionChecked or FormatASTChecked. package ast // ─── helpers ───────────────────────────────────────────────────────────────── @@ -52,7 +56,9 @@ func callFormatExpr(e Expression, opts FormatOptions) string { // Format returns the formatted SQL for the full AST. // -// Deprecated: use pkg/formatter.FormatAST() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatASTChecked() instead. func (a AST) Format(opts FormatOptions) string { if FormatASTFunc != nil { return FormatASTFunc(&a, opts) @@ -64,98 +70,126 @@ func (a AST) Format(opts FormatOptions) string { // Format returns the formatted SQL for this SELECT statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (s *SelectStatement) Format(opts FormatOptions) string { return callFormatStmt(s, opts) } // Format returns the formatted SQL for this INSERT statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (i *InsertStatement) Format(opts FormatOptions) string { return callFormatStmt(i, opts) } // Format returns the formatted SQL for this UPDATE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (u *UpdateStatement) Format(opts FormatOptions) string { return callFormatStmt(u, opts) } // Format returns the formatted SQL for this DELETE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (d *DeleteStatement) Format(opts FormatOptions) string { return callFormatStmt(d, opts) } // Format returns the formatted SQL for this CREATE TABLE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (c *CreateTableStatement) Format(opts FormatOptions) string { return callFormatStmt(c, opts) } // Format returns the formatted SQL for this ALTER TABLE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (a *AlterTableStatement) Format(opts FormatOptions) string { return callFormatStmt(a, opts) } // Format returns the formatted SQL for this CREATE INDEX statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (c *CreateIndexStatement) Format(opts FormatOptions) string { return callFormatStmt(c, opts) } // Format returns the formatted SQL for this MERGE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (m *MergeStatement) Format(opts FormatOptions) string { return callFormatStmt(m, opts) } // Format returns the formatted SQL for this CREATE VIEW statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (c *CreateViewStatement) Format(opts FormatOptions) string { return callFormatStmt(c, opts) } // Format returns the formatted SQL for this CREATE MATERIALIZED VIEW statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (c *CreateMaterializedViewStatement) Format(opts FormatOptions) string { return callFormatStmt(c, opts) } // Format returns the formatted SQL for this REFRESH MATERIALIZED VIEW statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (r *RefreshMaterializedViewStatement) Format(opts FormatOptions) string { return callFormatStmt(r, opts) } // Format returns the formatted SQL for this DROP statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (d *DropStatement) Format(opts FormatOptions) string { return callFormatStmt(d, opts) } // Format returns the formatted SQL for this TRUNCATE statement. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (t *TruncateStatement) Format(opts FormatOptions) string { return callFormatStmt(t, opts) } // Format returns the formatted SQL for this SET operation. // -// Deprecated: use pkg/formatter.FormatStatement() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatStatementChecked() instead. func (s *SetOperation) Format(opts FormatOptions) string { return callFormatStmt(s, opts) } @@ -164,35 +198,45 @@ func (s *SetOperation) Format(opts FormatOptions) string { // Format returns the formatted SQL for this CASE expression. // -// Deprecated: use pkg/formatter.FormatExpression() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatExpressionChecked() instead. func (c *CaseExpression) Format(opts FormatOptions) string { return callFormatExpr(c, opts) } // Format returns the formatted SQL for this BETWEEN expression. // -// Deprecated: use pkg/formatter.FormatExpression() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatExpressionChecked() instead. func (b *BetweenExpression) Format(opts FormatOptions) string { return callFormatExpr(b, opts) } // Format returns the formatted SQL for this IN expression. // -// Deprecated: use pkg/formatter.FormatExpression() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatExpressionChecked() instead. func (i *InExpression) Format(opts FormatOptions) string { return callFormatExpr(i, opts) } // Format returns the formatted SQL for this EXISTS expression. // -// Deprecated: use pkg/formatter.FormatExpression() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatExpressionChecked() instead. func (e *ExistsExpression) Format(opts FormatOptions) string { return callFormatExpr(e, opts) } // Format returns the formatted SQL for this subquery expression. // -// Deprecated: use pkg/formatter.FormatExpression() instead. +// Rendering errors return an empty string; use the checked API to report them. +// +// Deprecated: use pkg/formatter.FormatExpressionChecked() instead. func (s *SubqueryExpression) Format(opts FormatOptions) string { return callFormatExpr(s, opts) } diff --git a/pkg/sql/ast/pool_expression_release.go b/pkg/sql/ast/pool_expression_release.go index b600e7e2..70903c2a 100644 --- a/pkg/sql/ast/pool_expression_release.go +++ b/pkg/sql/ast/pool_expression_release.go @@ -338,6 +338,10 @@ func putExpressionBounded(expr Expression, depth int, mode releaseMode, maxWork, *e = LiteralValue{} literalValuePool.Put(e) } + case *UnicodeEscapeExpression: + if mode == releaseAll { + *e = UnicodeEscapeExpression{} + } case *FunctionCall: for _, arg := range e.Arguments { diff --git a/pkg/sql/ast/quote_names.go b/pkg/sql/ast/quote_names.go index cacfc737..c8b81bbe 100644 --- a/pkg/sql/ast/quote_names.go +++ b/pkg/sql/ast/quote_names.go @@ -598,10 +598,9 @@ func dataTypeIsPlain(typ string) bool { break } } - case (c == '"' || c == '`') && depth > 0: - // A delimited identifier inside an argument list (a named tuple - // element, an engine argument): closed, no escapes. - j, ok := skipDelimited(typ, i) + case c == '"' || c == '`' || c == '[' && i+1 < len(typ) && typ[i+1] != ']' && (typ[i+1] < '0' || typ[i+1] > '9'): + // A delimited type name or identifier argument: closed, no escapes. + j, ok := skipTypeDelimited(typ, i) if !ok { return false } diff --git a/pkg/sql/ast/render_ddl_name_quoting_632_test.go b/pkg/sql/ast/render_ddl_name_quoting_632_test.go index 6b0c2ffb..181e8b22 100644 --- a/pkg/sql/ast/render_ddl_name_quoting_632_test.go +++ b/pkg/sql/ast/render_ddl_name_quoting_632_test.go @@ -87,7 +87,7 @@ func ddlNameRenderCases() []aliasRenderCase { } return "" }}, - {"create_table_column_type", `CREATE TABLE t (a ` + hostileQ + `)`, hostile, func(s ast.Statement) string { + {"create_table_column_type", `CREATE TABLE t (a ` + hostileQ + `)`, hostileQ, func(s ast.Statement) string { if c := createTable(s); c != nil && len(c.Columns) > 0 { return c.Columns[0].Type } @@ -221,7 +221,7 @@ func ddlNameRenderCases() []aliasRenderCase { } return "" }}, - {"cast_type", `SELECT a::` + hostileQ + ` FROM t`, hostile, func(s ast.Statement) string { + {"cast_type", `SELECT a::` + hostileQ + ` FROM t`, hostileQ, func(s ast.Statement) string { if c, ok := firstColumnExpr(s).(*ast.CastExpression); ok { return c.Type } diff --git a/pkg/sql/ast/retarget_bitwise.go b/pkg/sql/ast/retarget_bitwise.go new file mode 100644 index 00000000..49aec81b --- /dev/null +++ b/pkg/sql/ast/retarget_bitwise.go @@ -0,0 +1,137 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import "fmt" + +// binaryForm says how a binary operator is written in a target dialect. +type binaryForm uint8 + +const ( + formKeep binaryForm = iota // the operator is written unchanged + formFunc // a two-argument function call + formXorOp // PostgreSQL # written as the target's ^ + formConcat // || concatenation written as CONCAT() + formRefuse // no equivalent: RetargetOperators fails +) + +// binaryTargetForm returns how the bitwise, concatenation and PostgreSQL-only +// operator op is written for target (normalized: "" is generic). It covers +// only operators whose spelling is the same in every source dialect that +// parses them, so it needs no source dialect: +// +// - # is PostgreSQL's bitwise XOR (# starts a comment in MySQL): ^ in the +// MySQL family and SQL Server, BITXOR() in Snowflake, bitXor() in +// ClickHouse; Oracle, SQLite and the generic dialect have no XOR. +// - << >> & | become Snowflake/ClickHouse functions, SQL Server +// LEFT_SHIFT()/RIGHT_SHIFT() and Oracle BITAND(); Oracle has no |, << or >>. +// - || concatenation becomes CONCAT() in MySQL and MariaDB, where || is OR. +// - PostgreSQL containment, JSON key, text search, overlap, distance and +// regular-expression operators are refused in the other known dialects. +func binaryTargetForm(op string, pipesOr bool, target string) (binaryForm, string) { + switch op { + case "#": + switch target { + case "postgresql": + return formKeep, "" + case "mysql", "mariadb", "sqlserver": + return formXorOp, "" + case "snowflake": + return formFunc, "BITXOR" + case "clickhouse": + return formFunc, "bitXor" + } + return formRefuse, "" + case "<<", ">>": + left := op == "<<" + switch target { + case "sqlserver": + return formFunc, pick(left, "LEFT_SHIFT", "RIGHT_SHIFT") + case "snowflake": + return formFunc, pick(left, "BITSHIFTLEFT", "BITSHIFTRIGHT") + case "clickhouse": + return formFunc, pick(left, "bitShiftLeft", "bitShiftRight") + case "oracle": + return formRefuse, "" + } + case "&": + switch target { + case "snowflake", "oracle": + return formFunc, "BITAND" + case "clickhouse": + return formFunc, "bitAnd" + } + case "|": + switch target { + case "snowflake": + return formFunc, "BITOR" + case "clickhouse": + return formFunc, "bitOr" + case "oracle": + return formRefuse, "" + } + case "||": + if !pipesOr && (target == "mysql" || target == "mariadb") { + return formConcat, "" + } + case "@>", "<@", "?", "?|", "?&", "#>", "#>>", "#-", "@@", "&&", "<->", "~", "~*", "!~", "!~*": + switch target { + case "mysql", "mariadb", "sqlserver", "oracle", "sqlite", "snowflake", "clickhouse": + return formRefuse, "" + } + } + return formKeep, "" +} + +func pick(first bool, a, b string) string { + if first { + return a + } + return b +} + +// rewriteBitwise applies binaryTargetForm to x. handled is false when x keeps +// its operator. +func (r *operatorRetargeter) rewriteBitwise(x *BinaryExpression, inOperator bool) (repl Expression, handled bool, err error) { + form, name := binaryTargetForm(x.Operator, x.PipesOr, r.to) + switch form { + case formFunc: + return &FunctionCall{Name: name, Arguments: []Expression{x.Left, x.Right}}, true, nil + case formXorOp: + return groupOperator(x, "^", inOperator, r.to == "sqlserver"), true, nil + case formConcat: + call := &FunctionCall{Name: "CONCAT", Arguments: r.concatArgs(x.Right, r.concatArgs(x.Left, nil))} + if r.concats == nil { + r.concats = map[*FunctionCall]struct{}{} + } + r.concats[call] = struct{}{} + return call, true, nil + case formRefuse: + what := "the PostgreSQL operator " + x.Operator + if x.Operator == "#" { + what = "the PostgreSQL bitwise XOR operator #" + } + return nil, true, fmt.Errorf("%s has no equivalent in %s", what, r.target()) + } + return nil, false, nil +} + +// concatArgs appends the operands of e, splicing in the arguments of a +// CONCAT() this retargeter already made for a nested || chain (operands are +// rewritten before their parent), so a || b || c becomes CONCAT(a, b, c). +func (r *operatorRetargeter) concatArgs(e Expression, out []Expression) []Expression { + if p, ok := e.(*ParenthesizedExpression); ok && p != nil { + if call, ok := p.Expr.(*FunctionCall); ok { + if _, made := r.concats[call]; made { + return append(out, call.Arguments...) + } + } + } + if call, ok := e.(*FunctionCall); ok { + if _, made := r.concats[call]; made { + return append(out, call.Arguments...) + } + } + return append(out, e) +} diff --git a/pkg/sql/ast/retarget_operators.go b/pkg/sql/ast/retarget_operators.go new file mode 100644 index 00000000..3339daa1 --- /dev/null +++ b/pkg/sql/ast/retarget_operators.go @@ -0,0 +1,389 @@ +// 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 ast + +import ( + "fmt" + "reflect" + "strings" +) + +// RetargetOperators rewrites stmt in place for a target dialect. An empty +// source uses expression metadata when the original dialect is unknown. +// It rewrites operators and literals whose meaning or existence depends on +// the dialect, or fails when the target has no +// equivalent; none of them is ever emitted unchanged where it would mean +// something else or not parse: +// +// - ^ is exponentiation in PostgreSQL (and the generic dialect) and +// bitwise XOR in MySQL and MariaDB (binding tighter than *) and SQL +// Server (at the + - level). Exponentiation becomes POWER(a, b) outside +// PostgreSQL. XOR keeps ^ within its family, becomes ^ with explicit +// parentheses between the MySQL family and SQL Server, # in PostgreSQL, +// BITXOR() in Snowflake and bitXor() in ClickHouse, and is refused +// elsewhere. +// - The PostgreSQL prefix operators @, |/ and ||/ become ABS(), SQRT() and +// CBRT() (where it exists); the factorial operators are refused. ~ becomes +// BITNOT()/bitNot() in Snowflake/ClickHouse and is refused in Oracle. +// - PostgreSQL # (XOR), the bitwise and shift operators, || concatenation +// and PostgreSQL-only operators follow binaryTargetForm. +// - SQL Server money literals ($5.00, $5) and Snowflake object constants +// ({'a': 1}) are refused in other dialects. +// - PostgreSQL U& escaped literals and identifiers are refused outside +// PostgreSQL because their source spelling cannot be copied safely. +func RetargetOperators(stmt Statement, from, to string) error { + if (from != "" && from == to) || stmt == nil { + return nil + } + r := operatorRetargeter{from: normalDialect(from), to: normalDialect(to), seen: map[uintptr]struct{}{}} + r.walk(reflect.ValueOf(stmt), false) + return r.err +} + +// normalDialect maps the generic dialect to "" and lower-cases the name. +func normalDialect(d string) string { + s := strings.ToLower(d) + if s == "generic" { + return "" + } + return s +} + +type operatorRetargeter struct { + from, to string + seen map[uintptr]struct{} + // concats holds the CONCAT() calls made for || so an enclosing || + // splices their arguments instead of nesting calls. + concats map[*FunctionCall]struct{} + err error +} + +var ( + binaryExprType = reflect.TypeOf(BinaryExpression{}) + unaryExprType = reflect.TypeOf(UnaryExpression{}) +) + +// walk visits every exported field reachable from v. inOperator reports that +// v is an operand of a binary or unary operator expression. +func (r *operatorRetargeter) walk(v reflect.Value, inOperator bool) { + if r.err != nil { + return + } + switch v.Kind() { + case reflect.Pointer: + if v.IsNil() { + return + } + ptr := v.Pointer() + if _, ok := r.seen[ptr]; ok { + return + } + r.seen[ptr] = struct{}{} + r.walk(v.Elem(), inOperator) + case reflect.Interface: + if !v.IsNil() { + r.walk(v.Elem(), inOperator) + } + case reflect.Struct: + t := v.Type() + operands := t == binaryExprType || t == unaryExprType + for i := 0; i < v.NumField(); i++ { + if t.Field(i).IsExported() { + r.slot(v.Field(i), operands) + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + r.slot(v.Index(i), false) + } + case reflect.Map: + iter := v.MapRange() + for iter.Next() { + value := reflect.New(v.Type().Elem()).Elem() + value.Set(iter.Value()) + r.slot(value, false) + if r.err != nil { + return + } + v.SetMapIndex(iter.Key(), value) + } + } +} + +// slot rewrites the expression held by a settable interface slot after +// rewriting its subtree. +func (r *operatorRetargeter) slot(slot reflect.Value, inOperator bool) { + r.walk(slot, inOperator) + if r.err != nil || slot.Kind() != reflect.Interface || slot.IsNil() || !slot.CanSet() { + return + } + e, ok := slot.Interface().(Expression) + if !ok { + return + } + repl, err := r.rewrite(e, inOperator) + if err != nil { + r.err = err + return + } + if repl != nil && reflect.TypeOf(repl).AssignableTo(slot.Type()) { + slot.Set(reflect.ValueOf(repl)) + } +} + +// rewrite returns the replacement for e, or nil to keep it. +func (r *operatorRetargeter) rewrite(e Expression, inOperator bool) (Expression, error) { + switch x := e.(type) { + case *BinaryExpression: + if x == nil || x.CustomOp != nil || x.Not { + return nil, nil + } + if x.Operator == "^" { + if x.BitwiseXor { + return r.rewriteXor(x, inOperator) + } + return r.rewritePower(x) + } + if repl, handled, err := r.rewriteBitwise(x, inOperator); handled { + return repl, err + } + if retargetExpressionPrecedence(x) >= 6 { + return groupTargetOperands(x, r.to), nil + } + case *UnaryExpression: + if x == nil { + return nil, nil + } + return r.rewritePGPrefix(x) + case *LiteralValue: + if x != nil && r.from == "sqlserver" && r.to != r.from && isMoneyLiteral(x) { + return nil, fmt.Errorf("the SQL Server money literal %v has no equivalent in %s", x.Value, r.target()) + } + case *UnicodeEscapeExpression: + if x != nil && r.to != "postgresql" && (r.from == "" || r.from != r.to) { + return nil, fmt.Errorf("the PostgreSQL Unicode escape syntax U& has no equivalent in %s", r.target()) + } + case *ObjectLiteral: + if x != nil && r.to != "snowflake" { + return nil, fmt.Errorf("the Snowflake object constant {...} has no equivalent in %s", r.target()) + } + } + return nil, nil +} + +func (r *operatorRetargeter) target() string { + if r.to == "" { + return "the generic dialect" + } + return r.to +} + +// rewritePower handles ^ as exponentiation (PostgreSQL, generic). +func (r *operatorRetargeter) rewritePower(x *BinaryExpression) (Expression, error) { + if r.to == "postgresql" { + return groupTargetOperands(x, r.to), nil + } + if r.to == "" { + return groupTargetOperands(x, r.to), nil + } + return &FunctionCall{Name: "POWER", Arguments: []Expression{x.Left, x.Right}}, nil +} + +// groupTargetOperands preserves a source tree when the target gives its +// operators different precedence. Source positions remain on the copied node. +func groupTargetOperands(x *BinaryExpression, target string) Expression { + out := *x + changed := false + if targetOperandNeedsParens(x, x.Left, false, target) { + out.Left = &ParenthesizedExpression{Expr: x.Left, Pos: x.Pos} + changed = true + } + if targetOperandNeedsParens(x, x.Right, true, target) { + out.Right = &ParenthesizedExpression{Expr: x.Right, Pos: x.Pos} + changed = true + } + if changed { + return &out + } + return nil +} + +func targetOperandNeedsParens(parent *BinaryExpression, child Expression, right bool, target string) bool { + if child == nil { + return false + } + if _, ok := child.(*ParenthesizedExpression); ok { + return false + } + p, c := targetExpressionPrecedence(parent, target), targetExpressionPrecedence(child, target) + if p != c { + return c < p + } + return right +} + +func targetExpressionPrecedence(e Expression, target string) int { + binary, ok := e.(*BinaryExpression) + if !ok || binary == nil || binary.CustomOp != nil { + return retargetExpressionPrecedence(e) * 10 + } + switch binary.Operator { + case "->", "->>", "#>", "#>>", "@>", "<@", "#-", "?", "?|", "?&": + switch target { + case "", "mysql", "mariadb": + return 110 + case "sqlite": + return 90 + } + case "||": + if target == "sqlite" && !binary.PipesOr { + return 90 + } + if target == "" || target == "mysql" || target == "mariadb" { + if binary.PipesOr { + return 10 + } + return 65 + } + case "^", "&", "|": + if target == "sqlserver" { + return 70 + } + if target == "" || target == "mysql" || target == "mariadb" { + switch binary.Operator { + case "|": + return 60 + case "&": + return 61 + } + } + case "<<", ">>": + if target == "" || target == "mysql" || target == "mariadb" { + return 62 + } + } + return retargetExpressionPrecedence(e) * 10 +} + +// rewriteXor handles ^ as bitwise XOR (MySQL, MariaDB, SQL Server). +func (r *operatorRetargeter) rewriteXor(x *BinaryExpression, inOperator bool) (Expression, error) { + from := r.from + if from == "" { + from = "mysql" + if x.XorAtAdditive { + from = "sqlserver" + } + } + if xorFamily(from) == xorFamily(r.to) { + return groupTargetOperands(x, r.to), nil + } + switch r.to { + case "mysql", "mariadb", "sqlserver": + return groupOperator(x, "^", inOperator, r.to == "sqlserver"), nil + case "postgresql": + return groupOperator(x, "#", inOperator, false), nil + case "snowflake": + return &FunctionCall{Name: "BITXOR", Arguments: []Expression{x.Left, x.Right}}, nil + case "clickhouse": + return &FunctionCall{Name: "bitXor", Arguments: []Expression{x.Left, x.Right}}, nil + } + return nil, fmt.Errorf("the bitwise XOR operator ^ has no equivalent in %s", r.target()) +} + +// xorFamily groups the dialects whose ^ is XOR at the same precedence. +func xorFamily(d string) string { + switch d { + case "mysql", "mariadb": + return "mysql" + } + return d +} + +// groupOperator returns x written with op and explicit parentheses around +// binary operands and, inside another operator, around itself, so the +// grouping of the source survives the target's precedence rules. +func groupOperator(x *BinaryExpression, op string, inOperator, xorAtAdditive bool) Expression { + out := *x + out.Operator = op + out.BitwiseXor = op == "^" + out.XorAtAdditive = out.BitwiseXor && xorAtAdditive + out.Left = parenthesizeBinary(out.Left) + out.Right = parenthesizeBinary(out.Right) + if inOperator { + return &ParenthesizedExpression{Expr: &out, Pos: x.Pos} + } + return &out +} + +func parenthesizeBinary(e Expression) Expression { + if b, ok := e.(*BinaryExpression); ok && b != nil { + return &ParenthesizedExpression{Expr: b, Pos: b.Pos} + } + return e +} + +// rewritePGPrefix handles the PostgreSQL-only prefix and postfix operators. +func (r *operatorRetargeter) rewritePGPrefix(x *UnaryExpression) (Expression, error) { + if r.to == "postgresql" { + return nil, nil + } + call := func(name string) Expression { + return &FunctionCall{Name: name, Arguments: []Expression{x.Expr}} + } + switch x.Operator { + case PGAbs: + return call("ABS"), nil + case PGSquareRoot: + return call("SQRT"), nil + case PGCubeRoot: + switch r.to { + case "snowflake", "clickhouse": + return call("CBRT"), nil + } + return nil, fmt.Errorf("the PostgreSQL cube root operator ||/ has no equivalent in %s", r.target()) + case PGBitwiseNot: + switch r.to { + case "snowflake": + return call("BITNOT"), nil + case "clickhouse": + return call("bitNot"), nil + case "oracle": + return nil, fmt.Errorf("the bitwise NOT operator ~ has no equivalent in %s", r.target()) + } + case PGPrefixFactorial: + return nil, fmt.Errorf("the PostgreSQL factorial operator !! has no equivalent in %s", r.target()) + case PGPostfixFactorial: + return nil, fmt.Errorf("the PostgreSQL factorial operator ! has no equivalent in %s", r.target()) + } + return nil, nil +} + +// isMoneyLiteral reports whether lit is a SQL Server money literal: $5.00, +// or $5, which the parser keeps as a placeholder. +func isMoneyLiteral(lit *LiteralValue) bool { + if lit.Type == "money" { + return true + } + v, ok := lit.Value.(string) + if !ok || lit.Type != "placeholder" || len(v) < 2 || v[0] != '$' { + return false + } + for i := 1; i < len(v); i++ { + if v[i] < '0' || v[i] > '9' { + return false + } + } + return true +} diff --git a/pkg/sql/ast/retarget_operators_test.go b/pkg/sql/ast/retarget_operators_test.go new file mode 100644 index 00000000..a73d85a9 --- /dev/null +++ b/pkg/sql/ast/retarget_operators_test.go @@ -0,0 +1,298 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/models" +) + +func TestRetargetOperatorsUnknownSource(t *testing.T) { + for _, tc := range []struct { + name, target, operator, function string + xor, additive, wantAdditive, wantError bool + }{ + {name: "power to mysql", target: "mysql", function: "POWER"}, + {name: "power to postgresql", target: "postgresql", operator: "^"}, + {name: "mysql xor to mariadb", target: "mariadb", xor: true, operator: "^"}, + {name: "sqlserver xor to mysql", target: "mysql", xor: true, additive: true, operator: "^"}, + {name: "mysql xor to sqlserver", target: "sqlserver", xor: true, operator: "^", wantAdditive: true}, + {name: "sqlserver xor to postgresql", target: "postgresql", xor: true, additive: true, operator: "#"}, + {name: "xor to generic", target: "generic", xor: true, wantError: true}, + {name: "xor to empty target", xor: true, wantError: true}, + } { + t.Run(tc.name, func(t *testing.T) { + original := &BinaryExpression{Left: &Identifier{Name: "a"}, Operator: "^", Right: &Identifier{Name: "b"}, BitwiseXor: tc.xor, XorAtAdditive: tc.additive} + stmt := &SelectStatement{Columns: []Expression{original}} + defer PutSelectStatement(stmt) + err := RetargetOperators(stmt, "", tc.target) + if tc.wantError { + if err == nil || !strings.Contains(err.Error(), "XOR") { + t.Fatalf("expected XOR error, got %v", err) + } + return + } + if err != nil { + t.Fatal(err) + } + if tc.function != "" { + fn, ok := stmt.Columns[0].(*FunctionCall) + if !ok || fn.Name != tc.function { + t.Fatalf("expected %s function, got %#v", tc.function, stmt.Columns[0]) + } + return + } + expr, ok := stmt.Columns[0].(*BinaryExpression) + if !ok || expr.Operator != tc.operator { + t.Fatalf("expected %s operator, got %#v", tc.operator, stmt.Columns[0]) + } + if expr.XorAtAdditive != tc.wantAdditive { + t.Errorf("XorAtAdditive = %v, want %v", expr.XorAtAdditive, tc.wantAdditive) + } + if tc.target == "mariadb" && expr != original { + t.Error("same-family XOR was unnecessarily rewritten") + } + }) + } +} + +func TestRetargetOperatorsUnknownPGPrefix(t *testing.T) { + stmt := &SelectStatement{Columns: []Expression{&UnaryExpression{Operator: PGAbs, Expr: &Identifier{Name: "a"}}}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "", "mysql"); err != nil { + t.Fatal(err) + } + fn, ok := stmt.Columns[0].(*FunctionCall) + if !ok || fn.Name != "ABS" { + t.Fatalf("expected ABS function, got %#v", stmt.Columns[0]) + } +} + +func TestRetargetOperatorsUnicodeEscape(t *testing.T) { + for _, tc := range []struct { + from, to string + wantError bool + }{ + {"postgresql", "postgresql", false}, {"", "postgresql", false}, + {"postgresql", "mysql", true}, {"", "generic", true}, + } { + t.Run(tc.from+"_to_"+tc.to, func(t *testing.T) { + stmt := &SelectStatement{Columns: []Expression{&UnicodeEscapeExpression{RawSQL: `U&'d\0061t'`}}} + defer PutSelectStatement(stmt) + err := RetargetOperators(stmt, tc.from, tc.to) + if (err != nil) != tc.wantError { + t.Fatalf("error = %v, wantError %v", err, tc.wantError) + } + }) + } +} + +func TestXorAtAdditiveReleaseModes(t *testing.T) { + for _, mode := range []releaseMode{releaseAll, releaseDrawn} { + expr := &BinaryExpression{Operator: "^", BitwiseXor: true, XorAtAdditive: true} + releaseExpression(expr, mode) + want := mode != releaseAll + if expr.XorAtAdditive != want { + t.Errorf("mode %v: XorAtAdditive = %v, want %v", mode, expr.XorAtAdditive, want) + } + } +} + +func TestRetargetPowerPostgreSQLGrouping(t *testing.T) { + for _, tc := range []struct { + name string + leftJSON, rightJSON bool + want string + }{ + {"left JSON", true, false, "SELECT (a -> b) ^ c"}, + {"right JSON", false, true, "SELECT a ^ (b -> c)"}, + {"plain PostgreSQL", false, false, "SELECT a ^ c"}, + } { + t.Run(tc.name, func(t *testing.T) { + expr := &BinaryExpression{Left: &Identifier{Name: "a"}, Operator: "^", Right: &Identifier{Name: "c"}, Pos: models.Location{Line: 1, Column: 15}} + if tc.leftJSON { + expr.Left = &BinaryExpression{Left: &Identifier{Name: "a"}, Operator: "->", Right: &Identifier{Name: "b"}, Pos: models.Location{Line: 1, Column: 10}} + } + if tc.rightJSON { + expr.Right = &BinaryExpression{Left: &Identifier{Name: "b"}, Operator: "->", Right: &Identifier{Name: "c"}, Pos: models.Location{Line: 1, Column: 15}} + } + stmt := &SelectStatement{Columns: []Expression{expr}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "", "postgresql"); err != nil { + t.Fatal(err) + } + if got := stmt.SQL(); got != tc.want { + t.Fatalf("SQL = %s, want %s", got, tc.want) + } + grouped, ok := stmt.Columns[0].(*BinaryExpression) + if !ok { + t.Fatalf("expected binary, got %T", stmt.Columns[0]) + } + if grouped.Pos != expr.Pos { + t.Errorf("operator position changed: %v vs %v", grouped.Pos, expr.Pos) + } + if !tc.leftJSON && !tc.rightJSON && grouped != expr { + t.Error("plain PostgreSQL power unnecessarily copied") + } + }) + } +} + +func TestRetargetOperatorsMapExpressions(t *testing.T) { + named := map[string]Expression{"OFFSET": &BinaryExpression{Left: &Identifier{Name: "a"}, Operator: "^", Right: &Identifier{Name: "b"}}} + stmt := &SelectStatement{Columns: []Expression{&Identifier{Name: "a"}}, From: []TableReference{{Name: "t", TimeTravel: &TimeTravelClause{Kind: "AT", Named: named}}}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "", "snowflake"); err != nil { + t.Fatal(err) + } + call, ok := named["OFFSET"].(*FunctionCall) + if !ok || call.Name != "POWER" { + t.Fatalf("expected POWER in map value, got %#v", named["OFFSET"]) + } +} + +func TestRetargetPostgreSQLNestedBinaryGrouping(t *testing.T) { + position := models.Location{Line: 1, Column: 12} + ident := func(name string) Expression { return &Identifier{Name: name} } + binary := func(left Expression, op string, right Expression) *BinaryExpression { + return &BinaryExpression{Left: left, Operator: op, Right: right, Pos: position} + } + for _, tc := range []struct { + name string + expr Expression + want string + }{ + {"generic nested power", binary(&ParenthesizedExpression{Expr: binary(binary(ident("a"), "->", ident("b")), "+", ident("c"))}, "^", ident("d")), "SELECT ((a -> b) + c) ^ d"}, + {"generic function argument", &FunctionCall{Name: "F", Arguments: []Expression{binary(binary(ident("a"), "->", ident("b")), "+", ident("c"))}}, "SELECT F((a -> b) + c)"}, + {"PostgreSQL precedence unchanged", binary(ident("a"), "->", binary(ident("b"), "+", binary(ident("c"), "^", ident("d")))), "SELECT a -> b + c ^ d"}, + } { + t.Run(tc.name, func(t *testing.T) { + stmt := &SelectStatement{Columns: []Expression{tc.expr}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "", "postgresql"); err != nil { + t.Fatal(err) + } + if got := stmt.SQL(); got != tc.want { + t.Fatalf("SQL = %s, want %s", got, tc.want) + } + }) + } +} + +func TestRetargetPowerMySQLJSONGrouping(t *testing.T) { + pos := models.Location{Line: 1, Column: 12} + power := &BinaryExpression{Left: &Identifier{Name: "c"}, Operator: "^", Right: &Identifier{Name: "d"}, Pos: pos} + sum := &BinaryExpression{Left: &Identifier{Name: "b"}, Operator: "+", Right: power, Pos: pos} + expr := &BinaryExpression{Left: &Identifier{Name: "a"}, Operator: "->", Right: sum, Pos: pos} + stmt := &SelectStatement{Columns: []Expression{expr}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "", "mysql"); err != nil { + t.Fatal(err) + } + want := "SELECT a -> (b + POWER(c, d))" + if got := stmt.SQL(); got != want { + t.Fatalf("SQL = %s, want %s", got, want) + } +} + +func TestRetargetMySQLBitwisePrecedence(t *testing.T) { + pos := models.Location{Line: 1, Column: 12} + binary := func(left Expression, op string, right Expression) *BinaryExpression { + return &BinaryExpression{Left: left, Operator: op, Right: right, Pos: pos} + } + or := binary(&Identifier{Name: "a"}, "|", &Identifier{Name: "b"}) + and := binary(or, "&", &Identifier{Name: "c"}) + xor := binary(and, "^", &Identifier{Name: "d"}) + xor.BitwiseXor, xor.XorAtAdditive = true, true + stmt := &SelectStatement{Columns: []Expression{xor}} + defer PutSelectStatement(stmt) + if err := RetargetOperators(stmt, "sqlserver", "mysql"); err != nil { + t.Fatal(err) + } + want := "SELECT ((a | b) & c) ^ d" + if got := stmt.SQL(); got != want { + t.Fatalf("SQL = %s, want %s", got, want) + } +} + +func TestRetargetPostgreSQLBitwiseAndOnlyOperators(t *testing.T) { + id := func(n string) Expression { return &Identifier{Name: n} } + bin := func(op string) Expression { return &BinaryExpression{Left: id("a"), Operator: op, Right: id("b")} } + for _, tc := range []struct { + name, target string + expr Expression + want string // rendered column, or "error" + }{ + {"xor mysql", "mysql", bin("#"), "a ^ b"}, + {"xor mariadb", "mariadb", bin("#"), "a ^ b"}, + {"xor sqlserver", "sqlserver", bin("#"), "a ^ b"}, + {"xor snowflake", "snowflake", bin("#"), "BITXOR(a, b)"}, + {"xor clickhouse", "clickhouse", bin("#"), "bitXor(a, b)"}, + {"xor oracle", "oracle", bin("#"), "error"}, + {"xor sqlite", "sqlite", bin("#"), "error"}, + {"xor generic", "generic", bin("#"), "error"}, + {"xor postgresql", "postgresql", bin("#"), "a # b"}, + {"not snowflake", "snowflake", &UnaryExpression{Operator: PGBitwiseNot, Expr: id("a")}, "BITNOT(a)"}, + {"not clickhouse", "clickhouse", &UnaryExpression{Operator: PGBitwiseNot, Expr: id("a")}, "bitNot(a)"}, + {"not oracle", "oracle", &UnaryExpression{Operator: PGBitwiseNot, Expr: id("a")}, "error"}, + {"not mysql", "mysql", &UnaryExpression{Operator: PGBitwiseNot, Expr: id("a")}, "~a"}, + {"shift sqlserver", "sqlserver", bin("<<"), "LEFT_SHIFT(a, b)"}, + {"shift snowflake", "snowflake", bin(">>"), "BITSHIFTRIGHT(a, b)"}, + {"shift clickhouse", "clickhouse", bin("<<"), "bitShiftLeft(a, b)"}, + {"shift oracle", "oracle", bin("<<"), "error"}, + {"shift mysql", "mysql", bin("<<"), "a << b"}, + {"and oracle", "oracle", bin("&"), "BITAND(a, b)"}, + {"or oracle", "oracle", bin("|"), "error"}, + {"or snowflake", "snowflake", bin("|"), "BITOR(a, b)"}, + {"and clickhouse", "clickhouse", bin("&"), "bitAnd(a, b)"}, + {"concat mysql", "mysql", &BinaryExpression{Left: bin("||"), Operator: "||", Right: id("c")}, "CONCAT(a, b, c)"}, + {"pipes or mysql", "mysql", &BinaryExpression{Left: id("a"), Operator: "OR", Right: id("b"), PipesOr: true}, "a || b"}, + {"concat oracle", "oracle", bin("||"), "a || b"}, + {"contains mysql", "mysql", bin("@>"), "error"}, + {"regex snowflake", "snowflake", bin("~"), "error"}, + {"json key sqlite", "sqlite", bin("?"), "error"}, + {"overlap generic", "generic", bin("&&"), "a && b"}, + } { + t.Run(tc.name, func(t *testing.T) { + orig, ok := tc.expr.(interface{ SQL() string }) + if !ok { + t.Fatalf("no SQL for %T", tc.expr) + } + // The cheap pre-check must agree with the rewrite. + if got, want := OperatorNeedsRetarget(tc.expr, tc.target) || childrenNeedRetarget(tc.expr, tc.target), tc.want != orig.SQL(); got != want { + t.Errorf("OperatorNeedsRetarget = %v, want %v", got, want) + } + stmt := &SelectStatement{Columns: []Expression{tc.expr}} + err := RetargetOperators(stmt, "", tc.target) + if tc.want == "error" { + if err == nil { + t.Fatalf("expected error, got %s", stmt.Columns[0].(interface{ SQL() string }).SQL()) + } + return + } + if err != nil { + t.Fatal(err) + } + s, ok := stmt.Columns[0].(interface{ SQL() string }) + if !ok { + t.Fatalf("no SQL for %T", stmt.Columns[0]) + } + if got := s.SQL(); got != tc.want { + t.Fatalf("got %q want %q", got, tc.want) + } + }) + } +} + +// childrenNeedRetarget reports whether any operand of e needs retargeting. +func childrenNeedRetarget(e Expression, target string) bool { + for _, c := range e.Children() { + if ce, ok := c.(Expression); ok && (OperatorNeedsRetarget(ce, target) || childrenNeedRetarget(ce, target)) { + return true + } + } + return false +} diff --git a/pkg/sql/ast/retarget_predicates.go b/pkg/sql/ast/retarget_predicates.go new file mode 100644 index 00000000..f346d06e --- /dev/null +++ b/pkg/sql/ast/retarget_predicates.go @@ -0,0 +1,110 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import "strings" + +// OperatorNeedsRetarget reports whether an expression itself would be changed +// or rejected by RetargetOperators with an unknown source dialect. Callers must +// also inspect its children. It does not allocate or modify the expression. +func OperatorNeedsRetarget(e Expression, target string) bool { + switch e.(type) { + case *BinaryExpression, *UnaryExpression, *UnicodeEscapeExpression, *ObjectLiteral: + default: + return false + } + // Only these target names affect the rules below. EqualFold keeps this + // predicate allocation-free even when the caller uses mixed case. + switch target { + case "", "postgresql", "mysql", "mariadb", "sqlserver", "snowflake", "clickhouse", "sqlite", "oracle", "generic": + default: + for _, name := range [...]string{"postgresql", "mysql", "mariadb", "sqlserver", "snowflake", "clickhouse", "sqlite", "oracle", "generic"} { + if strings.EqualFold(target, name) { + target = name + break + } + } + } + if target == "generic" { + target = "" + } + switch x := e.(type) { + case *BinaryExpression: + if x == nil || x.CustomOp != nil || x.Not { + return false + } + if x.Operator == "^" { + if x.BitwiseXor { + source := "mysql" + if x.XorAtAdditive { + source = "sqlserver" + } + if xorFamily(source) != xorFamily(target) { + return true + } + } else if target != "" && target != "postgresql" { + return true + } + } else if form, _ := binaryTargetForm(x.Operator, x.PipesOr, target); form != formKeep { + return true + } else if retargetExpressionPrecedence(x) < 6 { + return false + } + return targetOperandNeedsParens(x, x.Left, false, target) || targetOperandNeedsParens(x, x.Right, true, target) + case *UnaryExpression: + if x == nil || target == "postgresql" { + return false + } + switch x.Operator { + case PGAbs, PGSquareRoot, PGCubeRoot, PGPrefixFactorial, PGPostfixFactorial: + return true + case PGBitwiseNot: + return target == "snowflake" || target == "clickhouse" || target == "oracle" + } + case *UnicodeEscapeExpression: + return x != nil && target != "postgresql" + case *ObjectLiteral: + return x != nil && target != "snowflake" + } + return false +} + +// retargetExpressionPrecedence avoids case-conversion allocations on manually +// constructed binary nodes whose keyword operators are lower case. +func retargetExpressionPrecedence(e Expression) int { + x, ok := e.(*BinaryExpression) + if !ok || x == nil || x.CustomOp != nil { + return expressionPrecedence(e) + } + switch x.Operator { + case "=", "!=", "<>", "<", ">", "<=", ">=", "==": + return 5 + case "+", "-": + return 7 + case "*", "/", "%": + return 8 + case "^": + if x.BitwiseXor && x.XorAtAdditive { + return 7 + } + return 9 + } + if x.Operator == "" || x.Operator[0] < 'A' || x.Operator[0] > 'z' { + return 6 + } + for _, entry := range [...]struct { + op string + prec int + }{ + {"OR", 1}, {"XOR", 2}, {"AND", 3}, {"NOT", 4}, + {"IS", 5}, {"IS NOT", 5}, {"IS NULL", 5}, {"IS DISTINCT FROM", 5}, {"IS NOT DISTINCT FROM", 5}, + {"LIKE", 5}, {"ILIKE", 5}, {"SIMILAR TO", 5}, {"REGEXP", 5}, {"RLIKE", 5}, {"GLOB", 5}, {"MATCH", 5}, {"SOUNDS LIKE", 5}, + {"DIV", 8}, {"MOD", 8}, + } { + if strings.EqualFold(x.Operator, entry.op) { + return entry.prec + } + } + return 6 +} diff --git a/pkg/sql/ast/retarget_predicates_test.go b/pkg/sql/ast/retarget_predicates_test.go new file mode 100644 index 00000000..772f5983 --- /dev/null +++ b/pkg/sql/ast/retarget_predicates_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import ( + "fmt" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/models" +) + +func TestOperatorNeedsRetarget(t *testing.T) { + ident := func() Expression { return &Identifier{Name: "x"} } + binary := func(op string) *BinaryExpression { + return &BinaryExpression{Operator: op, Left: ident(), Right: ident()} + } + power := binary("^") + xor := binary("^") + xor.BitwiseXor = true + additiveXor := binary("^") + additiveXor.BitwiseXor, additiveXor.XorAtAdditive = true, true + jsonPower := binary("^") + jsonPower.Left = binary("->") + jsonPower.Pos = models.Location{Line: 1, Column: 4} + sum := binary("+") + sum.Right = binary("+") + groupedSum := binary("+") + groupedSum.Right = &ParenthesizedExpression{Expr: binary("+")} + for _, tc := range []struct { + name, target string + expr Expression + want bool + }{ + {"plain cast operand", "mysql", ident(), false}, + {"nil binary", "mysql", (*BinaryExpression)(nil), false}, + {"power PostgreSQL", "postgresql", power, false}, + {"power generic", "generic", power, false}, + {"power mysql", "mysql", power, true}, + {"power PostgreSQL grouping with position", "postgresql", jsonPower, true}, + {"power uppercase target", "POSTGRESQL", power, false}, + {"xor same family", "mariadb", xor, false}, + {"xor different family", "sqlserver", xor, true}, + {"xor additive same family", "sqlserver", additiveXor, false}, + {"xor unknown target", "oracle", xor, true}, + {"xor generic target", "generic", xor, true}, + {"arithmetic unchanged", "mysql", binary("+"), false}, + {"arithmetic grouping", "mysql", sum, true}, + {"existing arithmetic grouping", "mysql", groupedSum, false}, + {"lowercase keyword", "mysql", binary("div"), false}, + {"comparison unchanged", "mysql", binary("is distinct from"), false}, + {"abs same dialect", "postgresql", &UnaryExpression{Operator: PGAbs, Expr: ident()}, false}, + {"abs conversion", "mysql", &UnaryExpression{Operator: PGAbs, Expr: ident()}, true}, + {"factorial rejection", "mysql", &UnaryExpression{Operator: PGPrefixFactorial, Expr: ident()}, true}, + {"ordinary unary", "mysql", &UnaryExpression{Operator: Minus, Expr: ident()}, false}, + {"unicode same dialect", "postgresql", &UnicodeEscapeExpression{RawSQL: `U&'x'`}, false}, + {"unicode rejection", "mysql", &UnicodeEscapeExpression{RawSQL: `U&'x'`}, true}, + {"object same dialect", "snowflake", &ObjectLiteral{}, false}, + {"object rejection", "postgresql", &ObjectLiteral{}, true}, + } { + t.Run(tc.name, func(t *testing.T) { + before := fmt.Sprintf("%#v", tc.expr) + if got := OperatorNeedsRetarget(tc.expr, tc.target); got != tc.want { + t.Fatalf("needs retarget = %v, want %v", got, tc.want) + } + if allocs := testing.AllocsPerRun(100, func() { OperatorNeedsRetarget(tc.expr, tc.target) }); allocs != 0 { + t.Errorf("predicate allocated %g times", allocs) + } + if fmt.Sprintf("%#v", tc.expr) != before { + t.Fatal("predicate changed input") + } + stmt := &SelectStatement{Columns: []Expression{tc.expr}} + err := RetargetOperators(stmt, "", tc.target) + if changed := err != nil || stmt.Columns[0] != tc.expr; changed != tc.want { + t.Errorf("actual retarget changed/rejected = %v, want %v (error %v)", changed, tc.want, err) + } + }) + } +} + +func TestDataTypeNeedsRestyleForDialect(t *testing.T) { + for _, typ := range []string{ + "INT", "VARCHAR(64)", "INT[]", "INT[3]", `"char"`, "`MyType`", "[MyType][]", "[123]", "[a]]b]", `"a""b"`, + "Tuple(`x y` UInt8, \"z\" String)", `DateTime64(3, 'a"b')`, `Enum('a''"b', 'c')`, + `"unfinished`, `"valid"."unfinished`, `"valid"('unfinished)`, + } { + for _, target := range []string{"", "generic", "postgresql", "mysql", "mariadb", "sqlserver", "clickhouse", "MYSQL"} { + t.Run(target+"/"+typ, func(t *testing.T) { + cast := &CastExpression{Type: typ} + defer PutCastExpression(cast) + got := DataTypeNeedsRestyleForDialect(typ, target) + if allocs := testing.AllocsPerRun(100, func() { DataTypeNeedsRestyleForDialect(typ, target) }); allocs != 0 { + t.Errorf("predicate allocated %g times", allocs) + } + if cast.Type != typ { + t.Fatal("predicate changed input") + } + RestyleDataTypesForDialect(cast, target) + if want := cast.Type != typ; got != want { + t.Errorf("needs restyle = %v, actual changed = %v (%q -> %q)", got, want, typ, cast.Type) + } + }) + } + } +} diff --git a/pkg/sql/ast/sql.go b/pkg/sql/ast/sql.go index 300be3fc..9975dc25 100644 --- a/pkg/sql/ast/sql.go +++ b/pkg/sql/ast/sql.go @@ -406,7 +406,7 @@ func (b *BinaryExpression) writeSQL(buf *bytes.Buffer) { // prefix NOT, matching UnaryExpression, instead of the infix NOT form. if b.Right == nil && strings.EqualFold(op, "NOT") { buf.WriteString("NOT ") - writeExpr(buf, b.Left) + writeBinaryOperand(buf, b, b.Left, false) return } @@ -414,7 +414,7 @@ func (b *BinaryExpression) writeSQL(buf *bytes.Buffer) { // The parser stores the operator as "IS NULL" and uses the Not flag to // disambiguate `IS NOT NULL`, so honour it here to avoid dropping NOT. if strings.EqualFold(op, "IS NULL") { - writeExpr(buf, b.Left) + writeBinaryOperand(buf, b, b.Left, false) if b.Not { buf.WriteString(" IS NOT NULL") } else { @@ -426,25 +426,25 @@ func (b *BinaryExpression) writeSQL(buf *bytes.Buffer) { // Handle special operators like LIKE, ILIKE, SIMILAR TO if b.Not { if canon, ok := notQualifiedOperator(op); ok { - writeExpr(buf, b.Left) + writeBinaryOperand(buf, b, b.Left, false) buf.WriteString(" NOT ") buf.WriteString(canon) buf.WriteByte(' ') - writeExpr(buf, b.Right) + writeBinaryOperand(buf, b, b.Right, true) writeEscape(buf, b.DialectExtras().Escape) return } buf.WriteString("NOT (") - writeExpr(buf, b.Left) + writeBinaryOperand(buf, b, b.Left, false) buf.WriteByte(' ') writeOp() buf.WriteByte(' ') - writeExpr(buf, b.Right) + writeBinaryOperand(buf, b, b.Right, true) buf.WriteByte(')') return } - writeExpr(buf, b.Left) + writeBinaryOperand(buf, b, b.Left, false) buf.WriteByte(' ') if b.PipesOr && b.CustomOp == nil && strings.EqualFold(op, "OR") { // Keep the source spelling of a MySQL / MariaDB || OR (#781). @@ -453,7 +453,7 @@ func (b *BinaryExpression) writeSQL(buf *bytes.Buffer) { writeOp() } buf.WriteByte(' ') - writeExpr(buf, b.Right) + writeBinaryOperand(buf, b, b.Right, true) writeEscape(buf, b.DialectExtras().Escape) } @@ -501,17 +501,17 @@ func (u *UnaryExpression) writeSQL(buf *bytes.Buffer) { switch u.Operator { case Not: buf.WriteString("NOT ") - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) case PGPostfixFactorial: - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) buf.WriteByte('!') case Plus: buf.WriteByte('+') - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) case Minus: buf.WriteByte('-') start := buf.Len() - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) // "--" starts a line comment: separate a nested unary minus or a // negative literal from this operator ("- -1", not "--1"). if b := buf.Bytes(); len(b) > start && b[start] == '-' { @@ -522,19 +522,19 @@ func (u *UnaryExpression) writeSQL(buf *bytes.Buffer) { } case Prior: buf.WriteString("PRIOR ") - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) case MySQLBinary: buf.WriteString("BINARY ") - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) case PGAbs: // Always separate "@" from its operand: "@a", "@t.a" and "@abs(a)" // read as a placeholder or variable, and "@-5" as the operator "@-". buf.WriteString("@ ") - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) default: buf.WriteString(u.Operator.String()) start := buf.Len() - writeExpr(buf, u.Expr) + writeUnaryOperand(buf, u) // A symbolic prefix operator followed by an operand that starts with // an operator character would read as one longer operator ("@-5" is // the operator "@-"): keep them apart. diff --git a/pkg/sql/ast/sql_precedence.go b/pkg/sql/ast/sql_precedence.go new file mode 100644 index 00000000..7b5a5015 --- /dev/null +++ b/pkg/sql/ast/sql_precedence.go @@ -0,0 +1,136 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import ( + "bytes" + "strings" +) + +// expressionPrecedence describes the default (PostgreSQL) operator grammar. +func expressionPrecedence(e Expression) int { + switch x := e.(type) { + case *BinaryExpression: + if x == nil { + return 100 + } + if x.CustomOp != nil { + return 6 + } + switch strings.ToUpper(x.Operator) { + case "OR": + return 1 + case "XOR": + return 2 + case "AND": + return 3 + case "NOT": + return 4 + case "=", "!=", "<>", "<", ">", "<=", ">=", "==", "IS", "IS NOT", "IS NULL", "IS DISTINCT FROM", "IS NOT DISTINCT FROM", "LIKE", "ILIKE", "SIMILAR TO", "REGEXP", "RLIKE", "GLOB", "MATCH", "SOUNDS LIKE": + return 5 + case "+", "-": + return 7 + case "*", "/", "%", "DIV", "MOD": + return 8 + case "^": + if x.BitwiseXor && x.XorAtAdditive { + return 7 + } + return 9 + default: + return 6 + } + case *UnaryExpression: + if x != nil { + switch x.Operator { + case Not: + return 4 + case PGAbs, PGSquareRoot, PGCubeRoot, PGPrefixFactorial: + return 6 + } + } + return 10 + case *BetweenExpression, *InExpression, *AnyExpression, *AllExpression, *AnyAllExpression: + return 5 + case *AtTimeZoneExpression, *CollateExpression: + return 10 + default: + return 100 + } +} + +// parsedOperand reports whether child came from the parser, whose trees keep +// the source's explicit parentheses as ParenthesizedExpression nodes and +// group operators by the source dialect's grammar, so SQL() must not add any. +// A child carrying a source position is parsed; a hand-built child (no +// position) is grouped by expressionPrecedence even inside a parsed parent. +// Child kinds without a position field follow their parent. +func parsedOperand(parentPositioned bool, child Expression) bool { + switch x := child.(type) { + case *BinaryExpression: + return x.Pos.Line != 0 + case *UnaryExpression: + return x.Pos.Line != 0 + case *BetweenExpression: + return x.Pos.Line != 0 + case *InExpression: + return x.Pos.Line != 0 + case *AnyAllExpression: + return x.Pos.Line != 0 + case *AtTimeZoneExpression: + return x.Pos.Line != 0 + case *CollateExpression: + return x.Pos.Line != 0 + } + return parentPositioned +} + +// BinaryOperandNeedsParens preserves grouping for operands constructed +// without ParenthesizedExpression nodes; see parsedOperand. +func BinaryOperandNeedsParens(parent *BinaryExpression, child Expression, right bool) bool { + if parent == nil || child == nil { + return false + } + if _, ok := child.(*ParenthesizedExpression); ok { + return false + } + if parsedOperand(parent.Pos.Line != 0, child) { + return false + } + p, c := expressionPrecedence(parent), expressionPrecedence(child) + if c != p { + return c < p + } + // Comparisons are non-associative. Right operands at the same level need + // grouping even for arithmetic: floating point arithmetic is not associative. + return right || p == 5 +} + +func writeBinaryOperand(buf *bytes.Buffer, parent *BinaryExpression, child Expression, right bool) { + parens := BinaryOperandNeedsParens(parent, child, right) + if parens { + buf.WriteByte('(') + } + writeExpr(buf, child) + if parens { + buf.WriteByte(')') + } +} + +func writeUnaryOperand(buf *bytes.Buffer, parent *UnaryExpression) { + parens := expressionPrecedence(parent.Expr) < expressionPrecedence(parent) + if _, ok := parent.Expr.(*BinaryExpression); ok && expressionPrecedence(parent) == 6 && expressionPrecedence(parent.Expr) == 6 { + parens = true + } + if parens && parsedOperand(parent.Pos.Line != 0, parent.Expr) { + parens = false + } + if parens { + buf.WriteByte('(') + } + writeExpr(buf, parent.Expr) + if parens { + buf.WriteByte(')') + } +} diff --git a/pkg/sql/ast/sql_precedence_test.go b/pkg/sql/ast/sql_precedence_test.go new file mode 100644 index 00000000..6288d648 --- /dev/null +++ b/pkg/sql/ast/sql_precedence_test.go @@ -0,0 +1,126 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast_test + +import ( + "fmt" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/gosqlx" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" +) + +func TestSQLHandBuiltOperatorGrouping(t *testing.T) { + id := func(n string) ast.Expression { return &ast.Identifier{Name: n} } + bin := func(op string, l, r ast.Expression) *ast.BinaryExpression { + return &ast.BinaryExpression{Operator: op, Left: l, Right: r} + } + tests := []struct { + name string + expr ast.Expression + want string + }{ + {"logical", bin("AND", bin("OR", id("a"), id("b")), id("c")), "(a OR b) AND c"}, + {"right subtraction", bin("-", id("a"), bin("-", id("b"), id("c"))), "a - (b - c)"}, + {"multiply sum", bin("*", bin("+", id("a"), id("b")), id("c")), "(a + b) * c"}, + {"division", bin("/", id("a"), bin("*", id("b"), id("c"))), "a / (b * c)"}, + {"comparison", bin("=", bin("=", id("a"), id("b")), id("c")), "(a = b) = c"}, + {"concat", bin("+", bin("||", id("a"), id("b")), id("c")), "(a || b) + c"}, + {"json power", bin("^", bin("->", id("a"), &ast.LiteralValue{Type: "STRING", Value: "k"}), id("c")), "(a -> 'k') ^ c"}, + {"minus", &ast.UnaryExpression{Operator: ast.Minus, Expr: bin("+", id("a"), id("b"))}, "-(a + b)"}, + {"not", &ast.UnaryExpression{Operator: ast.Not, Expr: bin("OR", id("a"), id("b"))}, "NOT (a OR b)"}, + {"not operand", bin("=", &ast.UnaryExpression{Operator: ast.Not, Expr: id("a")}, id("b")), "(NOT a) = b"}, + {"abs operand", bin("^", &ast.UnaryExpression{Operator: ast.PGAbs, Expr: id("a")}, id("b")), "(@ a) ^ b"}, + {"sqrt operand", bin("^", &ast.UnaryExpression{Operator: ast.PGSquareRoot, Expr: id("a")}, id("b")), "(|/a) ^ b"}, + {"cbrt operand", bin("^", &ast.UnaryExpression{Operator: ast.PGCubeRoot, Expr: id("a")}, id("b")), "(||/a) ^ b"}, + {"abs json", &ast.UnaryExpression{Operator: ast.PGAbs, Expr: bin("->", id("a"), id("b"))}, "@ (a -> b)"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, ok := tt.expr.(interface{ SQL() string }) + if !ok { + t.Fatalf("no SQL for %T", tt.expr) + } + if got := s.SQL(); got != tt.want { + t.Fatalf("SQL=%q want %q", got, tt.want) + } + a, err := gosqlx.ParseWithDialect("SELECT "+tt.want, "postgresql") + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(a) + if got := a.SQL(); got != "SELECT "+tt.want { + t.Fatalf("round trip = %q", got) + } + stmt, ok := a.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", a.Statements[0]) + } + if want, got := operatorTree(tt.expr), operatorTree(stmt.Columns[0]); want != got { + t.Fatalf("tree changed: %s -> %s", want, got) + } + }) + } +} + +func operatorTree(e ast.Expression) string { + switch x := e.(type) { + case *ast.ParenthesizedExpression: + return operatorTree(x.Expr) + case *ast.BinaryExpression: + return "(" + operatorTree(x.Left) + " " + x.Operator + " " + operatorTree(x.Right) + ")" + case *ast.UnaryExpression: + return x.Operator.String() + "(" + operatorTree(x.Expr) + ")" + case *ast.Identifier: + return x.Name + case *ast.LiteralValue: + return fmt.Sprint(x.Value) + default: + return fmt.Sprintf("%T", e) + } +} + +func TestSQLHandBuiltChildInParsedTree(t *testing.T) { + // A hand-built operand inserted into a parsed tree is grouped by its own + // (missing) position, not by its parsed parent's. + id := func(n string) ast.Expression { return &ast.Identifier{Name: n} } + or := func() ast.Expression { return &ast.BinaryExpression{Left: id("c"), Operator: "OR", Right: id("d")} } + tests := []struct { + name, sql string + edit func(*ast.BinaryExpression) + want string + }{ + {"and right", "SELECT * FROM t WHERE a AND b", func(b *ast.BinaryExpression) { b.Right = or() }, "SELECT * FROM t WHERE a AND (c OR d)"}, + {"plus left", "SELECT * FROM t WHERE a + b", func(b *ast.BinaryExpression) { b.Left = or() }, "SELECT * FROM t WHERE (c OR d) + b"}, + {"minus right", "SELECT * FROM t WHERE a - b", func(b *ast.BinaryExpression) { + b.Right = &ast.BinaryExpression{Left: id("c"), Operator: "-", Right: id("d")} + }, "SELECT * FROM t WHERE a - (c - d)"}, + {"parsed child unchanged", "SELECT * FROM t WHERE a AND b OR c", func(*ast.BinaryExpression) {}, "SELECT * FROM t WHERE a AND b OR c"}, + {"not over hand-built", "SELECT * FROM t WHERE NOT a", nil, "SELECT * FROM t WHERE NOT (c OR d)"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree, err := gosqlx.ParseWithDialect(tt.sql, "postgresql") + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + sel, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + switch w := sel.Where.(type) { + case *ast.BinaryExpression: + tt.edit(w) + case *ast.UnaryExpression: + w.Expr = or() + default: + t.Fatalf("unexpected WHERE %T", sel.Where) + } + if got := tree.SQL(); got != tt.want { + t.Fatalf("SQL() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/pkg/sql/ast/unicode_escape.go b/pkg/sql/ast/unicode_escape.go new file mode 100644 index 00000000..20b5b620 --- /dev/null +++ b/pkg/sql/ast/unicode_escape.go @@ -0,0 +1,19 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +// UnicodeEscapeExpression preserves a PostgreSQL U& quoted literal or name, +// including its optional UESCAPE clause. RawSQL is validated by the tokenizer. +// The parser produces it only for U&'...' strings: U&"..." names are decoded +// into ordinary quoted identifiers. Identifier marks a hand-built U&"..." name. +// It is an unpooled leaf so ordinary identifiers and literals stay compact. +type UnicodeEscapeExpression struct { + RawSQL string + Identifier bool +} + +func (*UnicodeEscapeExpression) expressionNode() {} +func (e UnicodeEscapeExpression) SQL() string { return e.RawSQL } +func (e UnicodeEscapeExpression) TokenLiteral() string { return e.RawSQL } +func (e UnicodeEscapeExpression) Children() []Node { return nil } diff --git a/pkg/sql/ast/unicode_escape_release_test.go b/pkg/sql/ast/unicode_escape_release_test.go new file mode 100644 index 00000000..c670d20c --- /dev/null +++ b/pkg/sql/ast/unicode_escape_release_test.go @@ -0,0 +1,23 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import "testing" + +func TestUnicodeEscapeReleaseModes(t *testing.T) { + for _, mode := range []releaseMode{releaseAll, releaseDrawn} { + expr := &UnicodeEscapeExpression{RawSQL: `U&"d\0061t"`, Identifier: true} + if children := expr.Children(); len(children) != 0 { + t.Fatalf("leaf has children: %#v", children) + } + releaseExpression(expr, mode) + if mode == releaseAll { + if expr.RawSQL != "" || expr.Identifier { + t.Fatal("releaseAll retained Unicode metadata") + } + } else if expr.RawSQL == "" || !expr.Identifier { + t.Fatal("releaseDrawn cleared an unpooled leaf") + } + } +} diff --git a/pkg/sql/parser/ddl_column_ext.go b/pkg/sql/parser/ddl_column_ext.go index e1a2b829..e7e8eaeb 100644 --- a/pkg/sql/parser/ddl_column_ext.go +++ b/pkg/sql/parser/ddl_column_ext.go @@ -174,6 +174,7 @@ func (p *Parser) typeWordFollows(prev, first string) bool { func (p *Parser) parseColumnType() (string, error) { // Use parseColumnName to accept keyword-based type names such as // INTEGER, TEXT, REAL. + typeName := typeNameTokenSQL(p.currentToken.Token) base := p.parseColumnName() if base == nil { return "", goerrors.ExpectedTokenError( @@ -183,11 +184,11 @@ func (p *Parser) parseColumnType() (string, error) { "", ) } - typ := base.Name + typ := typeName for p.isType(models.TokenTypePeriod) && p.peekToken().Token.Value != "" && p.peekToken().Token.Type != models.TokenTypeMul { p.advance() // Consume . - typ += "." + p.currentToken.Token.Value + typ += "." + typeNameTokenSQL(p.currentToken.Token) p.advance() } first := strings.ToUpper(typ) diff --git a/pkg/sql/parser/expressions.go b/pkg/sql/parser/expressions.go index f94fdbd1..e19a6535 100644 --- a/pkg/sql/parser/expressions.go +++ b/pkg/sql/parser/expressions.go @@ -198,6 +198,9 @@ func (p *Parser) parseAndExpression() (ast.Expression, error) { // operator. When || is logical OR (p.pipesAsOr, MySQL/MariaDB default) the loop // does not consume it, leaving it for parseExpression at OR precedence. func (p *Parser) parseStringConcatExpression() (ast.Expression, error) { + if p.IsPostgreSQL() { + return p.parseBitwiseExpression(1) + } // Parse the left side using additive expression left, err := p.parseAdditiveExpression() if err != nil { @@ -210,7 +213,7 @@ func (p *Parser) parseStringConcatExpression() (ast.Expression, error) { // left operand. func (p *Parser) parseStringConcatTail(left ast.Expression) (ast.Expression, error) { // Handle || (string concatenation) operator (left-associative) - for !p.pipesAsOr && p.isType(models.TokenTypeStringConcat) { + for !p.pipesAsOr && p.isType(models.TokenTypeStringConcat) && !p.IsPostgreSQL() { opPos := p.currentLocation() operator := p.currentToken.Token.Value p.advance() // Consume || @@ -256,11 +259,12 @@ func (p *Parser) parseAdditiveTail(left ast.Expression) (ast.Expression, error) } left = &ast.BinaryExpression{ - Left: left, - Operator: operator, - Right: right, - BitwiseXor: operator == "^", // only SQL Server reaches here with ^ - Pos: opPos, + Left: left, + Operator: operator, + Right: right, + BitwiseXor: operator == "^", // only SQL Server reaches here with ^ + XorAtAdditive: operator == "^", + Pos: opPos, } } @@ -389,8 +393,10 @@ func (p *Parser) parseJSONExpression() (ast.Expression, error) { } } - // Handle JSON operators (left-associative for chaining like data->'a'->'b') - for p.isJSONOperator() { + // PostgreSQL JSON operators share the lower "any other operator" + // precedence handled by parseBitwiseExpression. Other dialects keep + // their existing JSON precedence. + for p.isJSONOperator() && !p.IsPostgreSQL() { opPos := p.currentLocation() operator := p.currentToken.Token.Value operatorType := p.currentToken.Token.Type diff --git a/pkg/sql/parser/expressions_datatype.go b/pkg/sql/parser/expressions_datatype.go index bea93605..6219b342 100644 --- a/pkg/sql/parser/expressions_datatype.go +++ b/pkg/sql/parser/expressions_datatype.go @@ -23,6 +23,7 @@ import ( goerrors "github.com/ajitpratap0/GoSQLX/pkg/errors" "github.com/ajitpratap0/GoSQLX/pkg/models" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" ) // parseDataType parses a SQL data type for CAST or :: expressions. @@ -45,7 +46,7 @@ func (p *Parser) parseDataTypeHint(paramHint string) (string, error) { return "", p.expectedError("data type") } - first := p.currentToken.Token.Value + first := typeNameTokenSQL(p.currentToken.Token) p.advance() // Consume type name // Fast path: a single plain word, as in x::int or CAST(x AS text). @@ -65,7 +66,7 @@ func (p *Parser) parseDataTypeHint(paramHint string) (string, error) { for p.isType(models.TokenTypePeriod) && (p.peekTypeAt(1) == models.TokenTypeIdentifier || p.peekTypeAt(1) == models.TokenTypeDoubleQuotedString) { p.advance() // Consume . sb.WriteByte('.') - sb.WriteString(p.currentToken.Token.Value) + sb.WriteString(typeNameTokenSQL(p.currentToken.Token)) p.advance() } @@ -139,6 +140,19 @@ func (p *Parser) parseDataTypeHint(paramHint string) (string, error) { return sb.String(), nil } +// typeNameTokenSQL preserves every explicit type-name delimiter: PostgreSQL +// "char", for example, names a different type from the bare CHAR keyword. +func typeNameTokenSQL(tok models.Token) string { + style := tok.Quote + if style == 0 && tok.Type == models.TokenTypeDoubleQuotedString { + style = '"' + } + if style == 0 { + return tok.Value + } + return ast.QuoteNameWith(tok.Value, ast.QuotedName(style)) +} + // typeWordClass groups first words of a data type by the words that may // follow them. type typeWordClass int diff --git a/pkg/sql/parser/expressions_literal.go b/pkg/sql/parser/expressions_literal.go index 711e2d05..e5f75fb1 100644 --- a/pkg/sql/parser/expressions_literal.go +++ b/pkg/sql/parser/expressions_literal.go @@ -30,6 +30,14 @@ import ( // parsePrimaryExpression parses a primary expression (literals, identifiers, function calls) func (p *Parser) parsePrimaryExpression() (ast.Expression, error) { + if p.currentToken.Token.Quote == 'U' { + raw := p.currentToken.Token.Value + // U&"..." names arrive as decoded quoted identifiers; only + // U&'...' strings reach here. + expr := &ast.UnicodeEscapeExpression{RawSQL: raw} + p.advance() + return expr, nil + } // Handle unary minus/plus (-6, -3.14, +x) if p.isType(models.TokenTypeMinus) || p.isType(models.TokenTypePlus) { unaryPos := p.currentLocation() @@ -291,7 +299,7 @@ func (p *Parser) parsePrimaryExpression() (ast.Expression, error) { identPos := p.currentLocation() identName := p.currentToken.Token.Value - identQuote := p.currentIdentQuote() + identQuote := p.digitLeadingQuote(identName, p.currentIdentQuote()) var identQuotes ast.NameQuote if p.nameQuoteRelevant(identQuote) { identQuotes = p.appendNamePartQuoted(0, identQuote) @@ -356,7 +364,7 @@ func (p *Parser) parsePrimaryExpression() (ast.Expression, error) { // be copied from Name before Name is overwritten. ident.Table = ident.Name ident.Name = p.currentToken.Token.Value - q := p.currentIdentQuote() + q := p.digitLeadingQuote(ident.Name, p.currentIdentQuote()) ident.Quoted = q == '"' if q != 0 { ident.QuoteStyle = q @@ -678,7 +686,7 @@ func (p *Parser) parseMultiPartColumnRef(ident *ast.Identifier) error { } case p.isQualifiedNamePart(): parts[n] = p.currentToken.Token.Value - q := p.currentIdentQuote() + q := p.digitLeadingQuote(parts[n], p.currentIdentQuote()) lastQuoted = q == '"' if q != 0 { ident.QuoteStyle = q @@ -861,3 +869,14 @@ func (p *Parser) parseObjectLiteral() (ast.Expression, error) { p.advance() // Consume } return obj, nil } + +// digitLeadingQuote returns quote, or MySQL's backtick when quote is zero and +// name is a bare digit-leading name: MySQL permits those (1col, t.1col), and +// the dialect-neutral renderer must delimit them with the MySQL delimiter, +// since "1col" is a string literal in MySQL's default mode. +func (p *Parser) digitLeadingQuote(name string, quote rune) rune { + if quote == 0 && name != "" && name[0] >= '0' && name[0] <= '9' && p.isMySQLFamily() { + return '`' + } + return quote +} diff --git a/pkg/sql/parser/expressions_operators.go b/pkg/sql/parser/expressions_operators.go index 6463b3a4..14d56b73 100644 --- a/pkg/sql/parser/expressions_operators.go +++ b/pkg/sql/parser/expressions_operators.go @@ -175,6 +175,7 @@ func (p *Parser) parseComparisonSuffix(left ast.Expression, afterComparison bool ) } } + opPos := p.currentLocation() p.advance() // Consume LIKE/ILIKE // Snowflake LIKE ANY / LIKE ALL / ILIKE ANY / ILIKE ALL: @@ -198,12 +199,13 @@ func (p *Parser) parseComparisonSuffix(left ast.Expression, afterComparison bool Operator: operator, Right: tuple, Not: notPrefix, + Pos: opPos, }, suffixPredicate, nil } // Parse the pattern at the string-concatenation level so arithmetic // and || bind tighter than LIKE: a LIKE b || '%' (#596). - return p.parsePatternMatch(left, operator, notPrefix, true) + return p.parsePatternMatch(left, operator, notPrefix, true, opPos) } // Check for REGEXP/RLIKE operator (MySQL) @@ -214,35 +216,39 @@ func (p *Parser) parseComparisonSuffix(left ast.Expression, afterComparison bool if strings.EqualFold(p.currentToken.Token.Value, "RLIKE") { operator = "RLIKE" } + opPos := p.currentLocation() p.advance() - return p.parsePatternMatch(left, operator, notPrefix, false) + return p.parsePatternMatch(left, operator, notPrefix, false, opPos) } // SIMILAR TO (PostgreSQL, Redshift): the TO keyword makes it unambiguous. if p.currentToken.Token.Type == models.TokenTypeIdentifier && equalsUpper(p.currentToken.Token.Value, "SIMILAR") && p.peekValueIs(1, "TO") { + opPos := p.currentLocation() p.advance() // Consume SIMILAR p.advance() // Consume TO - return p.parsePatternMatch(left, "SIMILAR TO", notPrefix, true) + return p.parsePatternMatch(left, "SIMILAR TO", notPrefix, true, opPos) } // SQLite GLOB and MATCH pattern operators. if p.currentToken.Token.Type == models.TokenTypeIdentifier && p.IsSQLite() { + opPos := p.currentLocation() switch { case equalsUpper(p.currentToken.Token.Value, "GLOB"): p.advance() - return p.parsePatternMatch(left, "GLOB", notPrefix, false) + return p.parsePatternMatch(left, "GLOB", notPrefix, false, opPos) case equalsUpper(p.currentToken.Token.Value, "MATCH"): p.advance() - return p.parsePatternMatch(left, "MATCH", notPrefix, false) + return p.parsePatternMatch(left, "MATCH", notPrefix, false, opPos) } } // MySQL SOUNDS LIKE. if p.currentToken.Token.Type == models.TokenTypeIdentifier && p.isMySQLFamily() && equalsUpper(p.currentToken.Token.Value, "SOUNDS") && p.peekTypeAt(1) == models.TokenTypeLike { + opPos := p.currentLocation() p.advance() // Consume SOUNDS p.advance() // Consume LIKE - return p.parsePatternMatch(left, "SOUNDS LIKE", notPrefix, false) + return p.parsePatternMatch(left, "SOUNDS LIKE", notPrefix, false, opPos) } // ClickHouse GLOBAL IN / GLOBAL NOT IN — GLOBAL is a distributed query modifier @@ -369,7 +375,8 @@ func (p *Parser) parseComparisonOperatorSuffix(left ast.Expression, afterCompari // parsePatternMatch parses the pattern of a LIKE-style operator whose keyword // has been consumed, with an optional ESCAPE clause when allowEscape is set. -func (p *Parser) parsePatternMatch(left ast.Expression, operator string, not, allowEscape bool) (ast.Expression, int, error) { +// opPos is the position of the operator keyword. +func (p *Parser) parsePatternMatch(left ast.Expression, operator string, not, allowEscape bool, opPos models.Location) (ast.Expression, int, error) { pattern, err := p.parseBitwiseExpression(1) if err != nil { return nil, suffixNone, goerrors.InvalidSyntaxError( @@ -390,6 +397,7 @@ func (p *Parser) parsePatternMatch(left ast.Expression, operator string, not, al Operator: operator, Right: pattern, Not: not, + Pos: opPos, } if escape != nil { node.EnsureExtras().Escape = escape diff --git a/pkg/sql/parser/expressions_ops.go b/pkg/sql/parser/expressions_ops.go index 5fdf7d7b..c493f1a6 100644 --- a/pkg/sql/parser/expressions_ops.go +++ b/pkg/sql/parser/expressions_ops.go @@ -73,12 +73,21 @@ func (p *Parser) peekValueIs(offset int, upper string) bool { // | like + and -, which parseAdditiveExpression handles, so they are not // reported here for that dialect. func (p *Parser) bitwiseOperator() (op string, prec, n int) { + pipePrec, shiftPrec := 1, 3 + if p.IsPostgreSQL() { + // PostgreSQL gives these operators the same left-associative + // "any other operator" precedence, below arithmetic and power. + if p.isJSONOperator() || p.isType(models.TokenTypeStringConcat) { + return p.currentToken.Token.Value, 2, 1 + } + pipePrec, shiftPrec = 2, 2 + } switch p.currentToken.Token.Type { case models.TokenTypePipe: if p.IsSQLServer() { return "", 0, 0 } - return "|", 1, 1 + return "|", pipePrec, 1 case models.TokenTypeAmpersand: if p.IsSQLServer() { return "", 0, 0 @@ -105,7 +114,7 @@ func (p *Parser) bitwiseOperator() (op string, prec, n int) { switch p.peekTypeAt(1) { case models.TokenTypeLt: if p.nextAdjacent(1) { - return "<<", 3, 2 + return "<<", shiftPrec, 2 } case models.TokenTypeArrow: // a <-> b: the distance operator of pg_trgm and pgvector. @@ -115,13 +124,13 @@ func (p *Parser) bitwiseOperator() (op string, prec, n int) { } case models.TokenTypeGt: if p.peekTypeAt(1) == models.TokenTypeGt && p.nextAdjacent(1) { - return ">>", 3, 2 + return ">>", shiftPrec, 2 } // Single-token forms produced by the tokenizer. case models.TokenTypeShiftLeft: - return "<<", 3, 1 + return "<<", shiftPrec, 1 case models.TokenTypeShiftRight: - return ">>", 3, 1 + return ">>", shiftPrec, 1 case models.TokenTypeSpaceship: if p.IsPostgreSQL() { return "<=>", 2, 1 @@ -250,21 +259,63 @@ func (p *Parser) parseBitwiseExpression(minPrec int) (ast.Expression, error) { if n == 0 || prec < minPrec { return left, nil } + legacyJSONCast := p.IsPostgreSQL() && p.isJSONOperator() opPos := p.currentLocation() for i := 0; i < n; i++ { p.advance() // Consume the operator token(s) } + rightStart := p.currentPos right, err := p.parseBitwiseExpression(prec + 1) if err != nil { return nil, err } - left = &ast.BinaryExpression{ + binary := &ast.BinaryExpression{ Left: left, Operator: op, Right: right, Pos: opPos, } + left = binary + // Preserve the established JSON ::type association when its right + // operand is just a cast. Arithmetic and power operands retain their + // lower JSON precedence because their root is a binary expression. + if cast, ok := right.(*ast.CastExpression); legacyJSONCast && ok { + casts := p.topLevelCastCount(rightStart) + if casts == 0 { + continue + } + inner := cast + for i := 1; i < casts; i++ { + nested, ok := inner.Expr.(*ast.CastExpression) + if !ok { + break + } + inner = nested + } + binary.Right = inner.Expr + inner.Expr = binary + left = cast + } + } +} + +// topLevelCastCount distinguishes postfix :: casts from explicit CAST calls +// and casts within a parenthesized or subscript expression in a parsed operand. +func (p *Parser) topLevelCastCount(start int) int { + depth, count := 0, 0 + for i := start; i < p.currentPos; i++ { + switch p.tokens[i].Token.Type { + case models.TokenTypeLParen, models.TokenTypeLBracket, models.TokenTypeLBrace: + depth++ + case models.TokenTypeRParen, models.TokenTypeRBracket, models.TokenTypeRBrace: + depth-- + case models.TokenTypeDoubleColon: + if depth == 0 { + count++ + } + } } + return count } // sqlServerBitwise reports whether the current token is a T-SQL bitwise diff --git a/pkg/sql/parser/json_precedence_test.go b/pkg/sql/parser/json_precedence_test.go new file mode 100644 index 00000000..2f8a2581 --- /dev/null +++ b/pkg/sql/parser/json_precedence_test.go @@ -0,0 +1,244 @@ +// 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_test + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" + "github.com/ajitpratap0/GoSQLX/pkg/sql/parser" +) + +func TestPostgreSQLJSONOperatorPrecedence(t *testing.T) { + for _, op := range []string{"->", "->>", "#>", "#>>", "@>", "<@", "#-", "?", "?|", "?&"} { + t.Run(op, func(t *testing.T) { + for _, tc := range []struct{ expression, leftOp, rightOp string }{ + {"a " + op + " b ^ 2", "", "^"}, + {"a ^ 2 " + op + " b", "^", ""}, + {"a + b " + op + " c * d", "+", "*"}, + } { + tree, err := parser.ParseWithDialect("SELECT "+tc.expression, keywords.DialectPostgreSQL) + if err != nil { + t.Fatalf("parse %q: %v", tc.expression, err) + } + defer ast.ReleaseAST(tree) + stmt, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + expr, ok := stmt.Columns[0].(*ast.BinaryExpression) + if !ok { + t.Fatalf("expected binary expression, got %T", stmt.Columns[0]) + } + if expr.Operator != op { + t.Fatalf("%s: root operator = %q, want %q", tc.expression, expr.Operator, op) + } + for _, child := range []struct { + expr ast.Expression + op string + }{{expr.Left, tc.leftOp}, {expr.Right, tc.rightOp}} { + binary, ok := child.expr.(*ast.BinaryExpression) + if child.op == "" { + if ok { + t.Errorf("unexpected child operator %q", binary.Operator) + } + continue + } + if !ok || binary.Operator != child.op { + t.Errorf("%s: expected child operator %q, got %#v", tc.expression, child.op, child.expr) + } + } + } + }) + } +} + +func TestPostgreSQLOtherOperatorsSharePrecedence(t *testing.T) { + for _, op := range []string{"|", "&", "#", "<<", ">>", "||", "->", "@>"} { + t.Run(op, func(t *testing.T) { + for _, tc := range []struct{ sql, root, left string }{ + {"SELECT a -> b " + op + " c", op, "->"}, + {"SELECT a " + op + " b -> c", "->", op}, + } { + tree, err := parser.ParseWithDialect(tc.sql, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + stmt, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + expr, ok := stmt.Columns[0].(*ast.BinaryExpression) + if !ok || expr.Operator != tc.root { + t.Fatalf("%s: expected root %q, got %#v", tc.sql, tc.root, stmt.Columns[0]) + } + left, ok := expr.Left.(*ast.BinaryExpression) + if !ok || left.Operator != tc.left { + t.Fatalf("%s: expected left %s expression, got %#v", tc.sql, tc.left, expr.Left) + } + } + }) + } +} + +func TestJSONOperatorPrecedenceOtherDialects(t *testing.T) { + for _, dialect := range []keywords.SQLDialect{keywords.DialectGeneric, keywords.DialectMySQL, keywords.DialectMariaDB} { + t.Run(string(dialect), func(t *testing.T) { + tree, err := parser.ParseWithDialect("SELECT a -> b ^ 2", dialect) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + stmt, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + expr, ok := stmt.Columns[0].(*ast.BinaryExpression) + if !ok || expr.Operator != "^" { + t.Fatalf("expected root ^, got %#v", stmt.Columns[0]) + } + left, ok := expr.Left.(*ast.BinaryExpression) + if !ok || left.Operator != "->" { + t.Fatalf("expected left -> expression, got %#v", expr.Left) + } + }) + } +} + +func TestPostgreSQLPositionOtherOperators(t *testing.T) { + for _, sql := range []string{ + "SELECT POSITION('a' || 'b' IN text_value)", + "SELECT POSITION(data ->> 'key' IN text_value)", + } { + tree, err := parser.ParseWithDialect(sql, keywords.DialectPostgreSQL) + if err != nil { + t.Fatalf("parse %q: %v", sql, err) + } + defer ast.ReleaseAST(tree) + } +} + +func TestXorPrecedenceMetadataRoundTrip(t *testing.T) { + for _, tc := range []struct { + dialect keywords.SQLDialect + additive bool + }{ + {keywords.DialectSQLServer, true}, {keywords.DialectMySQL, false}, {keywords.DialectMariaDB, false}, + } { + t.Run(string(tc.dialect), func(t *testing.T) { + tree, err := parser.ParseWithDialect("SELECT a ^ b", tc.dialect) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + output := tree.SQL() + again, err := parser.ParseWithDialect(output, tc.dialect) + if err != nil { + t.Fatalf("reparse %q: %v", output, err) + } + defer ast.ReleaseAST(again) + for _, candidate := range []*ast.AST{tree, again} { + stmt, ok := candidate.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", candidate.Statements[0]) + } + expr, ok := stmt.Columns[0].(*ast.BinaryExpression) + if !ok { + t.Fatalf("expected binary, got %T", stmt.Columns[0]) + } + if !expr.BitwiseXor || expr.XorAtAdditive != tc.additive { + t.Errorf("XOR metadata = %v/%v, want true/%v", expr.BitwiseXor, expr.XorAtAdditive, tc.additive) + } + } + }) + } +} + +func TestPostgreSQLJSONLegacyCastAssociation(t *testing.T) { + for _, sql := range []string{ + `SELECT * FROM events WHERE data @> '{"type": "click"}'::jsonb`, + `SELECT * FROM events WHERE data @> '{"type": "click"}'::text::jsonb`, + } { + tree, err := parser.ParseWithDialect(sql, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + stmt, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + cast, ok := stmt.Where.(*ast.CastExpression) + if !ok { + t.Fatalf("expected legacy cast of JSON expression, got %T", stmt.Where) + } + for { + nested, ok := cast.Expr.(*ast.CastExpression) + if !ok { + break + } + cast = nested + } + expr, ok := cast.Expr.(*ast.BinaryExpression) + if !ok || expr.Operator != "@>" { + t.Fatalf("expected JSON expression inside cast, got %#v", cast.Expr) + } + if sql == `SELECT * FROM events WHERE data @> '{"type": "click"}'::jsonb` { + want := `SELECT * FROM events WHERE CAST(data @> '{"type": "click"}' AS jsonb)` + if got := tree.SQL(); got != want { + t.Errorf("SQL = %s, want %s", got, want) + } + } + } + tree, err := parser.ParseWithDialect("SELECT a -> b::int ^ 2", keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + stmt, ok := tree.Statements[0].(*ast.SelectStatement) + if !ok { + t.Fatalf("expected SELECT, got %T", tree.Statements[0]) + } + expr, ok := stmt.Columns[0].(*ast.BinaryExpression) + if !ok || expr.Operator != "->" { + t.Fatalf("expected JSON root, got %#v", stmt.Columns[0]) + } + power, ok := expr.Right.(*ast.BinaryExpression) + if !ok || power.Operator != "^" { + t.Fatalf("expected power RHS, got %#v", expr.Right) + } + if _, ok := power.Left.(*ast.CastExpression); !ok { + t.Fatalf("expected cast of power's left operand, got %T", power.Left) + } +} + +func TestPostgreSQLJSONExplicitCastAssociation(t *testing.T) { + for _, tc := range []struct{ sql, want string }{ + {`SELECT data @> CAST('x' AS jsonb)`, `SELECT data @> CAST('x' AS jsonb)`}, + {`SELECT data @> CAST('x' AS text)::jsonb`, `SELECT CAST(data @> CAST('x' AS text) AS jsonb)`}, + {`SELECT data @> ('x'::jsonb)`, `SELECT data @> (CAST('x' AS jsonb))`}, + } { + tree, err := parser.ParseWithDialect(tc.sql, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + if got := tree.SQL(); got != tc.want { + t.Errorf("parse %s: SQL = %s, want %s", tc.sql, got, tc.want) + } + } +} diff --git a/pkg/sql/parser/literal_dialect_test.go b/pkg/sql/parser/literal_dialect_test.go new file mode 100644 index 00000000..d9662b80 --- /dev/null +++ b/pkg/sql/parser/literal_dialect_test.go @@ -0,0 +1,161 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package parser_test + +import ( + "reflect" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" + "github.com/ajitpratap0/GoSQLX/pkg/sql/parser" +) + +func TestLiteralDialectRoundTrip(t *testing.T) { + for _, tc := range []struct { + dialect keywords.SQLDialect + values []string + }{ + {keywords.DialectPostgreSQL, []string{"1_000", "0x1F", "0o17", "0b101", "1_000.000_1", `U&'d\0061t\+000061'`, `U&'d!0061t' UESCAPE '!'`}}, + {keywords.DialectMySQL, []string{"0x1F", "0b101", "1x", "1_000"}}, + {keywords.DialectSQLite, []string{"0x1F", "1_000"}}, + {keywords.DialectClickHouse, []string{"1_000", "0x1F", "0b101"}}, + {keywords.DialectSQLServer, []string{"0x1F"}}, + {keywords.DialectOracle, []string{"0x1F", "1d", "1f", "1.5f", "1e5d", "1.5D"}}, + {keywords.DialectSnowflake, []string{"0xFF"}}, + {keywords.DialectGeneric, []string{"0x1F"}}, + } { + for _, value := range tc.values { + t.Run(string(tc.dialect)+"/"+value, func(t *testing.T) { + sql := "SELECT " + value + tree, err := parser.ParseWithDialect(sql, tc.dialect) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + want := sql + if tc.dialect == keywords.DialectMySQL && (value == "1x" || value == "1_000") { + want = "SELECT `" + value + "`" + } + if got := tree.SQL(); got != want { + t.Fatalf("got %q want %q", got, want) + } + again, err := parser.ParseWithDialect(tree.SQL(), tc.dialect) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(again) + if !reflect.DeepEqual(tree.Statements, again.Statements) { + t.Fatal("round trip changed tree") + } + }) + } + } +} + +func TestLiteralDialectRejectTrailingJunk(t *testing.T) { + for _, dialect := range []keywords.SQLDialect{keywords.DialectPostgreSQL, keywords.DialectSQLServer, keywords.DialectOracle, keywords.DialectSnowflake, keywords.DialectGeneric} { + for _, value := range []string{"1x", "1__0", "1_"} { + tree, err := parser.ParseWithDialect("SELECT "+value, dialect) + if tree != nil { + ast.ReleaseAST(tree) + } + if err == nil { + t.Errorf("%s %s: expected error", dialect, value) + } + } + } +} + +func TestUnicodeNamesAreIdentifiers(t *testing.T) { + // U&"..." names decode into quoted identifiers usable wherever a name is; + // SQL() writes the decoded name (quoted when it needs to be). + for _, tc := range []struct{ sql, want string }{ + {`SELECT U&"d\0061t".a`, `SELECT dat.a`}, + {`SELECT a FROM U&"t"`, `SELECT a FROM t`}, + {`SELECT a AS U&"x"`, `SELECT a AS x`}, + {`SELECT x U&"y" FROM t`, `SELECT x AS y FROM t`}, + {`SELECT a FROM U&"d\0061t" AS U&"\0041"`, `SELECT a FROM dat "A"`}, + {`SELECT U&"A!0042" UESCAPE '!'`, `SELECT "AB"`}, + {`SELECT U&"a""b"`, `SELECT "a""b"`}, + } { + tree, err := parser.ParseWithDialect(tc.sql, keywords.DialectPostgreSQL) + if err != nil { + t.Errorf("%s: %v", tc.sql, err) + continue + } + got := tree.SQL() + ast.ReleaseAST(tree) + if got != tc.want { + t.Errorf("%s: got %q want %q", tc.sql, got, tc.want) + } + } +} + +func TestUnicodeEscapeCommentBeforeUESCAPE(t *testing.T) { + for _, sql := range []string{"SELECT U&'x' /* c */ UESCAPE '!'", "SELECT U&'x' -- c\n UESCAPE '!'"} { + tree, err := parser.ParseWithDialect(sql, keywords.DialectPostgreSQL) + if err != nil { + t.Errorf("%q: %v", sql, err) + continue + } + got := tree.SQL() + ast.ReleaseAST(tree) + if got != "SELECT U&'x' UESCAPE '!'" { + t.Errorf("%q: got %q", sql, got) + } + } +} + +func TestStorageSizesParse(t *testing.T) { + // Oracle storage sizes parse wherever Oracle takes a size, in the oracle + // and generic dialects, and render back unchanged. + for _, sql := range []string{ + "ALTER DATABASE DATAFILE 'f' RESIZE 100M", + "CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M MAXSIZE 1G", + "ALTER DATABASE DATAFILE 'f' AUTOEXTEND ON MAXSIZE 2G", + "CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M BLOCKSIZE 8K", + "CREATE PROFILE p LIMIT PRIVATE_SGA 15K", + "ALTER SYSTEM SET sga_target=2G", + "CREATE USER u IDENTIFIED BY p QUOTA 1G ON ts", + "CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M AUTOEXTEND ON NEXT 5M MAXSIZE 1G", + } { + for _, dialect := range []keywords.SQLDialect{keywords.DialectOracle, keywords.DialectGeneric} { + tree, err := parser.ParseWithDialect(sql, dialect) + if err != nil { + t.Errorf("%s %s: %v", dialect, sql, err) + continue + } + got := tree.SQL() + ast.ReleaseAST(tree) + if got != sql { + t.Errorf("%s %s: got %q", dialect, sql, got) + } + } + } +} + +func TestNumberThenAlias(t *testing.T) { + for _, tc := range []struct { + dialect keywords.SQLDialect + sql, want string + }{ + {keywords.DialectMySQL, "SELECT 1.5a, 1.0d, .5x", "SELECT 1.5 AS a, 1.0 AS d, .5 AS x"}, + {keywords.DialectMariaDB, "SELECT 1.5a", "SELECT 1.5 AS a"}, + {keywords.DialectOracle, "SELECT 1FROM dual", "SELECT 1 FROM dual"}, + {keywords.DialectSQLServer, "SELECT 1AS x", "SELECT 1 AS x"}, + {keywords.DialectGeneric, "SELECT * FROM t FETCH FIRST 5ROWS ONLY", "SELECT * FROM t FETCH FIRST 5 ROWS ONLY"}, + } { + tree, err := parser.ParseWithDialect(tc.sql, tc.dialect) + if err != nil { + t.Errorf("%s %s: %v", tc.dialect, tc.sql, err) + continue + } + got := tree.SQL() + ast.ReleaseAST(tree) + if got != tc.want { + t.Errorf("%s %s: got %q want %q", tc.dialect, tc.sql, got, tc.want) + } + } +} diff --git a/pkg/sql/parser/numeric_literal_type.go b/pkg/sql/parser/numeric_literal_type.go index 94058618..1ec0983b 100644 --- a/pkg/sql/parser/numeric_literal_type.go +++ b/pkg/sql/parser/numeric_literal_type.go @@ -23,7 +23,7 @@ func numericLiteralType(value string) string { if len(value) > 2 && value[0] == '0' && (value[1] == 'x' || value[1] == 'X') { return "int" } - if strings.ContainsAny(value, ".eE") { + if strings.ContainsAny(value, ".eEdDfF") { // dD/fF: Oracle 1d, 1.5f return "float" } return "int" diff --git a/pkg/sql/parser/utility_stmt.go b/pkg/sql/parser/utility_stmt.go index cc5ab5d6..3cbda0dd 100644 --- a/pkg/sql/parser/utility_stmt.go +++ b/pkg/sql/parser/utility_stmt.go @@ -960,7 +960,8 @@ func (p *Parser) isIdentifierAt(i int) bool { } // currentIdentQuote returns the opening delimiter of the current token when -// it is a delimited identifier ('"', '`' or '['), and 0 for a bare word. +// it is a delimited identifier ('"', '`' or '['), and 0 for a bare word. A +// bare digit-leading MySQL name (1col) reports '`' (see digitLeadingQuote). func (p *Parser) currentIdentQuote() rune { if p.isType(models.TokenTypeDoubleQuotedString) { return '"' @@ -971,6 +972,9 @@ func (p *Parser) currentIdentQuote() rune { return q } } + if p.isType(models.TokenTypeIdentifier) { + return p.digitLeadingQuote(p.currentToken.Token.Value, 0) + } return 0 } diff --git a/pkg/sql/tokenizer/bracket_escape_test.go b/pkg/sql/tokenizer/bracket_escape_test.go new file mode 100644 index 00000000..8994e7a7 --- /dev/null +++ b/pkg/sql/tokenizer/bracket_escape_test.go @@ -0,0 +1,27 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package tokenizer + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/models" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestSQLServerBracketEscapes(t *testing.T) { + for _, tc := range []struct{ sql, want string }{{`[a]]b]`, `a]b`}, {`[]]]`, `]`}, {`[a]]]]b]`, `a]]b`}} { + got := lexAll(t, keywords.DialectSQLServer, tc.sql) + if len(got) != 1 || got[0].typ != models.TokenTypeIdentifier || got[0].val != tc.want { + t.Errorf("%s: %v", tc.sql, got) + } + } + tk, err := NewWithDialect(keywords.DialectSQLServer) + if err != nil { + t.Fatal(err) + } + if _, err := tk.TokenizeString(`[a]]`); err == nil { + t.Fatal("expected unterminated identifier error") + } +} diff --git a/pkg/sql/tokenizer/lexical_extensions_test.go b/pkg/sql/tokenizer/lexical_extensions_test.go index 77cf37ec..d05006ca 100644 --- a/pkg/sql/tokenizer/lexical_extensions_test.go +++ b/pkg/sql/tokenizer/lexical_extensions_test.go @@ -56,7 +56,7 @@ func TestLexNumbers(t *testing.T) { {"hex in expression", "a = 0xFF AND b", []lexTok{ {models.TokenTypeIdentifier, "a"}, {models.TokenTypeEq, "="}, {num, "0xFF"}, {models.TokenTypeAnd, "AND"}, {models.TokenTypeIdentifier, "b"}}}, - {"0x without digits is zero then a word", "0x", []lexTok{{num, "0"}, {models.TokenTypeIdentifier, "x"}}}, + {"0x without digits is an identifier", "0x", []lexTok{{models.TokenTypeIdentifier, "0x"}}}, {"leading dot", ".5", []lexTok{{num, ".5"}}}, {"leading dot after operator", "a+.5", []lexTok{{models.TokenTypeIdentifier, "a"}, {models.TokenTypePlus, "+"}, {num, ".5"}}}, {"leading dot after comma", "(1,.25)", []lexTok{ diff --git a/pkg/sql/tokenizer/numeric_dialect_test.go b/pkg/sql/tokenizer/numeric_dialect_test.go new file mode 100644 index 00000000..ad324c46 --- /dev/null +++ b/pkg/sql/tokenizer/numeric_dialect_test.go @@ -0,0 +1,167 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package tokenizer + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/models" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestNumericDialectLiterals(t *testing.T) { + for _, tc := range []struct { + dialect keywords.SQLDialect + valid, invalid []string + }{ + {keywords.DialectPostgreSQL, []string{"1_000", "0x1F", "0o17", "0b101", "0x_FF", "0o_17", "0b_101", "1_000.000_1", "1e1_0"}, []string{"1x", "0x", "1__0", "1_", "0b102", "0x__FF", "0x_"}}, + {keywords.DialectSQLite, []string{"0x1F", "1_000"}, []string{"1x", "0b101", "1__0"}}, + {keywords.DialectClickHouse, []string{"0x1F", "1_000", "0b101"}, []string{"1x", "1_"}}, + {keywords.DialectSQLServer, []string{"0x1F"}, []string{"1x", "1_000", "0b101"}}, + // 0x hex stays accepted in every dialect, as before per-dialect lexing. + {keywords.DialectOracle, []string{"1000", "0x1F", "1d", "1f", "1.5f", "1e5d", "1.5D", "1.e5F"}, []string{"1x", "1_000", "0b101", "1dx", "1.5fa"}}, + {keywords.DialectSnowflake, []string{"1000", "0xFF"}, []string{"1x", "1_000", "0b101", "1d"}}, + {keywords.DialectBigQuery, []string{"1000", "0x1F"}, []string{"1x", "1_000", "0b101"}}, + {keywords.DialectRedshift, []string{"1000", "0x1F"}, []string{"1x", "1_000", "0b101"}}, + {keywords.DialectGeneric, []string{"1000", "0x1F"}, []string{"1x", "1_000", "0b101", "1d", "1.5f"}}, + } { + t.Run(string(tc.dialect), func(t *testing.T) { + tk, err := NewWithDialect(tc.dialect) + if err != nil { + t.Fatal(err) + } + for _, sql := range tc.valid { + tokens, err := tk.TokenizeString(sql) + if err != nil { + t.Errorf("%s: %v", sql, err) + continue + } + if len(tokens) != 2 || tokens[0].Token.Type != models.TokenTypeNumber || tokens[0].Token.Value != sql { + t.Errorf("%s: %v", sql, tokens) + } + } + for _, sql := range tc.invalid { + if _, err := tk.TokenizeString(sql); err == nil { + t.Errorf("%s: expected error", sql) + } + } + }) + } + for _, dialect := range []keywords.SQLDialect{keywords.DialectMySQL, keywords.DialectMariaDB} { + for _, sql := range []string{"1x", "1_000", "0x"} { + got := lexAll(t, dialect, sql) + if len(got) != 1 || got[0].typ != models.TokenTypeIdentifier || got[0].val != sql { + t.Errorf("%s %s: %v", dialect, sql, got) + } + } + for _, sql := range []string{"0x1F", "0b101"} { + got := lexAll(t, dialect, sql) + if len(got) != 1 || got[0].typ != models.TokenTypeNumber { + t.Errorf("%s %s: %v", dialect, sql, got) + } + } + } +} + +func TestStorageSizeUnits(t *testing.T) { + // Oracle storage sizes (and the generic dialect, which accepted them + // before per-dialect lexing) keep the established number/unit token pair + // wherever they appear, not only after SIZE. + for _, dialect := range []keywords.SQLDialect{keywords.DialectOracle, keywords.DialectGeneric} { + for _, prefix := range []string{"SIZE ", "QUOTA ", "NEXT ", "MAXSIZE ", "RESIZE ", "BLOCKSIZE ", "PRIVATE_SGA ", "sga_target="} { + for _, unit := range []string{"K", "M", "G", "T", "P", "E", "k", "m"} { + got := lexAll(t, dialect, prefix+"500"+unit) + n := len(got) + if n < 3 || got[n-2].typ != models.TokenTypeNumber || got[n-2].val != "500" || got[n-1].val != unit { + t.Errorf("%s %s: %v", dialect, prefix+"500"+unit, got) + } + } + } + tk, err := NewWithDialect(dialect) + if err != nil { + t.Fatal(err) + } + for _, sql := range []string{"SIZE 1x", "SIZE 1MB", "SIZE 1.5G", "SIZE 1_0M"} { + if _, err := tk.TokenizeString(sql); err == nil { + t.Errorf("%s %s: expected error", dialect, sql) + } + } + // 1e+5 and 1E5 stay exponents, not an exabyte unit. + for _, sql := range []string{"1e+5", "1E5", "1e-5"} { + got := lexAll(t, dialect, sql) + if len(got) != 1 || got[0].typ != models.TokenTypeNumber || got[0].val != sql { + t.Errorf("%s %s: %v", dialect, sql, got) + } + } + } + // Other dialects do not take storage units. + for _, dialect := range []keywords.SQLDialect{keywords.DialectPostgreSQL, keywords.DialectSQLite, keywords.DialectClickHouse} { + tk, err := NewWithDialect(dialect) + if err != nil { + t.Fatal(err) + } + if _, err := tk.TokenizeString("SIZE 10M"); err == nil { + t.Errorf("%s: expected error", dialect) + } + } +} + +func TestNumberBeforeKeyword(t *testing.T) { + // Oracle, SQL Server and the generic dialect end a number at a keyword + // written against it; other names still make the number invalid. + for _, dialect := range []keywords.SQLDialect{keywords.DialectOracle, keywords.DialectSQLServer, keywords.DialectGeneric} { + for _, tc := range []struct{ sql, num, word string }{ + {"1FROM", "1", "FROM"}, {"5ROWS", "5", "ROWS"}, {"1as", "1", "as"}, {"1UNION", "1", "UNION"}, + } { + got := lexAll(t, dialect, tc.sql) + if len(got) != 2 || got[0].typ != models.TokenTypeNumber || got[0].val != tc.num || got[1].val != tc.word { + t.Errorf("%s %s: %v", dialect, tc.sql, got) + } + } + tk, err := NewWithDialect(dialect) + if err != nil { + t.Fatal(err) + } + for _, sql := range []string{"1fromx", "1from$x", "1x"} { + if _, err := tk.TokenizeString(sql); err == nil { + t.Errorf("%s %s: expected error", dialect, sql) + } + } + } + tk, err := NewWithDialect(keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + if _, err := tk.TokenizeString("1FROM"); err == nil { + t.Error("postgresql 1FROM: expected trailing junk error") + } +} + +func TestMySQLNumberThenName(t *testing.T) { + // A MySQL number with a fraction or signed exponent ends before a name, + // which is then an alias (1.5a is 1.5 AS a); 1x and 1e5x stay names. + for _, dialect := range []keywords.SQLDialect{keywords.DialectMySQL, keywords.DialectMariaDB} { + for _, tc := range []struct { + sql string + want []lexTok + }{ + {"1.5a", []lexTok{{models.TokenTypeNumber, "1.5"}, {models.TokenTypeIdentifier, "a"}}}, + {"1.0d", []lexTok{{models.TokenTypeNumber, "1.0"}, {models.TokenTypeIdentifier, "d"}}}, + {".5x", []lexTok{{models.TokenTypeNumber, ".5"}, {models.TokenTypeIdentifier, "x"}}}, + {"1e+5x", []lexTok{{models.TokenTypeNumber, "1e+5"}, {models.TokenTypeIdentifier, "x"}}}, + {"1e5x", []lexTok{{models.TokenTypeIdentifier, "1e5x"}}}, + } { + got := lexAll(t, dialect, tc.sql) + if len(got) != len(tc.want) { + t.Errorf("%s %s: %v", dialect, tc.sql, got) + continue + } + for i := range got { + if got[i].typ != tc.want[i].typ || got[i].val != tc.want[i].val { + t.Errorf("%s %s: %v", dialect, tc.sql, got) + } + } + } + } +} diff --git a/pkg/sql/tokenizer/testdata/tokenizer_literal_golden.json b/pkg/sql/tokenizer/testdata/tokenizer_literal_golden.json index 26dc294e..004ec639 100644 --- a/pkg/sql/tokenizer/testdata/tokenizer_literal_golden.json +++ b/pkg/sql/tokenizer/testdata/tokenizer_literal_golden.json @@ -201,8 +201,9 @@ {"id":"file:../../../testdata/realworld/oracle/04_window_analytics.sql","content_sha":"0c60d4b6770c6c01b1178ba725b981f7e838866f7c2778b15f76c2a7478b4905","digests":["c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c","c692496ddd6a0b9c"]}, {"id":"file:../../../testdata/realworld/oracle/05_dialect_specific.sql","content_sha":"4a13ef1fec1bb3a19f18cdde597718f8b313d1f9f52533b3216b6af71209715f","digests":["b91b4872757909ea","b91b4872757909ea","b91b4872757909ea","b91b4872757909ea","b91b4872757909ea","2551e98fbb4e510f","b91b4872757909ea","b91b4872757909ea","b91b4872757909ea"]}, {"id":"file:../../../testdata/realworld/oracle/06_etl_merge.sql","content_sha":"9f02789dba1a1616892f8a6662ac65d4cccad24f172bb20ae077124134efe625","digests":["76881d2082230a10","76881d2082230a10","76881d2082230a10","76881d2082230a10","76881d2082230a10","864258bc0495dd91","864258bc0495dd91","76881d2082230a10","76881d2082230a10"]}, -{"id":"file:../../../testdata/realworld/oracle/07_admin_ops.sql","content_sha":"cfa6552d00c76197967627e49d757a5c9f0c1e0fd72303511d39b699068e4b4e","digests":["89a79b3957d295bd","89a79b3957d295bd","89a79b3957d295bd","89a79b3957d295bd","89a79b3957d295bd","305f1bd459ac97e5","e49f2bd1ff79408d","89a79b3957d295bd","89a79b3957d295bd"]}, +{"id":"file:../../../testdata/realworld/oracle/07_admin_ops.sql","content_sha":"cfa6552d00c76197967627e49d757a5c9f0c1e0fd72303511d39b699068e4b4e","digests":["ff466d11b15001eb","ff466d11b15001eb","7b28324fd0441a5d","7b28324fd0441a5d","ff466d11b15001eb","305f1bd459ac97e5","ff466d11b15001eb","ff466d11b15001eb","ff466d11b15001eb"]}, {"id":"file:../../../testdata/realworld/oracle/08_complex_nested.sql","content_sha":"54d48dc0a548a4d7b8ba25b9db956b0555249206e73ede4dffa715c11438d2a9","digests":["620dad4044aa231d","620dad4044aa231d","620dad4044aa231d","620dad4044aa231d","620dad4044aa231d","0d625cc3f03a2c21","0d625cc3f03a2c21","620dad4044aa231d","620dad4044aa231d"]}, +{"id":"file:../../../testdata/realworld/oracle/09_plsql_blocks.sql","content_sha":"bb1c0c669825a4e758ef08958709a12abbab5bed3314d3750aee7a340c21a091","digests":["df03a2ea90f14d3c","df03a2ea90f14d3c","df03a2ea90f14d3c","df03a2ea90f14d3c","df03a2ea90f14d3c","c5c31ba43ec88c38","c5c31ba43ec88c38","df03a2ea90f14d3c","df03a2ea90f14d3c"]}, {"id":"file:../../../testdata/realworld/postgresql/01_app_crud.sql","content_sha":"64aaf258c172e518df11f6a4812d61c891582f31874c6f251665f7e1cce549ab","digests":["739093d0608d9581","739093d0608d9581","739093d0608d9581","739093d0608d9581","61199b91a8cede2e","739093d0608d9581","61199b91a8cede2e","739093d0608d9581","739093d0608d9581"]}, {"id":"file:../../../testdata/realworld/postgresql/02_reporting.sql","content_sha":"b63e1bd7d7dd2b24371336fcb9b06567aeb7e16a8dc0465b563309367ef1b3dd","digests":["7e33fffab141ac65","7e33fffab141ac65","7e33fffab141ac65","7e33fffab141ac65","67a0d77e5cbdb976","7e33fffab141ac65","67a0d77e5cbdb976","7e33fffab141ac65","7e33fffab141ac65"]}, {"id":"file:../../../testdata/realworld/postgresql/03_ddl_migrations.sql","content_sha":"68df9cf369beaeac989e1487f0d96ea29901025f8ad7db14b4552b7b0884e6df","digests":["d798269ed652a1b2","d798269ed652a1b2","d798269ed652a1b2","d798269ed652a1b2","8a2326c1bcb23056","d798269ed652a1b2","8a2326c1bcb23056","d798269ed652a1b2","d798269ed652a1b2"]}, @@ -210,7 +211,7 @@ {"id":"file:../../../testdata/realworld/postgresql/05_dialect_specific.sql","content_sha":"dc4bcad1939455df60b8771058abcd80577f87c8b3eef4f2b50fd6bf5ed16bd9","digests":["a90126dc6e705745","05278c33ffe4d99e","5ff3561c8db313df","5ff3561c8db313df","8ec2aaa35ee4d7ad","694cf200f3eec85c","914143ed5bb3098b","c0d0effa810cad85","a229b464ed52a341"]}, {"id":"file:../../../testdata/realworld/postgresql/06_cte_dml_etl.sql","content_sha":"7b610fd3a9d525c09358979951af2e73fe761e48c611dade75d618a92fd33298","digests":["3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7","3c7723ffb97698a7"]}, {"id":"file:../../../testdata/realworld/postgresql/07_admin_ops.sql","content_sha":"5e16fc22e7c6ea0bf40dee351c3b69321df03746823090af66ba648c6685a8d9","digests":["2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332","2e62146a0f448332"]}, -{"id":"file:../../../testdata/realworld/postgresql/08_supabase_rls_misc.sql","content_sha":"0a1f2a6d944716c56f67278e4f7095c28dff81dfc28f11053709e643ad489545","digests":["e1595f417105ae28","8e280873bfafcc89","e1595f417105ae28","e1595f417105ae28","dffc3f29306bbd8e","8e280873bfafcc89","dffc3f29306bbd8e","e1595f417105ae28","e1595f417105ae28"]}, +{"id":"file:../../../testdata/realworld/postgresql/08_supabase_rls_misc.sql","content_sha":"0a1f2a6d944716c56f67278e4f7095c28dff81dfc28f11053709e643ad489545","digests":["e1595f417105ae28","8e280873bfafcc89","e1595f417105ae28","e1595f417105ae28","1ba2e02a5493b178","8e280873bfafcc89","dffc3f29306bbd8e","e1595f417105ae28","e1595f417105ae28"]}, {"id":"file:../../../testdata/realworld/snowflake/01_app_crud.sql","content_sha":"f83439700b07a91c80935b76df127e7b06b3565874b17b5a3d1d9d6a917cf06e","digests":["23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1","23423e5ef426aff1"]}, {"id":"file:../../../testdata/realworld/snowflake/02_reporting.sql","content_sha":"02a05250ee03c397155b1e216806482cdb8dbc5748e0fee7a327a2fd68e2a4e2","digests":["951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757","951ea8aab495f757"]}, {"id":"file:../../../testdata/realworld/snowflake/03_ddl_migrations.sql","content_sha":"305b9d9a4e604f32ffda42d2096cb2d449db0cee505dbac0245244fa09a146ef","digests":["bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced","bcc83313c6184ced"]}, @@ -243,6 +244,7 @@ {"id":"file:../../../testdata/realworld/sqlserver/08_procs_batches.sql","content_sha":"00e302af6209c4aefb752356ae04e76ebd727fc6c793dfbd16d00f6085bbe0dc","digests":["a80c628391e2dcc6","4096f2e47797609d","4096f2e47797609d","4096f2e47797609d","32a7da90f64d1742","4096f2e47797609d","a77fbeb4d5b7273f","a80c628391e2dcc6","a80c628391e2dcc6"]}, {"id":"file:../../../testdata/realworld/sqlserver/09_admin_dmv.sql","content_sha":"a0991caf114d8b06fc9a21577719b6ec48130451a870c6c2c75a1b20917eb395","digests":["37e19fbebd79586e","9190123aa223cf49","9190123aa223cf49","9190123aa223cf49","9190123aa223cf49","9190123aa223cf49","37e19fbebd79586e","37e19fbebd79586e","37e19fbebd79586e"]}, {"id":"file:../../../testdata/realworld/sqlserver/10_etl_mixed.sql","content_sha":"0fc6f5060b667e1eac30a6166056a2d5a1fb808f6e29458b159e7901cc5d06c2","digests":["579ad9fca98b75e2","b8d3f22dd51752a1","47042ac8dcf02739","47042ac8dcf02739","e575339fcb4e5753","b8d3f22dd51752a1","889061627d8c67e3","d025a64ec6a3dbbf","d7b36bb8f975a4fc"]}, +{"id":"file:../../../testdata/realworld/sqlserver/11_batches_no_semicolons.sql","content_sha":"7455e6d4baa9af05bc1650e2e1e1c95aefbebf8e67f78d00503047c79bd41fe3","digests":["bbacb39f08ef6771","523d11822f00beb0","523d11822f00beb0","523d11822f00beb0","8cea6eff8d2f6189","523d11822f00beb0","257b088f78420a95","bbacb39f08ef6771","bbacb39f08ef6771"]}, {"id":"file:../../../testdata/sql/basic_queries.sql","content_sha":"0948ca69ae63c18e038def59ed32f1bff75c19ecee6b865d9e71dedac7beb553","digests":["a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f","a8a6ac68d76fc41f"]}, {"id":"file:../../../testdata/sql/demo_queries.sql","content_sha":"e1c041c651316dfea12fcca0bd9bf75c72ba421324cd143e812790c11d87f906","digests":["5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62","5b497a821c8caa62"]}, {"id":"file:../../../testdata/sql/performance_tests.sql","content_sha":"47e67f7db63538e5eb0e0d6b313dd50260bef6be4d9a3aefec532a3981dcae1a","digests":["51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda","51d3764940af2dda"]}, @@ -497,6 +499,32 @@ {"id":"lit:../parser/aggregate_orderby_test.go:477","input":"SELECT STRING_AGG(name, ', ' ORDER BY) FROM users","digests":["d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099","d45272d2b3c99099"]}, {"id":"lit:../parser/aggregate_orderby_test.go:482","input":"SELECT STRING_AGG(name, ', ' ORDER BY name DESC ASC) FROM users","digests":["e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75","e8322ae51a3c8d75"]}, {"id":"lit:../parser/aggregate_orderby_test.go:508","input":"\n\t\tSELECT dept,\n\t\t\t (SELECT STRING_AGG(name, ', ' ORDER BY name)\n\t\t\t FROM employees e\n\t\t\t WHERE e.dept_id = d.id) as employees\n\t\tFROM departments d\n\t","digests":["29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4","29cefe09f3723ea4"]}, +{"id":"lit:../parser/alter_table_test.go:93","input":"ALTER TABLE t ON CLUSTER '{cluster}' DROP COLUMN x","digests":["c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83","c9128022d8fd2a83"]}, +{"id":"lit:../parser/alter_table_test.go:95","input":"'{cluster}'","digests":["32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a","32ff88f6f86e1a1a"]}, +{"id":"lit:../parser/alter_table_test.go:96","input":"OnCluster = %q, want '{cluster}'","digests":["a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278","a82f0cfd6ccc7278"]}, +{"id":"lit:../parser/alter_table_test.go:164","input":"ALTER TABLE t ADD (a NUMBER, b VARCHAR2(10) DEFAULT 'x')","digests":["ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14","ec489bfdc9b2fa14"]}, +{"id":"lit:../parser/alter_table_test.go:463","input":"ALTER TABLE t ALTER COLUMN a SET DEFAULT now() + interval '1 day'","digests":["663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9","663e7c9165c2ede9"]}, +{"id":"lit:../parser/alter_table_test.go:537","input":"ALTER TABLE t ATTACH PARTITION p FOR VALUES IN ('a')","digests":["b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f","b936a59def85400f"]}, +{"id":"lit:../parser/alter_table_test.go:537","input":"p FOR VALUES IN ('a')","digests":["172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db","172adc427634a8db"]}, +{"id":"lit:../parser/alter_table_test.go:554","input":"ALTER TABLE t SET DATA_RETENTION_TIME_IN_DAYS = 7, COMMENT = 'x'","digests":["92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba","92b0dd3dda042aba"]}, +{"id":"lit:../parser/alter_table_test.go:554","input":"DATA_RETENTION_TIME_IN_DAYS = 7, COMMENT = 'x'","digests":["70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c","70b3f5175ee20b2c"]}, +{"id":"lit:../parser/alter_table_test.go:558","input":"ALTER TABLE t DROP PARTITION ID '20240101'","digests":["0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47","0acc6f66aacbad47"]}, +{"id":"lit:../parser/alter_table_test.go:558","input":"'20240101'","digests":["903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0","903db61b21e413a0"]}, +{"id":"lit:../parser/alter_table_test.go:559","input":"ALTER TABLE t MOVE PARTITION '202401' TO TABLE e","digests":["fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b","fdf02c2add24d87b"]}, +{"id":"lit:../parser/alter_table_test.go:559","input":"'202401' TO TABLE e","digests":["b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff","b2c6146f62333bff"]}, +{"id":"lit:../parser/alter_table_test.go:561","input":"ALTER TABLE t CLEAR COLUMN a IN PARTITION '202401'","digests":["7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5","7eb3655e75953da5"]}, +{"id":"lit:../parser/alter_table_test.go:561","input":"a IN PARTITION '202401'","digests":["d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a","d616aa565886bf1a"]}, +{"id":"lit:../parser/alter_table_test.go:591","input":"ALTER TABLE t UPDATE a = 'x' IN PARTITION '202401' WHERE id = 1","digests":["2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d","2d390867dfd5276d"]}, +{"id":"lit:../parser/alter_table_test.go:594","input":"ALTER TABLE t DELETE IN PARTITION '202401' WHERE ts \u003c '2024-01-05'","digests":["3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1","3ae775c89aaf2ed1"]}, +{"id":"lit:../parser/alter_table_test.go:629","input":"ALTER SESSION SET NLS_LANGUAGE = 'AMERICAN' NLS_TERRITORY = 'AMERICA'","digests":["cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3","cc63b02c3f44e3c3"]}, +{"id":"lit:../parser/alter_table_test.go:630","input":"ALTER SESSION SET TIMEZONE = 'UTC'","digests":["2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18","2598110bd4e63c18"]}, +{"id":"lit:../parser/alter_table_test.go:632","input":"ALTER USER 'app'@'%' IDENTIFIED BY 'new'","digests":["3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994","3e7d2690b77af994"]}, +{"id":"lit:../parser/alter_table_test.go:638","input":"ALTER ACCOUNT SET TIMEZONE = 'UTC'","digests":["1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7","1650912af217b6f7"]}, +{"id":"lit:../parser/alter_table_test.go:643","input":"ALTER ROLE r WITH LOGIN PASSWORD 'x'","digests":["36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e","36fc319e201b030e"]}, +{"id":"lit:../parser/alter_table_test.go:644","input":"ALTER ROLE r IDENTIFIED BY 'x'","digests":["9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4","9cd9a1fb4c30b1d4"]}, +{"id":"lit:../parser/alter_table_test.go:741","input":"ALTER TABLE a.b.c SET DATA_RETENTION_TIME_IN_DAYS = 7, COMMENT = 'x'","digests":["3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7","3fd9629acca6f1a7"]}, +{"id":"lit:../parser/alter_table_test.go:742","input":"ALTER TABLE t ON CLUSTER c UPDATE a = 1, b = 2 IN PARTITION '1' WHERE c = 3","digests":["3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde","3294aa422652bbde"]}, +{"id":"lit:../parser/alter_table_test.go:743","input":"ALTER TABLE t DELETE IN PARTITION '1' WHERE c IN (1, 2)","digests":["312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec","312d56c76fea48ec"]}, {"id":"lit:../parser/array_constructor_test.go:57","input":"SELECT ARRAY['a', 'b', 'c']","digests":["72cfa4cd25e500ca","72cfa4cd25e500ca","72cfa4cd25e500ca","72cfa4cd25e500ca","cccdde94327b512e","72cfa4cd25e500ca","cccdde94327b512e","72cfa4cd25e500ca","72cfa4cd25e500ca"]}, {"id":"lit:../parser/array_constructor_test.go:93","input":"SELECT * FROM users WHERE tags @\u003e ARRAY['admin', 'moderator']","digests":["d6c63d61b3867a82","d6c63d61b3867a82","d6c63d61b3867a82","d6c63d61b3867a82","f3276e70915d8c61","d6c63d61b3867a82","f3276e70915d8c61","d6c63d61b3867a82","d6c63d61b3867a82"]}, {"id":"lit:../parser/array_constructor_test.go:98","input":"SELECT * FROM users WHERE ARRAY['user'] \u003c@ roles","digests":["ecfd9e7ed8a7a37c","ecfd9e7ed8a7a37c","ecfd9e7ed8a7a37c","ecfd9e7ed8a7a37c","48db6809be1a79c1","ecfd9e7ed8a7a37c","48db6809be1a79c1","ecfd9e7ed8a7a37c","ecfd9e7ed8a7a37c"]}, @@ -534,6 +562,34 @@ {"id":"lit:../parser/between_complex_test.go:518","input":"expected upper bound operator '*', got '%s'","digests":["b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df","b65eb8af313a99df"]}, {"id":"lit:../parser/between_complex_test.go:563","input":"expected lower bound operator '||', got '%s'","digests":["99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc","99231943086d63cc"]}, {"id":"lit:../parser/between_complex_test.go:572","input":"expected upper bound operator '||', got '%s'","digests":["888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902","888d61cd6a368902"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:63","input":"SELECT * FROM sales PIVOT (SUM(amount) FOR quarter IN ('Q1', 'Q2'))","digests":["d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f","d67927edf252a32f"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:65","input":"'Q1'","digests":["e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8","e2ae580698fb6eb8"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:65","input":"'Q2'","digests":["016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924","016ee3baa50d0924"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:69","input":"SELECT * FROM sales PIVOT (SUM(amount) FOR quarter IN ('Q1' AS first_quarter, 'Q2' second_quarter)) AS pivoted","digests":["52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6","52be38bd97fa75b6"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:71","input":"'Q1' AS first_quarter","digests":["b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5","b6e0232e0edd36d5"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:71","input":"'Q2' AS second_quarter","digests":["e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd","e82b00bba723d9dd"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:76","input":"SELECT * FROM produce PIVOT (SUM(sales) AS total_sales, COUNT(*) AS num_records FOR quarter IN ('Q1', 'Q2'))","digests":["bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b","bc9949703aede95b"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:93","input":"SELECT * FROM `proj.ds.sales` PIVOT (SUM(amount) FOR `quarter` IN ('Q1' AS `q 1`))","digests":["5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4","5d9dc677042a80b4"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:95","input":"'Q1' AS `q 1`","digests":["5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef","5b5350c012f30bef"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:99","input":"SELECT * FROM sales PIVOT (SUM(amount) AS `日本`, COUNT(*) `a``b` FOR quarter IN ('Q1'))","digests":["f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba","f7be1547bded97ba"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:100","input":"`日本`","digests":["60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3","60b09d749ca6fdf3"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:104","input":"SELECT * FROM (SELECT product, sales, quarter FROM produce) PIVOT (SUM(sales) FOR quarter IN ('Q1', 'Q2'))","digests":["db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404","db7da0090b768404"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:173","input":"SELECT * FROM produce UNPIVOT INCLUDE NULLS (sales FOR quarter IN (Q1 AS 'first', Q2 'second')) AS u","digests":["3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7","3e9a367c12d257f7"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:177","input":"Q1 AS 'first'","digests":["4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa","4a884503d38d35fa"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:177","input":"Q2 AS 'second'","digests":["b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653","b28e43045fe3a653"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:190","input":"SELECT * FROM produce UNPIVOT ((first_half_sales, second_half_sales) FOR semesters IN ((Q1, Q2) AS 'semester_1', (Q3, Q4) AS 'semester_2'))","digests":["a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8","a3cbbaa239c9acb8"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:193","input":"(Q1, Q2) AS 'semester_1'","digests":["6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141","6d5066ab5147b141"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:193","input":"(Q3, Q4) AS 'semester_2'","digests":["02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9","02be92352fc3bdf9"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:196","input":"SELECT * FROM produce UNPIVOT (`日本` FOR quarter IN (Q1, Q2))","digests":["dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94","dcf224500cbd3f94"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:200","input":"SELECT * FROM produce UNPIVOT ((`日本`, `a``b`) FOR quarter IN ((Q1, Q2)))","digests":["7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647","7ff7cdad8719e647"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:247","input":"SELECT * FROM t PIVOT (SUM(x) AS FOR q IN ('a'))","digests":["03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825","03eb29ff21262825"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:248","input":"SELECT * FROM t UNPIVOT ((a, b) FOR k IN ((c1, c2, c3) AS 'x'))","digests":["db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827","db8714aae8d5f827"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:252","input":"SELECT * FROM t PIVOT (SUM(x) FOR q IN ('a' AS))","digests":["011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c","011d47a01d52f57c"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:253","input":"SELECT * FROM t PIVOT (SUM(x) FOR q IN ('a' 'b'))","digests":["05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd","05e7958931c179fd"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:342","input":"SELECT * FROM t PIVOT (SUM(x), COUNT(*) FOR q IN ('a'))","digests":["6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9","6a46c6f75e477bf9"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:345","input":"SELECT * FROM t UNPIVOT (v FOR k IN (c1 'x'))","digests":["ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059","ad3ad32fb67a5059"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:362","input":"SELECT * FROM t UNPIVOT ((a, b) FOR k IN ((c1, c2) AS 'x'))","digests":["44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64","44d2dcfb5aebce64"]}, +{"id":"lit:../parser/bigquery_pivot_test.go:367","input":"SELECT * FROM t PIVOT (SUM(x) FOR q IN ('a' qa))","digests":["9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034","9ad6929692353034"]}, {"id":"lit:../parser/cast_test.go:76","input":"expected Expr to be identifier 'id'","digests":["a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea","a03dc64c0fcfbeea"]}, {"id":"lit:../parser/clause_alloc_free_test.go:167","input":"SELECT a FROM t WHERE x NOT LIKE 'a%'","digests":["5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81","5fe0ed82fabcad81"]}, {"id":"lit:../parser/clause_alloc_free_test.go:169","input":"SELECT a FROM t WHERE x NOT ILIKE 'a'","digests":["09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785","09c33059c9f57785"]}, @@ -607,6 +663,7 @@ {"id":"lit:../parser/connect_by_expressions_test.go:50","input":"CONNECT BY PRIOR id = parent_id AND CASE WHEN k = 1 THEN 'prior' ELSE 'x' END = 'x'","digests":["d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6","d9cb6f601cb5e7e6"]}, {"id":"lit:../parser/connect_by_expressions_test.go:51","input":"CONNECT BY PRIOR id = parent_id AND CASE k WHEN 'prior' THEN 1 ELSE 2 END = 1","digests":["0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24","0705d888e8d69b24"]}, {"id":"lit:../parser/connect_by_expressions_test.go:128","input":"SELECT id FROM t CONNECT BY PRIOR id = parent_id AND \"PRIOR\" + 1 = b","digests":["451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261","451d12cfc6a0e261"]}, +{"id":"lit:../parser/connect_by_expressions_test.go:138","input":"(AND (= (PRIOR id) parent_id) (= (+ \"PRIOR\" 1) b))","digests":["4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e","4f429302cbb3770e"]}, {"id":"lit:../parser/context_test.go:64","input":"INSERT INTO users (name) VALUES ('John')","digests":["387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2","387136d1fcfcffe2"]}, {"id":"lit:../parser/context_test.go:68","input":"UPDATE users SET name = 'Jane' WHERE id = 1","digests":["7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534","7629ad9604d68534"]}, {"id":"lit:../parser/core_fixes_test.go:126","input":"CORE-2: expected joinType \"NATURAL\", got %q","digests":["c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6","c00968a102b61ba6"]}, @@ -632,6 +689,41 @@ {"id":"lit:../parser/cte_test.go:231","input":"CTE %d: expected name '%s', got '%s'","digests":["1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2","1a6c687a4d8b88c2"]}, {"id":"lit:../parser/cte_test.go:279","input":"Expected CTE name 'sales_summary', got '%s'","digests":["4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9","4ff114a492634fd9"]}, {"id":"lit:../parser/cte_test.go:290","input":"Column %d: expected '%s', got '%s'","digests":["6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112","6d2ec45d4f134112"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:124","input":"id INT COMMENT 'the id'","digests":["fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde","fcaa6bb7111f7bde"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:125","input":"id INT COMMENT 'it''s'","digests":["b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42","b880dbddca379c42"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:143","input":"name text COLLATE \"C\" NOT NULL","digests":["a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec","a87a211d809571ec"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:144","input":"n text COLLATE pg_catalog.\"default\"","digests":["2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a","2cae637ff8c9614a"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:160","input":"v VARCHAR COMMENT 'x'","digests":["9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167","9ff9fa4c7c6c4167"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:172","input":"d String COMMENT 'x'","digests":["9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27","9ceaf1626e5c4c27"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:222","input":"CREATE TABLE t (id int COMMENT 'x')","digests":["bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc","bae161d18b6c33fc"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:285","input":"CREATE TABLE t (a UInt8, b String, PROJECTION p (SELECT a WHERE b = 'it''s')) ENGINE = MergeTree ORDER BY a","digests":["36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346","36bb25228d9b4346"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:285","input":"PROJECTION p (SELECT a WHERE b = 'it''s')","digests":["7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f","7ce5a4e76e99d13f"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:287","input":"CREATE TABLE t (index String DEFAULT 'type', a UInt8) ENGINE = MergeTree ORDER BY a","digests":["3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1","3ea8c27fdf926cc1"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:378","input":"CREATE TABLE t (c VARCHAR(5) CHARSET latin1 COLLATE latin1_bin COMMENT 'hi' DEFAULT 'x' ON UPDATE NOW(), ","digests":["f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e","f04e701ff181f21e"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:404","input":"COMMENT 'hi'","digests":["5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892","5c49f628254d2892"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:405","input":"COMMENT = %v, want COMMENT 'hi'","digests":["7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e","7f5501893738b88e"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:429","input":"CREATE TABLE t (a timestamp(3) with time zone, b int[3] ARRAY, c int GENERATED BY DEFAULT AS IDENTITY (START WITH 1), d text COLLATE pg_catalog.\"C\" REFERENCES u (id) NOT DEFERRABLE INITIALLY IMMEDIATE, LIKE v INCLUDING ALL)","digests":["cdd15231aa16ac6e","cdd15231aa16ac6e","cdd15231aa16ac6e","cdd15231aa16ac6e","b97badcf71ffbb37","cdd15231aa16ac6e","b97badcf71ffbb37","cdd15231aa16ac6e","cdd15231aa16ac6e"]}, +{"id":"lit:../parser/ddl_column_ext_test.go:430","input":"CREATE TABLE t (a INT(5) UNSIGNED ZEROFILL AS (b + 1) STORED COMMENT 'x' CHARSET utf8 ON UPDATE NOW() SRID 4326, UNIQUE KEY k USING BTREE (a(10) DESC, (a + 1)), FULLTEXT INDEX f (a))","digests":["9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5","9753d6e3f1c8b8e5"]}, +{"id":"lit:../parser/ddl_objects_test.go:67","input":"CREATE TABLE d AS l ENGINE = Distributed('c', 'db', 'l', rand())","digests":["3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742","3c8852a9ce07a742"]}, +{"id":"lit:../parser/ddl_objects_test.go:68","input":"CREATE TABLE d AS l ENGINE=Distributed('c', 'db', 'l', rand())","digests":["3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310","3c9e99eabb9d7310"]}, +{"id":"lit:../parser/ddl_objects_test.go:72","input":"CREATE TABLE b CLONE s BEFORE (STATEMENT =\u003e 'x')","digests":["ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce","ebe51431bd37e2ce"]}, +{"id":"lit:../parser/ddl_objects_test.go:73","input":"CREATE TABLE m1 PARTITION OF m FOR VALUES FROM ('2024-01-01') TO (MAXVALUE)","digests":["99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4","99dc0fd1679636e4"]}, +{"id":"lit:../parser/ddl_objects_test.go:74","input":"CREATE TABLE r1 PARTITION OF r FOR VALUES IN ('DE', 'FR')","digests":["597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e","597b69e7be160a2e"]}, +{"id":"lit:../parser/ddl_objects_test.go:87","input":"CREATE TABLE t (id INT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=100 COMMENT='a table' ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8","digests":["1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9","1e2c72f9a39be0f9"]}, +{"id":"lit:../parser/ddl_objects_test.go:88","input":"CREATE TABLE t (id INT) DEFAULT CHARACTER SET utf8mb4 COLLATE 'utf8mb4_bin'","digests":["1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4","1765405e36a9c5a4"]}, +{"id":"lit:../parser/ddl_objects_test.go:93","input":"CREATE OR REPLACE TABLE t (a INT) CLUSTER BY (a, b) COMMENT = 'x' DATA_RETENTION_TIME_IN_DAYS = 1","digests":["9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585","9db0bc6005a7a585"]}, +{"id":"lit:../parser/ddl_objects_test.go:94","input":"CREATE OR REPLACE TABLE t (a INT) CLUSTER BY (a, b) COMMENT='x' DATA_RETENTION_TIME_IN_DAYS=1","digests":["8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf","8d47a5930d42eecf"]}, +{"id":"lit:../parser/ddl_objects_test.go:98","input":"CREATE TABLE e (a UInt8, ts DateTime) ENGINE = MergeTree PARTITION BY toYYYYMM(ts) PRIMARY KEY a ORDER BY (a, ts) SAMPLE BY a SETTINGS index_granularity = 8192 COMMENT 'events'","digests":["eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc","eebe5b36162a39cc"]}, +{"id":"lit:../parser/ddl_objects_test.go:99","input":"CREATE TABLE e (a UInt8, ts DateTime) ENGINE=MergeTree PARTITION BY toYYYYMM(ts) PRIMARY KEY a ORDER BY (a, ts) SAMPLE BY a SETTINGS index_granularity = 8192 COMMENT 'events'","digests":["efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10","efca3d2b1b19bd10"]}, +{"id":"lit:../parser/ddl_objects_test.go:101","input":"CREATE TABLE m (ts DateTime) ENGINE = MergeTree ORDER BY ts TTL ts + INTERVAL 1 MONTH TO VOLUME 'cold', ts + INTERVAL 1 YEAR DELETE WHERE ts \u003e 0","digests":["2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679","2947f6e6cae4d679"]}, +{"id":"lit:../parser/ddl_objects_test.go:102","input":"CREATE TABLE m (ts DateTime) ENGINE=MergeTree ORDER BY ts TTL ts + INTERVAL 1 MONTH TO VOLUME 'cold', ts + INTERVAL 1 YEAR DELETE WHERE ts \u003e 0","digests":["e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740","e1b104117ab75740"]}, +{"id":"lit:../parser/ddl_objects_test.go:107","input":"CREATE TABLE h (\"hash\" UInt64) ENGINE=MergeTree() ORDER BY hash","digests":["86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394","86f9621a4bb74394"]}, +{"id":"lit:../parser/ddl_objects_test.go:112","input":"CREATE TABLE t (r VARCHAR2(10)) PARTITION BY LIST (r) (PARTITION p1 VALUES ('a', 'b'), PARTITION p2 VALUES (DEFAULT))","digests":["5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6","5ec66adb664e9ec6"]}, +{"id":"lit:../parser/ddl_objects_test.go:117","input":"CREATE TABLE t (d DATE) PARTITION BY RANGE (d) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) (PARTITION p0 VALUES LESS THAN (TO_DATE('2020-01-01', 'YYYY-MM-DD')))","digests":["4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378","4f44c2b60e50a378"]}, +{"id":"lit:../parser/ddl_objects_test.go:298","input":"CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"","digests":["b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d","b393ffb76fbc551d"]}, +{"id":"lit:../parser/ddl_objects_test.go:299","input":"CREATE TYPE mood AS ENUM ('sad', 'ok')","digests":["c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7","c4432acb2adfc8f7"]}, +{"id":"lit:../parser/ddl_objects_test.go:303","input":"CREATE DEFINER = 'root'@'localhost' PROCEDURE p() DELETE FROM s","digests":["6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89","6e8e4517e67e1b89"]}, +{"id":"lit:../parser/ddl_objects_test.go:308","input":"CREATE FILE FORMAT f TYPE = 'CSV'","digests":["e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c","e8866df84781bc4c"]}, {"id":"lit:../parser/dialect_helpers_test.go:134","input":"IsSnowflake() = false for WithDialect(\"snowflake\")","digests":["4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da","4f0b38f02511f7da"]}, {"id":"lit:../parser/dialect_pool_hygiene_test.go:37","input":"SELECT a FROM t WHERE b ILIKE 'x%'","digests":["f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a","f314265638f0ef2a"]}, {"id":"lit:../parser/dialect_pool_hygiene_test.go:38","input":"SELECT `a` FROM `t` LIMIT 5, 10","digests":["f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512","f365925f99b96512"]}, @@ -646,7 +738,9 @@ {"id":"lit:../parser/distinct_on_test.go:166","input":"Expected second DISTINCT ON column 'category', got '%s'","digests":["c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43","c2767cc363444e43"]}, {"id":"lit:../parser/distinct_on_test.go:232","input":"Expected DISTINCT ON function 'LOWER', got '%s'","digests":["50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e","50b4a1b2fdb8285e"]}, {"id":"lit:../parser/distinct_on_test.go:344","input":"Expected DISTINCT ON column 'e.dept_id', got '%s.%s'","digests":["5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753","5e2696347850e753"]}, -{"id":"lit:../parser/dml_ast_shape_test.go:299","input":"INSERT INTO FUNCTION s3('x') SETTINGS a = 1 FORMAT CSV","digests":["8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c"]}, +{"id":"lit:../parser/dml_ast_shape_test.go:59","input":"DELETE FROM \"t\" WHERE b = 2 RETURNING a","digests":["2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2","2beec87a5653cab2"]}, +{"id":"lit:../parser/dml_ast_shape_test.go:60","input":"DELETE FROM `t` WHERE b = 2","digests":["b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942","b731bee882559942"]}, +{"id":"lit:../parser/dml_ast_shape_test.go:338","input":"INSERT INTO FUNCTION s3('x') SETTINGS a = 1 FORMAT CSV","digests":["8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c","8cca444e9ac1467c"]}, {"id":"lit:../parser/double_quoted_identifier_test.go:59","input":"SELECT \"id\" FROM users","digests":["88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee","88a412e0c8826cee"]}, {"id":"lit:../parser/double_quoted_identifier_test.go:63","input":"SELECT id FROM \"users\"","digests":["516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259","516dbe33692bc259"]}, {"id":"lit:../parser/double_quoted_identifier_test.go:67","input":"SELECT \"id\", \"name\" FROM \"users\"","digests":["50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0","50909d5339b493c0"]}, @@ -690,7 +784,7 @@ {"id":"lit:../parser/double_quoted_identifier_test.go:465","input":"SELECT * FROM \"table name\"","digests":["d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3","d395b42bfb54e5a3"]}, {"id":"lit:../parser/error_context_issue616_test.go:67","input":"Use INTERVAL '1 day' or INTERVAL 1 DAY syntax","digests":["ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a","ff389d9b52125e9a"]}, {"id":"lit:../parser/error_context_issue616_test.go:69","input":"SELECT a::DECIMAL('x') FROM t","digests":["c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473","c22171057383c473"]}, -{"id":"lit:../parser/error_context_issue616_test.go:310","input":"%d builder call(s) pass something other than \"\" as sql (use \"\", WithHint for hints, WithCause for wrapped errors):\n%s","digests":["8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a"]}, +{"id":"lit:../parser/error_context_issue616_test.go:311","input":"%d builder call(s) pass something other than \"\" as sql (use \"\", WithHint for hints, WithCause for wrapped errors):\n%s","digests":["8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a","8f9518cf2ab7289a"]}, {"id":"lit:../parser/error_position_test.go:61","input":"SELECT 'é' /* ü */ FROM t WHERE ) x","digests":["d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507","d656c234e248b507"]}, {"id":"lit:../parser/error_recovery_test.go:180","input":"Error message should contain '%s', got: %v","digests":["76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7","76150695dee54ca7"]}, {"id":"lit:../parser/error_recovery_test.go:771","input":"missing FROM doesn't cascade","digests":["4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9","4bf5658a44be5ac9"]}, @@ -777,6 +871,17 @@ {"id":"lit:../parser/json_operators_test.go:271","input":"SELECT data ? 'active' AS is_active FROM users","digests":["ff01853efee9b23d","ff01853efee9b23d","085f6bd55cbec552","085f6bd55cbec552","ff01853efee9b23d","ff01853efee9b23d","085f6bd55cbec552","ff01853efee9b23d","ff01853efee9b23d"]}, {"id":"lit:../parser/json_operators_test.go:277","input":"SELECT * FROM users WHERE data ?| 'tags'","digests":["3cc4eecc643db997","3cc4eecc643db997","b2c2f5ca4875b50d","b2c2f5ca4875b50d","3cc4eecc643db997","3cc4eecc643db997","b2c2f5ca4875b50d","3cc4eecc643db997","3cc4eecc643db997"]}, {"id":"lit:../parser/json_operators_test.go:283","input":"SELECT * FROM users WHERE data ?\u0026 'keys'","digests":["415f1a03e4c4bd59","415f1a03e4c4bd59","6621efc6159510e2","6621efc6159510e2","415f1a03e4c4bd59","415f1a03e4c4bd59","6621efc6159510e2","415f1a03e4c4bd59","415f1a03e4c4bd59"]}, +{"id":"lit:../parser/json_precedence_test.go:124","input":"SELECT POSITION('a' || 'b' IN text_value)","digests":["5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea","5af435c6cc261aea"]}, +{"id":"lit:../parser/json_precedence_test.go:125","input":"SELECT POSITION(data -\u003e\u003e 'key' IN text_value)","digests":["bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2","bc08c4f89a3394e2"]}, +{"id":"lit:../parser/json_precedence_test.go:173","input":"SELECT * FROM events WHERE data @\u003e '{\"type\": \"click\"}'::jsonb","digests":["135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f","135447531be5d86f"]}, +{"id":"lit:../parser/json_precedence_test.go:174","input":"SELECT * FROM events WHERE data @\u003e '{\"type\": \"click\"}'::text::jsonb","digests":["ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc","ab7d1eae348a76bc"]}, +{"id":"lit:../parser/json_precedence_test.go:201","input":"SELECT * FROM events WHERE CAST(data @\u003e '{\"type\": \"click\"}' AS jsonb)","digests":["4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b","4fb185544aaebb8b"]}, +{"id":"lit:../parser/json_precedence_test.go:225","input":"expected cast of power's left operand, got %T","digests":["e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b","e99c6742775a304b"]}, +{"id":"lit:../parser/json_precedence_test.go:231","input":"SELECT data @\u003e CAST('x' AS jsonb)","digests":["3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f","3741863672f0ac1f"]}, +{"id":"lit:../parser/json_precedence_test.go:232","input":"SELECT data @\u003e CAST('x' AS text)::jsonb","digests":["fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e","fe0fe01a04b78f8e"]}, +{"id":"lit:../parser/json_precedence_test.go:232","input":"SELECT CAST(data @\u003e CAST('x' AS text) AS jsonb)","digests":["3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3","3678c56155b580c3"]}, +{"id":"lit:../parser/json_precedence_test.go:233","input":"SELECT data @\u003e ('x'::jsonb)","digests":["333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc","333b920966f364cc"]}, +{"id":"lit:../parser/json_precedence_test.go:233","input":"SELECT data @\u003e (CAST('x' AS jsonb))","digests":["89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204","89cd792af5486204"]}, {"id":"lit:../parser/keyword_identifiers_test.go:54","input":"SELECT 1 AS `exists`","digests":["d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307","d3789d3d92d07307"]}, {"id":"lit:../parser/keyword_identifiers_test.go:91","input":"SELECT key FROM settings WHERE key = 'a'","digests":["96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e","96ba9b16f079512e"]}, {"id":"lit:../parser/keyword_identifiers_test.go:142","input":"SELECT `key` FROM t","digests":["41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2","41b0fc33c413cef2"]}, @@ -793,6 +898,28 @@ {"id":"lit:../parser/keyword_identifiers_test.go:150","input":"SELECT \"select\" FROM t","digests":["0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c","0a1443a97889b25c"]}, {"id":"lit:../parser/keyword_identifiers_test.go:150","input":"\"select\"","digests":["084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd","084bf7ef6c65f2cd"]}, {"id":"lit:../parser/keyword_identifiers_test.go:175","input":"SELECT b `z` FROM t","digests":["3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c","3a2b7a92a3e4492c"]}, +{"id":"lit:../parser/literal_dialect_test.go:20","input":"U\u0026'd\\0061t\\+000061'","digests":["9c8c4b799b2ab092","65f749cfed52457c","05c401ad9b071f10","05c401ad9b071f10","53e23216552c4650","53e23216552c4650","53e23216552c4650","867918a6ff0e178f","79b9529af93ac4ec"]}, +{"id":"lit:../parser/literal_dialect_test.go:20","input":"U\u0026'd!0061t' UESCAPE '!'","digests":["01bd54cad507b1ab","3ad689d8cd5257c9","01bd54cad507b1ab","01bd54cad507b1ab","01bd54cad507b1ab","01bd54cad507b1ab","01bd54cad507b1ab","01bd54cad507b1ab","01bd54cad507b1ab"]}, +{"id":"lit:../parser/literal_dialect_test.go:39","input":"SELECT `","digests":["5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba","5f3c481ad208c1ba"]}, +{"id":"lit:../parser/literal_dialect_test.go:39","input":"`","digests":["04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786"]}, +{"id":"lit:../parser/literal_dialect_test.go:75","input":"SELECT U\u0026\"d\\0061t\".a","digests":["5aa5a0cbaa1b6423","d4565f3e02cd0b56","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423","5aa5a0cbaa1b6423"]}, +{"id":"lit:../parser/literal_dialect_test.go:76","input":"SELECT a FROM U\u0026\"t\"","digests":["68dd1550f9291abd","4b76333154d7930b","68dd1550f9291abd","68dd1550f9291abd","68dd1550f9291abd","68dd1550f9291abd","68dd1550f9291abd","68dd1550f9291abd","68dd1550f9291abd"]}, +{"id":"lit:../parser/literal_dialect_test.go:77","input":"SELECT a AS U\u0026\"x\"","digests":["7fbc923895f3f8d7","226fb3cec1ac1ec5","7fbc923895f3f8d7","7fbc923895f3f8d7","7fbc923895f3f8d7","7fbc923895f3f8d7","7fbc923895f3f8d7","7fbc923895f3f8d7","7fbc923895f3f8d7"]}, +{"id":"lit:../parser/literal_dialect_test.go:78","input":"SELECT x U\u0026\"y\" FROM t","digests":["50dd6cf68288bd83","003ff9b2e56cf87e","50dd6cf68288bd83","50dd6cf68288bd83","50dd6cf68288bd83","50dd6cf68288bd83","50dd6cf68288bd83","50dd6cf68288bd83","50dd6cf68288bd83"]}, +{"id":"lit:../parser/literal_dialect_test.go:79","input":"SELECT a FROM U\u0026\"d\\0061t\" AS U\u0026\"\\0041\"","digests":["8a0a381e8a4034ef","e8dd3c985873ed7a","8a0a381e8a4034ef","8a0a381e8a4034ef","8a0a381e8a4034ef","8a0a381e8a4034ef","8a0a381e8a4034ef","8a0a381e8a4034ef","8a0a381e8a4034ef"]}, +{"id":"lit:../parser/literal_dialect_test.go:79","input":"SELECT a FROM dat \"A\"","digests":["d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4","d26392ca937164b4"]}, +{"id":"lit:../parser/literal_dialect_test.go:80","input":"SELECT U\u0026\"A!0042\" UESCAPE '!'","digests":["ed916b227307d0f0","4f2c567c696d9ce8","ed916b227307d0f0","ed916b227307d0f0","ed916b227307d0f0","ed916b227307d0f0","ed916b227307d0f0","ed916b227307d0f0","ed916b227307d0f0"]}, +{"id":"lit:../parser/literal_dialect_test.go:80","input":"SELECT \"AB\"","digests":["0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b","0f1cb5d1be51175b"]}, +{"id":"lit:../parser/literal_dialect_test.go:81","input":"SELECT U\u0026\"a\"\"b\"","digests":["4af43265c6b73a1b","84604f1100fcdc90","4af43265c6b73a1b","4af43265c6b73a1b","4af43265c6b73a1b","4af43265c6b73a1b","4af43265c6b73a1b","4af43265c6b73a1b","4af43265c6b73a1b"]}, +{"id":"lit:../parser/literal_dialect_test.go:81","input":"SELECT \"a\"\"b\"","digests":["f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c","f1c901aa518b1b6c"]}, +{"id":"lit:../parser/literal_dialect_test.go:97","input":"SELECT U\u0026'x' /* c */ UESCAPE '!'","digests":["e8bc2bcc3479d4f1","89074e8d1f760847","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1","e8bc2bcc3479d4f1"]}, +{"id":"lit:../parser/literal_dialect_test.go:97","input":"SELECT U\u0026'x' -- c\n UESCAPE '!'","digests":["fad0ca894d78d9db","23762ea4f3efaf09","fad0ca894d78d9db","fad0ca894d78d9db","fad0ca894d78d9db","fad0ca894d78d9db","fad0ca894d78d9db","fad0ca894d78d9db","fad0ca894d78d9db"]}, +{"id":"lit:../parser/literal_dialect_test.go:105","input":"SELECT U\u0026'x' UESCAPE '!'","digests":["6d39021e00ac87dd","103f8ecf48965924","6d39021e00ac87dd","6d39021e00ac87dd","6d39021e00ac87dd","6d39021e00ac87dd","6d39021e00ac87dd","6d39021e00ac87dd","6d39021e00ac87dd"]}, +{"id":"lit:../parser/literal_dialect_test.go:115","input":"ALTER DATABASE DATAFILE 'f' RESIZE 100M","digests":["db6ddf6e6ac1b3c8","db6ddf6e6ac1b3c8","9c3e3adc5489e9fd","9c3e3adc5489e9fd","db6ddf6e6ac1b3c8","877b638e1b709644","db6ddf6e6ac1b3c8","db6ddf6e6ac1b3c8","db6ddf6e6ac1b3c8"]}, +{"id":"lit:../parser/literal_dialect_test.go:116","input":"CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M MAXSIZE 1G","digests":["48d941268af1f890","48d941268af1f890","3715cca1e6fd9fb7","3715cca1e6fd9fb7","48d941268af1f890","0f6e2be3a229019c","48d941268af1f890","48d941268af1f890","48d941268af1f890"]}, +{"id":"lit:../parser/literal_dialect_test.go:117","input":"ALTER DATABASE DATAFILE 'f' AUTOEXTEND ON MAXSIZE 2G","digests":["1aded3eccdf6d368","1aded3eccdf6d368","aa73219ff0a3c43d","aa73219ff0a3c43d","1aded3eccdf6d368","70d202f1d2d8a6cc","1aded3eccdf6d368","1aded3eccdf6d368","1aded3eccdf6d368"]}, +{"id":"lit:../parser/literal_dialect_test.go:118","input":"CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M BLOCKSIZE 8K","digests":["09792179f7428676","09792179f7428676","e2e2bee6e18b1901","e2e2bee6e18b1901","09792179f7428676","c3e49849c187fb62","09792179f7428676","09792179f7428676","09792179f7428676"]}, +{"id":"lit:../parser/literal_dialect_test.go:122","input":"CREATE TABLESPACE ts DATAFILE 'f' SIZE 10M AUTOEXTEND ON NEXT 5M MAXSIZE 1G","digests":["67ac5de6495e79ed","67ac5de6495e79ed","10e9933bca7b3b62","10e9933bca7b3b62","67ac5de6495e79ed","d3d67ff293d7bacd","67ac5de6495e79ed","67ac5de6495e79ed","67ac5de6495e79ed"]}, {"id":"lit:../parser/mariadb_bench_test.go:89","input":"SELECT * FROM t FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-01 00:00:00'","digests":["de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5","de156d8f1ef4feb5"]}, {"id":"lit:../parser/mariadb_bench_test.go:97","input":"SELECT * FROM t FOR SYSTEM_TIME BETWEEN TIMESTAMP '2023-01-01' AND TIMESTAMP '2023-12-31'","digests":["553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3","553793370a4258f3"]}, {"id":"lit:../parser/mariadb_bench_test.go:101","input":"SELECT * FROM t FOR SYSTEM_TIME FROM TIMESTAMP '2023-01-01' TO TIMESTAMP '2024-01-01'","digests":["1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2","1f2681615547a5e2"]}, @@ -823,7 +950,7 @@ {"id":"lit:../parser/mysql_issue521_review_test.go:61","input":"SELECT `my-db`.t FROM t","digests":["579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452","579047a690f2f452"]}, {"id":"lit:../parser/mysql_issue521_review_test.go:98","input":"a.\"\".b.c","digests":["287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db","287cb00e6eeaf5db"]}, {"id":"lit:../parser/mysql_issue521_review_test.go:118","input":"six-part reference: err = %v, want a 'more than 5 parts' error","digests":["7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28","7094d457f1741f28"]}, -{"id":"lit:../parser/mysql_issue521_review_test.go:130","input":"50k-part reference: err = %v, want a 'more than 5 parts' error","digests":["e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c","e28c4a051289c58c"]}, +{"id":"lit:../parser/mysql_issue521_review_test.go:130","input":"50k-part reference: err = %v, want a 'more than 5 parts' error","digests":["c610aa9a738a0617","c610aa9a738a0617","eeeb6886b781ecef","eeeb6886b781ecef","c610aa9a738a0617","e28c4a051289c58c","c610aa9a738a0617","c610aa9a738a0617","c610aa9a738a0617"]}, {"id":"lit:../parser/mysql_issue521_review_test.go:213","input":"SELECT * FROM (`a` JOIN `b` ON `a`.`id` = `b`.`id`) JOIN `c` ON `c`.`id` = `a`.`id`","digests":["f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5","f07eaae0976e04c5"]}, {"id":"lit:../parser/mysql_issue521_test.go:37","input":" `cu`.`customer_id` AS `ID`,\n","digests":["9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af","9d4ceab659e061af"]}, {"id":"lit:../parser/mysql_issue521_test.go:38","input":" CONCAT(`cu`.`first_name`, ' ', `cu`.`last_name`) AS `name`,\n","digests":["8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc","8f5b3ca40001ebfc"]}, @@ -854,7 +981,6 @@ {"id":"lit:../parser/mysql_issue521_test.go:228","input":"JOIN `sakila`.`address` `a` ON (`cu`.`address_id` = `a`.`address_id`) ","digests":["e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f","e9d8dc0f4142fa1f"]}, {"id":"lit:../parser/mysql_issue521_test.go:229","input":"JOIN `sakila`.`city` ON (`a`.`city_id` = `sakila`.`city`.`city_id`) ","digests":["d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05","d68b11e8496e9e05"]}, {"id":"lit:../parser/mysql_issue521_test.go:230","input":"JOIN `sakila`.`country` ON (`sakila`.`city`.`country_id` = `sakila`.`country`.`country_id`)","digests":["ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938","ab50cb1d9840a938"]}, -{"id":"lit:../parser/mysql_issue521_test.go:241","input":"`","digests":["04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786","04850a4242901786"]}, {"id":"lit:../parser/mysql_test.go:115","input":"SELECT `id`, `name` FROM `users`","digests":["edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6","edbc51e11097fbf6"]}, {"id":"lit:../parser/mysql_test.go:116","input":"SELECT `tbl`.`col` FROM `mydb`.`tbl`","digests":["f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e","f11531f7a58d5a0e"]}, {"id":"lit:../parser/mysql_test.go:117","input":"SELECT `select` FROM `from`","digests":["26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37","26f037bc6faf0b37"]}, @@ -865,6 +991,60 @@ {"id":"lit:../parser/mysql_test.go:248","input":"SELECT GROUP_CONCAT(name ORDER BY name SEPARATOR ', ') FROM users GROUP BY dept","digests":["78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8","78933056409158b8"]}, {"id":"lit:../parser/mysql_test.go:257","input":"SELECT * FROM articles WHERE MATCH(title, content) AGAINST('search term' IN NATURAL LANGUAGE MODE)","digests":["b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3","b804e898976511e3"]}, {"id":"lit:../parser/mysql_test.go:266","input":"SELECT * FROM users WHERE email REGEXP '^[a-z]+@[a-z]+$'","digests":["9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908","9ac4c824169bf908"]}, +{"id":"lit:../parser/name_quote_test.go:38","input":"SELECT \"users\".\"id\", \"Company\".\"name\" AS \"CompanyName\" FROM \"users\" JOIN \"companies\" AS \"Company\" ON \"Company\".\"id\" = \"users\".\"company_id\"","digests":["a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee","a534dbdec1799bee"]}, +{"id":"lit:../parser/name_quote_test.go:39","input":"SELECT users.id, \"Company\".name AS \"CompanyName\" FROM users INNER JOIN companies \"Company\" ON \"Company\".id = users.company_id","digests":["15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec","15759e82ad63ebec"]}, +{"id":"lit:../parser/name_quote_test.go:40","input":"SELECT \"Mixed\" FROM t","digests":["e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8","e4460868e579cba8"]}, +{"id":"lit:../parser/name_quote_test.go:41","input":"SELECT * FROM \"USERS\"","digests":["b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751","b219ddd0b9500751"]}, +{"id":"lit:../parser/name_quote_test.go:42","input":"SELECT * FROM \"Sales\".\"Orders\" o","digests":["652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf","652fd677c5ed44bf"]}, +{"id":"lit:../parser/name_quote_test.go:43","input":"SELECT \"s\".\"Orders\".\"Id\" FROM \"s\".\"Orders\"","digests":["9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff","9a453460c3d252ff"]}, +{"id":"lit:../parser/name_quote_test.go:43","input":"SELECT s.\"Orders\".\"Id\" FROM s.\"Orders\"","digests":["67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8","67dea1f7512ba4f8"]}, +{"id":"lit:../parser/name_quote_test.go:44","input":"SELECT \"T\".* FROM t AS \"T\"","digests":["048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3","048f847d6ac995f3"]}, +{"id":"lit:../parser/name_quote_test.go:44","input":"SELECT \"T\".* FROM t \"T\"","digests":["b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f","b35962cf20269b3f"]}, +{"id":"lit:../parser/name_quote_test.go:46","input":"WITH \"Cte\"(\"Total\", n) AS (SELECT 1, 2) SELECT \"Total\" FROM \"Cte\"","digests":["675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10","675a1adc31012d10"]}, +{"id":"lit:../parser/name_quote_test.go:47","input":"WITH \"Cte\" (\"Total\", n) AS (SELECT 1, 2) SELECT \"Total\" FROM \"Cte\"","digests":["3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab","3facb8fe1b84b0ab"]}, +{"id":"lit:../parser/name_quote_test.go:49","input":"SELECT s.\"Total\" FROM (SELECT amount AS \"Total\" FROM t) AS s","digests":["40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2","40faf669a54977a2"]}, +{"id":"lit:../parser/name_quote_test.go:50","input":"SELECT s.\"Total\" FROM (SELECT amount AS \"Total\" FROM t) s","digests":["80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679","80b58dd197053679"]}, +{"id":"lit:../parser/name_quote_test.go:52","input":"SELECT s.\"select\"(1), \"MySchema\".\"MyFunc\"(1), \"MyFunc\"(2), pg_catalog.now()","digests":["817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0","817aafd2f5f9cca0"]}, +{"id":"lit:../parser/name_quote_test.go:55","input":"SELECT \"G\".\"Elem\" FROM unnest(arr) AS \"G\"(\"Elem\")","digests":["fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05","fb0bb42cb849ef05"]}, +{"id":"lit:../parser/name_quote_test.go:56","input":"SELECT \"G\".\"Elem\" FROM unnest(arr) \"G\"(\"Elem\")","digests":["096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf","096f395f2e5581cf"]}, +{"id":"lit:../parser/name_quote_test.go:58","input":"SELECT SUM(x) OVER \"W\", SUM(y) OVER (\"W\" ORDER BY y) FROM t WINDOW \"W\" AS (ORDER BY x), \"W2\" AS (\"W\")","digests":["6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58","6781c2780b49fe58"]}, +{"id":"lit:../parser/name_quote_test.go:60","input":"SELECT * FROM a JOIN b USING (\"Id\", id2)","digests":["8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551","8fce1fbfbfc64551"]}, +{"id":"lit:../parser/name_quote_test.go:60","input":"SELECT * FROM a INNER JOIN b USING (\"Id\", id2)","digests":["fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8","fdeafe2323bb8cb8"]}, +{"id":"lit:../parser/name_quote_test.go:61","input":"INSERT INTO \"MyTable\" (\"ColA\", colb) VALUES (1, 2)","digests":["1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441","1d1765db7820d441"]}, +{"id":"lit:../parser/name_quote_test.go:63","input":"INSERT INTO t (a) VALUES (1) ON CONFLICT (\"Key\") DO NOTHING","digests":["73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e","73e1d48f05ac707e"]}, +{"id":"lit:../parser/name_quote_test.go:66","input":"UPDATE \"MyTable\" AS \"T\" SET \"ColA\" = 1, \"T\".\"ColB\" = 2 WHERE \"T\".\"ColC\" = 3","digests":["4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0","4f281e4889c669d0"]}, +{"id":"lit:../parser/name_quote_test.go:67","input":"UPDATE \"MyTable\" \"T\" SET \"ColA\" = 1, \"T\".\"ColB\" = 2 WHERE \"T\".\"ColC\" = 3","digests":["4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b","4697debc21bd5a0b"]}, +{"id":"lit:../parser/name_quote_test.go:68","input":"DELETE FROM \"MyTable\" AS \"T\" WHERE \"T\".id = 1","digests":["cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b","cecbec0834d0125b"]}, +{"id":"lit:../parser/name_quote_test.go:68","input":"DELETE FROM \"MyTable\" \"T\" WHERE \"T\".id = 1","digests":["a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8","a0c957d9f7026ea8"]}, +{"id":"lit:../parser/name_quote_test.go:70","input":"MERGE INTO \"T\" AS \"Tgt\" USING \"S\" \"Src\" ON \"Tgt\".id = \"Src\".id WHEN MATCHED THEN UPDATE SET \"Val\" = \"Src\".\"Val\" WHEN NOT MATCHED THEN INSERT (\"Id\", b) VALUES (1, 2)","digests":["5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad","5090042324fa15ad"]}, +{"id":"lit:../parser/name_quote_test.go:71","input":"MERGE INTO \"T\" \"Tgt\" USING \"S\" \"Src\" ON \"Tgt\".id = \"Src\".id WHEN MATCHED THEN UPDATE SET \"Val\" = \"Src\".\"Val\" WHEN NOT MATCHED THEN INSERT (\"Id\", b) VALUES (1, 2)","digests":["d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1","d3db8eba8d4ab3a1"]}, +{"id":"lit:../parser/name_quote_test.go:72","input":"CREATE TABLE \"MyTable\" (\"ColA\" int, \"order\" text, b int)","digests":["e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f","e2e60499a2b2c95f"]}, +{"id":"lit:../parser/name_quote_test.go:73","input":"CREATE VIEW \"V\" (\"A\", b) AS SELECT 1, 2","digests":["0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035","0d54458f8a1b9035"]}, +{"id":"lit:../parser/name_quote_test.go:74","input":"CREATE MATERIALIZED VIEW \"MV\" (\"A\", b) AS SELECT 1, 2","digests":["84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e","84e1e3b726d5a03e"]}, +{"id":"lit:../parser/name_quote_test.go:76","input":"CREATE TABLE \"Post\" (\"id\" SERIAL NOT NULL, \"authorId\" INTEGER REFERENCES \"User\"(\"Id\"), CONSTRAINT \"Post_pkey\" PRIMARY KEY (\"id\"), CONSTRAINT \"Post_fk\" FOREIGN KEY (\"authorId\") REFERENCES \"User\"(\"Id\"))","digests":["a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a","a364cb24eef7080a"]}, +{"id":"lit:../parser/name_quote_test.go:77","input":"CREATE TABLE \"Post\" (id SERIAL NOT NULL, \"authorId\" INTEGER REFERENCES \"User\" (\"Id\"), CONSTRAINT \"Post_pkey\" PRIMARY KEY (id), CONSTRAINT \"Post_fk\" FOREIGN KEY (\"authorId\") REFERENCES \"User\" (\"Id\"))","digests":["9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6","9c0e7a607e1ca8a6"]}, +{"id":"lit:../parser/name_quote_test.go:79","input":"CREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"Email\", name)","digests":["26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a","26c19191f26d561a"]}, +{"id":"lit:../parser/name_quote_test.go:80","input":"CREATE UNIQUE INDEX \"User_email_key\" ON \"User\" (\"Email\", name)","digests":["4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74","4085396458a69c74"]}, +{"id":"lit:../parser/name_quote_test.go:81","input":"DROP TABLE \"Post\", other, \"x y\"","digests":["d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5","d574cbb005aaa9b5"]}, +{"id":"lit:../parser/name_quote_test.go:82","input":"TRUNCATE \"Post\", other","digests":["1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6","1f3b545285b3d5e6"]}, +{"id":"lit:../parser/name_quote_test.go:82","input":"TRUNCATE TABLE \"Post\", other","digests":["9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98","9ca4bc1d1c6c5e98"]}, +{"id":"lit:../parser/name_quote_test.go:85","input":"SELECT 1 AS \"select\", 2 AS \"order\", \"user\", \"group\" FROM \"table\"","digests":["4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8","4d015a1894ac07b8"]}, +{"id":"lit:../parser/name_quote_test.go:87","input":"SELECT \"a\".\"b\" AS \"c\" FROM \"t\" AS \"a\"","digests":["4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2","4e3dbc216572a9f2"]}, +{"id":"lit:../parser/name_quote_test.go:90","input":"SELECT \"ID\", \"NAME\", \"CreatedAt\", \"lower\" FROM \"AppUsers\" WHERE \"ID\" = :1","digests":["a9f1a34041daf28f","a9f1a34041daf28f","a9f1a34041daf28f","a9f1a34041daf28f","a9f1a34041daf28f","27514c1ab22045b6","27514c1ab22045b6","a9f1a34041daf28f","a9f1a34041daf28f"]}, +{"id":"lit:../parser/name_quote_test.go:91","input":"SELECT ID, NAME, \"CreatedAt\", \"lower\" FROM \"AppUsers\" WHERE ID = :1","digests":["4f11446e1f892054","4f11446e1f892054","4f11446e1f892054","4f11446e1f892054","4f11446e1f892054","9d5a136750ffb792","9d5a136750ffb792","4f11446e1f892054","4f11446e1f892054"]}, +{"id":"lit:../parser/name_quote_test.go:92","input":"SELECT a AS \"total\", b AS \"TOTAL\" FROM t","digests":["eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e","eac45d92fb09d82e"]}, +{"id":"lit:../parser/name_quote_test.go:92","input":"SELECT a AS \"total\", b AS TOTAL FROM t","digests":["e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346","e5b12304b9adb346"]}, +{"id":"lit:../parser/name_quote_test.go:94","input":"SELECT id FROM t CONNECT BY PRIOR id = parent_id AND \"PRIOR\" = 1","digests":["535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb","535af9ea90f9e3cb"]}, +{"id":"lit:../parser/name_quote_test.go:98","input":"SELECT \"a#b\" FROM t","digests":["460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0","460fb9d2e2c05ff0"]}, +{"id":"lit:../parser/name_quote_test.go:102","input":"SELECT EXISTS(SELECT * FROM `users` WHERE `email` = 'a') AS `exists`","digests":["b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f","b498fcbba3d89b6f"]}, +{"id":"lit:../parser/name_quote_test.go:103","input":"SELECT EXISTS (SELECT * FROM users WHERE email = 'a') AS `exists`","digests":["c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a","c5c06c9f99241b2a"]}, +{"id":"lit:../parser/name_quote_test.go:105","input":"SELECT 1 AS `select`, `Company`.`x` FROM `order` `group`, `Company`","digests":["93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28","93e93ed78a968a28"]}, +{"id":"lit:../parser/name_quote_test.go:106","input":"SELECT 1 AS `select`, Company.x FROM `order` `group`, Company","digests":["a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88","a40022b0273e4f88"]}, +{"id":"lit:../parser/name_quote_test.go:107","input":"INSERT INTO t (`select`) VALUES (1)","digests":["7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6","7b527f9505884aa6"]}, +{"id":"lit:../parser/name_quote_test.go:137","input":"SELECT \"S\".\"T\".\"Col\" AS \"A\", x.\"y\" FROM \"S\".\"Tbl\" AS \"B\"","digests":["9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c","9b854b0c7632cc6c"]}, +{"id":"lit:../parser/name_quote_test.go:151","input":"AliasQuote = %08b, want part 0 with '\"'","digests":["97a1ded67c798027","97a1ded67c798027","7f045c9f55563cac","7f045c9f55563cac","97a1ded67c798027","97a1ded67c798027","97a1ded67c798027","97a1ded67c798027","97a1ded67c798027"]}, +{"id":"lit:../parser/name_quote_test.go:167","input":"x.\"y\" Quotes = %08b, want 0 (lower-case quotes are not needed)","digests":["d1204bfb829676d5","d1204bfb829676d5","d862cb76a9672bac","d862cb76a9672bac","d1204bfb829676d5","d1204bfb829676d5","d1204bfb829676d5","d1204bfb829676d5","d1204bfb829676d5"]}, +{"id":"lit:../parser/name_quote_test.go:181","input":"SELECT \"A\".\"B\" AS \"C\", \"F\"(1) FROM \"T\" AS \"U\" JOIN \"V\" \"W\" ON \"U\".x = \"W\".x","digests":["32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff","32e6bf1d89bf2cff"]}, {"id":"lit:../parser/negative_parser_test.go:418","input":"SELECT 'hello FROM users","digests":["d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50","d36edf4527f1ef50"]}, {"id":"lit:../parser/negative_parser_test.go:503","input":"'widget'","digests":["f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e","f728bac616b1410e"]}, {"id":"lit:../parser/negative_parser_test.go:647","input":"'hello'","digests":["afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65","afb870809d3e3b65"]}, @@ -1134,8 +1314,63 @@ {"id":"lit:../parser/qualified_identifier_test.go:59","input":"SELECT `sakila`.`city`.`city` FROM t","digests":["0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89","0172928f48e32c89"]}, {"id":"lit:../parser/qualified_identifier_test.go:141","input":"got Table=%q Name=%q, want Table=\"a.b\" Name=\"c\"","digests":["d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164","d1f232f362f8c164"]}, {"id":"lit:../parser/qualified_name_issue613_test.go:150","input":"CREATE STAGE a.b.c.d.e.f.g URL='s3://bucket/x'","digests":["2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f","2b3010f5e968b37f"]}, -{"id":"lit:../parser/qualified_name_issue613_test.go:174","input":"CREATE STAGE a.b.c URL='s3://bucket/x'","digests":["1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44"]}, -{"id":"lit:../parser/qualified_name_issue613_test.go:215","input":"Parse(%q) error = %v, want 'identifier after .'","digests":["d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08"]}, +{"id":"lit:../parser/qualified_name_issue613_test.go:175","input":"CREATE STAGE a.b.c URL='s3://bucket/x'","digests":["1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44","1003425016ba5f44"]}, +{"id":"lit:../parser/qualified_name_issue613_test.go:216","input":"Parse(%q) error = %v, want 'identifier after .'","digests":["d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08","d37d1c7aee080d08"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:20","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING XMLPARSE(DOCUMENT E'\u003crows/\u003e') COLUMNS id int PATH 'id')","digests":["eaf1fbc2437e0640","4d337b5ecb124869","eaf1fbc2437e0640","eaf1fbc2437e0640","eaf1fbc2437e0640","eaf1fbc2437e0640","eaf1fbc2437e0640","eaf1fbc2437e0640","eaf1fbc2437e0640"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:20","input":"DOCUMENT E'\u003crows/\u003e'","digests":["171f4604e0b89d93","ea8cf59e5001a9f7","171f4604e0b89d93","171f4604e0b89d93","171f4604e0b89d93","171f4604e0b89d93","171f4604e0b89d93","171f4604e0b89d93","171f4604e0b89d93"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:21","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING XMLPARSE(DOCUMENT $$\u003crows/\u003e$$) COLUMNS id int PATH 'id')","digests":["e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea","e5b45028ae35eeea"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:22","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING XMLPARSE(DOCUMENT E'') COLUMNS id int PATH '')","digests":["52d0f6fcdc4ab4da","35b443dc19f1dc47","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da","52d0f6fcdc4ab4da"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:22","input":"DOCUMENT E''","digests":["84281ed6895834ff","de707d44c354650f","84281ed6895834ff","84281ed6895834ff","84281ed6895834ff","84281ed6895834ff","84281ed6895834ff","84281ed6895834ff","84281ed6895834ff"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:23","input":"SELECT * FROM JSON_TABLE(doc, '$[*]' COLUMNS (name VARCHAR(50) PATH '$.name' DEFAULT N'unknown' ON EMPTY)) jt","digests":["5b1076b8667dff2c","c82dc1802decfc12","c82dc1802decfc12","c82dc1802decfc12","c82dc1802decfc12","c82dc1802decfc12","5b1076b8667dff2c","5b1076b8667dff2c","5b1076b8667dff2c"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:23","input":"DEFAULT N'unknown' ON EMPTY","digests":["26fd9593fdf3f500","3b9ed704c77cbf66","3b9ed704c77cbf66","3b9ed704c77cbf66","3b9ed704c77cbf66","3b9ed704c77cbf66","26fd9593fdf3f500","26fd9593fdf3f500","26fd9593fdf3f500"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:24","input":"SELECT * FROM JSON_TABLE(doc, '$[*]' COLUMNS (name VARCHAR(50) PATH '$.name' DEFAULT N'' ON EMPTY)) jt","digests":["82855a702b053885","7823b0fe3144de41","7823b0fe3144de41","7823b0fe3144de41","7823b0fe3144de41","7823b0fe3144de41","82855a702b053885","82855a702b053885","82855a702b053885"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:24","input":"DEFAULT N'' ON EMPTY","digests":["7b0bbce971945c9d","79be6c8615da695a","79be6c8615da695a","79be6c8615da695a","79be6c8615da695a","79be6c8615da695a","7b0bbce971945c9d","7b0bbce971945c9d","7b0bbce971945c9d"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:25","input":"SELECT * FROM JSON_TABLE(doc, '$[*]' COLUMNS (name VARCHAR(50) PATH '$.name' DEFAULT _utf8mb4'text' ON EMPTY)) jt","digests":["7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e","7fcb11b6c0b9277e"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:25","input":"DEFAULT _utf8mb4'text' ON EMPTY","digests":["b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5","b0722f0f3ad190d5"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:26","input":"SELECT * FROM JSON_TABLE(doc, '$' COLUMNS (name VARCHAR2(50) PATH '$.name' DEFAULT q'[it's text]' ON EMPTY)) jt","digests":["2b36ede8164eab0e","2b36ede8164eab0e","2b36ede8164eab0e","2b36ede8164eab0e","2b36ede8164eab0e","d1992899b90f010a","2b36ede8164eab0e","2b36ede8164eab0e","2b36ede8164eab0e"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:26","input":"DEFAULT 'it''s text' ON EMPTY","digests":["2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d","2e3db0c125866b7d"]}, +{"id":"lit:../parser/raw_clause_literal_regression_test.go:52","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING XMLPARSE(DOCUMENT E'back\\\\slash') COLUMNS id int PATH 'id')","digests":["a6d8bd4a62d7c9cd","b2ca4549f5b49918","a6d8bd4a62d7c9cd","a6d8bd4a62d7c9cd","a85dc085b1d2d667","a85dc085b1d2d667","a85dc085b1d2d667","a6d8bd4a62d7c9cd","a6d8bd4a62d7c9cd"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:60","input":"VALUES (1, 'a'), (2, 'b')","digests":["fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264","fa044fd9fdece264"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:128","input":"SELECT @ \"X\" FROM t","digests":["412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b","412c3383d002d14b"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:129","input":"SELECT @ E'x'","digests":["dfda25027bc50a03","54f0ddde6c3e05f7","dfda25027bc50a03","dfda25027bc50a03","dfda25027bc50a03","dfda25027bc50a03","dfda25027bc50a03","dfda25027bc50a03","dfda25027bc50a03"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:182","input":"SELECT 1 FROM t WHERE x = BINARY 'a@b.c'","digests":["c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847","c478edaabb209847"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:183","input":"SELECT x FROM t WHERE x REGEXP BINARY 'A'","digests":["da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3","da9f480adbfbaae3"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:218","input":"CREATE TABLE o (a INT, FOREIGN KEY (a) REFERENCES \"Sch\".\"Tbl\" (id))","digests":["76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379","76121c5a1bc26379"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:241","input":"SELECT a AS 'alias with space' FROM t","digests":["b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248","b1754c3ce5743248"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:241","input":"SELECT a AS `alias with space` FROM t","digests":["d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed","d650c4038625d6ed"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:242","input":"SELECT a AS 'plain' FROM t","digests":["f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2","f6837183ac7e6db2"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:243","input":"SELECT a AS 'x y' FROM t","digests":["874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5","874105b6d8c473c5"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:243","input":"SELECT a AS `x y` FROM t","digests":["21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8","21fd8447161f4df8"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:244","input":"SELECT a AS \"alias with space\" FROM t","digests":["b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e","b809cbc128ce3a7e"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:246","input":"SELECT a AS 'x' FROM t","digests":["246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67","246dc84d0ab7ce67"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:253","input":"SELECT a FROM employees VERSIONS BETWEEN TIMESTAMP SYSTIMESTAMP - INTERVAL '1' DAY AND SYSTIMESTAMP WHERE employee_id = 100","digests":["d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469","d121d42868473469"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:339","input":"SELECT jt.* FROM events, JSON_TABLE(payload, '$.items[*]' COLUMNS (id INT PATH '$.id', name VARCHAR(50) PATH '$.name')) AS jt","digests":["82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3","82e87bc4b902eec3"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:340","input":"SELECT jt.* FROM events, JSON_TABLE(payload, '$.items[*]' COLUMNS (id INT PATH '$.id', name VARCHAR(50) PATH '$.name')) jt","digests":["3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f","3e7a547f4f28da7f"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:342","input":"SELECT o.id FROM orders o, JSON_TABLE(o.items, '$[*]' COLUMNS (rn FOR ORDINALITY, qty INT PATH '$.qty' DEFAULT '1' ON EMPTY)) AS jt","digests":["025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269","025084fd97819269"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:343","input":"SELECT o.id FROM orders o, JSON_TABLE(o.items, '$[*]' COLUMNS (rn FOR ORDINALITY, qty INT PATH '$.qty' DEFAULT '1' ON EMPTY)) jt","digests":["127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43","127308bb2cb10f43"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:345","input":"SELECT * FROM JSON_TABLE(:doc, '$.items[*]' COLUMNS (id NUMBER PATH '$.id')) jt","digests":["fab9715c4629dfdd","fab9715c4629dfdd","fab9715c4629dfdd","fab9715c4629dfdd","fab9715c4629dfdd","20da23f531064d1b","20da23f531064d1b","fab9715c4629dfdd","fab9715c4629dfdd"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:348","input":"SELECT * FROM JSON_TABLE(d, '$' COLUMNS (a INT PATH '$.a')) jt","digests":["0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384","0453aa21f85a3384"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:365","input":"COLUMNS (a INT PATH '$.a')","digests":["880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b","880d1255e3d1f00b"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:373","input":"DELETE HISTORY FROM accounts BEFORE SYSTEM_TIME '2024-01-01 00:00:00'","digests":["b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163","b1adf5e23ea10163"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:391","input":"DELETE HISTORY FROM accounts BEFORE SYSTEM_TIME '2024-01-01'","digests":["188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394","188e1991a8036394"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:407","input":"SELECT COLUMNS('a|b') APPLY (sum) FROM t","digests":["ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7","ab37d782f2f778d7"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:411","input":"SELECT COLUMNS('a') EXCEPT (b) APPLY(avg) FROM t","digests":["487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58","487a9d3ca6b0ba58"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:411","input":"SELECT COLUMNS('a') EXCEPT (b) APPLY (avg) FROM t","digests":["184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61","184b27e363ca9a61"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:418","input":"SELECT {'a': 1, 'b': 'two'} AS obj, [1, 2, 3] AS arr","digests":["01ad3f88fdeed0ae","01ad3f88fdeed0ae","01ad3f88fdeed0ae","01ad3f88fdeed0ae","a3e1082bdcf69c5e","01ad3f88fdeed0ae","a3e1082bdcf69c5e","01ad3f88fdeed0ae","01ad3f88fdeed0ae"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:421","input":"SELECT {'k': {'n': [1, 2]}, 'f': f(x) + 1} FROM t","digests":["8369bf6e51866eb5","8369bf6e51866eb5","8369bf6e51866eb5","8369bf6e51866eb5","ed7c74809c6a6ad4","8369bf6e51866eb5","ed7c74809c6a6ad4","8369bf6e51866eb5","8369bf6e51866eb5"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:423","input":"SELECT {'a': 1, 'b': 2}","digests":["1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931","1b72724da3346931"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:441","input":"SELECT * FROM OPENROWSET(BULK 'C:\\data\\file.csv', SINGLE_CLOB) AS x","digests":["f7362d766d801c97","312cd38cfc30c2e6","164b166c46df3921","164b166c46df3921","312cd38cfc30c2e6","312cd38cfc30c2e6","312cd38cfc30c2e6","0c0781d2a8496ff2","1bafcd0d4beaa4ac"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:442","input":"SELECT * FROM OPENROWSET(BULK 'C:\\data\\file.csv', SINGLE_CLOB) x","digests":["df30926c41ca346a","f02a664632a432b9","1ae253bbe35d19dd","1ae253bbe35d19dd","f02a664632a432b9","f02a664632a432b9","f02a664632a432b9","bd8f081fc96b28fb","f8a4768c59751a7c"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:444","input":"SELECT STUFF((SELECT N',' + Name FROM dbo.Customers FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, N'')","digests":["6dbcd5fd870f2513","edd0aefcf943985b","edd0aefcf943985b","edd0aefcf943985b","edd0aefcf943985b","edd0aefcf943985b","6dbcd5fd870f2513","6dbcd5fd870f2513","6dbcd5fd870f2513"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:447","input":"SELECT (SELECT 1 FOR XML PATH, TYPE).query('a').exist('b')","digests":["f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549","f1d4860b4198d549"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:451","input":"SELECT (x).value('.', 'int')","digests":["4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb","4495cbad938b39fb"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:474","input":"INSERT INTO dest SELECT * FROM src LOG ERRORS INTO err$_dest ('nightly load') REJECT LIMIT UNLIMITED","digests":["0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c","0e32fe6bd27a976c"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:557","input":"SELECT JSON_OBJECT('id' VALUE employee_id, KEY 'name' VALUE last_name, 'x': 1, 'p', 2) FROM employees","digests":["ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff","ad4c892f5d431dff"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:560","input":"SELECT XMLELEMENT(\"emp\", XMLATTRIBUTES(employee_id AS \"id\", x AS y), last_name), XMLFOREST(a AS b) FROM employees","digests":["d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49","d60e799848405d49"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:563","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING XMLTYPE(:x) COLUMNS id NUMBER PATH 'id', nm VARCHAR2(50) PATH 'name')","digests":["be14b0aa35a555ff","be14b0aa35a555ff","be14b0aa35a555ff","be14b0aa35a555ff","be14b0aa35a555ff","d36f65a5be63f79a","d36f65a5be63f79a","be14b0aa35a555ff","be14b0aa35a555ff"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:566","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING data COLUMNS id int PATH '@id') AS t","digests":["a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e","a48d6e7cbc8c192e"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:567","input":"SELECT * FROM XMLTABLE('/rows/row' PASSING data COLUMNS id int PATH '@id') t","digests":["238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459","238c133e6aabb459"]}, +{"id":"lit:../parser/render_syntax_w3_test.go:569","input":"SELECT * FROM sales_hist PARTITION FOR (TO_DATE('2024-05-01', 'YYYY-MM-DD')) s","digests":["62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62","62864e30ce5a4c62"]}, {"id":"lit:../parser/schema_qualified_test.go:164","input":"INSERT INTO public.users (name) VALUES ('test')","digests":["df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8","df2fbe50db5deda8"]}, {"id":"lit:../parser/schema_qualified_test.go:207","input":"UPDATE public.users SET name = 'test'","digests":["1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3","1e19ca9eb2d37be3"]}, {"id":"lit:../parser/schema_qualified_test.go:212","input":"UPDATE myschema.users SET name = 'test' WHERE id = 1","digests":["b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d","b69d36af78b67d9d"]}, @@ -1151,7 +1386,7 @@ {"id":"lit:../parser/select_ext_test.go:381","input":"SELECT * FROM t WHERE c = @c OPTION (MAXDOP 1, RECOMPILE, OPTIMIZE FOR (@c UNKNOWN), LABEL = 'x')","digests":["e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec","e28415a5730576ec"]}, {"id":"lit:../parser/select_ext_test.go:411","input":"SELECT 'ok' FORMAT Null","digests":["7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b","7a5ae92237628a1b"]}, {"id":"lit:../parser/select_ext_test.go:536","input":"SELECT a FROM t INTO OUTFILE 'x'","digests":["44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d","44bb34e1975b6d0d"]}, -{"id":"lit:../parser/set_operations_test.go:307","input":"Expected CTE name 'regional', got '%s'","digests":["e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0"]}, +{"id":"lit:../parser/set_operations_test.go:305","input":"Expected CTE name 'regional', got '%s'","digests":["e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0","e7e2f162ff72e1d0"]}, {"id":"lit:../parser/snowflake_create_objects_test.go:19","input":"CREATE STAGE my_stage URL='s3://bucket/path' CREDENTIALS=(AWS_KEY_ID='abc' AWS_SECRET_KEY='xyz')","digests":["36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81","36a8dd24048f4f81"]}, {"id":"lit:../parser/snowflake_create_objects_test.go:21","input":"CREATE FILE FORMAT my_csv TYPE = CSV FIELD_DELIMITER = ','","digests":["4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96","4dcffea8a7cf1c96"]}, {"id":"lit:../parser/snowflake_create_objects_test.go:25","input":"CREATE TASK daily_refresh WAREHOUSE = compute_wh SCHEDULE = 'USING CRON 0 0 * * * UTC' AS INSERT INTO t SELECT 1","digests":["c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07","c64d99ec4caaeb07"]}, @@ -1201,9 +1436,33 @@ {"id":"lit:../parser/snowflake_time_travel_test.go:25","input":"SELECT t.id FROM users AT (TIMESTAMP =\u003e '2024-01-01'::TIMESTAMP) t WHERE t.id = 1","digests":["70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f","70476418f7b3e66f"]}, {"id":"lit:../parser/snowflake_variant_path_test.go:27","input":"SELECT col:\"weird key\" FROM t","digests":["1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b","1198402422f1742b"]}, {"id":"lit:../parser/snowflake_variant_path_test.go:28","input":"SELECT id FROM t WHERE payload:status::string = 'active'","digests":["e904fcd5aa05483a","e904fcd5aa05483a","e904fcd5aa05483a","e904fcd5aa05483a","e904fcd5aa05483a","71055b35e8784196","71055b35e8784196","e904fcd5aa05483a","e904fcd5aa05483a"]}, +{"id":"lit:../parser/statement_boundary_test.go:60","input":"SELECT a FROM t\nPRINT 'done'","digests":["a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6","a3901bfdb2295ca6"]}, +{"id":"lit:../parser/statement_boundary_test.go:62","input":"SELECT a FROM t WHERE x = 1 PRINT 'done'","digests":["09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a","09fb36e42f03c64a"]}, +{"id":"lit:../parser/statement_boundary_test.go:76","input":"SELECT 1\nGO\nSELECT 2\nGO 3\nPRINT 'x'","digests":["fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d","fa09bbd44523075d"]}, +{"id":"lit:../parser/statement_boundary_test.go:112","input":"IF OBJECT_ID(N'dbo.t') IS NOT NULL DROP TABLE dbo.t\nCREATE TABLE dbo.t (a INT)","digests":["c24bf6f00c43b8e8","63c45c75625bb682","63c45c75625bb682","63c45c75625bb682","63c45c75625bb682","63c45c75625bb682","c24bf6f00c43b8e8","c24bf6f00c43b8e8","c24bf6f00c43b8e8"]}, +{"id":"lit:../parser/statement_boundary_test.go:117","input":"IF @x = 1 PRINT 'a'; ELSE PRINT 'b'; SELECT 3","digests":["a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35","a067e8d8984dda35"]}, +{"id":"lit:../parser/statement_boundary_test.go:124","input":"IF @a = 1 IF @b = 1 PRINT 'ab' ELSE PRINT 'a'\nSELECT 1","digests":["3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01","3aac24aed11f1c01"]}, +{"id":"lit:../parser/statement_boundary_test.go:191","input":"SELECT a FROM t\nPRINT 'x'","digests":["0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3","0131937c1f9ec9a3"]}, +{"id":"lit:../parser/statement_boundary_test.go:209","input":"IF OBJECT_ID(N'x') IS NOT NULL DROP TABLE x\nCREATE TABLE x (a INT)","digests":["21dd46ffabf93a8e","717391a4e0dd4926","717391a4e0dd4926","717391a4e0dd4926","717391a4e0dd4926","717391a4e0dd4926","21dd46ffabf93a8e","21dd46ffabf93a8e","21dd46ffabf93a8e"]}, +{"id":"lit:../parser/statement_boundary_test.go:210","input":"IF OBJECT_ID(N'x') IS NOT NULL DROP TABLE x","digests":["8a3e2c984614e1be","c784839c41718855","c784839c41718855","c784839c41718855","c784839c41718855","c784839c41718855","8a3e2c984614e1be","8a3e2c984614e1be","8a3e2c984614e1be"]}, +{"id":"lit:../parser/statement_boundary_test.go:211","input":"IF @a = 1 PRINT 'a'; ELSE PRINT 'b'; SELECT 1","digests":["324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06","324459d362996e06"]}, +{"id":"lit:../parser/statement_boundary_test.go:211","input":"IF @a = 1 PRINT 'a'; ELSE PRINT 'b'","digests":["defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c","defdb75f8d958f4c"]}, +{"id":"lit:../parser/string_literal_cache_test.go:54","input":"'a\\\\b'","digests":["64ccbac76fec8607","226fd60161712292","64ccbac76fec8607","64ccbac76fec8607","226fd60161712292","226fd60161712292","226fd60161712292","64ccbac76fec8607","64ccbac76fec8607"]}, +{"id":"lit:../parser/string_literal_cache_test.go:56","input":"'a\\b'","digests":["c2cdf4f25a1e6062","99d4094c1ece9dab","81f332235c61b8be","81f332235c61b8be","99d4094c1ece9dab","99d4094c1ece9dab","99d4094c1ece9dab","81f332235c61b8be","81f332235c61b8be"]}, {"id":"lit:../parser/sustained_load_test.go:248","input":"INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com')","digests":["995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e","995bc33f3f25437e"]}, {"id":"lit:../parser/sustained_load_test.go:249","input":"UPDATE users SET email = 'newemail@example.com' WHERE id = 1","digests":["146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489","146cd07b3e583489"]}, {"id":"lit:../parser/sustained_load_test.go:581","input":"SELECT\n\t\t\tdept,\n\t\t\tname,\n\t\t\tsalary,\n\t\t\tROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) as dept_rank,\n\t\t\tRANK() OVER (ORDER BY salary DESC) as global_rank,\n\t\t\tLAG(salary, 1) OVER (PARTITION BY dept ORDER BY salary DESC) as prev_salary,\n\t\t\tAVG(salary) OVER (PARTITION BY dept) as dept_avg_salary\n\t\tFROM employees\n\t\tWHERE hire_date \u003e '2020-01-01'","digests":["3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12","3785e1c09c48fb12"]}, +{"id":"lit:../parser/table_factor_test.go:280","input":"SELECT * FROM t FOR SYSTEM_TIME CONTAINED IN ('2024-01-01', '2025-01-01') AS x","digests":["a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941","a5dc030a0258f941"]}, +{"id":"lit:../parser/table_factor_test.go:296","input":"SELECT * FROM (VALUES (1, 'a'), (2, 'b')) AS v(id, name)","digests":["f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939","f8fbd2febe584939"]}, +{"id":"lit:../parser/table_factor_test.go:319","input":"SELECT * FROM STRING_SPLIT('a,b', ',') s","digests":["578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00","578c7d808643ea00"]}, +{"id":"lit:../parser/table_factor_test.go:324","input":"SELECT * FROM json_each('[1]')","digests":["190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0","190cba48300521d0"]}, +{"id":"lit:../parser/table_factor_test.go:397","input":"WITH b (id) AS (SELECT 1 FROM dual) SEARCH BREADTH FIRST BY id SET ord CYCLE id SET c TO 'Y' DEFAULT 'N' SELECT * FROM b","digests":["5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d","5cbebb8f623fb05d"]}, +{"id":"lit:../parser/table_factor_test.go:461","input":"SELECT * FROM m PIVOT (SUM(a) FOR q IN ('A')) AS p (k, a)","digests":["c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3","c1ab4b43c0c480a3"]}, +{"id":"lit:../parser/table_factor_test.go:466","input":"SELECT * FROM w UNPIVOT EXCLUDE NULLS ((u, r) FOR p IN ((u1, r1) AS 'P1'))","digests":["fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b","fef34bd66d7ac96b"]}, +{"id":"lit:../parser/table_factor_test.go:469","input":"(u1, r1) AS 'P1'","digests":["7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520","7d3952a4ddce6520"]}, +{"id":"lit:../parser/table_factor_test.go:482","input":"SELECT * FROM VALUES (1, 'a'), (2, 'b') AS v (id, name)","digests":["56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313","56a378820e62b313"]}, +{"id":"lit:../parser/table_factor_test.go:486","input":"SELECT $1 FROM @my_stage/2024/data.csv (FILE_FORMAT =\u003e 'f') t","digests":["9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0","9ed3c2eeb5c194e0"]}, +{"id":"lit:../parser/table_factor_test.go:509","input":"SELECT * FROM OPENJSON(@j) WITH (Id INT '$.id', R NVARCHAR(MAX) AS JSON) AS x","digests":["a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d","a2fdc5850703421d"]}, {"id":"lit:../parser/truncate_test.go:221","input":"Expected CascadeType 'CASCADE', got %q","digests":["d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d","d48b57e9f6f6ec1d"]}, {"id":"lit:../parser/truncate_test.go:248","input":"Expected CascadeType 'RESTRICT', got %q","digests":["bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f","bf14772959c5ba1f"]}, {"id":"lit:../parser/truncate_test.go:343","input":"Expected TokenLiteral 'TRUNCATE TABLE', got %q","digests":["fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa","fb525c3eb0447aaa"]}, @@ -1249,42 +1508,69 @@ {"id":"lit:../parser/uat_bugs_test.go:271","input":"Bug 3: hint should contain 'expected', got: %s","digests":["c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f","c0fce459bcbfe37f"]}, {"id":"lit:../parser/uat_bugs_test.go:335","input":"expected argument 'price', got %q","digests":["b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4","b32352812ac42df4"]}, {"id":"lit:../parser/uat_bugs_test.go:412","input":"ON DUPLICATE KEY UPDATE description = COALESCE(VALUES(description), 'unknown')","digests":["0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008","0db32182b8f38008"]}, -{"id":"lit:../parser/utility_stmt_test.go:113","input":"BEGIN dbms_stats.gather_schema_stats('APP')","digests":["b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8"]}, -{"id":"lit:../parser/utility_stmt_test.go:150","input":"SET LOCAL statement_timeout = '5s'","digests":["969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325"]}, -{"id":"lit:../parser/utility_stmt_test.go:151","input":"SET SESSION TIME ZONE 'UTC'","digests":["e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a"]}, -{"id":"lit:../parser/utility_stmt_test.go:154","input":"SET @@session.sql_mode = 'TRADITIONAL'","digests":["4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","ac5369753a376c10","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47"]}, -{"id":"lit:../parser/utility_stmt_test.go:190","input":"USE `my-app`","digests":["b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006"]}, -{"id":"lit:../parser/utility_stmt_test.go:225","input":"CALL refresh_stats(2024, 'full')","digests":["1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf"]}, -{"id":"lit:../parser/utility_stmt_test.go:229","input":"CALL db.schema.sp_load('2024-01-01')","digests":["93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8"]}, -{"id":"lit:../parser/utility_stmt_test.go:230","input":"CALL my_proc(1, 'a', NULL)","digests":["9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507"]}, -{"id":"lit:../parser/utility_stmt_test.go:271","input":"COMMENT ON TABLE users IS 'it''s users'","digests":["d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0"]}, -{"id":"lit:../parser/utility_stmt_test.go:275","input":"NOTIFY channel, 'hello'","digests":["d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1"]}, -{"id":"lit:../parser/utility_stmt_test.go:277","input":"DO $$ BEGIN RAISE NOTICE 'hi'; END $$","digests":["7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171"]}, -{"id":"lit:../parser/utility_stmt_test.go:284","input":"GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION","digests":["fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472"]}, -{"id":"lit:../parser/utility_stmt_test.go:285","input":"LOAD DATA INFILE '/tmp/data.csv' INTO TABLE users FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n'","digests":["ed5b13ae55a6c155","9214c34355bbcb05","ed5b13ae55a6c155","ed5b13ae55a6c155","9214c34355bbcb05","9214c34355bbcb05","9214c34355bbcb05","ed5b13ae55a6c155","ed5b13ae55a6c155"]}, -{"id":"lit:../parser/utility_stmt_test.go:293","input":"PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?'","digests":["81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a"]}, -{"id":"lit:../parser/utility_stmt_test.go:295","input":"XA START 'a'","digests":["b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776"]}, -{"id":"lit:../parser/utility_stmt_test.go:297","input":"SHOW VARIABLES LIKE 'max_%'","digests":["aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0"]}, -{"id":"lit:../parser/utility_stmt_test.go:299","input":"SHOW GRANTS FOR 'app'@'%'","digests":["dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b"]}, -{"id":"lit:../parser/utility_stmt_test.go:302","input":"COPY INTO raw_events FROM @my_stage/events/ FILE_FORMAT = (TYPE = 'JSON')","digests":["172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782"]}, -{"id":"lit:../parser/utility_stmt_test.go:305","input":"EXECUTE IMMEDIATE 'SELECT 1'","digests":["70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708"]}, -{"id":"lit:../parser/utility_stmt_test.go:309","input":"KILL QUERY WHERE query_id = 'abc' SYNC","digests":["3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d"]}, -{"id":"lit:../parser/utility_stmt_test.go:311","input":"SHOW TABLES FROM system LIKE 'query%'","digests":["3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2"]}, -{"id":"lit:../parser/utility_stmt_test.go:313","input":"ATTACH DATABASE 'archive.db' AS archive","digests":["2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45"]}, -{"id":"lit:../parser/utility_stmt_test.go:315","input":"VACUUM INTO 'backup.db'","digests":["f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5"]}, -{"id":"lit:../parser/utility_stmt_test.go:325","input":"EXEC sp_executesql N'SELECT 1 WHERE x = ''y''', N'@Id INT', @Id = 5","digests":["5e710559c4e30990","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","5e710559c4e30990","5e710559c4e30990","5e710559c4e30990"]}, -{"id":"lit:../parser/utility_stmt_test.go:327","input":"PRINT N'Done'","digests":["827940c12fc10788","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","827940c12fc10788","827940c12fc10788","827940c12fc10788"]}, -{"id":"lit:../parser/utility_stmt_test.go:328","input":"THROW 50001, N'Custom error', 1","digests":["3df7bd065d70c2e2","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","3df7bd065d70c2e2","3df7bd065d70c2e2","3df7bd065d70c2e2"]}, -{"id":"lit:../parser/utility_stmt_test.go:329","input":"BULK INSERT dbo.Staging FROM 'C:\\data\\file.csv' WITH (FIRSTROW = 2)","digests":["c92964785409be0e","e76d869bd475ea63","6d90507cc548bccd","6d90507cc548bccd","e76d869bd475ea63","e76d869bd475ea63","e76d869bd475ea63","0577ca329ea87e12","e973ea83c1a57d87"]}, -{"id":"lit:../parser/utility_stmt_test.go:369","input":"SELECT 1 PRINT 'x'","digests":["c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730","c4acae958d46c730"]}, -{"id":"lit:../parser/utility_stmt_test.go:400","input":"PRINT 'x'","digests":["52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af"]}, -{"id":"lit:../parser/utility_stmt_test.go:509","input":"SET @p = 'C:\\\\tmp\\\\x'","digests":["42cfe964971334ca","7a6f6e002c47b2c0","42cfe964971334ca","42cfe964971334ca","7a6f6e002c47b2c0","7a6f6e002c47b2c0","7a6f6e002c47b2c0","42cfe964971334ca","42cfe964971334ca"]}, -{"id":"lit:../parser/utility_stmt_test.go:510","input":"GRANT SELECT ON `my db`.`t``x` TO 'u'@'%'","digests":["e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434"]}, -{"id":"lit:../parser/utility_stmt_test.go:511","input":"SET @p = 'C:\\tmp\\x'","digests":["ce31b8a9d2a26a3a","0c10e2fd4aea34c8","b9a6fba442a1bd31","b9a6fba442a1bd31","0c10e2fd4aea34c8","0c10e2fd4aea34c8","0c10e2fd4aea34c8","ce31b8a9d2a26a3a","1e36018d92940e5e"]}, -{"id":"lit:../parser/utility_stmt_test.go:513","input":"PRINT N'it''s'","digests":["7e6c9aee828fb2e7","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","7e6c9aee828fb2e7","7e6c9aee828fb2e7","7e6c9aee828fb2e7"]}, -{"id":"lit:../parser/utility_stmt_test.go:514","input":"COMMENT ON TABLE \"My Table\" IS 'x'","digests":["7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9"]}, -{"id":"lit:../parser/utility_stmt_test.go:515","input":"DO $q$ SELECT '$$' $q$","digests":["09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660"]}, -{"id":"lit:../parser/utility_stmt_test.go:516","input":"SET p = 'C:\\tmp'","digests":["510eefaac0951fe0","8a081c63d38df35f","510eefaac0951fe0","510eefaac0951fe0","8a081c63d38df35f","8a081c63d38df35f","8a081c63d38df35f","510eefaac0951fe0","510eefaac0951fe0"]}, +{"id":"lit:../parser/utility_implicit_end_test.go:57","input":"BEGIN TRANSACTION t1 WITH MARK 'x' UPDATE t SET a = 1","digests":["1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc","1a8e405ca5c2aefc"]}, +{"id":"lit:../parser/utility_implicit_end_test.go:78","input":"PRINT 'hello' SELECT 1","digests":["e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29","e19fac8da1e55f29"]}, +{"id":"lit:../parser/utility_implicit_end_test.go:80","input":"RAISERROR ('x', 10, 1) WITH NOWAIT SELECT 1","digests":["c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06","c38cc9825daeba06"]}, +{"id":"lit:../parser/utility_implicit_end_test.go:98","input":"GRANT SELECT, INSERT ON db.* TO 'u'@'h'","digests":["39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d","39d0be4ce6ef893d"]}, +{"id":"lit:../parser/utility_shape_test.go:64","input":"SET SESSION sql_mode = ''","digests":["dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11","dbc2dcf93f04fb11"]}, +{"id":"lit:../parser/utility_shape_test.go:67","input":"SET @@global.sql_mode = 'ANSI'","digests":["d83eece80b2053e9","d83eece80b2053e9","d83eece80b2053e9","d83eece80b2053e9","72fdebcd55e9bc57","d83eece80b2053e9","d83eece80b2053e9","d83eece80b2053e9","d83eece80b2053e9"]}, +{"id":"lit:../parser/utility_shape_test.go:71","input":"SHOW GLOBAL STATUS LIKE 'Threads%'","digests":["11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa","11725b8cdd8e07aa"]}, +{"id":"lit:../parser/utility_shape_test.go:72","input":"SHOW VARIABLES WHERE Variable_name = 'x'","digests":["6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d","6cafd2c910cdbd5d"]}, +{"id":"lit:../parser/utility_shape_test.go:73","input":"GRANT SELECT ON db.* TO 'u'@'h'","digests":["2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7","2a8af78047b9ddc7"]}, +{"id":"lit:../parser/utility_shape_test.go:77","input":"SET TIME ZONE 'UTC'","digests":["fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6","fcbb17cb058539e6"]}, +{"id":"lit:../parser/utility_stmt_test.go:115","input":"BEGIN dbms_stats.gather_schema_stats('APP')","digests":["b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8","b1dd6367bfc192f8"]}, +{"id":"lit:../parser/utility_stmt_test.go:152","input":"SET LOCAL statement_timeout = '5s'","digests":["969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325","969ee10e40795325"]}, +{"id":"lit:../parser/utility_stmt_test.go:153","input":"SET SESSION TIME ZONE 'UTC'","digests":["e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a","e31b1ca2d1981d8a"]}, +{"id":"lit:../parser/utility_stmt_test.go:156","input":"SET @@session.sql_mode = 'TRADITIONAL'","digests":["4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","ac5369753a376c10","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47","4deb54a14ae77c47"]}, +{"id":"lit:../parser/utility_stmt_test.go:192","input":"USE `my-app`","digests":["b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006","b8d05bcd2bbb8006"]}, +{"id":"lit:../parser/utility_stmt_test.go:227","input":"CALL refresh_stats(2024, 'full')","digests":["1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf","1082b416d560eabf"]}, +{"id":"lit:../parser/utility_stmt_test.go:231","input":"CALL db.schema.sp_load('2024-01-01')","digests":["93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8","93b3e20eeff40ab8"]}, +{"id":"lit:../parser/utility_stmt_test.go:232","input":"CALL my_proc(1, 'a', NULL)","digests":["9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507","9086021f0a02a507"]}, +{"id":"lit:../parser/utility_stmt_test.go:273","input":"COMMENT ON TABLE users IS 'it''s users'","digests":["d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0","d963f3a6f36e65a0"]}, +{"id":"lit:../parser/utility_stmt_test.go:277","input":"NOTIFY channel, 'hello'","digests":["d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1","d8993c30fde331f1"]}, +{"id":"lit:../parser/utility_stmt_test.go:279","input":"DO $$ BEGIN RAISE NOTICE 'hi'; END $$","digests":["7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171","7fdcfbf04a62a171"]}, +{"id":"lit:../parser/utility_stmt_test.go:286","input":"GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION","digests":["fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472","fa2d91ce0dd6f472"]}, +{"id":"lit:../parser/utility_stmt_test.go:287","input":"LOAD DATA INFILE '/tmp/data.csv' INTO TABLE users FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n'","digests":["ed5b13ae55a6c155","9214c34355bbcb05","ed5b13ae55a6c155","ed5b13ae55a6c155","9214c34355bbcb05","9214c34355bbcb05","9214c34355bbcb05","ed5b13ae55a6c155","ed5b13ae55a6c155"]}, +{"id":"lit:../parser/utility_stmt_test.go:295","input":"PREPARE stmt FROM 'SELECT * FROM users WHERE id = ?'","digests":["81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a","81e922389ce6a23a"]}, +{"id":"lit:../parser/utility_stmt_test.go:297","input":"XA START 'a'","digests":["b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776","b26a300ef10c1776"]}, +{"id":"lit:../parser/utility_stmt_test.go:299","input":"SHOW VARIABLES LIKE 'max_%'","digests":["aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0","aa3263b776642bb0"]}, +{"id":"lit:../parser/utility_stmt_test.go:301","input":"SHOW GRANTS FOR 'app'@'%'","digests":["dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b","dcebee36f0e6eb6b"]}, +{"id":"lit:../parser/utility_stmt_test.go:304","input":"COPY INTO raw_events FROM @my_stage/events/ FILE_FORMAT = (TYPE = 'JSON')","digests":["172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782","172008e358d03782"]}, +{"id":"lit:../parser/utility_stmt_test.go:307","input":"EXECUTE IMMEDIATE 'SELECT 1'","digests":["70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708","70cfd00fec2cf708"]}, +{"id":"lit:../parser/utility_stmt_test.go:311","input":"KILL QUERY WHERE query_id = 'abc' SYNC","digests":["3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d","3913c137cb84f93d"]}, +{"id":"lit:../parser/utility_stmt_test.go:313","input":"SHOW TABLES FROM system LIKE 'query%'","digests":["3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2","3aa2fe619d9aedb2"]}, +{"id":"lit:../parser/utility_stmt_test.go:315","input":"ATTACH DATABASE 'archive.db' AS archive","digests":["2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45","2b1be7503658eb45"]}, +{"id":"lit:../parser/utility_stmt_test.go:317","input":"VACUUM INTO 'backup.db'","digests":["f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5","f71a2bc09ceccdd5"]}, +{"id":"lit:../parser/utility_stmt_test.go:327","input":"EXEC sp_executesql N'SELECT 1 WHERE x = ''y''', N'@Id INT', @Id = 5","digests":["5e710559c4e30990","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","a3959fc9bb3a20ca","5e710559c4e30990","5e710559c4e30990","5e710559c4e30990"]}, +{"id":"lit:../parser/utility_stmt_test.go:329","input":"PRINT N'Done'","digests":["827940c12fc10788","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","7f604ff81b214227","827940c12fc10788","827940c12fc10788","827940c12fc10788"]}, +{"id":"lit:../parser/utility_stmt_test.go:330","input":"THROW 50001, N'Custom error', 1","digests":["3df7bd065d70c2e2","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","208ea895b837fcb0","3df7bd065d70c2e2","3df7bd065d70c2e2","3df7bd065d70c2e2"]}, +{"id":"lit:../parser/utility_stmt_test.go:331","input":"BULK INSERT dbo.Staging FROM 'C:\\data\\file.csv' WITH (FIRSTROW = 2)","digests":["c92964785409be0e","e76d869bd475ea63","6d90507cc548bccd","6d90507cc548bccd","e76d869bd475ea63","e76d869bd475ea63","e76d869bd475ea63","0577ca329ea87e12","e973ea83c1a57d87"]}, +{"id":"lit:../parser/utility_stmt_test.go:403","input":"PRINT 'x'","digests":["52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af","52f0b27550ab21af"]}, +{"id":"lit:../parser/utility_stmt_test.go:512","input":"SET @p = 'C:\\\\tmp\\\\x'","digests":["42cfe964971334ca","7a6f6e002c47b2c0","42cfe964971334ca","42cfe964971334ca","7a6f6e002c47b2c0","7a6f6e002c47b2c0","7a6f6e002c47b2c0","42cfe964971334ca","42cfe964971334ca"]}, +{"id":"lit:../parser/utility_stmt_test.go:513","input":"GRANT SELECT ON `my db`.`t``x` TO 'u'@'%'","digests":["e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434","e8551d0a73e12434"]}, +{"id":"lit:../parser/utility_stmt_test.go:514","input":"SET @p = 'C:\\tmp\\x'","digests":["ce31b8a9d2a26a3a","0c10e2fd4aea34c8","b9a6fba442a1bd31","b9a6fba442a1bd31","0c10e2fd4aea34c8","0c10e2fd4aea34c8","0c10e2fd4aea34c8","ce31b8a9d2a26a3a","1e36018d92940e5e"]}, +{"id":"lit:../parser/utility_stmt_test.go:516","input":"PRINT N'it''s'","digests":["7e6c9aee828fb2e7","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","2a79120bdc9580c2","7e6c9aee828fb2e7","7e6c9aee828fb2e7","7e6c9aee828fb2e7"]}, +{"id":"lit:../parser/utility_stmt_test.go:517","input":"COMMENT ON TABLE \"My Table\" IS 'x'","digests":["7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9","7e1f805029d6c6f9"]}, +{"id":"lit:../parser/utility_stmt_test.go:518","input":"DO $q$ SELECT '$$' $q$","digests":["09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660","09652bed6a0e0660"]}, +{"id":"lit:../parser/utility_stmt_test.go:519","input":"SET p = 'C:\\tmp'","digests":["510eefaac0951fe0","8a081c63d38df35f","510eefaac0951fe0","510eefaac0951fe0","8a081c63d38df35f","8a081c63d38df35f","8a081c63d38df35f","510eefaac0951fe0","510eefaac0951fe0"]}, +{"id":"lit:../parser/utility_txn_test.go:89","input":"COPY t FROM 'x'\nDROP TABLE t","digests":["a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6","a81c441240f175d6"]}, +{"id":"lit:../parser/utility_txn_test.go:99","input":"COMMENT ON TABLE t IS 'x'\nDELETE FROM t","digests":["8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe","8764bc45f65544fe"]}, +{"id":"lit:../parser/utility_txn_test.go:116","input":"CREATE ROLE r\nWITH LOGIN PASSWORD 'x'","digests":["667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f","667f157b5b9e959f"]}, +{"id":"lit:../parser/utility_txn_test.go:124","input":"LOAD DATA INFILE 'f' INTO TABLE t\nSET c = 1","digests":["80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616","80589903594c7616"]}, +{"id":"lit:../parser/utility_txn_test.go:140","input":"BEGIN\nCOPY t FROM 'x'","digests":["07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf","07e61e7b6305e7bf"]}, +{"id":"lit:../parser/utility_txn_test.go:179","input":"COMMIT PREPARED 'x'","digests":["2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15","2739a41960223c15"]}, +{"id":"lit:../parser/utility_txn_test.go:180","input":"ROLLBACK PREPARED 'x'","digests":["e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e","e41ddb9448dcd26e"]}, +{"id":"lit:../parser/utility_txn_test.go:197","input":"BEGIN TRAN t1 WITH MARK 'x'","digests":["346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb","346366c359f08efb"]}, +{"id":"lit:../parser/utility_txn_test.go:198","input":"BEGIN TRAN @t WITH MARK N'desc'","digests":["f02404e5cc63bc8c","bdcc808c363547db","bdcc808c363547db","bdcc808c363547db","bdcc808c363547db","bdcc808c363547db","f02404e5cc63bc8c","f02404e5cc63bc8c","f02404e5cc63bc8c"]}, +{"id":"lit:../parser/utility_txn_test.go:208","input":"COMMIT COMMENT 'x' WRITE NOWAIT","digests":["7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf","7653a0e800ed0ccf"]}, +{"id":"lit:../parser/utility_txn_test.go:209","input":"COMMIT FORCE 'x', 12","digests":["ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216","ae4377820316a216"]}, +{"id":"lit:../parser/utility_txn_test.go:210","input":"ROLLBACK FORCE 'x'","digests":["b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2","b35bd6c458ace3f2"]}, +{"id":"lit:../parser/utility_txn_test.go:299","input":"IF UPDATE(col) PRINT 'x'","digests":["abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286","abf82cff7a23b286"]}, +{"id":"lit:../parser/utility_txn_test.go:303","input":"IF @x = 1\n PRINT 'a'\nELSE\n PRINT 'b'","digests":["0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305","0c0107710f598305"]}, +{"id":"lit:../parser/validate_string_test.go:36","input":"SELECT `a` FROM `t` LIMIT 5","digests":["6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213","6c815a2bb5fec213"]}, +{"id":"lit:../parser/validate_string_test.go:88","input":"SELECT \"Mixed\", b AS x FROM s.\"T\" t JOIN u ON t.id = u.id WHERE c \u003e 1","digests":["810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24","810e874fd50c2d24"]}, +{"id":"lit:../parser/validate_string_test.go:89","input":"SELECT `col`, COUNT(*) FROM `db`.`tbl` GROUP BY `col` LIMIT 3","digests":["5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5","5fd4cb4a130d98d5"]}, +{"id":"lit:../parser/validate_string_test.go:105","input":"SELECT zz%d, yy FROM qq%d WHERE ww = 'v%d'","digests":["46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387","46352abcd5796387"]}, {"id":"lit:../parser/window_frame_issue609_test.go:72","input":"Start.Value = %#v, want \"1\"","digests":["927cdab8f0e0af7a","927cdab8f0e0af7a","927cdab8f0e0af7a","927cdab8f0e0af7a","dd553632b0123809","927cdab8f0e0af7a","927cdab8f0e0af7a","927cdab8f0e0af7a","927cdab8f0e0af7a"]}, {"id":"lit:../parser/window_frame_issue609_test.go:148","input":"SELECT sum(x) OVER (ORDER BY d RANGE BETWEEN INTERVAL '1' DAY PRECEDING AND CURRENT ROW) FROM t","digests":["64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce","64bf25c4808d26ce"]}, {"id":"lit:../parser/window_frame_issue609_test.go:164","input":"SELECT INTERVAL '1-2' YEAR TO MONTH FROM t","digests":["ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a","ea899eb299e8ce5a"]}, @@ -1484,25 +1770,30 @@ {"id":"lit:quoted_zero_copy_test.go:123","input":"“a””b”","digests":["95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9","95570a839dd293d9"]}, {"id":"lit:quoted_zero_copy_test.go:126","input":"x \"abc","digests":["00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f","00adc69c946d828f"]}, {"id":"lit:quoted_zero_copy_test.go:133","input":"a`b","digests":["1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df","1911547a4d1584df"]}, -{"id":"lit:quoted_zero_copy_test.go:149","input":"N'a’; DROP TABLE pwn; --'","digests":["b5e9423ac9f2cb15","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","b5e9423ac9f2cb15","b5e9423ac9f2cb15","b5e9423ac9f2cb15"]}, -{"id":"lit:quoted_zero_copy_test.go:297","input":"SELECT 'it''s', 'clean', \"Quo\"\"ted\", \"Plain\", 'a\\tb' FROM t","digests":["c51062cf4a24d3d0","e1c635e27066aa14","c51062cf4a24d3d0","c51062cf4a24d3d0","e1c635e27066aa14","e1c635e27066aa14","e1c635e27066aa14","c51062cf4a24d3d0","c51062cf4a24d3d0"]}, -{"id":"lit:quoted_zero_copy_test.go:313","input":"SELECT '","digests":["e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d"]}, -{"id":"lit:quoted_zero_copy_test.go:313","input":"zz''","digests":["5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0"]}, -{"id":"lit:quoted_zero_copy_test.go:313","input":"', \"","digests":["0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f"]}, -{"id":"lit:quoted_zero_copy_test.go:313","input":"yy\"\"","digests":["7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326"]}, -{"id":"lit:quoted_zero_copy_test.go:313","input":"\" FROM t","digests":["9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac"]}, -{"id":"lit:quoted_zero_copy_test.go:317","input":"SELECT 'q''q', \"w\"\"w\"","digests":["d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3"]}, -{"id":"lit:quoted_zero_copy_test.go:342","input":"SELECT 'it''s'","digests":["613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3"]}, -{"id":"lit:quoted_zero_copy_test.go:350","input":"a''","digests":["cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e"]}, -{"id":"lit:quoted_zero_copy_test.go:350","input":"'","digests":["56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd"]}, -{"id":"lit:quoted_zero_copy_test.go:369","input":"', 'a''b' FROM t","digests":["11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be"]}, -{"id":"lit:quoted_zero_copy_test.go:401","input":"pooled tokenizer's litBuf holds a previous request's literal text","digests":["ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a"]}, -{"id":"lit:quoted_zero_copy_test.go:566","input":"'value%03d'","digests":["a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74"]}, -{"id":"lit:quoted_zero_copy_test.go:573","input":"\"Col%03d\"","digests":["d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551"]}, -{"id":"lit:quoted_zero_copy_test.go:578","input":"`col%03d`","digests":["d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624"]}, -{"id":"lit:quoted_zero_copy_test.go:586","input":"'abcd'","digests":["de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98"]}, -{"id":"lit:quoted_zero_copy_test.go:587","input":"'a''b'","digests":["1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773"]}, +{"id":"lit:quoted_zero_copy_test.go:153","input":"N'a’; DROP TABLE pwn; --'","digests":["b5e9423ac9f2cb15","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","9a1bb011b8cb9d92","b5e9423ac9f2cb15","b5e9423ac9f2cb15","b5e9423ac9f2cb15"]}, +{"id":"lit:quoted_zero_copy_test.go:301","input":"SELECT 'it''s', 'clean', \"Quo\"\"ted\", \"Plain\", 'a\\tb' FROM t","digests":["c51062cf4a24d3d0","e1c635e27066aa14","c51062cf4a24d3d0","c51062cf4a24d3d0","e1c635e27066aa14","e1c635e27066aa14","e1c635e27066aa14","c51062cf4a24d3d0","c51062cf4a24d3d0"]}, +{"id":"lit:quoted_zero_copy_test.go:317","input":"SELECT '","digests":["e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d","e237ed01bdc4c80d"]}, +{"id":"lit:quoted_zero_copy_test.go:317","input":"zz''","digests":["5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0","5bfa9cdfd63b4cd0"]}, +{"id":"lit:quoted_zero_copy_test.go:317","input":"', \"","digests":["0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f","0d1b6381cf80c38f"]}, +{"id":"lit:quoted_zero_copy_test.go:317","input":"yy\"\"","digests":["7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326","7be7b83d02e73326"]}, +{"id":"lit:quoted_zero_copy_test.go:317","input":"\" FROM t","digests":["9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac","9d7472d8255f58ac"]}, +{"id":"lit:quoted_zero_copy_test.go:321","input":"SELECT 'q''q', \"w\"\"w\"","digests":["d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3","d161c95b343ff3a3"]}, +{"id":"lit:quoted_zero_copy_test.go:346","input":"SELECT 'it''s'","digests":["613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3","613661c77e3c36d3"]}, +{"id":"lit:quoted_zero_copy_test.go:354","input":"a''","digests":["cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e","cea7aa162b0a634e"]}, +{"id":"lit:quoted_zero_copy_test.go:354","input":"'","digests":["56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd","56a9f1e4148687cd"]}, +{"id":"lit:quoted_zero_copy_test.go:373","input":"', 'a''b' FROM t","digests":["11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be","11c27bce1d5cb4be"]}, +{"id":"lit:quoted_zero_copy_test.go:405","input":"pooled tokenizer's litBuf holds a previous request's literal text","digests":["ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a","ee523b587cf3484a"]}, +{"id":"lit:quoted_zero_copy_test.go:570","input":"'value%03d'","digests":["a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74","a2d87ede8bd5ef74"]}, +{"id":"lit:quoted_zero_copy_test.go:577","input":"\"Col%03d\"","digests":["d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551","d3a8eb6c248b2551"]}, +{"id":"lit:quoted_zero_copy_test.go:582","input":"`col%03d`","digests":["d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624","d093a8cf39ee8624"]}, +{"id":"lit:quoted_zero_copy_test.go:590","input":"'abcd'","digests":["de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98","de5ba984ea2e7b98"]}, +{"id":"lit:quoted_zero_copy_test.go:591","input":"'a''b'","digests":["1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773","1f150d15a0391773"]}, {"id":"lit:scalability_bench_test.go:227","input":"SELECT id FROM users WHERE name LIKE 'John%'","digests":["36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa","36aa69ab019de1aa"]}, +{"id":"lit:scratch_test.go:35","input":"-- lead\nSELECT a /* mid */, \"Q\"\nFROM t\nORDER BY 1","digests":["abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b","abcd396eea61c07b"]}, +{"id":"lit:scratch_test.go:36","input":"SELECT 'it''s', E'a\\nb', 1.5e3, $1 FROM t","digests":["6668e09de6d3c71a","15d21fb385b3fd9a","6668e09de6d3c71a","6668e09de6d3c71a","458cc83732ca7e55","458cc83732ca7e55","458cc83732ca7e55","6668e09de6d3c71a","6668e09de6d3c71a"]}, +{"id":"lit:scratch_test.go:37","input":"SELECT `a`, 'x\\'y' FROM `db`.`t` LIMIT 5","digests":["df6a08cf55868fa4","f65351c1ea683adf","df6a08cf55868fa4","df6a08cf55868fa4","f65351c1ea683adf","f65351c1ea683adf","f65351c1ea683adf","df6a08cf55868fa4","df6a08cf55868fa4"]}, +{"id":"lit:scratch_test.go:163","input":"SELECT a, 'unterminated","digests":["749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447","749666f4a6c92447"]}, +{"id":"lit:scratch_test.go:219","input":"SELECT 'oops","digests":["c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5","c66d6319020fb3e5"]}, {"id":"lit:string_escape_modes_test.go:57","input":"SELECT 'a\\b'","digests":["8b0bfb49b6f01d15","d39836481a4f7c67","935a3366825112bf","935a3366825112bf","d39836481a4f7c67","d39836481a4f7c67","d39836481a4f7c67","935a3366825112bf","935a3366825112bf"]}, {"id":"lit:string_escape_modes_test.go:58","input":"SELECT 'x\\.y'","digests":["3b13dce095261345","3a7a938aac619afa","80064e25a36dd53d","80064e25a36dd53d","3a7a938aac619afa","3a7a938aac619afa","3a7a938aac619afa","80064e25a36dd53d","3a7a938aac619afa"]}, {"id":"lit:string_escape_modes_test.go:59","input":"SELECT '\\'","digests":["c751359b15b07600","bc6f22e5b628b6a7","c751359b15b07600","c751359b15b07600","bc6f22e5b628b6a7","bc6f22e5b628b6a7","bc6f22e5b628b6a7","c751359b15b07600","c751359b15b07600"]}, @@ -1548,16 +1839,20 @@ {"id":"lit:string_escape_strict_test.go:162","input":"SELECT 'a\\vb'","digests":["2295ac82815d73b1","78f58d59f686afab","cfd8bd93e3543348","cfd8bd93e3543348","78f58d59f686afab","78f58d59f686afab","78f58d59f686afab","cfd8bd93e3543348","dc571a7fe9a94325"]}, {"id":"lit:string_escape_strict_test.go:165","input":"SELECT '\\x41'","digests":["4da6cd9a3d6aa259","653e8fcaa1c83e18","7e0459e3fdfb6307","7e0459e3fdfb6307","653e8fcaa1c83e18","653e8fcaa1c83e18","653e8fcaa1c83e18","16cd1e2f00fd5ef7","16cd1e2f00fd5ef7"]}, {"id":"lit:string_escape_strict_test.go:166","input":"SELECT '\\xzz'","digests":["52fd26cd1ac79029","205440c0652527b4","a2af7e89c3cff26e","a2af7e89c3cff26e","205440c0652527b4","205440c0652527b4","205440c0652527b4","52fd26cd1ac79029","205440c0652527b4"]}, -{"id":"lit:string_escape_strict_test.go:169","input":"SELECT '\\x1a'","digests":["81cd89887dcaed07","f108bd1cdb72942c","6051af59dc3e3e90","6051af59dc3e3e90","f108bd1cdb72942c","f108bd1cdb72942c","f108bd1cdb72942c","81cd89887dcaed07","81cd89887dcaed07"]}, -{"id":"lit:string_escape_strict_test.go:170","input":"SELECT '\\xff'","digests":["b8369c56db456978","c70407385464c966","7f2a758a0ed23179","7f2a758a0ed23179","c70407385464c966","c70407385464c966","c70407385464c966","b8369c56db456978","b8369c56db456978"]}, -{"id":"lit:string_escape_strict_test.go:183","input":"SELECT N'a\u0000b'","digests":["1d28cf7782c0aaeb","1d28cf7782c0aaeb","035e4889793f26a6","035e4889793f26a6","1d28cf7782c0aaeb","1d28cf7782c0aaeb","1d28cf7782c0aaeb","1d28cf7782c0aaeb","611ae4f3d80b2237"]}, -{"id":"lit:string_escape_strict_test.go:193","input":"SELECT \"a\u0000b\"","digests":["f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba"]}, -{"id":"lit:string_escape_strict_test.go:210","input":"SELECT '''abc'''","digests":["4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc"]}, -{"id":"lit:string_escape_strict_test.go:211","input":"SELECT '''a'b'''","digests":["10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8"]}, -{"id":"lit:string_escape_strict_test.go:216","input":"SELECT '''a'","digests":["2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9"]}, -{"id":"lit:string_escape_strict_test.go:216","input":"'a","digests":["82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890"]}, -{"id":"lit:string_escape_strict_test.go:218","input":"SELECT ''''''","digests":["ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da"]}, -{"id":"lit:string_escape_strict_test.go:219","input":"SELECT 'a'''","digests":["274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51"]}, +{"id":"lit:string_escape_strict_test.go:168","input":"SELECT 'http:\\/\\/x'","digests":["63239b768918624e","9734e91616890dd7","fcca2f5b33f2f4ce","fcca2f5b33f2f4ce","9734e91616890dd7","9734e91616890dd7","9734e91616890dd7","fcca2f5b33f2f4ce","fcca2f5b33f2f4ce"]}, +{"id":"lit:string_escape_strict_test.go:169","input":"SELECT 'k\\=v'","digests":["69a3ece5fea91514","770591e4962b3d21","f7be2ca9275d3d20","f7be2ca9275d3d20","770591e4962b3d21","770591e4962b3d21","770591e4962b3d21","f7be2ca9275d3d20","f7be2ca9275d3d20"]}, +{"id":"lit:string_escape_strict_test.go:172","input":"SELECT '\\x1a'","digests":["81cd89887dcaed07","f108bd1cdb72942c","6051af59dc3e3e90","6051af59dc3e3e90","f108bd1cdb72942c","f108bd1cdb72942c","f108bd1cdb72942c","81cd89887dcaed07","81cd89887dcaed07"]}, +{"id":"lit:string_escape_strict_test.go:173","input":"SELECT '\\xff'","digests":["b8369c56db456978","c70407385464c966","7f2a758a0ed23179","7f2a758a0ed23179","c70407385464c966","c70407385464c966","c70407385464c966","b8369c56db456978","b8369c56db456978"]}, +{"id":"lit:string_escape_strict_test.go:175","input":"SELECT 'a\\eb'","digests":["5933b39ede60e1a9","45846f93f2851369","d7fa9f00572eef4c","d7fa9f00572eef4c","45846f93f2851369","45846f93f2851369","45846f93f2851369","d7fa9f00572eef4c","5933b39ede60e1a9"]}, +{"id":"lit:string_escape_strict_test.go:176","input":"SELECT 'a\\Nb'","digests":["6f1bcf7ab0e16890","b96e9af2870d2e7d","4b7a62dca0ef2ab3","4b7a62dca0ef2ab3","b96e9af2870d2e7d","b96e9af2870d2e7d","b96e9af2870d2e7d","4b7a62dca0ef2ab3","6f1bcf7ab0e16890"]}, +{"id":"lit:string_escape_strict_test.go:188","input":"SELECT N'a\u0000b'","digests":["1d28cf7782c0aaeb","1d28cf7782c0aaeb","035e4889793f26a6","035e4889793f26a6","1d28cf7782c0aaeb","1d28cf7782c0aaeb","1d28cf7782c0aaeb","1d28cf7782c0aaeb","611ae4f3d80b2237"]}, +{"id":"lit:string_escape_strict_test.go:198","input":"SELECT \"a\u0000b\"","digests":["f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba","f00c45315d9f44ba"]}, +{"id":"lit:string_escape_strict_test.go:216","input":"SELECT '''abc'''","digests":["4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc","4907f71febfb94fc"]}, +{"id":"lit:string_escape_strict_test.go:217","input":"SELECT '''a'b'''","digests":["10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8","10af4595c667dee8"]}, +{"id":"lit:string_escape_strict_test.go:222","input":"SELECT '''a'","digests":["2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9","2ed5a6448e859bf9"]}, +{"id":"lit:string_escape_strict_test.go:222","input":"'a","digests":["82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890","82f8d0393f88c890"]}, +{"id":"lit:string_escape_strict_test.go:224","input":"SELECT ''''''","digests":["ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da","ba8457d0554094da"]}, +{"id":"lit:string_escape_strict_test.go:225","input":"SELECT 'a'''","digests":["274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51","274af99da0344a51"]}, {"id":"lit:token_capacity_test.go:64","input":"SELECT 用户.名前, café FROM пользователи WHERE 名前 = 'こんにちは'","digests":["a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab","a7f6209e65c45bab"]}, {"id":"lit:token_capacity_test.go:65","input":"SELECT a\u003e=b, a\u003c\u003eb, a||b, j-\u003e'k', j@\u003e'{}', x::int FROM t WHERE a\u003c=b AND c!=d","digests":["0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45","0757a7bc6b7a4a45"]}, {"id":"lit:token_capacity_test.go:67","input":"SELECT 'hello world and more words', \"quoted ident\" FROM t","digests":["c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013","c7ed30e3ddace013"]}, @@ -1717,6 +2012,32 @@ {"id":"lit:tsql_test.go:191","input":"expected Quote='N', got %q","digests":["7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18","7808a2313f77aa18"]}, {"id":"lit:tsql_test.go:229","input":"expected 2 tokens for SQLite N'hello', got %d","digests":["8c403842bca51cb8","3f54de5631cff365","3f54de5631cff365","3f54de5631cff365","3f54de5631cff365","3f54de5631cff365","8c403842bca51cb8","8c403842bca51cb8","8c403842bca51cb8"]}, {"id":"lit:tsql_test.go:262","input":"expected value 'My Column', got %q","digests":["7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e","7f7a8b1e9a73772e"]}, +{"id":"lit:unicode_escape_test.go:19","input":"u\u0026'a''b'","digests":["a011bc5d7d46a34b","72c7660f79436f60","a011bc5d7d46a34b","a011bc5d7d46a34b","a011bc5d7d46a34b","a011bc5d7d46a34b","a011bc5d7d46a34b","a011bc5d7d46a34b","a011bc5d7d46a34b"]}, +{"id":"lit:unicode_escape_test.go:19","input":"U\u0026'!+01F600' UESCAPE '!'","digests":["47e88af85a19bc00","e3b76eb21182ccf4","47e88af85a19bc00","47e88af85a19bc00","47e88af85a19bc00","47e88af85a19bc00","47e88af85a19bc00","47e88af85a19bc00","47e88af85a19bc00"]}, +{"id":"lit:unicode_escape_test.go:19","input":"U\u0026'\\\\'","digests":["2693d9367ae8cb56","6998b1991baa2b36","2693d9367ae8cb56","2693d9367ae8cb56","2ff3bacb920a7abc","2ff3bacb920a7abc","2ff3bacb920a7abc","2693d9367ae8cb56","2693d9367ae8cb56"]}, +{"id":"lit:unicode_escape_test.go:19","input":"U\u0026'\\D83D\\DE00'","digests":["092abd68a76d3660","3ad2437020fa3671","3bb2623dded9899c","3bb2623dded9899c","d3069d59724d0d7e","d3069d59724d0d7e","d3069d59724d0d7e","3bb2623dded9899c","d3069d59724d0d7e"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'\\zzzz'","digests":["e75dd1c97a7a432e","d483da9b29839454","ced740409165989e","ced740409165989e","4bec17203bb543c1","4bec17203bb543c1","4bec17203bb543c1","ced740409165989e","4bec17203bb543c1"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'\\0000'","digests":["e71314d421e44b02","89a2decc75e48143","0930dc20853daa52","0930dc20853daa52","e9a5f361ce5c0c42","e9a5f361ce5c0c42","e9a5f361ce5c0c42","e71314d421e44b02","0930dc20853daa52"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'\\+110000'","digests":["b175d8515f58066a","6522f2385a6c005a","abf05faf9dc0e9da","abf05faf9dc0e9da","22541a376d7e71f3","22541a376d7e71f3","22541a376d7e71f3","abf05faf9dc0e9da","22541a376d7e71f3"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'\\D800'","digests":["86e45241027e407d","1a447025759b46f5","e38089b143222ce6","e38089b143222ce6","ba3c0195e14833c5","ba3c0195e14833c5","ba3c0195e14833c5","e38089b143222ce6","ba3c0195e14833c5"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'x' UESCAPE 'ab'","digests":["b0f0ddc11227b139","42788bfb0944c3bd","b0f0ddc11227b139","b0f0ddc11227b139","b0f0ddc11227b139","b0f0ddc11227b139","b0f0ddc11227b139","b0f0ddc11227b139","b0f0ddc11227b139"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'x' UESCAPE '+'","digests":["e2f933601c6d5f0e","cd04dba962475ca4","e2f933601c6d5f0e","e2f933601c6d5f0e","e2f933601c6d5f0e","e2f933601c6d5f0e","e2f933601c6d5f0e","e2f933601c6d5f0e","e2f933601c6d5f0e"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'x' UESCAPE '0'","digests":["cbfcf633e6ea6e1c","712b3f4a16132013","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c","cbfcf633e6ea6e1c"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'x' UESCAPE ''","digests":["de8258303fe348c2","3482cf6806c7e9db","de8258303fe348c2","de8258303fe348c2","de8258303fe348c2","de8258303fe348c2","de8258303fe348c2","de8258303fe348c2","de8258303fe348c2"]}, +{"id":"lit:unicode_escape_test.go:29","input":"U\u0026'unterminated","digests":["1f7abf88607fd8e9","f3d6d9065845f9fd","1f7abf88607fd8e9","1f7abf88607fd8e9","1f7abf88607fd8e9","1f7abf88607fd8e9","1f7abf88607fd8e9","1f7abf88607fd8e9","1f7abf88607fd8e9"]}, +{"id":"lit:unicode_escape_test.go:39","input":"U\u0026\"d\\0061t\"","digests":["4c5610b36d002e3b","075c8519474960dc","4c5610b36d002e3b","4c5610b36d002e3b","4c5610b36d002e3b","4c5610b36d002e3b","4c5610b36d002e3b","4c5610b36d002e3b","4c5610b36d002e3b"]}, +{"id":"lit:unicode_escape_test.go:40","input":"u\u0026\"d!0061t\" UESCAPE '!'","digests":["00cb56f236243eca","c3ef8dfc163db17c","00cb56f236243eca","00cb56f236243eca","00cb56f236243eca","00cb56f236243eca","00cb56f236243eca","00cb56f236243eca","00cb56f236243eca"]}, +{"id":"lit:unicode_escape_test.go:41","input":"U\u0026\"A\"\"b\"","digests":["d028d25d61b2c9c2","651d4c6988d3240e","d028d25d61b2c9c2","d028d25d61b2c9c2","d028d25d61b2c9c2","d028d25d61b2c9c2","d028d25d61b2c9c2","d028d25d61b2c9c2","d028d25d61b2c9c2"]}, +{"id":"lit:unicode_escape_test.go:41","input":"A\"b","digests":["6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13","6a45c544a2d74a13"]}, +{"id":"lit:unicode_escape_test.go:42","input":"U\u0026\"\\D83D\\DE00\"","digests":["2348eac7ec70519b","ef7265b01cd90ef4","2348eac7ec70519b","2348eac7ec70519b","2348eac7ec70519b","2348eac7ec70519b","2348eac7ec70519b","2348eac7ec70519b","2348eac7ec70519b"]}, +{"id":"lit:unicode_escape_test.go:43","input":"U\u0026\"x\\\\y\"","digests":["efc61db43d1cb3a8","335f58175152a7c2","efc61db43d1cb3a8","efc61db43d1cb3a8","efc61db43d1cb3a8","efc61db43d1cb3a8","efc61db43d1cb3a8","efc61db43d1cb3a8","efc61db43d1cb3a8"]}, +{"id":"lit:unicode_escape_test.go:44","input":"U\u0026\"d!0061t\" /* c */ UESCAPE /* d */ '!'","digests":["a4699c852cd1e944","4b2d99273e805ae0","a4699c852cd1e944","a4699c852cd1e944","a4699c852cd1e944","a4699c852cd1e944","a4699c852cd1e944","a4699c852cd1e944","a4699c852cd1e944"]}, +{"id":"lit:unicode_escape_test.go:56","input":"U\u0026'x' /* c */ UESCAPE '!'","digests":["59ada72f68bc704a","e72e691105cc3699","59ada72f68bc704a","59ada72f68bc704a","59ada72f68bc704a","59ada72f68bc704a","59ada72f68bc704a","59ada72f68bc704a","59ada72f68bc704a"]}, +{"id":"lit:unicode_escape_test.go:56","input":"U\u0026'x' -- c\n UESCAPE '!'","digests":["a4f3ff43b6901ce6","656cfac92878a1fc","a4f3ff43b6901ce6","a4f3ff43b6901ce6","a4f3ff43b6901ce6","a4f3ff43b6901ce6","a4f3ff43b6901ce6","a4f3ff43b6901ce6","a4f3ff43b6901ce6"]}, +{"id":"lit:unicode_escape_test.go:56","input":"U\u0026'x'/**/UESCAPE/**/'!'","digests":["ca1e4819b9f28039","1b4249a512ca5299","ca1e4819b9f28039","ca1e4819b9f28039","ca1e4819b9f28039","ca1e4819b9f28039","ca1e4819b9f28039","ca1e4819b9f28039","ca1e4819b9f28039"]}, +{"id":"lit:unicode_escape_test.go:67","input":"U\u0026'x' UESCAPE '!'","digests":["175ace8289fb2d45","9f46cc0005b6b332","175ace8289fb2d45","175ace8289fb2d45","175ace8289fb2d45","175ace8289fb2d45","175ace8289fb2d45","175ace8289fb2d45","175ace8289fb2d45"]}, +{"id":"lit:unicode_escape_test.go:77","input":"U\u0026'x' /* c */ , 1","digests":["04467c67672de006","a13d4b074b40beda","04467c67672de006","04467c67672de006","04467c67672de006","04467c67672de006","04467c67672de006","04467c67672de006","04467c67672de006"]}, +{"id":"lit:unicode_escape_test.go:81","input":"U\u0026'x'","digests":["d469311416d7e395","9c3b16ca55bd7781","d469311416d7e395","d469311416d7e395","d469311416d7e395","d469311416d7e395","d469311416d7e395","d469311416d7e395","d469311416d7e395"]}, {"id":"lit:zero_copy_bench_test.go:73","input":"('alpha','beta','gamma','delta')","digests":["c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd","c41fe693936827fd"]}, {"id":"lit:zero_copy_bench_test.go:79","input":"\"Col%d\"","digests":["bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63","bc0bcc24637b2d63"]}, {"id":"lit:zero_copy_bench_test.go:81","input":" FROM \"Schema\".\"Table\"","digests":["4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8","4d2f8ceb85b277a8"]}, @@ -1726,6 +2047,6 @@ {"id":"lit:zero_copy_test.go:69","input":"SELECT 'it''s', \"a\"\"b\" FROM t","digests":["c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7","c6a3d0a0ff66a9d7"]}, {"id":"lit:zero_copy_test.go:78","input":"SELECT 'x","digests":["2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a","2b3df1bbe4df305a"]}, {"id":"lit:zero_copy_test.go:220","input":"select \"Quoted Col\" from t","digests":["6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a","6f83458dafad946a"]}, -{"id":"lit:zero_copy_test.go:449","input":"TokenizeString allocs for N=1500: %v, want at most N=10's %v + 8","digests":["c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff"]}, -{"id":"lit:zero_copy_test.go:460","input":"Tokenize([]byte) uses %v allocs, want at most TokenizeString's %v + 1","digests":["82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d"]} +{"id":"lit:zero_copy_test.go:472","input":"TokenizeString allocs for N=1500: %v, want at most N=10's %v + 8","digests":["c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff","c46e73fbf1ed43ff"]}, +{"id":"lit:zero_copy_test.go:483","input":"Tokenize([]byte) uses %v allocs, want at most TokenizeString's %v + 1","digests":["82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d","82a0779c2c8ade9d"]} ]} diff --git a/pkg/sql/tokenizer/tokenizer.go b/pkg/sql/tokenizer/tokenizer.go index 6cd6d6f8..6fa44f57 100644 --- a/pkg/sql/tokenizer/tokenizer.go +++ b/pkg/sql/tokenizer/tokenizer.go @@ -1058,6 +1058,11 @@ func isIdentifierStart(r rune) bool { // readIdentifier reads an identifier (e.g. foo or foo.bar) func (t *Tokenizer) readIdentifier() (models.Token, error) { + if (t.input[t.pos.Index] == 'U' || t.input[t.pos.Index] == 'u') && t.pos.Index+2 < len(t.input) && + t.input[t.pos.Index+1] == '&' && (t.input[t.pos.Index+2] == '\'' || t.input[t.pos.Index+2] == '"') && + t.dialect == keywords.DialectPostgreSQL { + return t.readUnicodeEscape() + } start := t.pos.Index r, size := utf8.DecodeRune(t.input[t.pos.Index:]) t.pos.AdvanceRune(r, size) @@ -1832,113 +1837,186 @@ func (t *Tokenizer) readTripleQuotedStringSlow(startPos Position, quote rune) (m ) } -// readNumber reads an integer/float +// readNumber reads a dialect-specific numeric literal without decoding its spelling. func (t *Tokenizer) readNumber(buf []byte) (models.Token, error) { - var start int - if buf == nil { - start = t.pos.Index - } else { - start = t.pos.Index - len(buf) - } - - // Hexadecimal literal: 0x1F (MySQL, MariaDB, SQLite, ClickHouse, - // PostgreSQL 16+). Only when a hex digit follows the prefix, so "0x" alone - // stays the number 0 followed by a word. - if t.input[t.pos.Index] == '0' && t.pos.Index+2 < len(t.input) && - (t.input[t.pos.Index+1] == 'x' || t.input[t.pos.Index+1] == 'X') && - isHexDigit(t.input[t.pos.Index+2]) { - t.pos.Index += 2 - t.pos.Column += 2 - for t.pos.Index < len(t.input) && isHexDigit(t.input[t.pos.Index]) { + start := t.pos.Index - len(buf) + separators := t.dialect == keywords.DialectPostgreSQL || t.dialect == keywords.DialectSQLite || t.dialect == keywords.DialectClickHouse + mysql := t.dialect == keywords.DialectMySQL || t.dialect == keywords.DialectMariaDB + if t.input[t.pos.Index] == '0' && t.pos.Index+1 < len(t.input) { + base := byte(0) + switch t.input[t.pos.Index+1] { + case 'x', 'X': + // 0x hex literals are accepted in every dialect, as before + // per-dialect lexing (SQL Server binary, MySQL/SQLite/ + // ClickHouse/PostgreSQL 16 hex, BigQuery hex integers). + base = 16 + case 'b', 'B': + if t.dialect == keywords.DialectPostgreSQL || t.dialect == keywords.DialectClickHouse || mysql { + base = 2 + } + case 'o', 'O': + if t.dialect == keywords.DialectPostgreSQL { + base = 8 + } + } + if base != 0 { + t.pos.Index += 2 + t.pos.Column += 2 + digits := t.pos.Index + // PostgreSQL also permits one separator immediately after a base prefix. + if t.dialect == keywords.DialectPostgreSQL && t.pos.Index+1 < len(t.input) && + t.input[t.pos.Index] == '_' && numericDigit(t.input[t.pos.Index+1], base) { + t.pos.Index++ + t.pos.Column++ + } + t.readNumericDigits(base, separators) + if digits == t.pos.Index { + if mysql { + t.scanIdentifierTail() + return models.Token{Type: models.TokenTypeIdentifier, Value: t.src[start:t.pos.Index]}, nil + } + return t.invalidNumeric(start) + } + return t.finishNumber(start, mysql) + } + } + t.readNumericDigits(10, separators) + if t.pos.Index < len(t.input) && t.input[t.pos.Index] == '.' { + next := t.pos.Index + 1 + if next < len(t.input) && (t.input[next] == '.' || (nextIsWordStart(t.input[next:]) && !t.exponentAt(next))) { + return t.finishNumber(start, mysql) + } + t.pos.Index++ + t.pos.Column++ + t.readNumericDigits(10, separators) + } + if (t.dialect == keywords.DialectOracle || t.dialect == keywords.DialectGeneric) && t.storageUnitFollows(start) { + return models.Token{Type: models.TokenTypeNumber, Value: t.src[start:t.pos.Index]}, nil + } + if t.pos.Index < len(t.input) && (t.input[t.pos.Index] == 'e' || t.input[t.pos.Index] == 'E') { + if mysql && !t.exponentAt(t.pos.Index) && !strings.Contains(t.src[start:t.pos.Index], ".") { + return t.finishNumber(start, true) + } + t.pos.Index++ + t.pos.Column++ + if t.pos.Index < len(t.input) && (t.input[t.pos.Index] == '+' || t.input[t.pos.Index] == '-') { t.pos.Index++ t.pos.Column++ } - return models.Token{Type: models.TokenTypeNumber, Value: t.src[start:t.pos.Index]}, nil + digits := t.pos.Index + t.readNumericDigits(10, separators) + if digits == t.pos.Index { + return t.invalidNumeric(start) + } + } + // Oracle BINARY_DOUBLE / BINARY_FLOAT literals: 1d, 1.5f, 1e5D. + if t.dialect == keywords.DialectOracle && t.pos.Index < len(t.input) && + strings.IndexByte("dDfF", t.input[t.pos.Index]) >= 0 && !t.identCharAt(t.pos.Index+1) { + t.pos.Index++ + t.pos.Column++ } + return t.finishNumber(start, mysql) +} - // Read integer part - for t.pos.Index < len(t.input) { - r, size := utf8.DecodeRune(t.input[t.pos.Index:]) - if r < '0' || r > '9' { - break +// storageUnitFollows reports whether the integer just read is an Oracle +// storage size such as 100M, 8K or 2G: one K/M/G/T/P/E unit letter that is +// not itself followed by a name character (nor, for E, an exponent sign). +// Oracle accepts these wherever a size is expected (SIZE, MAXSIZE, RESIZE, +// BLOCKSIZE, QUOTA, PRIVATE_SGA, ALTER SYSTEM SET ...); the generic dialect +// keeps accepting them too. The number and unit stay separate tokens so +// statement rendering retains its existing spelling and spacing. +func (t *Tokenizer) storageUnitFollows(start int) bool { + i := t.pos.Index + if i >= len(t.input) || strings.IndexByte("kKmMgGtTpPeE", t.input[i]) < 0 || t.identCharAt(i+1) { + return false + } + if (t.input[i] == 'e' || t.input[i] == 'E') && i+1 < len(t.input) && (t.input[i+1] == '+' || t.input[i+1] == '-') { + return false + } + for j := start; j < i; j++ { + if t.input[j] < '0' || t.input[j] > '9' { + return false } - t.pos.AdvanceRune(r, size) } + return true +} - if t.pos.Index >= len(t.input) { - return models.Token{ - Type: models.TokenTypeNumber, - Value: t.src[start:t.pos.Index], - }, nil +// identCharAt reports whether the input at idx starts a name character. +func (t *Tokenizer) identCharAt(idx int) bool { + if idx >= len(t.input) { + return false } + r, _ := utf8.DecodeRune(t.input[idx:]) + return isIdentifierChar(r) +} - // Look for decimal point - r, size := utf8.DecodeRune(t.input[t.pos.Index:]) - if r == '.' { - // "5." is a complete number (a trailing decimal point) unless the dot - // belongs to what follows: a second dot or a word ("1.foo", "1..2"). - // An exponent after the dot ("1.e5") still belongs to the number. - if next := t.pos.Index + 1; next < len(t.input) && - (t.input[next] == '.' || (nextIsWordStart(t.input[next:]) && !t.exponentAt(next))) { - return models.Token{ - Type: models.TokenTypeNumber, - Value: t.src[start:t.pos.Index], - }, nil +// readNumericDigits permits a single separator only between valid digits. +func (t *Tokenizer) readNumericDigits(base byte, separators bool) { + digit := false + for t.pos.Index < len(t.input) { + c := t.input[t.pos.Index] + if numericDigit(c, base) { + digit = true + } else if separators && digit && c == '_' && t.pos.Index+1 < len(t.input) && numericDigit(t.input[t.pos.Index+1], base) { + digit = false + } else { + break } - t.pos.AdvanceRune(r, size) + t.pos.Index++ + t.pos.Column++ + } +} - // Fractional digits are optional ("5."). - for t.pos.Index < len(t.input) { - r, size = utf8.DecodeRune(t.input[t.pos.Index:]) - if r < '0' || r > '9' { - break - } - t.pos.AdvanceRune(r, size) - } +func numericDigit(c, base byte) bool { + if c >= '0' && c <= '9' { + return c-'0' < base } + return base == 16 && (c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') +} - // Look for exponent +func (t *Tokenizer) finishNumber(start int, mysql bool) (models.Token, error) { if t.pos.Index < len(t.input) { - r, size = utf8.DecodeRune(t.input[t.pos.Index:]) - if r == 'e' || r == 'E' { - t.pos.AdvanceRune(r, size) - - // Optional sign - if t.pos.Index < len(t.input) { - r, size = utf8.DecodeRune(t.input[t.pos.Index:]) - if r == '+' || r == '-' { - t.pos.AdvanceRune(r, size) + r, _ := utf8.DecodeRune(t.input[t.pos.Index:]) + if isIdentifierChar(r) { + if mysql { + // MySQL names may begin with digits (1x, 1e5x), but a + // number with a fraction or signed exponent ends there and + // the name that follows is an alias: 1.5a is 1.5 AS a. + if strings.ContainsAny(t.src[start:t.pos.Index], ".+-") { + return models.Token{Type: models.TokenTypeNumber, Value: t.src[start:t.pos.Index]}, nil } + t.scanIdentifierTail() + return models.Token{Type: models.TokenTypeIdentifier, Value: t.src[start:t.pos.Index]}, nil } - - // Must have at least one digit - if t.pos.Index >= len(t.input) { - value := t.src[start:t.pos.Index] - return models.Token{}, errors.InvalidNumberError(value+" (expected digit in exponent)", t.getCurrentPosition(), t.src) - } - - r, _ = utf8.DecodeRune(t.input[t.pos.Index:]) - if r < '0' || r > '9' { - value := t.src[start:t.pos.Index] - return models.Token{}, errors.InvalidNumberError(value+" (expected digit in exponent)", t.getCurrentPosition(), t.src) - } - - // Read exponent part - for t.pos.Index < len(t.input) { - r, size = utf8.DecodeRune(t.input[t.pos.Index:]) - if r < '0' || r > '9' { - _ = size // Mark as intentionally unused - break - } - t.pos.AdvanceRune(r, size) + // Oracle and SQL Server end a number at a keyword written + // against it (SELECT 1FROM dual, FETCH FIRST 5ROWS ONLY); the + // generic dialect keeps accepting that too. + if (t.dialect == keywords.DialectOracle || t.dialect == keywords.DialectSQLServer || + t.dialect == keywords.DialectGeneric) && t.keywordAt() { + return models.Token{Type: models.TokenTypeNumber, Value: t.src[start:t.pos.Index]}, nil } + return t.invalidNumeric(start) } } + return models.Token{Type: models.TokenTypeNumber, Value: t.src[start:t.pos.Index]}, nil +} - return models.Token{ - Type: models.TokenTypeNumber, - Value: t.src[start:t.pos.Index], - }, nil +// keywordAt reports whether the name starting at the cursor is a keyword. +func (t *Tokenizer) keywordAt() bool { + save := t.pos + t.scanIdentifierTail() + word := t.src[save.Index:t.pos.Index] + t.pos = save + var kbuf [keywordKeyBufLen]byte + if len(word) > keywordKeyBufLen || !asciiUpper(kbuf[:len(word)], word) { + return false + } + _, ok := keywordTokenTypes[string(kbuf[:len(word)])] + return ok +} + +func (t *Tokenizer) invalidNumeric(start int) (models.Token, error) { + return models.Token{}, errors.InvalidNumberError(t.src[start:t.pos.Index]+" (trailing junk after numeric literal)", t.getCurrentPosition(), t.src) } // isHexDigit reports whether c is an ASCII hexadecimal digit. @@ -2005,10 +2083,20 @@ func (t *Tokenizer) readPunctuation() (models.Token, error) { if t.dialect == keywords.DialectSQLServer || t.dialect == keywords.DialectSQLite { t.pos.AdvanceRune(r, size) // Consume [ contentStart := t.pos.Index + escaped := false for t.pos.Index < len(t.input) { ch, chSize := utf8.DecodeRune(t.input[t.pos.Index:]) if ch == ']' { + if t.dialect == keywords.DialectSQLServer && t.pos.Index+1 < len(t.input) && t.input[t.pos.Index+1] == ']' { + escaped = true + t.pos.Index += 2 + t.pos.Column += 2 + continue + } v := t.src[contentStart:t.pos.Index] + if escaped { + v = strings.ReplaceAll(v, "]]", "]") + } t.pos.AdvanceRune(ch, chSize) // Consume ] return models.Token{Type: models.TokenTypeIdentifier, Value: v, Quote: '['}, nil } diff --git a/pkg/sql/tokenizer/unicode_escape.go b/pkg/sql/tokenizer/unicode_escape.go new file mode 100644 index 00000000..866004a4 --- /dev/null +++ b/pkg/sql/tokenizer/unicode_escape.go @@ -0,0 +1,211 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package tokenizer + +import ( + "strings" + "unicode" + "unicode/utf16" + "unicode/utf8" + + "github.com/ajitpratap0/GoSQLX/pkg/errors" + "github.com/ajitpratap0/GoSQLX/pkg/models" +) + +// readUnicodeEscape lexes a PostgreSQL U&'...' string or U&"..." name with +// its optional UESCAPE clause. A string keeps its validated spelling in one +// TokenTypeUnicodeStringLiteral token; a name is decoded into an ordinary +// double-quoted identifier token so it works wherever a name does. +func (t *Tokenizer) readUnicodeEscape() (models.Token, error) { + start := t.pos + quote := t.input[t.pos.Index+2] + t.pos.Index += 3 + t.pos.Column += 3 + body := t.pos.Index + end, err := t.scanUnicodeQuote(quote) + if err != nil { + return models.Token{}, err + } + escape := rune('\\') + literalEnd := t.pos.Index + after := t.pos + comments := len(t.Comments) + commented, err := t.skipSpaceAndComments() + i := t.pos.Index + if err == nil && i+7 <= len(t.input) && strings.EqualFold(t.src[i:i+7], "UESCAPE") && + (i+7 == len(t.input) || !nextIsWordStart(t.input[i+7:])) { + t.pos.Index += 7 + t.pos.Column += 7 + more, err := t.skipSpaceAndComments() + if err != nil { + return models.Token{}, err + } + commented = commented || more + if t.pos.Index >= len(t.input) || t.input[t.pos.Index] != '\'' { + return models.Token{}, errors.UnexpectedCharError('U', t.toSQLPosition(start), t.src) + } + t.pos.Index++ + t.pos.Column++ + char := t.pos.Index + _, err = t.scanUnicodeQuote('\'') + if err != nil { + return models.Token{}, err + } + value := t.src[char : t.pos.Index-1] + r, n := utf8.DecodeRuneInString(value) + if n != len(value) || r == utf8.RuneError || unicode.IsSpace(r) || strings.ContainsRune("0123456789abcdefABCDEF+'\"", r) { + return models.Token{}, errors.UnexpectedCharError(r, t.toSQLPosition(start), t.src) + } + escape = r + } else { + // Not followed by UESCAPE: give back the whitespace and any + // comments so the main loop records them as usual. + t.pos = after + t.Comments = t.Comments[:comments] + commented = false + } + content := t.src[body:end] + if quote == '"' { + content = strings.ReplaceAll(content, `""`, `"`) + } + name, ok := decodeUnicodeEscapes(content, escape) + if !ok { + return models.Token{}, errors.UnexpectedCharError(escape, t.toSQLPosition(start), t.src) + } + if quote == '"' { + return models.Token{Type: models.TokenTypeIdentifier, Value: name, Quote: '"'}, nil + } + raw := t.src[start.Index:t.pos.Index] + if commented { + // Comments between the literal and UESCAPE are recorded in + // t.Comments; keep them out of the literal's spelling. + raw = t.src[start.Index:literalEnd] + " UESCAPE '" + string(escape) + "'" + } + return models.Token{Type: models.TokenTypeUnicodeStringLiteral, Value: raw, Quote: 'U'}, nil +} + +// skipSpaceAndComments skips whitespace and comments, recording the +// comments, and reports whether any comment was skipped. +func (t *Tokenizer) skipSpaceAndComments() (bool, error) { + commented := false + for { + t.skipWhitespace() + if t.pos.Index+1 >= len(t.input) { + return commented, nil + } + switch { + case t.input[t.pos.Index] == '-' && t.input[t.pos.Index+1] == '-': + if err := t.skipLineComment(); err != nil { + return commented, err + } + case t.input[t.pos.Index] == '/' && t.input[t.pos.Index+1] == '*': + if err := t.skipBlockComment(); err != nil { + return commented, err + } + default: + return commented, nil + } + commented = true + } +} + +func (t *Tokenizer) scanUnicodeQuote(quote byte) (int, error) { + for t.pos.Index < len(t.input) { + if t.input[t.pos.Index] == quote { + end := t.pos.Index + t.pos.Index++ + t.pos.Column++ + if t.pos.Index < len(t.input) && t.input[t.pos.Index] == quote { + t.pos.Index++ + t.pos.Column++ + continue + } + return end, nil + } + r, n := utf8.DecodeRune(t.input[t.pos.Index:]) + t.pos.AdvanceRune(r, n) + } + return 0, errors.UnterminatedStringError(t.getCurrentPosition(), t.src) +} + +// decodeUnicodeEscapes validates the escapes in s (\XXXX, \+XXXXXX, a +// doubled escape character, UTF-16 surrogate pairs) and returns the decoded +// text. +func decodeUnicodeEscapes(s string, escape rune) (string, bool) { + if !utf8.ValidString(s) { + return "", false + } + var b strings.Builder + b.Grow(len(s)) + high := rune(0) + for i := 0; i < len(s); { + r, n := utf8.DecodeRuneInString(s[i:]) + i += n + if r != escape { + if high != 0 { + return "", false + } + b.WriteRune(r) + continue + } + if i >= len(s) { + return "", false + } + r, n = utf8.DecodeRuneInString(s[i:]) + if r == escape { + if high != 0 { + return "", false + } + b.WriteRune(r) + i += n + continue + } + count := 4 + if s[i] == '+' { + count = 6 + i++ + } + if len(s)-i < count { + return "", false + } + var value rune + for j := 0; j < count; j++ { + c := s[i+j] + if !isHexDigit(c) { + return "", false + } + value *= 16 + switch { + case c <= '9': + value += rune(c - '0') + case c <= 'F': + value += rune(c - 'A' + 10) + default: + value += rune(c - 'a' + 10) + } + } + switch { + case value >= 0xD800 && value <= 0xDBFF: + if high != 0 { + return "", false + } + high = value + case value >= 0xDC00 && value <= 0xDFFF: + if high == 0 { + return "", false + } + b.WriteRune(utf16.DecodeRune(high, value)) + high = 0 + case high != 0 || value == 0 || !utf8.ValidRune(value): + return "", false + default: + b.WriteRune(value) + } + i += count + } + if high != 0 { + return "", false + } + return b.String(), true +} diff --git a/pkg/sql/tokenizer/unicode_escape_test.go b/pkg/sql/tokenizer/unicode_escape_test.go new file mode 100644 index 00000000..f21f2f17 --- /dev/null +++ b/pkg/sql/tokenizer/unicode_escape_test.go @@ -0,0 +1,93 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package tokenizer + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/models" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestUnicodeEscapeTokens(t *testing.T) { + tk, err := NewWithDialect(keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + for _, sql := range []string{`U&'d\0061t\+000061'`, `U&'d!0061t' UESCAPE '!'`, `u&'a''b'`, `U&'!+01F600' UESCAPE '!'`, `U&'\\'`, `U&'\D83D\DE00'`} { + tokens, err := tk.TokenizeString(sql) + if err != nil { + t.Errorf("%s: %v", sql, err) + continue + } + if len(tokens) != 2 || tokens[0].Token.Type != models.TokenTypeUnicodeStringLiteral || tokens[0].Token.Value != sql || tokens[0].Token.Quote != 'U' { + t.Errorf("%s: %v", sql, tokens) + } + } + for _, sql := range []string{`U&'\zzzz'`, `U&'\0000'`, `U&'\+110000'`, `U&'\D800'`, `U&'x' UESCAPE 'ab'`, `U&'x' UESCAPE '+'`, `U&'x' UESCAPE '0'`, `U&'x' UESCAPE ''`, `U&'unterminated`} { + if _, err := tk.TokenizeString(sql); err == nil { + t.Errorf("%s: expected error", sql) + } + } +} + +func TestUnicodeEscapeNames(t *testing.T) { + // U&"..." names decode into ordinary double-quoted identifier tokens. + for _, tc := range []struct{ sql, name string }{ + {`U&"d\0061t"`, "dat"}, + {`u&"d!0061t" UESCAPE '!'`, "dat"}, + {`U&"A""b"`, `A"b`}, + {`U&"\D83D\DE00"`, "\U0001F600"}, + {`U&"x\\y"`, `x\y`}, + {`U&"d!0061t" /* c */ UESCAPE /* d */ '!'`, "dat"}, + } { + got := lexAll(t, keywords.DialectPostgreSQL, tc.sql) + if len(got) != 1 || got[0].typ != models.TokenTypeIdentifier || got[0].val != tc.name { + t.Errorf("%s: %v", tc.sql, got) + } + } +} + +func TestUnicodeEscapeCommentsBeforeUESCAPE(t *testing.T) { + // Comments between the literal and UESCAPE are whitespace: they are + // recorded as comments and kept out of the literal's spelling. + for _, sql := range []string{"U&'x' /* c */ UESCAPE '!'", "U&'x' -- c\n UESCAPE '!'", "U&'x'/**/UESCAPE/**/'!'"} { + sql = strings.ReplaceAll(sql, `\n`, "\n") + tk, err := NewWithDialect(keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + tokens, err := tk.TokenizeString(sql) + if err != nil { + t.Errorf("%q: %v", sql, err) + continue + } + if len(tokens) != 2 || tokens[0].Token.Value != "U&'x' UESCAPE '!'" || len(tk.Comments) == 0 { + t.Errorf("%q: %v comments=%v", sql, tokens, tk.Comments) + } + } + // Without UESCAPE the comment is recorded once and the literal ends at + // its quote. + tk, err := NewWithDialect(keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + tokens, err := tk.TokenizeString("U&'x' /* c */ , 1") + if err != nil { + t.Fatal(err) + } + if len(tokens) != 4 || tokens[0].Token.Value != "U&'x'" || len(tk.Comments) != 1 { + t.Errorf("%v comments=%v", tokens, tk.Comments) + } +} + +func TestOtherDialectsKeepUnicodeAmpersandTokens(t *testing.T) { + for _, dialect := range []keywords.SQLDialect{keywords.DialectMySQL, keywords.DialectSQLServer, keywords.DialectSQLite, keywords.DialectOracle} { + tokens := lexAll(t, dialect, `U&'x'`) + if len(tokens) != 3 || tokens[0].val != "U" || tokens[1].val != "&" || tokens[2].val != "x" { + t.Errorf("%s: %v", dialect, tokens) + } + } +} diff --git a/pkg/transform/format_errors_test.go b/pkg/transform/format_errors_test.go new file mode 100644 index 00000000..bc2dcb6c --- /dev/null +++ b/pkg/transform/format_errors_test.go @@ -0,0 +1,27 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package transform + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestFormatSQLWithDialectCheckedRejectsUnsupportedXor(t *testing.T) { + tree, err := ParseSQLWithDialect("SELECT a ^ b", keywords.DialectMySQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + out, err := FormatSQLWithDialectChecked(tree.Statements[0], keywords.DialectSQLite) + if err == nil || !strings.Contains(err.Error(), "XOR") || out != "" { + t.Fatalf("got %q, %v; want XOR error", out, err) + } + if out := FormatSQLWithDialect(tree.Statements[0], keywords.DialectSQLite); out != "" { + t.Fatalf("legacy wrapper = %q; want documented empty result", out) + } +} diff --git a/pkg/transform/transform.go b/pkg/transform/transform.go index 938fbb2d..254a590b 100644 --- a/pkg/transform/transform.go +++ b/pkg/transform/transform.go @@ -232,13 +232,20 @@ func ParseSQL(sql string) (*ast.AST, error) { // // The output uses compact style with minimal whitespace. Use this after applying // transforms to obtain the final SQL to execute or log. +// It returns an empty string on rendering errors; use FormatSQLChecked to handle errors. // // Example: // // sql := transform.FormatSQL(stmt) // // "SELECT id, name FROM users WHERE active = true LIMIT 10" func FormatSQL(stmt ast.Statement) string { - return formatter.FormatStatement(stmt, ast.CompactStyle()) + out, _ := FormatSQLChecked(stmt) + return out +} + +// FormatSQLChecked is FormatSQL with semantic rendering errors returned to the caller. +func FormatSQLChecked(stmt ast.Statement) (string, error) { + return formatter.FormatStatementChecked(stmt, ast.CompactStyle()) } // FormatSQLWithDialect converts an AST statement back into a compact SQL string @@ -246,16 +253,24 @@ func FormatSQL(stmt ast.Statement) string { // FETCH FIRST for Oracle, LIMIT for PostgreSQL/MySQL/etc.). // // Pass keywords.DialectGeneric or an empty SQLDialect for generic behavior -// identical to FormatSQL. +// identical to FormatSQL. It returns an empty string on rendering errors; use +// FormatSQLWithDialectChecked to handle errors. // // Example: // // sql := transform.FormatSQLWithDialect(stmt, keywords.DialectSQLServer) // // "SELECT TOP 100 * FROM users" func FormatSQLWithDialect(stmt ast.Statement, dialect keywords.SQLDialect) string { + out, _ := FormatSQLWithDialectChecked(stmt, dialect) + return out +} + +// FormatSQLWithDialectChecked is FormatSQLWithDialect with semantic rendering +// errors returned to the caller when the target cannot express the source tree. +func FormatSQLWithDialectChecked(stmt ast.Statement, dialect keywords.SQLDialect) (string, error) { opts := ast.CompactStyle() opts.Dialect = string(dialect) - return formatter.FormatStatement(stmt, opts) + return formatter.FormatStatementChecked(stmt, opts) } // ParseSQLWithDialect parses a SQL string using dialect-specific tokenization and diff --git a/pkg/transpiler/format_errors_test.go b/pkg/transpiler/format_errors_test.go new file mode 100644 index 00000000..351e4b8c --- /dev/null +++ b/pkg/transpiler/format_errors_test.go @@ -0,0 +1,19 @@ +// Copyright 2026 GoSQLX Authors +// SPDX-License-Identifier: Apache-2.0 + +package transpiler_test + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" + "github.com/ajitpratap0/GoSQLX/pkg/transpiler" +) + +func TestTranspileRejectsUnsupportedXor(t *testing.T) { + out, err := transpiler.Transpile("SELECT a ^ b", keywords.DialectMySQL, keywords.DialectSQLite) + if err == nil || !strings.Contains(err.Error(), "XOR") || out != "" { + t.Fatalf("got %q, %v; want XOR error", out, err) + } +} diff --git a/pkg/transpiler/operators.go b/pkg/transpiler/operators.go index 93285919..9fb755f0 100644 --- a/pkg/transpiler/operators.go +++ b/pkg/transpiler/operators.go @@ -15,249 +15,11 @@ package transpiler import ( - "fmt" - "reflect" - "strings" - "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) -// retargetOperators rewrites operators and literals whose meaning or -// existence depends on the dialect, or fails when the target has no -// equivalent; none of them is ever emitted unchanged where it would mean -// something else or not parse: -// -// - ^ is exponentiation in PostgreSQL (and the generic dialect) and -// bitwise XOR in MySQL and MariaDB (binding tighter than *) and SQL -// Server (at the + - level). Exponentiation becomes POWER(a, b) outside -// PostgreSQL. XOR keeps ^ within its family, becomes ^ with explicit -// parentheses between the MySQL family and SQL Server, # in PostgreSQL, -// BITXOR() in Snowflake and bitXor() in ClickHouse, and is refused -// elsewhere. -// - The PostgreSQL prefix operators @, |/ and ||/ become ABS(), SQRT() and -// CBRT() (where it exists); the factorial operators are refused. -// - SQL Server money literals ($5.00, $5) and Snowflake object constants -// ({'a': 1}) are refused in other dialects. +// retargetOperators preserves operator meaning in the target dialect. func retargetOperators(stmt ast.Statement, from, to keywords.SQLDialect) error { - if from == to || stmt == nil { - return nil - } - r := operatorRetargeter{from: normalDialect(from), to: normalDialect(to), seen: map[uintptr]struct{}{}} - r.walk(reflect.ValueOf(stmt), false) - return r.err -} - -// normalDialect maps the generic dialect to "" and lower-cases the name. -func normalDialect(d keywords.SQLDialect) string { - s := strings.ToLower(string(d)) - if s == string(keywords.DialectGeneric) { - return "" - } - return s -} - -type operatorRetargeter struct { - from, to string - seen map[uintptr]struct{} - err error -} - -var ( - binaryExprType = reflect.TypeOf(ast.BinaryExpression{}) - unaryExprType = reflect.TypeOf(ast.UnaryExpression{}) -) - -// walk visits every exported field reachable from v. inOperator reports that -// v is an operand of a binary or unary operator expression. -func (r *operatorRetargeter) walk(v reflect.Value, inOperator bool) { - if r.err != nil { - return - } - switch v.Kind() { - case reflect.Pointer: - if v.IsNil() { - return - } - ptr := v.Pointer() - if _, ok := r.seen[ptr]; ok { - return - } - r.seen[ptr] = struct{}{} - r.walk(v.Elem(), inOperator) - case reflect.Interface: - if !v.IsNil() { - r.walk(v.Elem(), inOperator) - } - case reflect.Struct: - t := v.Type() - operands := t == binaryExprType || t == unaryExprType - for i := 0; i < v.NumField(); i++ { - if t.Field(i).IsExported() { - r.slot(v.Field(i), operands) - } - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - r.slot(v.Index(i), false) - } - } -} - -// slot rewrites the expression held by a settable interface slot after -// rewriting its subtree. -func (r *operatorRetargeter) slot(slot reflect.Value, inOperator bool) { - r.walk(slot, inOperator) - if r.err != nil || slot.Kind() != reflect.Interface || slot.IsNil() || !slot.CanSet() { - return - } - e, ok := slot.Interface().(ast.Expression) - if !ok { - return - } - repl, err := r.rewrite(e, inOperator) - if err != nil { - r.err = err - return - } - if repl != nil && reflect.TypeOf(repl).AssignableTo(slot.Type()) { - slot.Set(reflect.ValueOf(repl)) - } -} - -// rewrite returns the replacement for e, or nil to keep it. -func (r *operatorRetargeter) rewrite(e ast.Expression, inOperator bool) (ast.Expression, error) { - switch x := e.(type) { - case *ast.BinaryExpression: - if x == nil || x.Operator != "^" || x.CustomOp != nil || x.Not { - return nil, nil - } - if x.BitwiseXor { - return r.rewriteXor(x, inOperator) - } - return r.rewritePower(x) - case *ast.UnaryExpression: - if x == nil { - return nil, nil - } - return r.rewritePGPrefix(x) - case *ast.LiteralValue: - if x != nil && r.from == string(keywords.DialectSQLServer) && r.to != r.from && isMoneyLiteral(x) { - return nil, fmt.Errorf("the SQL Server money literal %v has no equivalent in %s", x.Value, r.target()) - } - case *ast.ObjectLiteral: - if x != nil && r.to != string(keywords.DialectSnowflake) { - return nil, fmt.Errorf("the Snowflake object constant {...} has no equivalent in %s", r.target()) - } - } - return nil, nil -} - -func (r *operatorRetargeter) target() string { - if r.to == "" { - return "the generic dialect" - } - return r.to -} - -// rewritePower handles ^ as exponentiation (PostgreSQL, generic). -func (r *operatorRetargeter) rewritePower(x *ast.BinaryExpression) (ast.Expression, error) { - if r.to == "" || r.to == string(keywords.DialectPostgreSQL) { - return nil, nil // same operator and precedence - } - return &ast.FunctionCall{Name: "POWER", Arguments: []ast.Expression{x.Left, x.Right}}, nil -} - -// rewriteXor handles ^ as bitwise XOR (MySQL, MariaDB, SQL Server). -func (r *operatorRetargeter) rewriteXor(x *ast.BinaryExpression, inOperator bool) (ast.Expression, error) { - if xorFamily(r.from) == xorFamily(r.to) { - return nil, nil // same operator and precedence - } - switch r.to { - case string(keywords.DialectMySQL), string(keywords.DialectMariaDB), string(keywords.DialectSQLServer): - return groupOperator(x, "^", inOperator), nil - case string(keywords.DialectPostgreSQL): - return groupOperator(x, "#", inOperator), nil - case string(keywords.DialectSnowflake): - return &ast.FunctionCall{Name: "BITXOR", Arguments: []ast.Expression{x.Left, x.Right}}, nil - case string(keywords.DialectClickHouse): - return &ast.FunctionCall{Name: "bitXor", Arguments: []ast.Expression{x.Left, x.Right}}, nil - } - return nil, fmt.Errorf("the bitwise XOR operator ^ has no equivalent in %s", r.target()) -} - -// xorFamily groups the dialects whose ^ is XOR at the same precedence. -func xorFamily(d string) string { - switch d { - case string(keywords.DialectMySQL), string(keywords.DialectMariaDB): - return "mysql" - } - return d -} - -// groupOperator returns x written with op and explicit parentheses around -// binary operands and, inside another operator, around itself, so the -// grouping of the source survives the target's precedence rules. -func groupOperator(x *ast.BinaryExpression, op string, inOperator bool) ast.Expression { - out := *x - out.Operator = op - out.BitwiseXor = op == "^" - out.Left = parenthesizeBinary(out.Left) - out.Right = parenthesizeBinary(out.Right) - if inOperator { - return &ast.ParenthesizedExpression{Expr: &out, Pos: x.Pos} - } - return &out -} - -func parenthesizeBinary(e ast.Expression) ast.Expression { - if b, ok := e.(*ast.BinaryExpression); ok && b != nil { - return &ast.ParenthesizedExpression{Expr: b, Pos: b.Pos} - } - return e -} - -// rewritePGPrefix handles the PostgreSQL-only prefix and postfix operators. -func (r *operatorRetargeter) rewritePGPrefix(x *ast.UnaryExpression) (ast.Expression, error) { - if r.to == string(keywords.DialectPostgreSQL) { - return nil, nil - } - call := func(name string) ast.Expression { - return &ast.FunctionCall{Name: name, Arguments: []ast.Expression{x.Expr}} - } - switch x.Operator { - case ast.PGAbs: - return call("ABS"), nil - case ast.PGSquareRoot: - return call("SQRT"), nil - case ast.PGCubeRoot: - switch r.to { - case string(keywords.DialectSnowflake), string(keywords.DialectClickHouse): - return call("CBRT"), nil - } - return nil, fmt.Errorf("the PostgreSQL cube root operator ||/ has no equivalent in %s", r.target()) - case ast.PGPrefixFactorial: - return nil, fmt.Errorf("the PostgreSQL factorial operator !! has no equivalent in %s", r.target()) - case ast.PGPostfixFactorial: - return nil, fmt.Errorf("the PostgreSQL factorial operator ! has no equivalent in %s", r.target()) - } - return nil, nil -} - -// isMoneyLiteral reports whether lit is a SQL Server money literal: $5.00, -// or $5, which the parser keeps as a placeholder. -func isMoneyLiteral(lit *ast.LiteralValue) bool { - if lit.Type == "money" { - return true - } - v, ok := lit.Value.(string) - if !ok || lit.Type != "placeholder" || len(v) < 2 || v[0] != '$' { - return false - } - for i := 1; i < len(v); i++ { - if v[i] < '0' || v[i] > '9' { - return false - } - } - return true + return ast.RetargetOperators(stmt, string(from), string(to)) } diff --git a/pkg/transpiler/quoted_types_test.go b/pkg/transpiler/quoted_types_test.go new file mode 100644 index 00000000..0bf2b17e --- /dev/null +++ b/pkg/transpiler/quoted_types_test.go @@ -0,0 +1,99 @@ +// 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 transpiler_test + +import ( + "strings" + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/formatter" + "github.com/ajitpratap0/GoSQLX/pkg/sql/ast" + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" + "github.com/ajitpratap0/GoSQLX/pkg/sql/parser" + "github.com/ajitpratap0/GoSQLX/pkg/transpiler" +) + +func TestQuotedTypeNames(t *testing.T) { + for _, tc := range []struct{ sql, typ string }{ + {`SELECT CAST(x AS "MyType")`, `"MyType"`}, + {`SELECT x::"char"`, `"char"`}, + {`CREATE TABLE t (a "MyDomain")`, `"MyDomain"`}, + {`SELECT CAST(x AS "MySchema"."MyType"[])`, `"MySchema"."MyType"[]`}, + {`SELECT CAST(x AS "a""b")`, `"a""b"`}, + } { + t.Run(tc.sql, func(t *testing.T) { + tree, err := parser.ParseWithDialect(tc.sql, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + for _, got := range []string{quotedTypeStatementSQL(t, tree.Statements[0]), formatter.FormatAST(tree, ast.CompactStyle())} { + if !strings.Contains(got, tc.typ) { + t.Fatalf("type %s lost in %s", tc.typ, got) + } + again, err := parser.ParseWithDialect(got, keywords.DialectPostgreSQL) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(again) + if quotedTypeStatementSQL(t, again.Statements[0]) != quotedTypeStatementSQL(t, tree.Statements[0]) { + t.Fatalf("round trip changed: %s", got) + } + } + }) + } +} + +func TestTranspileQuotedTypeNames(t *testing.T) { + for _, tc := range []struct { + sql string + target keywords.SQLDialect + want string + }{ + {`SELECT CAST(x AS "MyType")`, keywords.DialectMySQL, "CAST(x AS `MyType`)"}, + {`SELECT x::"char"`, keywords.DialectSQLServer, "CAST(x AS [char])"}, + {`CREATE TABLE t (a "MyDomain")`, keywords.DialectMySQL, "a `MyDomain`"}, + {`SELECT CAST(x AS "MySchema"."a]b")`, keywords.DialectSQLServer, "CAST(x AS [MySchema].[a]]b])"}, + {"SELECT CAST(x AS `MyType`)", keywords.DialectPostgreSQL, `CAST(x AS "MyType")`}, + } { + t.Run(tc.sql+string(tc.target), func(t *testing.T) { + from := keywords.DialectPostgreSQL + if strings.Contains(tc.sql, "`") { + from = keywords.DialectMySQL + } + got, err := transpiler.Transpile(tc.sql, from, tc.target) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(got, tc.want) { + t.Fatalf("got %s, want %s", got, tc.want) + } + tree, err := parser.ParseWithDialect(got, tc.target) + if err != nil { + t.Fatal(err) + } + defer ast.ReleaseAST(tree) + }) + } +} + +func quotedTypeStatementSQL(t *testing.T, stmt ast.Statement) string { + t.Helper() + renderer, ok := stmt.(interface{ SQL() string }) + if !ok { + t.Fatalf("statement %T lacks SQL()", stmt) + } + return renderer.SQL() +} diff --git a/pkg/transpiler/quoting.go b/pkg/transpiler/quoting.go index c2ad19b2..499da957 100644 --- a/pkg/transpiler/quoting.go +++ b/pkg/transpiler/quoting.go @@ -43,4 +43,5 @@ func requoteIdentifiers(stmt ast.Statement, from, to keywords.SQLDialect) { return } ast.SetNameQuoteStyle(stmt, identifierDelimiter(to), to == keywords.DialectOracle) + ast.RestyleDataTypes(stmt, identifierDelimiter(to)) } diff --git a/pkg/transpiler/transpiler.go b/pkg/transpiler/transpiler.go index e5f8f6ab..5ac9adaa 100644 --- a/pkg/transpiler/transpiler.go +++ b/pkg/transpiler/transpiler.go @@ -104,5 +104,9 @@ func Transpile(sql string, from, to keywords.SQLDialect) (string, error) { // and set-operation grouping. style := ast.CompactStyle() style.Dialect = string(to) - return formatter.FormatAST(tree, style), nil + out, err := formatter.FormatASTChecked(tree, style) + if err != nil { + return "", fmt.Errorf("format: %w", err) + } + return out, nil }