Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/gosqlx/cmd/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ func validateRun(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
}

// Reject unknown dialect names early before any parsing.
if validateDialect != "" && !keywords.IsValidDialect(validateDialect) {
return fmt.Errorf("unknown SQL dialect %q; valid dialects: postgresql, mysql, sqlserver, oracle, sqlite, snowflake, bigquery, redshift", validateDialect)
}

// Handle stdin input
if ShouldReadFromStdin(args) {
return validateFromStdin(cmd)
Expand Down
24 changes: 13 additions & 11 deletions cmd/gosqlx/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func DefaultConfig() *Config {
Compact: false,
},
Validation: ValidationConfig{
Dialect: "postgresql",
Dialect: "",
StrictMode: false,
Recursive: false,
Pattern: "*.sql",
Expand Down Expand Up @@ -343,17 +343,19 @@ func (c *Config) Validate() error {
return fmt.Errorf("format.max_line_length must be between 0 and 500, got %d", c.Format.MaxLineLength)
}

// Validate dialect
validDialects := []string{"postgresql", "mysql", "sqlserver", "oracle", "sqlite", "generic"}
dialectValid := false
for _, d := range validDialects {
if c.Validation.Dialect == d {
dialectValid = true
break
// Validate dialect (empty string means permissive/no dialect gates)
if c.Validation.Dialect != "" {
validDialects := []string{"postgresql", "mysql", "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 !dialectValid {
return fmt.Errorf("validate.dialect must be one of: %v, got '%s'", validDialects, c.Validation.Dialect)
}

// Validate output format
Expand Down
4 changes: 2 additions & 2 deletions cmd/gosqlx/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func TestDefaultConfig(t *testing.T) {
}

// Test validation defaults
if cfg.Validation.Dialect != "postgresql" {
t.Errorf("expected dialect to be 'postgresql', got '%s'", cfg.Validation.Dialect)
if cfg.Validation.Dialect != "" {
t.Errorf("expected dialect to be '' (empty = permissive), got '%s'", cfg.Validation.Dialect)
}
if cfg.Validation.StrictMode {
t.Error("expected strict_mode to be false")
Expand Down
4 changes: 2 additions & 2 deletions cmd/gosqlx/internal/config/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ func TestConfigWithFlags(t *testing.T) {
if cfg.Format.Indent != 2 {
t.Errorf("expected default indent 2, got %d", cfg.Format.Indent)
}
if cfg.Validation.Dialect != "postgresql" {
t.Errorf("expected default dialect 'postgresql', got '%s'", cfg.Validation.Dialect)
if cfg.Validation.Dialect != "" {
t.Errorf("expected default dialect '' (empty), got '%s'", cfg.Validation.Dialect)
}

// Simulate CLI flags overriding config
Expand Down
4 changes: 2 additions & 2 deletions cmd/gosqlx/internal/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ func GetSchema() *ConfigSchema {
Desc string `yaml:"description"`
}{
Options: ValidDialects,
Default: "postgresql",
Desc: "SQL dialect for validation",
Default: "",
Desc: "SQL dialect for validation (empty = permissive, accepts all dialects)",
},
StrictMode: struct {
Default bool `yaml:"default"`
Expand Down
4 changes: 2 additions & 2 deletions cmd/gosqlx/internal/config/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,8 @@ func TestGetSchema(t *testing.T) {
if len(schema.Validation.Dialect.Options) != len(ValidDialects) {
t.Errorf("expected %d dialect options, got %d", len(ValidDialects), len(schema.Validation.Dialect.Options))
}
if schema.Validation.Dialect.Default != "postgresql" {
t.Errorf("expected validate.dialect.default to be 'postgresql', got '%s'", schema.Validation.Dialect.Default)
if schema.Validation.Dialect.Default != "" {
t.Errorf("expected validate.dialect.default to be '' (empty), got '%s'", schema.Validation.Dialect.Default)
}
if schema.Validation.Dialect.Desc == "" {
t.Error("validate.dialect.description should not be empty")
Expand Down
79 changes: 79 additions & 0 deletions pkg/sql/keywords/dialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,33 @@ func DialectKeywords(dialect SQLDialect) []Keyword {
return SQLITE_SPECIFIC
case DialectSQLServer:
return SQLSERVER_SPECIFIC
case DialectOracle:
return ORACLE_SPECIFIC
default:
return nil
}
}

// IsValidDialect reports whether name is a recognised SQL dialect identifier.
// An empty string is also considered valid (meaning "use the default dialect").
//
// Example:
//
// keywords.IsValidDialect("mysql") // true
// keywords.IsValidDialect("fakesql") // false
// keywords.IsValidDialect("") // true (default)
func IsValidDialect(name string) bool {
if name == "" {
return true
}
for _, d := range AllDialects() {
if string(d) == name {
return true
}
}
return false
}

// AllDialects returns all supported SQL dialect identifiers.
// This includes both fully implemented dialects (with dialect-specific keywords)
// and placeholder dialects that currently use only the base keyword set.
Expand Down Expand Up @@ -237,3 +259,60 @@ var SQLSERVER_SPECIFIC = []Keyword{
{Word: "SCOPE_IDENTITY", Type: models.TokenTypeKeyword},
{Word: "OUTPUT", Type: models.TokenTypeKeyword},
}

// ORACLE_SPECIFIC contains Oracle Database-specific keywords and extensions.
// These keywords are recognized when using DialectOracle.
//
// Examples: ROWNUM, ROWID, SYSDATE, DUAL, CONNECT BY, NVL, DECODE
var ORACLE_SPECIFIC = []Keyword{
{Word: "ROWNUM", Type: models.TokenTypeKeyword},
{Word: "ROWID", Type: models.TokenTypeKeyword},
{Word: "SYSDATE", Type: models.TokenTypeKeyword},
{Word: "SYSTIMESTAMP", Type: models.TokenTypeKeyword},
{Word: "DUAL", Type: models.TokenTypeKeyword},
{Word: "NOCYCLE", Type: models.TokenTypeKeyword},
{Word: "PRIOR", Type: models.TokenTypeKeyword},
{Word: "LEVEL", Type: models.TokenTypeKeyword},
{Word: "CONNECT_BY_ROOT", Type: models.TokenTypeKeyword},
{Word: "NVL", Type: models.TokenTypeKeyword},
{Word: "NVL2", Type: models.TokenTypeKeyword},
{Word: "DECODE", Type: models.TokenTypeKeyword},
{Word: "GREATEST", Type: models.TokenTypeKeyword},
{Word: "LEAST", Type: models.TokenTypeKeyword},
{Word: "LPAD", Type: models.TokenTypeKeyword},
{Word: "RPAD", Type: models.TokenTypeKeyword},
{Word: "INSTR", Type: models.TokenTypeKeyword},
{Word: "SUBSTR", Type: models.TokenTypeKeyword},
{Word: "TRUNC", Type: models.TokenTypeKeyword},
{Word: "ROUND", Type: models.TokenTypeKeyword},
{Word: "MOD", Type: models.TokenTypeKeyword},
{Word: "NCHAR", Type: models.TokenTypeKeyword},
{Word: "VARCHAR2", Type: models.TokenTypeKeyword},
{Word: "NUMBER", Type: models.TokenTypeKeyword},
{Word: "CLOB", Type: models.TokenTypeKeyword},
{Word: "BLOB", Type: models.TokenTypeKeyword},
{Word: "NCLOB", Type: models.TokenTypeKeyword},
{Word: "RAW", Type: models.TokenTypeKeyword},
{Word: "LONG", Type: models.TokenTypeKeyword},
{Word: "PIPELINED", Type: models.TokenTypeKeyword},
{Word: "BULK", Type: models.TokenTypeKeyword},
{Word: "COLLECT", Type: models.TokenTypeKeyword},
{Word: "FORALL", Type: models.TokenTypeKeyword},
{Word: "EXCEPTION", Type: models.TokenTypeKeyword},
{Word: "PRAGMA", Type: models.TokenTypeKeyword},
{Word: "SEQUENCE", Type: models.TokenTypeKeyword},
{Word: "NEXTVAL", Type: models.TokenTypeKeyword},
{Word: "CURRVAL", Type: models.TokenTypeKeyword},
{Word: "NOCACHE", Type: models.TokenTypeKeyword},
{Word: "NOCOPY", Type: models.TokenTypeKeyword},
{Word: "SUBPARTITION", Type: models.TokenTypeKeyword},
{Word: "STORAGE", Type: models.TokenTypeKeyword},
{Word: "FLASHBACK", Type: models.TokenTypeKeyword},
{Word: "VERSIONS", Type: models.TokenTypeKeyword},
{Word: "SCN", Type: models.TokenTypeKeyword},
{Word: "TIMESTAMP", Type: models.TokenTypeKeyword},
{Word: "MINVALUE", Type: models.TokenTypeKeyword},
{Word: "INCREMENT", Type: models.TokenTypeKeyword},
{Word: "NOMINVALUE", Type: models.TokenTypeKeyword},
{Word: "NOMAXVALUE", Type: models.TokenTypeKeyword},
}
Loading
Loading