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
19 changes: 18 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,24 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0


# The CLI's unstamped --version and the LSP report gosqlx.Version, so a
# tag must match the Version const in pkg/gosqlx. Fail before building.
- name: Check tag matches gosqlx.Version
env:
TAG: ${{ github.ref_name }}
run: |
src="$(sed -n 's/^const Version = "\([^"]*\)".*/\1/p' pkg/gosqlx/gosqlx.go)"
if [ -z "$src" ]; then
echo "::error file=pkg/gosqlx/gosqlx.go::could not find 'const Version = \"...\"'"
exit 1
fi
if [ "${TAG#v}" != "$src" ]; then
echo "::error file=pkg/gosqlx/gosqlx.go::tag ${TAG} does not match gosqlx.Version ${src}; bump the const before tagging"
exit 1
fi
echo "tag ${TAG} matches gosqlx.Version ${src}"

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Formatting turns one-line SQL into a readable query:
- **[Analysis](https://gosqlx.dev/docs/reference/linting-rules/)** - SQL injection scanner, 30 lint rules (L001-L030), 20 optimizer rules and metadata extraction.
- **[Tooling](https://gosqlx.dev/docs/cli-guide/)** - AST-based formatter, query transforms API, VS Code extension and GitHub Action.
- **[Syntax highlighting](docs/HIGHLIGHTING.md)** - dialect-aware highlighting for terminals (ANSI), HTML/CSS, JSON spans, LSP semantic tokens and WASM; `gosqlx highlight query.sql` or `gosqlx.HighlightANSI(sql)`.
- **[Multi-dialect](https://gosqlx.dev/docs/reference/sql-compatibility/)** - 8 dialects with dedicated grammar support: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse. The `--dialect` flag and `WithDialect` accept 11 names: those 8 (`postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `clickhouse`) plus `generic`, and `bigquery` and `redshift`, which have keyword recognition and basic parsing only.
- **[Multi-dialect](https://gosqlx.dev/docs/reference/sql-compatibility/)** - 8 dialects with dedicated grammar support: PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite, Snowflake, ClickHouse. The `--dialect` flag and `WithDialect` accept 11 names: those 8 (`postgresql`, `mysql`, `mariadb`, `sqlserver`, `oracle`, `sqlite`, `snowflake`, `clickhouse`) plus `generic`, `bigquery` and `redshift`. BigQuery and Redshift have no dialect-specific keyword set (they use the generic keywords) and only capability-gated grammar, such as BigQuery's `QUALIFY`, `UNNEST` in `FROM` and `(struct).field` access.
- **[AI-ready](https://gosqlx.dev/docs/integrations/mcp/)** - MCP server with 7 tools, public remote endpoint, Streamable HTTP.
- **[Tested](https://gosqlx.dev/docs/reference/sql-compatibility/)** - race-detector clean in CI, ~80-85% SQL-99 (estimated).

Expand Down
27 changes: 6 additions & 21 deletions cmd/gosqlx/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ import (

"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules"
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
sqlsecurity "github.com/ajitpratap0/GoSQLX/pkg/sql/security"
Expand Down Expand Up @@ -411,25 +409,12 @@ func runSecurityScan(sql string, filename string, outWriter io.Writer) int {
return result.TotalCount
}

// createLinter creates a new linter instance with configured rules
// createLinter creates a new linter instance with the core rule set
// (L001-L010) from rules.CoreRules, using --max-length for L005.
func createLinter() *linter.Linter {
l := linter.New(
// Whitespace rules (L001, L002, L003, L004, L005, L010)
whitespace.NewTrailingWhitespaceRule(), // L001
whitespace.NewMixedIndentationRule(), // L002
whitespace.NewConsecutiveBlankLinesRule(1), // L003
whitespace.NewIndentationDepthRule(4, 4), // L004
whitespace.NewLongLinesRule(lintMaxLength), // L005
whitespace.NewRedundantWhitespaceRule(), // L010

// Style rules (L006, L008, L009)
style.NewColumnAlignmentRule(), // L006
style.NewCommaPlacementRule(style.CommaTrailing), // L008
style.NewAliasingConsistencyRule(true), // L009

// Keyword rules (L007)
keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007
)
cfg := rules.DefaultCoreConfig()
cfg.MaxLineLength = lintMaxLength
l := linter.New(rules.CoreRules(cfg)...)
if lintDialect != "" {
l.SetDialect(sqlkeywords.SQLDialect(lintDialect))
}
Expand Down
7 changes: 4 additions & 3 deletions cmd/gosqlx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/cmdutil"
"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/lspcmd"
"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/optimizecmd"
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
)

// Version is the current version of gosqlx CLI.
Expand All @@ -43,7 +44,7 @@ import (
// - ClickHouse SQL dialect support (PREWHERE, FINAL, GLOBAL IN)
// - LSP semantic tokens + diagnostic debouncing
// - Parser API consolidation (ParseFromModelTokens canonical)
var Version = "1.14.0"
var Version = gosqlx.Version

var (
// verbose enables detailed output for debugging and troubleshooting.
Expand Down Expand Up @@ -131,7 +132,6 @@ Key features:
• CI/CD integration with proper exit codes

Performance: 1.5M+ operations/second sustained, 1.97M peak. 100-1000x faster than competitors.`,
Version: "1.14.0",
}

// Execute adds all child commands to the root command and sets flags appropriately.
Expand Down Expand Up @@ -177,7 +177,8 @@ func Execute() error {
func init() {
// Report the Version variable from --version so release builds can set it
// with -ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=<tag>".
// A plain `go build` keeps the hard-coded default.
// A plain `go build` reports the library's gosqlx.Version, so the CLI and
// the library (and the LSP, which reports gosqlx.Version) cannot drift.
rootCmd.Version = Version

// Global flags available to all subcommands
Expand Down
35 changes: 26 additions & 9 deletions cmd/gosqlx/cmd/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,33 @@

package cmd

import "testing"
import (
"bytes"
"strings"
"testing"

// TestRootCmdVersionUsesVersionVar guards the release ldflags contract:
// --version must print the Version variable, which release builds override
// with -X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=<tag>.
func TestRootCmdVersionUsesVersionVar(t *testing.T) {
if Version == "" {
t.Fatal("Version must have a non-empty default for dev builds")
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
)

// TestRootCmdVersionReportsLibraryVersion runs `gosqlx --version` and checks
// that an unstamped build reports the library's gosqlx.Version, so the CLI and
// library versions cannot drift. Release builds override cmd.Version with
// -ldflags "-X github.com/ajitpratap0/GoSQLX/cmd/gosqlx/cmd.Version=<tag>".
func TestRootCmdVersionReportsLibraryVersion(t *testing.T) {
var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs([]string{"--version"})
t.Cleanup(func() {
rootCmd.SetOut(nil)
rootCmd.SetErr(nil)
rootCmd.SetArgs(nil)
})

if err := rootCmd.Execute(); err != nil {
t.Fatalf("gosqlx --version: %v", err)
}
if rootCmd.Version != Version {
t.Errorf("rootCmd.Version = %q, want Version %q", rootCmd.Version, Version)
if got := out.String(); !strings.Contains(got, gosqlx.Version) {
t.Errorf("gosqlx --version output %q does not contain gosqlx.Version %q", got, gosqlx.Version)
}
}
36 changes: 19 additions & 17 deletions cmd/gosqlx/internal/actioncmd/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,36 @@ import (
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules"
"github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
)

// builtinRules returns all built-in lint rules. This is the single source of
// truth - both defaultLinter() and allRules() derive from it.
// actionCoreConfig is the action command's own configuration of the core
// rules, more lenient than `gosqlx lint`: L003 allows 2 blank lines, L004
// depth 10, L005 120 columns.
var actionCoreConfig = rules.CoreConfig{
MaxConsecutiveBlankLines: 2,
MaxIndentDepth: 10,
IndentSize: 4,
MaxLineLength: 120,
}

// builtinRules returns all built-in lint rules (the core rules L001-L010 from
// rules.CoreRules), sorted by rule ID so violations keep the action's
// established L001..L010 output order. This is the single source of truth -
// both defaultLinter() and allRules() derive from it.
func builtinRules() []linter.Rule {
return []linter.Rule{
whitespace.NewTrailingWhitespaceRule(), // L001
whitespace.NewMixedIndentationRule(), // L002
whitespace.NewConsecutiveBlankLinesRule(2), // L003
whitespace.NewIndentationDepthRule(10, 4), // L004
whitespace.NewLongLinesRule(120), // L005
style.NewColumnAlignmentRule(), // L006
keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007
style.NewCommaPlacementRule(style.CommaTrailing), // L008
style.NewAliasingConsistencyRule(true), // L009
whitespace.NewRedundantWhitespaceRule(), // L010
}
rs := rules.CoreRules(actionCoreConfig)
slices.SortFunc(rs, func(a, b linter.Rule) int { return strings.Compare(a.ID(), b.ID()) })
return rs
}

// actionFlags holds the flag values for the action subcommand,
Expand Down
31 changes: 7 additions & 24 deletions pkg/cbinding/cbinding_ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ import (
"github.com/ajitpratap0/GoSQLX/pkg/formatter"
"github.com/ajitpratap0/GoSQLX/pkg/gosqlx"
"github.com/ajitpratap0/GoSQLX/pkg/linter"
lintkeywords "github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
lintrules "github.com/ajitpratap0/GoSQLX/pkg/linter/rules"
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
"github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
Expand Down Expand Up @@ -420,28 +418,13 @@ func parseLintOptions(raw string) (LintOptions, error) {
return opts, nil
}

// newLinter builds the rule set of the `gosqlx lint` CLI command
// (createLinter in cmd/gosqlx/cmd/lint.go, also mirrored by pkg/mcp's
// newFullLinter): L001-L010 with the CLI's rule configuration. Keep the three
// in sync when the CLI's default rule set changes.
// newLinter builds the rule set of the `gosqlx lint` CLI command: the core
// rules L001-L010 from rules.CoreRules with the CLI's configuration and the
// given L005 limit.
func newLinter(maxLineLength int, dialect string) *linter.Linter {
l := linter.New(
// Whitespace rules (L001, L002, L003, L004, L005, L010)
whitespace.NewTrailingWhitespaceRule(), // L001
whitespace.NewMixedIndentationRule(), // L002
whitespace.NewConsecutiveBlankLinesRule(1), // L003
whitespace.NewIndentationDepthRule(4, 4), // L004
whitespace.NewLongLinesRule(maxLineLength), // L005
whitespace.NewRedundantWhitespaceRule(), // L010

// Style rules (L006, L008, L009)
style.NewColumnAlignmentRule(), // L006
style.NewCommaPlacementRule(style.CommaTrailing), // L008
style.NewAliasingConsistencyRule(true), // L009

// Keyword rules (L007)
lintkeywords.NewKeywordCaseRule(lintkeywords.CaseUpper), // L007
)
cfg := lintrules.DefaultCoreConfig()
cfg.MaxLineLength = maxLineLength
l := linter.New(lintrules.CoreRules(cfg)...)
if dialect != "" {
l.SetDialect(keywords.SQLDialect(dialect))
}
Expand Down
30 changes: 8 additions & 22 deletions pkg/gosqlx/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,10 @@ import (
"sync"

"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
lintrules "github.com/ajitpratap0/GoSQLX/pkg/linter/rules"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/naming"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/performance"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/safety"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
sqlkeywords "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
)

Expand Down Expand Up @@ -116,25 +114,13 @@ func LintWithDialect(sql string, dialect sqlkeywords.SQLDialect) (*LintResult, e
return &LintResult{Violations: fileResult.Violations}, nil
}

// allRules returns all 30 built-in lint rules with sensible defaults.
// allRules returns all 30 built-in lint rules with sensible defaults: the
// core rules L001-L010 from rules.CoreRules (with a 120-column L005 limit)
// followed by L011-L030.
func allRules() []linter.Rule {
return []linter.Rule{
// Whitespace rules (L001-L005, L010)
whitespace.NewTrailingWhitespaceRule(), // L001
whitespace.NewMixedIndentationRule(), // L002
whitespace.NewConsecutiveBlankLinesRule(1), // L003
whitespace.NewIndentationDepthRule(4, 4), // L004
whitespace.NewLongLinesRule(120), // L005
whitespace.NewRedundantWhitespaceRule(), // L010

// Style rules (L006, L008, L009)
style.NewColumnAlignmentRule(), // L006
style.NewCommaPlacementRule(style.CommaTrailing), // L008
style.NewAliasingConsistencyRule(true), // L009

// Keyword rules (L007)
keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007

core := lintrules.DefaultCoreConfig()
core.MaxLineLength = 120
return append(lintrules.CoreRules(core),
// Safety rules (L011-L015)
safety.NewDeleteWithoutWhereRule(), // L011
safety.NewUpdateWithoutWhereRule(), // L012
Expand All @@ -160,5 +146,5 @@ func allRules() []linter.Rule {
naming.NewMissingOrderByLimitRule(), // L028
naming.NewSubqueryCanBeJoinRule(), // L029
naming.NewDistinctOnManyColumnsRule(), // L030
}
)
}
80 changes: 80 additions & 0 deletions pkg/linter/rules/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// 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 rules assembles the built-in lint rules from its sub-packages
// (whitespace, style, keywords, safety, performance, naming) into ready-made
// rule sets. It lives above pkg/linter because every rule sub-package imports
// pkg/linter, so pkg/linter itself cannot import them.
package rules

import (
"github.com/ajitpratap0/GoSQLX/pkg/linter"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/keywords"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/style"
"github.com/ajitpratap0/GoSQLX/pkg/linter/rules/whitespace"
)

// CoreConfig holds the settings on which users of the core rule set
// (L001-L010) differ. The other core rules take no settings here: L006,
// L008 (trailing commas), L009 (explicit AS) and L007 (upper-case keywords)
// are configured identically everywhere.
type CoreConfig struct {
// MaxConsecutiveBlankLines is the L003 limit.
MaxConsecutiveBlankLines int
// MaxIndentDepth and IndentSize configure L004.
MaxIndentDepth int
IndentSize int
// MaxLineLength is the L005 limit.
MaxLineLength int
}

// DefaultCoreConfig returns the configuration of the `gosqlx lint` CLI
// command (L003 1 blank line, L004 depth 4 with 4-space indents, L005 100
// columns), which the MCP lint_sql tool and the C binding's gosqlx_lint share.
func DefaultCoreConfig() CoreConfig {
return CoreConfig{
MaxConsecutiveBlankLines: 1,
MaxIndentDepth: 4,
IndentSize: 4,
MaxLineLength: 100,
}
}

// CoreRules returns new instances of the ten core style rules L001-L010,
// configured by cfg. This is the single source of the rule set used by the
// `gosqlx lint` CLI, the `gosqlx action` command, the MCP lint_sql tool, the
// C binding and the first ten rules of gosqlx.Lint.
//
// The rules come grouped by sub-package, not by ID: whitespace (L001-L005,
// L010), then style (L006, L008, L009), then keywords (L007). The linter
// reports violations in rule order, so this order is part of the output.
func CoreRules(cfg CoreConfig) []linter.Rule {
return []linter.Rule{
// Whitespace rules (L001-L005, L010)
whitespace.NewTrailingWhitespaceRule(), // L001
whitespace.NewMixedIndentationRule(), // L002
whitespace.NewConsecutiveBlankLinesRule(cfg.MaxConsecutiveBlankLines), // L003
whitespace.NewIndentationDepthRule(cfg.MaxIndentDepth, cfg.IndentSize), // L004
whitespace.NewLongLinesRule(cfg.MaxLineLength), // L005
whitespace.NewRedundantWhitespaceRule(), // L010

// Style rules (L006, L008, L009)
style.NewColumnAlignmentRule(), // L006
style.NewCommaPlacementRule(style.CommaTrailing), // L008
style.NewAliasingConsistencyRule(true), // L009

// Keyword rules (L007)
keywords.NewKeywordCaseRule(keywords.CaseUpper), // L007
}
}
Loading
Loading