Skip to content

Commit d56e87c

Browse files
authored
Merge branch 'main' into feat/dml-transforms
2 parents 5b4bffd + be53bcd commit d56e87c

27 files changed

+2939
-0
lines changed

pkg/linter/rule.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,41 @@ func (r BaseRule) CanAutoFix() bool {
229229
// Use this to validate that user-specified rule names in configuration
230230
// files (e.g., .gosqlx.yml) reference actual rules.
231231
var ValidRuleIDs = map[string]string{
232+
// Whitespace rules
232233
"L001": "Trailing Whitespace",
233234
"L002": "Mixed Indentation",
234235
"L003": "Consecutive Blank Lines",
235236
"L004": "Indentation Depth",
236237
"L005": "Long Lines",
237238
"L006": "Column Alignment",
239+
// Style rules
238240
"L007": "Keyword Case Consistency",
239241
"L008": "Comma Placement",
240242
"L009": "Aliasing Consistency",
241243
"L010": "Redundant Whitespace",
244+
// Safety rules
245+
"L011": "Delete Without WHERE",
246+
"L012": "Update Without WHERE",
247+
"L013": "Drop Without IF EXISTS",
248+
"L014": "Truncate Table",
249+
"L015": "Select Into Outfile",
250+
// Performance rules
251+
"L016": "Select Star",
252+
"L017": "Missing WHERE on Full Scan",
253+
"L018": "Leading Wildcard LIKE",
254+
"L019": "NOT IN With NULL Risk",
255+
"L020": "Correlated Subquery in SELECT",
256+
"L021": "OR Instead of IN",
257+
"L022": "Function on Indexed Column",
258+
"L023": "Implicit Cross Join",
259+
// Naming/style rules
260+
"L024": "Table Alias Required",
261+
"L025": "Reserved Keyword Identifier",
262+
"L026": "Implicit Column List in INSERT",
263+
"L027": "UNION Instead of UNION ALL",
264+
"L028": "Missing ORDER BY with LIMIT",
265+
"L029": "Subquery Can Be JOIN",
266+
"L030": "Distinct on Many Columns",
242267
}
243268

244269
// IsValidRuleID checks whether a rule ID corresponds to an implemented rule.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package naming
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/ajitpratap0/GoSQLX/pkg/linter"
21+
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
22+
)
23+
24+
const distinctColumnThreshold = 5
25+
26+
// DistinctOnManyColumnsRule (L030) warns when DISTINCT is used with many columns.
27+
// DISTINCT on many columns is often a sign of a missing GROUP BY or denormalized
28+
// data. It also forces a sort over all projected columns, which is expensive.
29+
type DistinctOnManyColumnsRule struct{ linter.BaseRule }
30+
31+
// NewDistinctOnManyColumnsRule creates a new L030 rule instance.
32+
func NewDistinctOnManyColumnsRule() *DistinctOnManyColumnsRule {
33+
return &DistinctOnManyColumnsRule{
34+
BaseRule: linter.NewBaseRule(
35+
"L030",
36+
"Distinct on Many Columns",
37+
"DISTINCT on many columns suggests a missing GROUP BY or data quality issue",
38+
linter.SeverityWarning,
39+
false,
40+
),
41+
}
42+
}
43+
44+
// Check inspects SELECT statements for DISTINCT with many columns.
45+
func (r *DistinctOnManyColumnsRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
46+
if ctx.AST == nil {
47+
return nil, nil
48+
}
49+
var violations []linter.Violation
50+
for _, stmt := range ctx.AST.Statements {
51+
sel, ok := stmt.(*ast.SelectStatement)
52+
if !ok {
53+
continue
54+
}
55+
if !sel.Distinct {
56+
continue
57+
}
58+
colCount := len(sel.Columns)
59+
if colCount >= distinctColumnThreshold {
60+
violations = append(violations, linter.Violation{
61+
Rule: r.ID(),
62+
RuleName: r.Name(),
63+
Severity: r.Severity(),
64+
Message: fmt.Sprintf("DISTINCT on %d columns is expensive and may indicate a missing GROUP BY or join issue", colCount),
65+
Location: sel.Pos,
66+
Suggestion: "Consider using GROUP BY with aggregate functions, or investigate whether the query structure can be simplified",
67+
})
68+
}
69+
}
70+
return violations, nil
71+
}
72+
73+
// Fix is a no-op: replacing DISTINCT with GROUP BY requires semantic understanding.
74+
func (r *DistinctOnManyColumnsRule) Fix(content string, violations []linter.Violation) (string, error) {
75+
return content, nil
76+
}

pkg/linter/rules/naming/doc.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package naming provides linter rules for SQL naming conventions and style.
16+
//
17+
// Rules:
18+
// - L024: Table alias required (multi-table queries)
19+
// - L025: Reserved keyword used as identifier
20+
// - L026: Implicit column list in INSERT
21+
// - L027: UNION instead of UNION ALL
22+
// - L028: Missing ORDER BY with LIMIT
23+
// - L029: Subquery in WHERE can be a JOIN
24+
// - L030: DISTINCT on many columns
25+
package naming
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package naming
16+
17+
import (
18+
"github.com/ajitpratap0/GoSQLX/pkg/linter"
19+
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
20+
)
21+
22+
// ImplicitColumnListRule (L026) flags INSERT statements without an explicit column list.
23+
// INSERT INTO table VALUES (...) is fragile — it breaks when columns are added/reordered.
24+
type ImplicitColumnListRule struct{ linter.BaseRule }
25+
26+
// NewImplicitColumnListRule creates a new L026 rule instance.
27+
func NewImplicitColumnListRule() *ImplicitColumnListRule {
28+
return &ImplicitColumnListRule{
29+
BaseRule: linter.NewBaseRule(
30+
"L026",
31+
"Implicit Column List in INSERT",
32+
"INSERT without explicit column list is fragile and breaks on schema changes",
33+
linter.SeverityWarning,
34+
false,
35+
),
36+
}
37+
}
38+
39+
// Check inspects INSERT statements for missing column lists.
40+
func (r *ImplicitColumnListRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
41+
if ctx.AST == nil {
42+
return nil, nil
43+
}
44+
var violations []linter.Violation
45+
for _, stmt := range ctx.AST.Statements {
46+
ins, ok := stmt.(*ast.InsertStatement)
47+
if !ok {
48+
continue
49+
}
50+
// If there are VALUES but no explicit column list, flag it
51+
if len(ins.Values) > 0 && len(ins.Columns) == 0 {
52+
violations = append(violations, linter.Violation{
53+
Rule: r.ID(),
54+
RuleName: r.Name(),
55+
Severity: r.Severity(),
56+
Message: "INSERT INTO " + ins.TableName + " has no explicit column list",
57+
Location: ins.Pos,
58+
Suggestion: "Specify columns explicitly: INSERT INTO " + ins.TableName + " (col1, col2, ...) VALUES (...)",
59+
})
60+
}
61+
}
62+
return violations, nil
63+
}
64+
65+
// Fix is a no-op: adding column list requires schema knowledge.
66+
func (r *ImplicitColumnListRule) Fix(content string, violations []linter.Violation) (string, error) {
67+
return content, nil
68+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2026 GoSQLX Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package naming
16+
17+
import (
18+
"github.com/ajitpratap0/GoSQLX/pkg/linter"
19+
"github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
20+
)
21+
22+
// MissingOrderByLimitRule (L028) flags queries that use LIMIT/OFFSET without ORDER BY.
23+
// Without ORDER BY, the rows returned by LIMIT are non-deterministic — different
24+
// executions may return different rows, making pagination unreliable.
25+
type MissingOrderByLimitRule struct{ linter.BaseRule }
26+
27+
// NewMissingOrderByLimitRule creates a new L028 rule instance.
28+
func NewMissingOrderByLimitRule() *MissingOrderByLimitRule {
29+
return &MissingOrderByLimitRule{
30+
BaseRule: linter.NewBaseRule(
31+
"L028",
32+
"Missing ORDER BY with LIMIT",
33+
"LIMIT without ORDER BY produces non-deterministic results",
34+
linter.SeverityWarning,
35+
false,
36+
),
37+
}
38+
}
39+
40+
// Check inspects SELECT statements for LIMIT/OFFSET without ORDER BY.
41+
func (r *MissingOrderByLimitRule) Check(ctx *linter.Context) ([]linter.Violation, error) {
42+
if ctx.AST == nil {
43+
return nil, nil
44+
}
45+
var violations []linter.Violation
46+
for _, stmt := range ctx.AST.Statements {
47+
sel, ok := stmt.(*ast.SelectStatement)
48+
if !ok {
49+
continue
50+
}
51+
hasLimit := sel.Limit != nil || sel.Fetch != nil
52+
if !hasLimit {
53+
continue
54+
}
55+
hasOffset := sel.Offset != nil || (sel.Fetch != nil && sel.Fetch.OffsetValue != nil)
56+
hasOrderBy := len(sel.OrderBy) > 0
57+
if !hasOrderBy {
58+
msg := "LIMIT without ORDER BY produces non-deterministic results"
59+
if hasOffset {
60+
msg = "LIMIT/OFFSET without ORDER BY produces non-deterministic pagination"
61+
}
62+
violations = append(violations, linter.Violation{
63+
Rule: r.ID(),
64+
RuleName: r.Name(),
65+
Severity: r.Severity(),
66+
Message: msg,
67+
Location: sel.Pos,
68+
Suggestion: "Add ORDER BY to ensure deterministic row selection with LIMIT",
69+
})
70+
}
71+
}
72+
return violations, nil
73+
}
74+
75+
// Fix is a no-op: choosing the right ORDER BY requires business logic.
76+
func (r *MissingOrderByLimitRule) Fix(content string, violations []linter.Violation) (string, error) {
77+
return content, nil
78+
}

0 commit comments

Comments
 (0)