Skip to content

Commit 1ef4a7f

Browse files
Ajit Pratap SinghAjit Pratap Singh
authored andcommitted
docs: add SQL validator and formatter tutorials (DOC-002)
Implemented the first two tutorials in a progressive tutorial series: Tutorial 1: Building a SQL Validator for CI/CD - Complete documentation in docs/tutorials/01-sql-validator-cicd.md - Working example code in examples/tutorials/01-sql-validator/ - Features: file validation, directory scanning, error reporting, exit codes - Integration examples for GitHub Actions, GitLab CI, and pre-commit hooks Tutorial 2: Creating a SQL Formatter with Custom Rules - Complete documentation in docs/tutorials/02-custom-sql-formatter.md - Working example code in examples/tutorials/02-sql-formatter/ - Features: configurable formatting, keyword casing, indentation, operators - Integration examples for pre-commit hooks and CI validation Both tutorials are beginner-friendly, completable in <30 minutes each, and include fully runnable, tested code examples. Related to issue #58
1 parent 6b3c468 commit 1ef4a7f

17 files changed

Lines changed: 2491 additions & 0 deletions

File tree

docs/tutorials/01-sql-validator-cicd.md

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.

docs/tutorials/02-custom-sql-formatter.md

Lines changed: 972 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Validate SQL Files
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
validate:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v4
18+
with:
19+
go-version: '1.21'
20+
21+
- name: Build SQL Validator
22+
run: |
23+
cd examples/tutorials/01-sql-validator
24+
go build -o sql-validator
25+
26+
- name: Validate SQL Files
27+
run: |
28+
cd examples/tutorials/01-sql-validator
29+
./sql-validator testdata/valid.sql
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Tutorial 1: SQL Validator for CI/CD
2+
3+
This is the working code example for Tutorial 1. It demonstrates how to build a SQL validator using GoSQLX.
4+
5+
## Quick Start
6+
7+
```bash
8+
# Build the validator
9+
go build -o sql-validator
10+
11+
# Validate a single file
12+
./sql-validator testdata/valid.sql
13+
14+
# Validate all SQL files in a directory
15+
./sql-validator testdata/
16+
```
17+
18+
## What It Does
19+
20+
This validator:
21+
- Scans directories recursively for `.sql` files
22+
- Validates SQL syntax using GoSQLX tokenizer and parser
23+
- Reports errors with detailed messages
24+
- Returns proper exit codes for CI/CD integration
25+
26+
## Expected Output
27+
28+
### Valid SQL File
29+
30+
```
31+
Validating file: testdata/valid.sql
32+
33+
=== SQL Validation Results ===
34+
35+
✓ testdata/valid.sql
36+
37+
=== Summary ===
38+
Total files: 1
39+
Valid: 1
40+
Invalid: 0
41+
42+
All SQL files are valid!
43+
```
44+
45+
### Invalid SQL File
46+
47+
```
48+
Validating file: testdata/invalid.sql
49+
50+
=== SQL Validation Results ===
51+
52+
✗ testdata/invalid.sql
53+
Error: parse error: ...
54+
55+
=== Summary ===
56+
Total files: 1
57+
Valid: 0
58+
Invalid: 1
59+
```
60+
61+
## Integration
62+
63+
See `.github/workflows/validate.yml` for GitHub Actions integration example.
64+
65+
For the complete tutorial, see: `docs/tutorials/01-sql-validator-cicd.md`
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module github.com/ajitpratap0/GoSQLX/examples/tutorials/01-sql-validator
2+
3+
go 1.24.0
4+
5+
replace github.com/ajitpratap0/GoSQLX => ../../../
6+
7+
require github.com/ajitpratap0/GoSQLX v0.0.0-00010101000000-000000000000
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func main() {
9+
if len(os.Args) < 2 {
10+
fmt.Println("Usage: sql-validator <file-or-directory>")
11+
fmt.Println("\nExamples:")
12+
fmt.Println(" sql-validator query.sql")
13+
fmt.Println(" sql-validator ./migrations")
14+
fmt.Println(" sql-validator .")
15+
os.Exit(1)
16+
}
17+
18+
target := os.Args[1]
19+
20+
// Check if target exists
21+
info, err := os.Stat(target)
22+
if err != nil {
23+
fmt.Printf("Error: %v\n", err)
24+
os.Exit(1)
25+
}
26+
27+
var results []ValidationResult
28+
29+
// Process file or directory
30+
if info.IsDir() {
31+
fmt.Printf("Scanning directory: %s\n", target)
32+
results, err = ValidateDirectory(target)
33+
if err != nil {
34+
fmt.Printf("Error scanning directory: %v\n", err)
35+
os.Exit(1)
36+
}
37+
} else {
38+
fmt.Printf("Validating file: %s\n", target)
39+
result := ValidateFile(target)
40+
results = []ValidationResult{result}
41+
}
42+
43+
// Print results
44+
PrintResults(results)
45+
46+
// Exit with error code if any files are invalid
47+
for _, result := range results {
48+
if !result.Valid {
49+
os.Exit(1)
50+
}
51+
}
52+
53+
fmt.Println("\nAll SQL files are valid!")
54+
os.Exit(0)
55+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SELECT id, name, email
2+
WHERE active = true;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
SELECT id, name, email
2+
FROM users
3+
WHERE active = true
4+
ORDER BY created_at DESC;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
10+
"github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer"
11+
)
12+
13+
// ValidationResult holds the result of validating a single SQL file
14+
type ValidationResult struct {
15+
FilePath string
16+
Valid bool
17+
Error error
18+
}
19+
20+
// ValidateFile validates a single SQL file
21+
func ValidateFile(filePath string) ValidationResult {
22+
// Read the file
23+
content, err := os.ReadFile(filePath)
24+
if err != nil {
25+
return ValidationResult{
26+
FilePath: filePath,
27+
Valid: false,
28+
Error: fmt.Errorf("failed to read file: %w", err),
29+
}
30+
}
31+
32+
// Get tokenizer from pool
33+
tkz := tokenizer.GetTokenizer()
34+
defer tokenizer.PutTokenizer(tkz)
35+
36+
// Tokenize the SQL
37+
tokens, err := tkz.Tokenize(content)
38+
if err != nil {
39+
return ValidationResult{
40+
FilePath: filePath,
41+
Valid: false,
42+
Error: fmt.Errorf("tokenization error: %w", err),
43+
}
44+
}
45+
46+
// Convert tokens for parser
47+
parserTokens, err := parser.ConvertTokensForParser(tokens)
48+
if err != nil {
49+
return ValidationResult{
50+
FilePath: filePath,
51+
Valid: false,
52+
Error: fmt.Errorf("token conversion error: %w", err),
53+
}
54+
}
55+
56+
// Create parser
57+
p := parser.NewParser()
58+
defer p.Release()
59+
60+
// Parse the tokens
61+
_, err = p.Parse(parserTokens)
62+
if err != nil {
63+
return ValidationResult{
64+
FilePath: filePath,
65+
Valid: false,
66+
Error: fmt.Errorf("parse error: %w", err),
67+
}
68+
}
69+
70+
return ValidationResult{
71+
FilePath: filePath,
72+
Valid: true,
73+
Error: nil,
74+
}
75+
}
76+
77+
// ValidateDirectory recursively validates all .sql files in a directory
78+
func ValidateDirectory(dirPath string) ([]ValidationResult, error) {
79+
var results []ValidationResult
80+
81+
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
82+
if err != nil {
83+
return err
84+
}
85+
86+
// Skip directories
87+
if info.IsDir() {
88+
return nil
89+
}
90+
91+
// Only process .sql files
92+
if !strings.HasSuffix(strings.ToLower(path), ".sql") {
93+
return nil
94+
}
95+
96+
// Validate the file
97+
result := ValidateFile(path)
98+
results = append(results, result)
99+
100+
return nil
101+
})
102+
103+
if err != nil {
104+
return nil, fmt.Errorf("failed to walk directory: %w", err)
105+
}
106+
107+
return results, nil
108+
}
109+
110+
// PrintResults prints validation results in a user-friendly format
111+
func PrintResults(results []ValidationResult) {
112+
validCount := 0
113+
invalidCount := 0
114+
115+
fmt.Println("\n=== SQL Validation Results ===\n")
116+
117+
for _, result := range results {
118+
if result.Valid {
119+
fmt.Printf("✓ %s\n", result.FilePath)
120+
validCount++
121+
} else {
122+
fmt.Printf("✗ %s\n", result.FilePath)
123+
fmt.Printf(" Error: %v\n\n", result.Error)
124+
invalidCount++
125+
}
126+
}
127+
128+
fmt.Printf("\n=== Summary ===\n")
129+
fmt.Printf("Total files: %d\n", len(results))
130+
fmt.Printf("Valid: %d\n", validCount)
131+
fmt.Printf("Invalid: %d\n", invalidCount)
132+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
repos:
2+
- repo: local
3+
hooks:
4+
- id: format-sql
5+
name: Format SQL Files
6+
entry: sql-formatter format -i
7+
language: system
8+
files: \.sql$
9+
pass_filenames: true

0 commit comments

Comments
 (0)