From c5a6ab952c909b649b4e7be5c8b10a41b642682e Mon Sep 17 00:00:00 2001 From: Ajit Pratap Singh Date: Tue, 20 Oct 2026 02:17:43 +0530 Subject: [PATCH] fix(cli): derive every dialect list from keywords.DialectNames Add keywords.DialectNames as the single source of truth and derive CLI --dialect help, validation errors, the MCP validate_sql enum, config validation and the WASM bridge from it. Update the GitHub Action, VS Code gosqlx.dialect enum and docs/example configs to the full 11-dialect list, with drift tests covering package.json and docs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KTj1PEbxzVCETkeYrZKcrE --- .../workflows/examples/.gosqlx-example.yml | 2 +- .gosqlx.example.yml | 2 +- .gosqlx.yml | 2 +- CHANGELOG.md | 13 ++ action.yml | 4 +- action/scripts/validate.sh | 2 +- cmd/gosqlx/cmd/analyze.go | 2 +- cmd/gosqlx/cmd/cmd_utils.go | 17 +- cmd/gosqlx/cmd/config_template.yml | 2 +- cmd/gosqlx/cmd/dialect_sync_test.go | 180 ++++++++++++++++++ cmd/gosqlx/cmd/format.go | 2 +- cmd/gosqlx/cmd/lint.go | 2 +- cmd/gosqlx/cmd/parse.go | 2 +- cmd/gosqlx/cmd/transpile.go | 3 + cmd/gosqlx/cmd/validate.go | 6 +- cmd/gosqlx/doc.go | 2 +- cmd/gosqlx/internal/config/README.md | 4 +- cmd/gosqlx/internal/config/config.go | 16 +- cmd/gosqlx/internal/config/doc.go | 4 +- cmd/gosqlx/internal/config/template.yml | 2 +- docs/CLI_GUIDE.md | 4 +- docs/CONFIGURATION.md | 6 +- docs/MCP_GUIDE.md | 4 +- pkg/config/README.md | 4 +- pkg/config/config.go | 23 +-- pkg/config/config_test.go | 14 ++ pkg/config/doc.go | 4 +- pkg/config/lsp.go | 5 +- pkg/lsp/doc.go | 2 +- pkg/mcp/server.go | 7 +- pkg/mcp/server_test.go | 25 +++ pkg/sql/keywords/dialect.go | 23 +++ pkg/sql/keywords/dialect_names_test.go | 51 +++++ pkg/sql/parser/validate.go | 11 +- vscode-extension/README.md | 2 +- vscode-extension/package.json | 7 +- .../src/test/unit/commands.test.ts | 4 +- .../src/test/unit/errorRecovery.test.ts | 2 +- .../src/test/unit/validation.test.ts | 17 +- vscode-extension/src/utils/validation.ts | 8 +- wasm/internal/bridge/bridge.go | 24 +-- wasm/internal/bridge/bridge_test.go | 10 + 42 files changed, 424 insertions(+), 102 deletions(-) create mode 100644 cmd/gosqlx/cmd/dialect_sync_test.go create mode 100644 pkg/sql/keywords/dialect_names_test.go diff --git a/.github/workflows/examples/.gosqlx-example.yml b/.github/workflows/examples/.gosqlx-example.yml index 95a9305c..75f03400 100644 --- a/.github/workflows/examples/.gosqlx-example.yml +++ b/.github/workflows/examples/.gosqlx-example.yml @@ -5,7 +5,7 @@ # Validation settings validate: # SQL dialect to use (auto-detect if not specified) - # Options: postgresql, mysql, sqlserver, oracle, sqlite + # Options: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse dialect: postgresql # Enable strict validation mode (more rigorous checks) diff --git a/.gosqlx.example.yml b/.gosqlx.example.yml index b2d8e4e4..1300d477 100644 --- a/.gosqlx.example.yml +++ b/.gosqlx.example.yml @@ -30,7 +30,7 @@ format: # Validate Configuration # Controls SQL validation behavior validate: - # SQL dialect: postgresql, mysql, sqlserver, oracle, sqlite, generic + # SQL dialect: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse # Default: postgresql dialect: postgresql diff --git a/.gosqlx.yml b/.gosqlx.yml index 6494338d..0c1a38ee 100644 --- a/.gosqlx.yml +++ b/.gosqlx.yml @@ -2,7 +2,7 @@ # Place this file in your project root as .gosqlx.yml # SQL dialect (auto-detect if not specified) -# Options: postgresql, mysql, sqlserver, oracle, sqlite +# Options: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse dialect: postgresql # Linting rules diff --git a/CHANGELOG.md b/CHANGELOG.md index f33b1f40..9134c7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -370,6 +370,19 @@ acceptance, CLI refusals), so read **Behavior changes** before upgrading. - **Readable-style output that did not re-parse**: inner `;` and `;;` removed (#602, #631); readable-style re-parse failures over the testdata corpus fell from 36 to 6. +- **Dialect lists now agree everywhere**: the `--dialect` help on `parse`, + `format`, `analyze`, `lint` and `validate`, the CLI's unknown-dialect error, + config-file validation, the MCP `validate_sql` schema, the playground WASM + bridge, the GitHub Action and the VS Code `gosqlx.dialect` setting all list or + accept the same 11 names: `generic`, `postgresql`, `mysql`, `mariadb`, + `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift`, + `clickhouse`. Before, help listed 7, the error omitted `generic` and + `clickhouse`, `pkg/config` rejected `generic`, `bigquery`, `redshift` and + `clickhouse`, and the VS Code setting offered 6 (and suggested `mysql` for + `mariadb`). The Go lists derive from the new `keywords.DialectNames()`, and a + test checks the VS Code extension and the example configs and READMEs against + it. Names stay case-sensitive in the CLI. `transpile --from/--to` keep their + own list of transpilable dialects. ### Performance Numbers are quoted from each PR (benchstat on an Apple M4 unless stated; the diff --git a/action.yml b/action.yml index 737d2ea5..0bf5362d 100644 --- a/action.yml +++ b/action.yml @@ -48,7 +48,7 @@ inputs: default: '' dialect: - description: 'SQL dialect: postgresql, mysql, sqlserver, oracle, sqlite (default: auto-detect)' + description: 'SQL dialect: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse (default: auto-detect)' required: false default: '' @@ -211,7 +211,7 @@ runs: # Add dialect if provided DIALECT="${{ inputs.dialect }}" - if [ -n "$DIALECT" ] && [[ "$DIALECT" =~ ^(postgresql|mysql|sqlserver|oracle|sqlite)$ ]]; then + if [ -n "$DIALECT" ] && [[ "$DIALECT" =~ ^(generic|postgresql|mysql|mariadb|sqlserver|oracle|sqlite|snowflake|bigquery|redshift|clickhouse)$ ]]; then CMD="$CMD --dialect $DIALECT" fi diff --git a/action/scripts/validate.sh b/action/scripts/validate.sh index f46e7b8d..d4349b9b 100755 --- a/action/scripts/validate.sh +++ b/action/scripts/validate.sh @@ -38,7 +38,7 @@ fi DIALECT="${INPUT_DIALECT:-}" if [ -n "$DIALECT" ]; then - if [[ "$DIALECT" =~ ^(postgresql|mysql|sqlserver|oracle|sqlite)$ ]]; then + if [[ "$DIALECT" =~ ^(generic|postgresql|mysql|mariadb|sqlserver|oracle|sqlite|snowflake|bigquery|redshift|clickhouse)$ ]]; then CMD="$CMD --dialect $DIALECT" else echo "::warning::Invalid dialect '$DIALECT', skipping dialect flag" diff --git a/cmd/gosqlx/cmd/analyze.go b/cmd/gosqlx/cmd/analyze.go index c2b0beba..14452628 100644 --- a/cmd/gosqlx/cmd/analyze.go +++ b/cmd/gosqlx/cmd/analyze.go @@ -212,5 +212,5 @@ func init() { analyzeCmd.Flags().BoolVar(&analyzePerformance, "performance", false, "focus on performance optimization analysis (config: analyze.performance)") analyzeCmd.Flags().BoolVar(&analyzeComplexity, "complexity", false, "focus on complexity metrics (config: analyze.complexity)") analyzeCmd.Flags().BoolVar(&analyzeAll, "all", false, "comprehensive analysis (config: analyze.all)") - analyzeCmd.Flags().StringVar(&analyzeDialect, "dialect", "", "SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: postgresql)") + analyzeCmd.Flags().StringVar(&analyzeDialect, "dialect", "", dialectFlagUsage("default: postgresql")) } diff --git a/cmd/gosqlx/cmd/cmd_utils.go b/cmd/gosqlx/cmd/cmd_utils.go index a305eff7..d8383be8 100644 --- a/cmd/gosqlx/cmd/cmd_utils.go +++ b/cmd/gosqlx/cmd/cmd_utils.go @@ -16,6 +16,7 @@ package cmd import ( "fmt" + "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -53,13 +54,27 @@ func trackChangedFlags(cmd *cobra.Command) map[string]bool { // validateDialectName returns an error if dialect is a non-empty, unrecognized // SQL dialect name. An empty string means "use the default dialect" and is // always accepted. +// Names are matched case-sensitively, as keywords.IsValidDialect does. func validateDialectName(dialect string) error { if dialect != "" && !keywords.IsValidDialect(dialect) { - return fmt.Errorf("unknown SQL dialect %q; valid dialects: postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift", dialect) + return fmt.Errorf("unknown SQL dialect %q; valid dialects: %s", dialect, validDialectsText()) } return nil } +// validDialectsText returns the comma-separated list of user-selectable +// dialect names, derived from keywords.DialectNames so CLI help and errors +// never drift from what the library accepts. +func validDialectsText() string { + return strings.Join(keywords.DialectNames(), ", ") +} + +// dialectFlagUsage builds the help text for a --dialect flag. suffix is the +// command-specific default note, e.g. "default: postgresql". +func dialectFlagUsage(suffix string) string { + return fmt.Sprintf("SQL dialect: %s (%s)", validDialectsText(), suffix) +} + // tokenizerForDialect returns a tokenizer configured for the given dialect // together with a release function that must be called when tokenization is // complete (use defer). diff --git a/cmd/gosqlx/cmd/config_template.yml b/cmd/gosqlx/cmd/config_template.yml index 9a087f6f..719d9226 100644 --- a/cmd/gosqlx/cmd/config_template.yml +++ b/cmd/gosqlx/cmd/config_template.yml @@ -23,7 +23,7 @@ format: # Validation settings - controls SQL validation behavior validate: # SQL dialect for validation - # Options: postgresql, mysql, sqlserver, oracle, sqlite, generic + # Options: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse # Default: postgresql dialect: postgresql diff --git a/cmd/gosqlx/cmd/dialect_sync_test.go b/cmd/gosqlx/cmd/dialect_sync_test.go new file mode 100644 index 00000000..ab162e67 --- /dev/null +++ b/cmd/gosqlx/cmd/dialect_sync_test.go @@ -0,0 +1,180 @@ +// 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 cmd - dialect_sync_test.go +// Drift guards: every user-facing dialect list (CLI --dialect help, CLI +// errors, VS Code extension settings) must match keywords.DialectNames(). + +package cmd + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +// vscodeExtensionDir is the VS Code extension directory relative to this +// package (cmd/gosqlx/cmd). +var vscodeExtensionDir = filepath.Join("..", "..", "..", "vscode-extension") + +func TestDialectFlagHelp_ListsEveryDialect(t *testing.T) { + names := keywords.DialectNames() + found := 0 + var walk func(c *cobra.Command) + walk = func(c *cobra.Command) { + if f := c.Flags().Lookup("dialect"); f != nil { + found++ + for _, name := range names { + if !strings.Contains(f.Usage, name) { + t.Errorf("%q --dialect help %q does not mention %q", c.CommandPath(), f.Usage, name) + } + } + } + for _, sub := range c.Commands() { + walk(sub) + } + } + walk(rootCmd) + // parse, format, analyze, lint, validate at minimum. + if found < 5 { + t.Errorf("found %d commands with a --dialect flag, want at least 5", found) + } +} + +func TestValidateDialectName_AcceptsLibraryDialects(t *testing.T) { + for _, name := range keywords.DialectNames() { + if err := validateDialectName(name); err != nil { + t.Errorf("validateDialectName(%q) = %v, want nil", name, err) + } + } + + // Case handling is unchanged: names are matched case-sensitively. + if err := validateDialectName("MySQL"); err == nil { + t.Error(`validateDialectName("MySQL") = nil, want error (case-sensitive)`) + } + + err := validateDialectName("bogus") + if err == nil { + t.Fatal(`validateDialectName("bogus") = nil, want error`) + } + for _, name := range keywords.DialectNames() { + if !strings.Contains(err.Error(), name) { + t.Errorf("error %q does not list dialect %q", err.Error(), name) + } + } +} + +func TestVSCodeDialectSetting_MatchesLibrary(t *testing.T) { + data, err := os.ReadFile(filepath.Join(vscodeExtensionDir, "package.json")) + if err != nil { + t.Fatalf("read vscode-extension/package.json: %v", err) + } + var manifest struct { + Contributes struct { + Configuration struct { + Properties map[string]json.RawMessage `json:"properties"` + } `json:"configuration"` + } `json:"contributes"` + } + if err := json.Unmarshal(data, &manifest); err != nil { + t.Fatalf("parse vscode-extension/package.json: %v", err) + } + raw, ok := manifest.Contributes.Configuration.Properties["gosqlx.dialect"] + if !ok { + t.Fatal("gosqlx.dialect setting not found in vscode-extension/package.json") + } + var setting struct { + Enum []string `json:"enum"` + Default string `json:"default"` + } + if err := json.Unmarshal(raw, &setting); err != nil { + t.Fatalf("parse gosqlx.dialect setting: %v", err) + } + want := keywords.DialectNames() + if !reflect.DeepEqual(setting.Enum, want) { + t.Errorf("gosqlx.dialect enum = %v, want %v (keywords.DialectNames)", setting.Enum, want) + } + if !keywords.IsValidDialect(setting.Default) { + t.Errorf("gosqlx.dialect default %q is not a valid dialect", setting.Default) + } +} + +func TestVSCodeValidDialects_MatchesLibrary(t *testing.T) { + data, err := os.ReadFile(filepath.Join(vscodeExtensionDir, "src", "utils", "validation.ts")) + if err != nil { + t.Fatalf("read vscode-extension/src/utils/validation.ts: %v", err) + } + block := regexp.MustCompile(`(?s)export const VALID_DIALECTS = \[(.*?)\] as const;`).FindSubmatch(data) + if block == nil { + t.Fatal("VALID_DIALECTS declaration not found in validation.ts") + } + var got []string + for _, m := range regexp.MustCompile(`'([^']*)'`).FindAllSubmatch(block[1], -1) { + got = append(got, string(m[1])) + } + want := keywords.DialectNames() + if !reflect.DeepEqual(got, want) { + t.Errorf("VALID_DIALECTS = %v, want %v (keywords.DialectNames)", got, want) + } +} + +// TestDocDialectLists_MatchLibrary checks the hand-written dialect lists in +// docs and example configs. Each file must contain at least one line naming +// postgresql and mysql, and every such line must name every dialect. +func TestDocDialectLists_MatchLibrary(t *testing.T) { + repoRoot := filepath.Join("..", "..", "..") + files := []string{ + "cmd/gosqlx/doc.go", + "cmd/gosqlx/internal/config/README.md", + "pkg/config/README.md", + ".gosqlx.example.yml", + ".gosqlx.yml", + ".github/workflows/examples/.gosqlx-example.yml", + "docs/MCP_GUIDE.md", + "docs/CLI_GUIDE.md", + "docs/CONFIGURATION.md", + } + listLine := regexp.MustCompile("`?postgresql`?, `?mysql`?") + names := keywords.DialectNames() + for _, rel := range files { + data, err := os.ReadFile(filepath.Join(repoRoot, filepath.FromSlash(rel))) + if err != nil { + t.Errorf("read %s: %v", rel, err) + continue + } + lists := 0 + for i, line := range strings.Split(string(data), "\n") { + if !listLine.MatchString(line) { + continue + } + lists++ + for _, name := range names { + if !strings.Contains(line, name) { + t.Errorf("%s:%d: dialect list does not mention %q: %s", rel, i+1, name, strings.TrimSpace(line)) + } + } + } + if lists == 0 { + t.Errorf("%s: no dialect list found (update this test if the list moved)", rel) + } + } +} diff --git a/cmd/gosqlx/cmd/format.go b/cmd/gosqlx/cmd/format.go index 23f3a255..c394a081 100644 --- a/cmd/gosqlx/cmd/format.go +++ b/cmd/gosqlx/cmd/format.go @@ -249,7 +249,7 @@ func init() { formatCmd.Flags().BoolVar(&formatCompact, "compact", false, "compact format (config: format.compact)") formatCmd.Flags().BoolVar(&formatCheck, "check", false, "check if files need formatting (CI mode)") formatCmd.Flags().IntVar(&formatMaxLine, "max-line", 80, "maximum line length (config: format.max_line_length)") - formatCmd.Flags().StringVar(&formatDialect, "dialect", "", "SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: postgresql)") + formatCmd.Flags().StringVar(&formatDialect, "dialect", "", dialectFlagUsage("default: postgresql")) // Add negation flags formatCmd.Flags().BoolVar(&formatUppercase, "no-uppercase", false, "keep original keyword case") diff --git a/cmd/gosqlx/cmd/lint.go b/cmd/gosqlx/cmd/lint.go index e4474edf..cc27d2c4 100644 --- a/cmd/gosqlx/cmd/lint.go +++ b/cmd/gosqlx/cmd/lint.go @@ -445,5 +445,5 @@ func init() { lintCmd.Flags().IntVar(&lintMaxLength, "max-length", 100, "maximum line length (L005 rule)") lintCmd.Flags().BoolVar(&lintFailOnWarn, "fail-on-warn", false, "exit with error code on warnings") lintCmd.Flags().BoolVar(&lintSecurity, "security", false, "run security scanner (LIKE injection, tautology, UNION injection)") - lintCmd.Flags().StringVar(&lintDialect, "dialect", "", "SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: postgresql)") + lintCmd.Flags().StringVar(&lintDialect, "dialect", "", dialectFlagUsage("default: postgresql")) } diff --git a/cmd/gosqlx/cmd/parse.go b/cmd/gosqlx/cmd/parse.go index fae129e4..c32d74e8 100644 --- a/cmd/gosqlx/cmd/parse.go +++ b/cmd/gosqlx/cmd/parse.go @@ -182,5 +182,5 @@ func init() { parseCmd.Flags().BoolVar(&parseShowAST, "ast", false, "show detailed AST structure") parseCmd.Flags().BoolVar(&parseShowTokens, "tokens", false, "show tokenization output") parseCmd.Flags().BoolVar(&parseTreeView, "tree", false, "show tree visualization") - parseCmd.Flags().StringVar(&parseDialect, "dialect", "", "SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: postgresql)") + parseCmd.Flags().StringVar(&parseDialect, "dialect", "", dialectFlagUsage("default: postgresql")) } diff --git a/cmd/gosqlx/cmd/transpile.go b/cmd/gosqlx/cmd/transpile.go index 2a089030..ba07b949 100644 --- a/cmd/gosqlx/cmd/transpile.go +++ b/cmd/gosqlx/cmd/transpile.go @@ -82,6 +82,9 @@ Examples: } func init() { + // The transpile dialect list is deliberately separate from + // keywords.DialectNames(): it names the dialects parseDialectFlag maps for + // gosqlx.Transpile (and accepts the postgres/mssql aliases). transpileCmd.Flags().String("from", "mysql", "Source dialect (mysql, postgres, sqlite, sqlserver, oracle, snowflake, clickhouse, mariadb)") transpileCmd.Flags().String("to", "postgres", "Target dialect (mysql, postgres, sqlite, sqlserver, oracle, snowflake, clickhouse, mariadb)") rootCmd.AddCommand(transpileCmd) diff --git a/cmd/gosqlx/cmd/validate.go b/cmd/gosqlx/cmd/validate.go index 51153f26..cfcf39ee 100644 --- a/cmd/gosqlx/cmd/validate.go +++ b/cmd/gosqlx/cmd/validate.go @@ -88,8 +88,8 @@ func validateRun(cmd *cobra.Command, args []string) error { } // Reject unknown dialect names early before any parsing. - if validateDialect != "" && !keywords.IsValidDialect(validateDialect) { - return fmt.Errorf("unknown SQL dialect %q; valid dialects: postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift", validateDialect) + if err := validateDialectName(validateDialect); err != nil { + return err } // Handle stdin input @@ -334,7 +334,7 @@ func init() { validateCmd.Flags().BoolVarP(&validateQuiet, "quiet", "q", false, "quiet mode (exit code only)") validateCmd.Flags().BoolVar(&validateQuiet, "check", false, "check mode (alias for --quiet): exit code only, no output") validateCmd.Flags().BoolVarP(&validateStats, "stats", "s", false, "show performance statistics") - validateCmd.Flags().StringVar(&validateDialect, "dialect", "", "SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (config: validate.dialect)") + validateCmd.Flags().StringVar(&validateDialect, "dialect", "", dialectFlagUsage("config: validate.dialect")) validateCmd.Flags().BoolVar(&validateStrict, "strict", false, "enable strict validation mode (config: validate.strict_mode)") validateCmd.Flags().StringVar(&validateOutputFormat, "output-format", "text", "output format: text, json, sarif") validateCmd.Flags().StringVar(&validateOutputFile, "output-file", "", "output file path (default: stdout)") diff --git a/cmd/gosqlx/doc.go b/cmd/gosqlx/doc.go index 4da72367..ce5f50c2 100644 --- a/cmd/gosqlx/doc.go +++ b/cmd/gosqlx/doc.go @@ -114,7 +114,7 @@ // -p, --pattern string File pattern for recursive processing (default "*.sql") // -q, --quiet Quiet mode (exit code only) // -s, --stats Show performance statistics -// --dialect string SQL dialect: postgresql, mysql, sqlserver, oracle, sqlite +// --dialect string SQL dialect: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse // --strict Enable strict validation mode // --output-format string Output format: text, json, sarif (default "text") // --output-file string Output file path (default: stdout) diff --git a/cmd/gosqlx/internal/config/README.md b/cmd/gosqlx/internal/config/README.md index fe92ed96..481bb148 100644 --- a/cmd/gosqlx/internal/config/README.md +++ b/cmd/gosqlx/internal/config/README.md @@ -50,7 +50,7 @@ type FormatConfig struct { ```go type ValidationConfig struct { - Dialect string // SQL dialect (postgresql, mysql, sqlserver, oracle, sqlite, generic) + Dialect string // SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) StrictMode bool // Enable strict validation Recursive bool // Recursively process directories Pattern string // File pattern for recursive processing @@ -298,7 +298,7 @@ The package validates configuration values to ensure correctness: - **indent**: Must be between 0 and 8 - **max_line_length**: Must be between 0 and 500 -- **dialect**: Must be one of: postgresql, mysql, sqlserver, oracle, sqlite, generic +- **dialect**: Must be one of: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse - **output.format**: Must be one of: json, yaml, table, tree, auto ## Testing diff --git a/cmd/gosqlx/internal/config/config.go b/cmd/gosqlx/internal/config/config.go index d1665099..6a56b30b 100644 --- a/cmd/gosqlx/internal/config/config.go +++ b/cmd/gosqlx/internal/config/config.go @@ -20,6 +20,8 @@ import ( "path/filepath" "gopkg.in/yaml.v3" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) // Config represents the complete GoSQLX CLI configuration. @@ -84,7 +86,7 @@ type FormatConfig struct { // Controls validation behavior including dialect selection and security limits. // // Fields: -// - Dialect: SQL dialect for validation (postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite, generic) +// - Dialect: SQL dialect for validation (any name from keywords.DialectNames()) // - StrictMode: Enable strict validation rules (default: false) // - Recursive: Recursively process directories (default: false) // - Pattern: File pattern for recursive processing (default: "*.sql") @@ -345,16 +347,8 @@ func (c *Config) Validate() error { // Validate dialect (empty string means permissive/no dialect gates) if c.Validation.Dialect != "" { - validDialects := []string{"postgresql", "mysql", "mariadb", "snowflake", "sqlserver", "oracle", "sqlite", "generic"} - dialectValid := false - for _, d := range validDialects { - if c.Validation.Dialect == d { - dialectValid = true - break - } - } - if !dialectValid { - return fmt.Errorf("validate.dialect must be one of: %v, got '%s'", validDialects, c.Validation.Dialect) + if !keywords.IsValidDialect(c.Validation.Dialect) { + return fmt.Errorf("validate.dialect must be one of: %v, got '%s'", keywords.DialectNames(), c.Validation.Dialect) } } diff --git a/cmd/gosqlx/internal/config/doc.go b/cmd/gosqlx/internal/config/doc.go index a991f043..ee9970b1 100644 --- a/cmd/gosqlx/internal/config/doc.go +++ b/cmd/gosqlx/internal/config/doc.go @@ -152,7 +152,7 @@ // max_file_size: 10485760 # Maximum file size in bytes // // Fields: -// - Dialect: SQL dialect (postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite, generic) +// - Dialect: SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) // - StrictMode: Enable strict validation rules (default: false) // - Recursive: Recursively process directories (default: false) // - Pattern: File pattern for recursive processing (default: "*.sql") @@ -195,7 +195,7 @@ // - MaxLineLength: 0-500 characters // // Validation validation: -// - Dialect: Must be one of: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite, generic +// - Dialect: Must be one of keywords.DialectNames() // // Output validation: // - Format: Must be one of: json, yaml, table, tree, auto diff --git a/cmd/gosqlx/internal/config/template.yml b/cmd/gosqlx/internal/config/template.yml index 9a087f6f..719d9226 100644 --- a/cmd/gosqlx/internal/config/template.yml +++ b/cmd/gosqlx/internal/config/template.yml @@ -23,7 +23,7 @@ format: # Validation settings - controls SQL validation behavior validate: # SQL dialect for validation - # Options: postgresql, mysql, sqlserver, oracle, sqlite, generic + # Options: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse # Default: postgresql dialect: postgresql diff --git a/docs/CLI_GUIDE.md b/docs/CLI_GUIDE.md index 5c579763..7fc34149 100644 --- a/docs/CLI_GUIDE.md +++ b/docs/CLI_GUIDE.md @@ -99,7 +99,7 @@ gosqlx validate [file...] [flags] | Flag | Type | Default | Description | |------|------|---------|-------------| -| `--dialect` | string | postgresql | SQL dialect (postgresql, mysql, sqlserver, oracle, sqlite) | +| `--dialect` | string | postgresql | SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) | | `-r, --recursive` | bool | false | Recursively process directories | | `-p, --pattern` | string | `*.sql` | File pattern for recursive processing | | `-q, --quiet` | bool | false | Quiet mode (exit code only) | @@ -681,7 +681,7 @@ format: # Validation settings - controls SQL validation behavior validate: - dialect: postgresql # SQL dialect (postgresql, mysql, sqlserver, oracle, sqlite, generic) + dialect: postgresql # SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) strict_mode: false # Enable strict validation recursive: false # Recursively process directories pattern: "*.sql" # File pattern for recursive processing diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d69729e5..0e533db8 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -99,7 +99,7 @@ GoSQLX searches for configuration files in this order: | Option | Default | Type | Options | |--------|---------|------|---------| -| `validate.dialect` | `postgresql` | string | postgresql, mysql, sqlserver, oracle, sqlite, generic | +| `validate.dialect` | `postgresql` | string | generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse | | `validate.strict_mode` | `false` | bool | - | | `validate.recursive` | `false` | bool | - | | `validate.pattern` | `*.sql` | string | Any glob pattern | @@ -177,7 +177,7 @@ format: SQL dialect for validation. -**Options**: `postgresql`, `mysql`, `sqlserver`, `oracle`, `sqlite`, `generic` +**Options**: `generic`, `postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift`, `clickhouse` ```yaml validate: @@ -378,7 +378,7 @@ analyze: ### Dialect Validation -Valid values: `postgresql`, `mysql`, `sqlserver`, `oracle`, `sqlite`, `generic` +Valid values: `generic`, `postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift`, `clickhouse` ### Output Format Validation diff --git a/docs/MCP_GUIDE.md b/docs/MCP_GUIDE.md index 04dec099..6d3c4460 100644 --- a/docs/MCP_GUIDE.md +++ b/docs/MCP_GUIDE.md @@ -32,7 +32,7 @@ The GoSQLX MCP server (`gosqlx-mcp`) exposes all GoSQLX SQL capabilities as [Mod - **7 SQL Tools**: validate, format, parse, extract metadata, security scan, lint, and composite analyze - **Streamable HTTP**: Compatible with any MCP client that supports the streamable HTTP transport - **Optional Bearer Auth**: Protect the server with a token when exposing to a network -- **Multi-Dialect Validation**: postgresql, mysql, sqlite, sqlserver, oracle, snowflake, generic +- **Multi-Dialect Validation**: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse - **Concurrent Analysis**: `analyze_sql` fans out all 6 tools via `sync.WaitGroup` - one round trip for a full SQL health report - **Zero Business Logic Duplication**: Every tool delegates to the existing `pkg/gosqlx`, `pkg/linter`, and `pkg/sql/security` packages @@ -256,7 +256,7 @@ All tools accept a required `sql` string parameter. The server returns tool-sema | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `sql` | string | yes | - | The SQL string to validate | -| `dialect` | string | no | *(generic)* | One of: `generic`, `mysql`, `postgresql`, `sqlite`, `sqlserver`, `oracle`, `snowflake` | +| `dialect` | string | no | *(generic)* | One of: `generic`, `postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift`, `clickhouse` | #### Response diff --git a/pkg/config/README.md b/pkg/config/README.md index 37b1f816..6d980015 100644 --- a/pkg/config/README.md +++ b/pkg/config/README.md @@ -102,7 +102,7 @@ format: ```yaml validation: - dialect: postgresql # SQL dialect (postgresql, mysql, sqlserver, oracle, sqlite) + dialect: postgresql # SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) strict_mode: false # Enable strict validation recursive: false # Recursively validate directories pattern: "*.sql" # File pattern for validation @@ -185,7 +185,7 @@ if err := cfg.Validate(); err != nil { Validation rules: - **Format.Indent**: Must be non-negative - **Format.MaxLineLength**: Must be non-negative -- **Validation.Dialect**: Must be one of: postgresql, mysql, sqlserver, oracle, sqlite +- **Validation.Dialect**: Must be one of: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse - **Validation.Security.MaxFileSize**: Must be non-negative - **Output.Format**: Must be one of: text, json, yaml - **LSP.TraceServer**: Must be one of: off, messages, verbose diff --git a/pkg/config/config.go b/pkg/config/config.go index 7cca0d8a..300cff8f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,7 +16,10 @@ package config import ( "fmt" + "strings" "time" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) // Bool returns a pointer to the given bool value. @@ -77,12 +80,14 @@ type FormatConfig struct { // ValidationConfig holds SQL validation options for the parser and validator. // // The Dialect field determines which SQL keywords and syntax are recognized. -// Supported values: "postgresql", "mysql", "mariadb", "snowflake", "sqlserver", "oracle", "sqlite". +// Supported values are those returned by keywords.DialectNames(): "generic", +// "postgresql", "mysql", "mariadb", "sqlserver", "oracle", "sqlite", +// "snowflake", "bigquery", "redshift", "clickhouse". // // The Pattern field is used for recursive file validation and supports standard // glob patterns like "*.sql", "queries/**/*.sql", etc. type ValidationConfig struct { - Dialect string `yaml:"dialect" json:"dialect"` // SQL dialect: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: "postgresql") + Dialect string `yaml:"dialect" json:"dialect"` // SQL dialect: any name from keywords.DialectNames() (default: "postgresql") StrictMode *bool `yaml:"strict_mode" json:"strictMode"` // Enable strict validation mode (default: false) Recursive *bool `yaml:"recursive" json:"recursive"` // Recursively validate files in directories (default: false) Pattern string `yaml:"pattern" json:"pattern"` // File pattern for recursive validation (default: "*.sql") @@ -212,17 +217,9 @@ func (c *Config) Validate() error { } // Validate Validation settings - validDialects := map[string]bool{ - "postgresql": true, - "mysql": true, - "mariadb": true, - "snowflake": true, - "sqlserver": true, - "oracle": true, - "sqlite": true, - } - if c.Validation.Dialect != "" && !validDialects[c.Validation.Dialect] { - return fmt.Errorf("validation.dialect must be one of: postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite; got %q", c.Validation.Dialect) + if c.Validation.Dialect != "" && !keywords.IsValidDialect(c.Validation.Dialect) { + return fmt.Errorf("validation.dialect must be one of: %s; got %q", + strings.Join(keywords.DialectNames(), ", "), c.Validation.Dialect) } if c.Validation.Security.MaxFileSize < 0 { return fmt.Errorf("validation.security.max_file_size must be non-negative, got %d", c.Validation.Security.MaxFileSize) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index daff813d..f1ae6f43 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -101,6 +101,20 @@ func TestConfigValidation(t *testing.T) { }, wantError: true, }, + { + name: "clickhouse dialect", + modifier: func(c *Config) { + c.Validation.Dialect = "clickhouse" + }, + wantError: false, + }, + { + name: "generic dialect", + modifier: func(c *Config) { + c.Validation.Dialect = "generic" + }, + wantError: false, + }, { name: "invalid output format", modifier: func(c *Config) { diff --git a/pkg/config/doc.go b/pkg/config/doc.go index d2834f48..bf118b47 100644 --- a/pkg/config/doc.go +++ b/pkg/config/doc.go @@ -47,7 +47,7 @@ // // Validation: SQL validation and dialect settings // -// - dialect: Target SQL dialect - postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite (default: postgresql) +// - dialect: Target SQL dialect - generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse (default: postgresql) // - strict_mode: Enable strict validation mode (default: false) // - recursive: Recursively validate files in directories (default: false) // - pattern: File pattern for recursive validation (default: "*.sql") @@ -246,7 +246,7 @@ // Validation checks: // // - Format: Non-negative indent and max_line_length -// - Validation: Valid dialect (postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite) +// - Validation: Valid dialect (any name from keywords.DialectNames()) // - Output: Valid format (text, json, yaml) // - LSP: Non-negative rate limits, timeouts, and size limits // - LSP: Valid trace server level (off, messages, verbose) diff --git a/pkg/config/lsp.go b/pkg/config/lsp.go index 076e8a01..232851ab 100644 --- a/pkg/config/lsp.go +++ b/pkg/config/lsp.go @@ -18,7 +18,10 @@ import ( "encoding/json" "fmt" "reflect" + "strings" "time" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) // LoadFromLSPInitOptions loads configuration from LSP initialization options. @@ -335,7 +338,7 @@ func GetLSPConfigSections() []LSPConfigSection { Description: "SQL validation options", DefaultValue: ToLSPSettings(defaults)["validation"], Properties: map[string]interface{}{ - "dialect": "SQL dialect (postgresql, mysql, mariadb, snowflake, sqlserver, oracle, sqlite)", + "dialect": "SQL dialect (" + strings.Join(keywords.DialectNames(), ", ") + ")", "strictMode": "Enable strict validation mode", }, }, diff --git a/pkg/lsp/doc.go b/pkg/lsp/doc.go index af005859..50e0676f 100644 --- a/pkg/lsp/doc.go +++ b/pkg/lsp/doc.go @@ -237,7 +237,7 @@ Add to LSP.sublime-settings: The LSP server can be configured via .gosqlx.yml in your project root: - # SQL dialect (postgresql, mysql, sqlite, sqlserver, oracle, generic) + # SQL dialect (generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse) dialect: postgresql # Linting rules (see docs/LINTING_RULES.md) diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index ed288fdc..abde17e5 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -19,9 +19,12 @@ import ( "fmt" "log" "net/http" + "strings" "github.com/mark3labs/mcp-go/mcp" mcpserver "github.com/mark3labs/mcp-go/server" + + sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) // Server wraps the MCP server with all GoSQLX tools registered. @@ -99,8 +102,8 @@ func (s *Server) registerTools() { mcp.Description("The SQL string to validate"), ), mcp.WithString("dialect", - mcp.Description("SQL dialect: generic, mysql, postgresql, sqlite, sqlserver, oracle, snowflake"), - mcp.Enum("generic", "mysql", "postgresql", "sqlite", "sqlserver", "oracle", "snowflake"), + mcp.Description("SQL dialect: "+strings.Join(sqlkeywords.DialectNames(), ", ")), + mcp.Enum(sqlkeywords.DialectNames()...), ), ), handleValidateSQL, diff --git a/pkg/mcp/server_test.go b/pkg/mcp/server_test.go index 1224ec96..d7bc132b 100644 --- a/pkg/mcp/server_test.go +++ b/pkg/mcp/server_test.go @@ -19,8 +19,11 @@ import ( "fmt" "net" "net/http" + "reflect" "testing" "time" + + sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) func TestNew_CreatesServer(t *testing.T) { @@ -180,3 +183,25 @@ func TestServer_Start_WithAuth(t *testing.T) { t.Fatal("server did not shut down within 5s") } } + +// TestValidateSQL_DialectEnumMatchesLibrary guards against the validate_sql +// tool schema drifting from the dialects the library accepts. +func TestValidateSQL_DialectEnumMatchesLibrary(t *testing.T) { + srv := New(DefaultConfig()) + tool := srv.mcpSrv.GetTool("validate_sql") + if tool == nil { + t.Fatal("validate_sql tool not registered") + } + prop, ok := tool.Tool.InputSchema.Properties["dialect"].(map[string]any) + if !ok { + t.Fatalf("dialect property has unexpected type %T", tool.Tool.InputSchema.Properties["dialect"]) + } + got, ok := prop["enum"].([]string) + if !ok { + t.Fatalf("dialect enum has unexpected type %T", prop["enum"]) + } + want := sqlkeywords.DialectNames() + if !reflect.DeepEqual(got, want) { + t.Errorf("validate_sql dialect enum = %v, want %v", got, want) + } +} diff --git a/pkg/sql/keywords/dialect.go b/pkg/sql/keywords/dialect.go index fda36eb8..453a3b19 100644 --- a/pkg/sql/keywords/dialect.go +++ b/pkg/sql/keywords/dialect.go @@ -156,6 +156,29 @@ func AllDialects() []SQLDialect { } } +// DialectNames returns the user-selectable SQL dialect names as plain strings, +// in the same stable order as AllDialects. It is the single source of truth +// for user-facing dialect lists (CLI flag help, CLI and config error messages, +// MCP tool schemas, editor settings); every name it returns is accepted by +// IsValidDialect. DialectUnknown is not included because it is not a +// meaningful user choice. +// +// Each call returns a fresh slice that the caller may modify. +// +// Example: +// +// fmt.Println(strings.Join(keywords.DialectNames(), ", ")) +// // generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, +// // snowflake, bigquery, redshift, clickhouse +func DialectNames() []string { + dialects := AllDialects() + names := make([]string, len(dialects)) + for i, d := range dialects { + names[i] = string(d) + } + return names +} + // GetCompoundKeywords returns the compound keywords map. // Compound keywords are multi-word SQL keywords like "GROUP BY", "ORDER BY", // "GROUPING SETS", "MATERIALIZED VIEW", etc. diff --git a/pkg/sql/keywords/dialect_names_test.go b/pkg/sql/keywords/dialect_names_test.go new file mode 100644 index 00000000..ab31247b --- /dev/null +++ b/pkg/sql/keywords/dialect_names_test.go @@ -0,0 +1,51 @@ +// 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 keywords_test + +import ( + "testing" + + "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" +) + +func TestDialectNames(t *testing.T) { + names := keywords.DialectNames() + all := keywords.AllDialects() + if len(names) != len(all) { + t.Fatalf("DialectNames() has %d entries, AllDialects() has %d", len(names), len(all)) + } + seen := make(map[string]bool, len(names)) + for i, name := range names { + if name != string(all[i]) { + t.Errorf("DialectNames()[%d] = %q, want %q (same order as AllDialects)", i, name, all[i]) + } + if !keywords.IsValidDialect(name) { + t.Errorf("DialectNames() entry %q rejected by IsValidDialect", name) + } + if name == string(keywords.DialectUnknown) || name == "" { + t.Errorf("DialectNames() must not contain %q", name) + } + if seen[name] { + t.Errorf("DialectNames() contains duplicate %q", name) + } + seen[name] = true + } + + // The returned slice is a fresh copy; mutating it must not affect later calls. + names[0] = "mutated" + if keywords.DialectNames()[0] == "mutated" { + t.Error("DialectNames() returned a shared slice") + } +} diff --git a/pkg/sql/parser/validate.go b/pkg/sql/parser/validate.go index f7bf6f3e..c58b7054 100644 --- a/pkg/sql/parser/validate.go +++ b/pkg/sql/parser/validate.go @@ -86,16 +86,7 @@ func trimBytes(b []byte) []byte { // validDialectList returns a human-readable comma-separated list of all valid // dialect names for use in error messages. func validDialectList() string { - dialects := keywords.AllDialects() - names := make([]string, 0, len(dialects)) - for _, d := range dialects { - // Belt-and-suspenders: DialectUnknown is not in AllDialects(), but guard - // here in case a future refactor inadvertently adds it back. - if d != keywords.DialectUnknown { - names = append(names, string(d)) - } - } - return strings.Join(names, ", ") + return strings.Join(keywords.DialectNames(), ", ") } // ParseBytes parses SQL from a []byte input without requiring a string conversion. diff --git a/vscode-extension/README.md b/vscode-extension/README.md index e64bd86d..1318b091 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -81,7 +81,7 @@ This extension contributes the following settings: | `gosqlx.format.indentSize` | `2` | Resource | Number of spaces for indentation | | `gosqlx.format.uppercaseKeywords` | `true` | Resource | Convert keywords to uppercase | | `gosqlx.validation.enable` | `true` | Resource | Enable real-time SQL validation | -| `gosqlx.dialect` | `generic` | Resource | SQL dialect for validation | +| `gosqlx.dialect` | `generic` | Resource | SQL dialect for validation: `generic`, `postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `bigquery`, `redshift`, `clickhouse` | | `gosqlx.timeouts.startup` | `10000` | Window | Language server startup timeout (ms) | | `gosqlx.timeouts.validation` | `5000` | Window | Executable validation timeout (ms) | | `gosqlx.timeouts.analysis` | `30000` | Window | SQL analysis timeout (ms) | diff --git a/vscode-extension/package.json b/vscode-extension/package.json index ffe408f5..c496d31b 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -119,9 +119,14 @@ "generic", "postgresql", "mysql", + "mariadb", "sqlserver", "oracle", - "sqlite" + "sqlite", + "snowflake", + "bigquery", + "redshift", + "clickhouse" ], "default": "generic", "scope": "resource", diff --git a/vscode-extension/src/test/unit/commands.test.ts b/vscode-extension/src/test/unit/commands.test.ts index 59ae4905..5e29b4c8 100644 --- a/vscode-extension/src/test/unit/commands.test.ts +++ b/vscode-extension/src/test/unit/commands.test.ts @@ -27,7 +27,7 @@ suite('Configuration Validation Unit Tests', () => { }); test('validateDialect should accept valid dialects', () => { - const validDialects = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite']; + const validDialects = ['generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', 'snowflake', 'bigquery', 'redshift', 'clickhouse']; for (const dialect of validDialects) { const result = validateDialect(dialect); assert.strictEqual(result.valid, true, `Dialect ${dialect} should be valid`); @@ -349,7 +349,7 @@ function validateIndentSize(value: number): ValidationResult { } function validateDialect(dialect: string): ValidationResult { - const validDialects = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite']; + const validDialects = ['generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', 'snowflake', 'bigquery', 'redshift', 'clickhouse']; if (!validDialects.includes(dialect.toLowerCase())) { return { valid: false, message: `Invalid dialect. Valid options: ${validDialects.join(', ')}` }; } diff --git a/vscode-extension/src/test/unit/errorRecovery.test.ts b/vscode-extension/src/test/unit/errorRecovery.test.ts index 76bfb24e..6b0ad6bd 100644 --- a/vscode-extension/src/test/unit/errorRecovery.test.ts +++ b/vscode-extension/src/test/unit/errorRecovery.test.ts @@ -585,7 +585,7 @@ class ConfigWithFallback { if (dialectValue === 'postgres') { dialectValue = 'postgresql'; } - const validDialects = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite']; + const validDialects = ['generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', 'snowflake', 'bigquery', 'redshift', 'clickhouse']; if (validDialects.includes(dialectValue as string)) { result.dialect = dialectValue as string; } diff --git a/vscode-extension/src/test/unit/validation.test.ts b/vscode-extension/src/test/unit/validation.test.ts index fd110bec..11f36ffb 100644 --- a/vscode-extension/src/test/unit/validation.test.ts +++ b/vscode-extension/src/test/unit/validation.test.ts @@ -115,7 +115,7 @@ suite('Indent Size Validation Edge Cases', () => { suite('Dialect Validation Edge Cases', () => { test('should accept valid dialects in lowercase', () => { - const dialects = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite']; + const dialects = ['generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', 'snowflake', 'bigquery', 'redshift', 'clickhouse']; for (const dialect of dialects) { const result = validateDialect(dialect); assert.strictEqual(result.valid, true, `${dialect} should be valid`); @@ -123,7 +123,7 @@ suite('Dialect Validation Edge Cases', () => { }); test('should accept valid dialects in uppercase', () => { - const dialects = ['GENERIC', 'POSTGRESQL', 'MYSQL', 'SQLSERVER', 'ORACLE', 'SQLITE']; + const dialects = ['GENERIC', 'POSTGRESQL', 'MYSQL', 'MARIADB', 'SQLSERVER', 'ORACLE', 'SQLITE', 'SNOWFLAKE', 'BIGQUERY', 'REDSHIFT', 'CLICKHOUSE']; for (const dialect of dialects) { const result = validateDialect(dialect); assert.strictEqual(result.valid, true, `${dialect} should be valid`); @@ -131,7 +131,7 @@ suite('Dialect Validation Edge Cases', () => { }); test('should accept valid dialects in mixed case', () => { - const dialects = ['Generic', 'PostgreSQL', 'MySQL', 'SQLServer', 'Oracle', 'SQLite']; + const dialects = ['Generic', 'PostgreSQL', 'MySQL', 'MariaDB', 'SQLServer', 'Oracle', 'SQLite', 'Snowflake', 'BigQuery', 'Redshift', 'ClickHouse']; for (const dialect of dialects) { const result = validateDialect(dialect); assert.strictEqual(result.valid, true, `${dialect} should be valid`); @@ -165,9 +165,9 @@ suite('Dialect Validation Edge Cases', () => { assert.ok(result.suggestion?.includes('postgresql')); }); - test('should suggest mysql for mariadb', () => { + test('should accept mariadb as its own dialect', () => { const result = validateDialect('mariadb'); - assert.ok(result.suggestion?.includes('mysql')); + assert.strictEqual(result.valid, true); }); test('should suggest sqlserver for mssql', () => { @@ -583,7 +583,7 @@ function validateDialect(dialect: unknown): ValidationResult { return { valid: false, message: 'Dialect must be a string', - suggestion: 'Valid dialects: generic, postgresql, mysql, sqlserver, oracle, sqlite' + suggestion: 'Valid dialects: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse' }; } @@ -593,11 +593,11 @@ function validateDialect(dialect: unknown): ValidationResult { return { valid: false, message: 'Dialect cannot be empty', - suggestion: 'Valid dialects: generic, postgresql, mysql, sqlserver, oracle, sqlite' + suggestion: 'Valid dialects: generic, postgresql, mysql, mariadb, sqlserver, oracle, sqlite, snowflake, bigquery, redshift, clickhouse' }; } - const validDialects = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite']; + const validDialects = ['generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', 'snowflake', 'bigquery', 'redshift', 'clickhouse']; if (!validDialects.includes(normalized)) { // Check for common aliases @@ -605,7 +605,6 @@ function validateDialect(dialect: unknown): ValidationResult { 'postgres': 'postgresql', 'pg': 'postgresql', 'pgsql': 'postgresql', - 'mariadb': 'mysql', 'mssql': 'sqlserver', 'sql server': 'sqlserver', 'ora': 'oracle', diff --git a/vscode-extension/src/utils/validation.ts b/vscode-extension/src/utils/validation.ts index 5ba626dd..6d52a05c 100644 --- a/vscode-extension/src/utils/validation.ts +++ b/vscode-extension/src/utils/validation.ts @@ -9,7 +9,12 @@ export interface ValidationResult { suggestion?: string; } -export const VALID_DIALECTS = ['generic', 'postgresql', 'mysql', 'sqlserver', 'oracle', 'sqlite'] as const; +// Must match keywords.DialectNames() in the Go library and the gosqlx.dialect +// enum in package.json (both checked by cmd/gosqlx/cmd/dialect_sync_test.go). +export const VALID_DIALECTS = [ + 'generic', 'postgresql', 'mysql', 'mariadb', 'sqlserver', 'oracle', 'sqlite', + 'snowflake', 'bigquery', 'redshift', 'clickhouse' +] as const; export type SqlDialect = typeof VALID_DIALECTS[number]; export const SQL_LANGUAGE_IDS = ['sql', 'pgsql', 'mysql', 'plpgsql', 'tsql', 'plsql', 'gosqlx'] as const; @@ -315,7 +320,6 @@ function findClosestDialect(input: string): string | undefined { 'postgres': 'postgresql', 'pg': 'postgresql', 'pgsql': 'postgresql', - 'mariadb': 'mysql', 'mssql': 'sqlserver', 'sql server': 'sqlserver', 'microsoftsql': 'sqlserver', diff --git a/wasm/internal/bridge/bridge.go b/wasm/internal/bridge/bridge.go index 935d7554..be4e1020 100644 --- a/wasm/internal/bridge/bridge.go +++ b/wasm/internal/bridge/bridge.go @@ -34,23 +34,15 @@ import ( "github.com/ajitpratap0/GoSQLX/pkg/sql/security" ) -// dialectMap maps the playground's dialect strings to keywords.SQLDialect constants. -var dialectMap = map[string]sqlkeywords.SQLDialect{ - "generic": sqlkeywords.DialectGeneric, - "postgresql": sqlkeywords.DialectPostgreSQL, - "mysql": sqlkeywords.DialectMySQL, - "mariadb": sqlkeywords.DialectMariaDB, - "clickhouse": sqlkeywords.DialectClickHouse, - "sqlite": sqlkeywords.DialectSQLite, - "sqlserver": sqlkeywords.DialectSQLServer, - "oracle": sqlkeywords.DialectOracle, - "snowflake": sqlkeywords.DialectSnowflake, -} - -// Dialect resolves a playground dialect string. Unknown or empty values -// resolve to "" (the parser's default dialect). +// Dialect resolves a playground dialect string. Any name accepted by +// keywords.IsValidDialect (see keywords.DialectNames) resolves to that +// dialect; unknown or empty values resolve to "" (the parser's default +// dialect). func Dialect(name string) sqlkeywords.SQLDialect { - return dialectMap[name] + if name == "" || !sqlkeywords.IsValidDialect(name) { + return "" + } + return sqlkeywords.SQLDialect(name) } // Parse parses sql with the given playground dialect. diff --git a/wasm/internal/bridge/bridge_test.go b/wasm/internal/bridge/bridge_test.go index d38e672d..2c5f7aca 100644 --- a/wasm/internal/bridge/bridge_test.go +++ b/wasm/internal/bridge/bridge_test.go @@ -18,6 +18,8 @@ import ( "encoding/json" "strings" "testing" + + sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords" ) const ( @@ -212,4 +214,12 @@ func TestDialect(t *testing.T) { if Dialect("nope") != "" { t.Error("unknown dialect should resolve to the default") } + if Dialect("") != "" { + t.Error("empty dialect should resolve to the default") + } + for _, name := range sqlkeywords.DialectNames() { + if got := Dialect(name); string(got) != name { + t.Errorf("Dialect(%q) = %q, want %q", name, got, name) + } + } }