package lint import ( "fmt" pg_query "github.com/pganalyze/pg_query_go/v6" "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" "git.warky.dev/wdevs/pgtidy/pkg/pgast" ) // COR001 — SELECT *. // Selecting all columns by wildcard is fragile: column additions/removals // break callers silently. Enumerate the columns you need explicitly. type ruleCOR001 struct{} func (ruleCOR001) ID() string { return "COR001" } func (ruleCOR001) Severity() diagnostics.Severity { return diagnostics.SeverityHint } func (ruleCOR001) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic { var out []diagnostics.Diagnostic for _, raw := range stmts { walkSelectStar(raw.Stmt, src, &out) } return out } func walkSelectStar(node *pg_query.Node, src string, out *[]diagnostics.Diagnostic) { if node == nil { return } sel, ok := node.GetNode().(*pg_query.Node_SelectStmt) if !ok { return } checkSelectStmt(sel.SelectStmt, src, out) } func checkSelectStmt(s *pg_query.SelectStmt, src string, out *[]diagnostics.Diagnostic) { if s == nil { return } for _, t := range s.TargetList { rt, ok := t.GetNode().(*pg_query.Node_ResTarget) if !ok { continue } cr, ok := rt.ResTarget.Val.GetNode().(*pg_query.Node_ColumnRef) if !ok { continue } for _, f := range cr.ColumnRef.Fields { if _, isStar := f.GetNode().(*pg_query.Node_AStar); isStar { line, col := pgast.LocationToLineCol(src, int(cr.ColumnRef.Location)) *out = append(*out, diagnostics.Diagnostic{ RuleID: "COR001", Severity: diagnostics.SeverityHint, Message: "SELECT * is fragile; enumerate the columns explicitly", Line: line, Col: col, }) } } } // Recurse into set-operation branches (UNION, INTERSECT, EXCEPT). checkSelectStmt(s.Larg, src, out) checkSelectStmt(s.Rarg, src, out) } // COR002 — UPDATE without WHERE. // An UPDATE with no WHERE clause modifies every row in the table. type ruleCOR002 struct{} func (ruleCOR002) ID() string { return "COR002" } func (ruleCOR002) Severity() diagnostics.Severity { return diagnostics.SeverityWarning } func (ruleCOR002) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic { var out []diagnostics.Diagnostic for _, raw := range stmts { u, ok := raw.Stmt.GetNode().(*pg_query.Node_UpdateStmt) if !ok { continue } if u.UpdateStmt.WhereClause != nil { continue } line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation))) out = append(out, diagnostics.Diagnostic{ RuleID: "COR002", Severity: diagnostics.SeverityWarning, Message: fmt.Sprintf("UPDATE %q has no WHERE clause; this modifies every row", relName(u.UpdateStmt.Relation)), Line: line, Col: col, }) } return out } // COR003 — DELETE without WHERE. // A DELETE with no WHERE clause removes every row from the table. // Use TRUNCATE if you intend a full wipe; it is faster and explicit. type ruleCOR003 struct{} func (ruleCOR003) ID() string { return "COR003" } func (ruleCOR003) Severity() diagnostics.Severity { return diagnostics.SeverityWarning } func (ruleCOR003) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Diagnostic { var out []diagnostics.Diagnostic for _, raw := range stmts { d, ok := raw.Stmt.GetNode().(*pg_query.Node_DeleteStmt) if !ok { continue } if d.DeleteStmt.WhereClause != nil { continue } line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation))) out = append(out, diagnostics.Diagnostic{ RuleID: "COR003", Severity: diagnostics.SeverityWarning, Message: fmt.Sprintf("DELETE FROM %q has no WHERE clause; this removes every row (use TRUNCATE if intentional)", relName(d.DeleteStmt.Relation)), Line: line, Col: col, }) } return out }