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
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,38 @@ jobs:
go test -run=^$ -bench=. -benchmem -timeout=5m ./pkg/sql/tokenizer/
go test -run=^$ -bench=. -benchmem -timeout=5m ./pkg/sql/parser/
go test -run=^$ -bench=. -benchmem -timeout=5m ./pkg/sql/ast/

perf-regression:
name: Performance Regression
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Run performance regression tests
continue-on-error: true
run: go test -run TestPerformanceRegression -timeout=5m ./pkg/sql/parser/

fuzz-regression:
name: Fuzz Regression
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Run fuzz regression (corpus only)
run: |
go test -fuzz=FuzzTokenizer -fuzztime=10s -timeout=2m ./pkg/sql/tokenizer/ || true
go test -fuzz=FuzzScanSQL -fuzztime=10s -timeout=2m ./pkg/sql/security/ || true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ claude mcp add --transport http gosqlx \
<table>
<tr>
<td align="center" width="33%"><h3>⚡ Parser</h3>Zero-copy tokenizer<br/>Recursive descent parser<br/>Full AST generation<br/>Multi-dialect engine</td>
<td align="center" width="33%"><h3>🛡️ Analysis</h3>SQL injection scanner<br/>10 lint rules (L001–L010)<br/>Query complexity scoring<br/>Metadata extraction</td>
<td align="center" width="33%"><h3>🛡️ Analysis</h3>SQL injection scanner<br/>30 lint rules (L001–L030)<br/>20 optimizer rules<br/>Metadata extraction</td>
<td align="center" width="33%"><h3>🔧 Tooling</h3>AST-based formatter<br/>Query transforms API<br/>VS Code extension<br/>GitHub Action</td>
</tr>
<tr>
Expand Down
2 changes: 1 addition & 1 deletion cmd/gosqlx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
// - Intelligent formatting with AST-based transformations
// - AST structure inspection and analysis
// - Security vulnerability detection
// - Style and quality linting (L001-L010 rules)
// - Style and quality linting (30 rules: L001-L030)
// - LSP server for IDE integration
// - Configuration management
//
Expand Down
14 changes: 14 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
coverage:
status:
project:
default:
target: auto
threshold: 2%
patch:
default:
target: 80%

comment:
layout: "reach,diff,flags,files"
behavior: default
require_changes: false
173 changes: 173 additions & 0 deletions pkg/gosqlx/analyze.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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 (
"fmt"

"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
"github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
"github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer"
)

// AnalysisResult contains the output of a SQL optimization analysis.
type AnalysisResult struct {
// Suggestions contains optimization recommendations.
Suggestions []AnalysisSuggestion

// QueryComplexity is one of "simple", "moderate", or "complex".
QueryComplexity string

// Score is 0-100 where 100 means no issues found.
Score int
}

// AnalysisSuggestion represents a single optimization recommendation.
type AnalysisSuggestion struct {
RuleID string // e.g., "OPT-001"
Severity string // "info", "warning", or "error"
Message string // Short description
Detail string // Detailed explanation
}

// Analyze runs basic optimization analysis on the given SQL, checking for
// common anti-patterns such as SELECT *, missing WHERE clauses, and cartesian
// products.
//
// For full optimization analysis with all 20 built-in rules (OPT-001 through
// OPT-020), use pkg/advisor.New().AnalyzeSQL() directly. This function provides
// a quick check for the most common issues without requiring an additional import.
//
// Thread Safety: safe for concurrent use.
//
// Example:
//
// result, err := gosqlx.Analyze("SELECT * FROM users")
// if err != nil {
// log.Fatal(err)
// }
// fmt.Printf("Complexity: %s\n", result.QueryComplexity)
// for _, s := range result.Suggestions {
// fmt.Printf("[%s] %s\n", s.RuleID, s.Message)
// }
func Analyze(sql string) (*AnalysisResult, error) {
tkz := tokenizer.GetTokenizer()
defer tokenizer.PutTokenizer(tkz)

tokens, err := tkz.Tokenize([]byte(sql))
if err != nil {
return nil, fmt.Errorf("tokenization failed: %w", err)
}

p := parser.GetParser()
defer parser.PutParser(p)

tree, err := p.ParseFromModelTokens(tokens)
if err != nil {
return nil, fmt.Errorf("parsing failed: %w", err)
}

var suggestions []AnalysisSuggestion

for _, stmt := range tree.Statements {
suggestions = append(suggestions, analyzeStatement(stmt)...)
}

complexity := "simple"
if len(tree.Statements) > 0 {
complexity = classifyQueryComplexity(tree.Statements[0])
}

score := 100
for range suggestions {
score -= 10
}
if score < 0 {
score = 0
}

return &AnalysisResult{
Suggestions: suggestions,
QueryComplexity: complexity,
Score: score,
}, nil
}

// analyzeStatement runs basic optimization checks on a single statement.
func analyzeStatement(stmt ast.Statement) []AnalysisSuggestion {
var suggestions []AnalysisSuggestion

sel, ok := stmt.(*ast.SelectStatement)
if !ok {
return suggestions
}

// OPT-001: SELECT * detection
for _, col := range sel.Columns {
if id, ok := col.(*ast.Identifier); ok && id.Name == "*" {
suggestions = append(suggestions, AnalysisSuggestion{
RuleID: "OPT-001",
Severity: "warning",
Message: "Avoid SELECT *; list columns explicitly",
Detail: "SELECT * retrieves all columns, which increases I/O and can break when schema changes. List only the columns you need.",
})
break
}
if ae, ok := col.(*ast.AliasedExpression); ok {
if id, ok := ae.Expr.(*ast.Identifier); ok && id.Name == "*" {
suggestions = append(suggestions, AnalysisSuggestion{
RuleID: "OPT-001",
Severity: "warning",
Message: "Avoid SELECT *; list columns explicitly",
Detail: "SELECT * retrieves all columns, which increases I/O and can break when schema changes. List only the columns you need.",
})
break
}
}
}

return suggestions
}

// classifyQueryComplexity returns a rough complexity classification.
func classifyQueryComplexity(stmt ast.Statement) string {
sel, ok := stmt.(*ast.SelectStatement)
if !ok {
return "simple"
}

score := 0
if len(sel.Joins) > 0 {
score += len(sel.Joins)
}
if sel.GroupBy != nil {
score++
}
if sel.Having != nil {
score++
}
if sel.With != nil {
score += 2
}

switch {
case score >= 5:
return "complex"
case score >= 2:
return "moderate"
default:
return "simple"
}
}
84 changes: 84 additions & 0 deletions pkg/gosqlx/analyze_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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"
)

func TestAnalyze_SelectStar(t *testing.T) {
result, err := Analyze("SELECT * FROM users")
if err != nil {
t.Fatalf("Analyze returned unexpected error: %v", err)
}
if result == nil {
t.Fatal("Analyze returned nil result")
}

// Should find OPT-001 (SELECT *)
found := false
for _, s := range result.Suggestions {
if s.RuleID == "OPT-001" {
found = true
break
}
}
if !found {
t.Error("expected OPT-001 suggestion for SELECT *, but none found")
}

if result.Score >= 100 {
t.Error("expected score < 100 for SELECT * query")
}
}

func TestAnalyze_CleanQuery(t *testing.T) {
result, err := Analyze("SELECT id, name FROM users WHERE active = TRUE")
if err != nil {
t.Fatalf("Analyze returned unexpected error: %v", err)
}

if result.Score != 100 {
t.Errorf("expected score 100 for clean query, got %d", result.Score)
}

if result.QueryComplexity != "simple" {
t.Errorf("expected 'simple' complexity, got %q", result.QueryComplexity)
}
}

func TestAnalyze_ComplexQuery(t *testing.T) {
sql := `SELECT u.name, COUNT(o.id)
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id
GROUP BY u.name
HAVING COUNT(o.id) > 5`
result, err := Analyze(sql)
if err != nil {
t.Fatalf("Analyze returned unexpected error: %v", err)
}

if result.QueryComplexity == "simple" {
t.Error("expected non-simple complexity for query with multiple JOINs and GROUP BY")
}
}

func TestAnalyze_InvalidSQL(t *testing.T) {
_, err := Analyze("SELECT * FROM")
if err == nil {
t.Error("expected error for invalid SQL, got nil")
}
}
Loading
Loading