Skip to content

Commit 0affaf6

Browse files
authored
linter: add require-table-schema rule (#1046)
Enforce that all table DDL (CREATE TABLE, CREATE TABLE AS, ALTER TABLE, DROP TABLE) uses schema-qualified names to prevent ambiguity from search_path changes.
1 parent c05c51c commit 0affaf6

7 files changed

Lines changed: 168 additions & 2 deletions

β€Žcrates/squawk_linter/src/lib.rsβ€Ž

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ use rules::renaming_table;
5454
use rules::require_concurrent_index_creation;
5555
use rules::require_concurrent_index_deletion;
5656
use rules::require_enum_value_ordering;
57+
use rules::require_table_schema;
5758
use rules::require_timeout_settings;
5859
use rules::transaction_nesting;
5960
// xtask:new-rule:rule-import
@@ -92,9 +93,18 @@ pub enum Rule {
9293
RequireTimeoutSettings,
9394
BanUncommittedTransaction,
9495
RequireEnumValueOrdering,
96+
RequireTableSchema,
9597
// xtask:new-rule:error-name
9698
}
9799

100+
impl Rule {
101+
/// Rules that are opt-in are not enabled by default.
102+
/// They must be explicitly included via configuration.
103+
pub fn is_opt_in(&self) -> bool {
104+
matches!(self, Rule::RequireTableSchema)
105+
}
106+
}
107+
98108
impl TryFrom<&str> for Rule {
99109
type Error = String;
100110

@@ -135,6 +145,7 @@ impl TryFrom<&str> for Rule {
135145
"require-timeout-settings" => Ok(Rule::RequireTimeoutSettings),
136146
"ban-uncommitted-transaction" => Ok(Rule::BanUncommittedTransaction),
137147
"require-enum-value-ordering" => Ok(Rule::RequireEnumValueOrdering),
148+
"require-table-schema" => Ok(Rule::RequireTableSchema),
138149
// xtask:new-rule:str-name
139150
_ => Err(format!("Unknown violation name: {s}")),
140151
}
@@ -198,6 +209,7 @@ impl fmt::Display for Rule {
198209
Rule::RequireTimeoutSettings => "require-timeout-settings",
199210
Rule::BanUncommittedTransaction => "ban-uncommitted-transaction",
200211
Rule::RequireEnumValueOrdering => "require-enum-value-ordering",
212+
Rule::RequireTableSchema => "require-table-schema",
201213
// xtask:new-rule:variant-to-name
202214
};
203215
write!(f, "{val}")
@@ -428,6 +440,9 @@ impl Linter {
428440
if self.rules.contains(&Rule::RequireEnumValueOrdering) {
429441
require_enum_value_ordering(self, file);
430442
}
443+
if self.rules.contains(&Rule::RequireTableSchema) {
444+
require_table_schema(self, file);
445+
}
431446
// xtask:new-rule:rule-call
432447

433448
// locate any ignores in the file
@@ -452,12 +467,16 @@ impl Linter {
452467
}
453468

454469
pub fn with_all_rules() -> Self {
455-
let rules = all::<Rule>().collect::<FxHashSet<_>>();
470+
let rules = all::<Rule>()
471+
.filter(|r| !r.is_opt_in())
472+
.collect::<FxHashSet<_>>();
456473
Linter::from(rules)
457474
}
458475

459476
pub fn without_rules(exclude: &[Rule]) -> Self {
460-
let all_rules = all::<Rule>().collect::<FxHashSet<_>>();
477+
let all_rules = all::<Rule>()
478+
.filter(|r| !r.is_opt_in())
479+
.collect::<FxHashSet<_>>();
461480
let mut exclude_set = FxHashSet::with_capacity_and_hasher(exclude.len(), FxBuildHasher);
462481
for e in exclude {
463482
exclude_set.insert(e);

β€Žcrates/squawk_linter/src/rules/mod.rsβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub(crate) mod renaming_table;
2727
pub(crate) mod require_concurrent_index_creation;
2828
pub(crate) mod require_concurrent_index_deletion;
2929
pub(crate) mod require_enum_value_ordering;
30+
pub(crate) mod require_table_schema;
3031
pub(crate) mod require_timeout_settings;
3132
pub(crate) mod transaction_nesting;
3233
// xtask:new-rule:mod-decl
@@ -60,6 +61,7 @@ pub(crate) use renaming_table::renaming_table;
6061
pub(crate) use require_concurrent_index_creation::require_concurrent_index_creation;
6162
pub(crate) use require_concurrent_index_deletion::require_concurrent_index_deletion;
6263
pub(crate) use require_enum_value_ordering::require_enum_value_ordering;
64+
pub(crate) use require_table_schema::require_table_schema;
6365
pub(crate) use require_timeout_settings::require_timeout_settings;
6466
pub(crate) use transaction_nesting::transaction_nesting;
6567
// xtask:new-rule:export
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use squawk_syntax::{
2+
Parse, SourceFile,
3+
ast::{self, AstNode},
4+
};
5+
6+
use crate::{Linter, Rule, Violation};
7+
8+
pub(crate) fn require_table_schema(ctx: &mut Linter, parse: &Parse<SourceFile>) {
9+
let file = parse.tree();
10+
for stmt in file.stmts() {
11+
match stmt {
12+
ast::Stmt::CreateTable(create_table) => {
13+
check_path(ctx, create_table.path(), create_table.syntax());
14+
}
15+
ast::Stmt::CreateTableAs(create_table_as) => {
16+
check_path(ctx, create_table_as.path(), create_table_as.syntax());
17+
}
18+
ast::Stmt::AlterTable(alter_table) => {
19+
let path = alter_table.relation_name().and_then(|r| r.path());
20+
check_path(ctx, path, alter_table.syntax());
21+
}
22+
ast::Stmt::DropTable(drop_table) => {
23+
check_path(ctx, drop_table.path(), drop_table.syntax());
24+
}
25+
_ => {}
26+
}
27+
}
28+
}
29+
30+
fn check_path(ctx: &mut Linter, path: Option<ast::Path>, syntax: &squawk_syntax::SyntaxNode) {
31+
if let Some(path) = path {
32+
if path.qualifier().is_none() {
33+
ctx.report(Violation::for_node(
34+
Rule::RequireTableSchema,
35+
"Table name is not schema-qualified. Use schema.table (e.g., public.my_table)."
36+
.into(),
37+
syntax,
38+
));
39+
}
40+
}
41+
}
42+
43+
#[cfg(test)]
44+
mod test {
45+
use insta::assert_snapshot;
46+
47+
use crate::Rule;
48+
use crate::test_utils::{lint_errors, lint_ok};
49+
50+
#[test]
51+
fn create_table_err() {
52+
let sql = r#"
53+
CREATE TABLE my_table (id int);
54+
"#;
55+
assert_snapshot!(lint_errors(sql, Rule::RequireTableSchema));
56+
}
57+
58+
#[test]
59+
fn create_table_ok() {
60+
let sql = r#"
61+
CREATE TABLE public.my_table (id int);
62+
"#;
63+
lint_ok(sql, Rule::RequireTableSchema);
64+
}
65+
66+
#[test]
67+
fn alter_table_err() {
68+
let sql = r#"
69+
ALTER TABLE my_table ADD COLUMN name text;
70+
"#;
71+
assert_snapshot!(lint_errors(sql, Rule::RequireTableSchema));
72+
}
73+
74+
#[test]
75+
fn alter_table_ok() {
76+
let sql = r#"
77+
ALTER TABLE public.my_table ADD COLUMN name text;
78+
"#;
79+
lint_ok(sql, Rule::RequireTableSchema);
80+
}
81+
82+
#[test]
83+
fn drop_table_err() {
84+
let sql = r#"
85+
DROP TABLE my_table;
86+
"#;
87+
assert_snapshot!(lint_errors(sql, Rule::RequireTableSchema));
88+
}
89+
90+
#[test]
91+
fn drop_table_ok() {
92+
let sql = r#"
93+
DROP TABLE public.my_table;
94+
"#;
95+
lint_ok(sql, Rule::RequireTableSchema);
96+
}
97+
98+
#[test]
99+
fn create_table_as_err() {
100+
let sql = r#"
101+
CREATE TABLE my_table AS SELECT 1;
102+
"#;
103+
assert_snapshot!(lint_errors(sql, Rule::RequireTableSchema));
104+
}
105+
106+
#[test]
107+
fn create_table_as_ok() {
108+
let sql = r#"
109+
CREATE TABLE public.my_table AS SELECT 1;
110+
"#;
111+
lint_ok(sql, Rule::RequireTableSchema);
112+
}
113+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/squawk_linter/src/rules/require_table_schema.rs
3+
expression: "lint_errors(sql, Rule::RequireTableSchema)"
4+
---
5+
warning[require-table-schema]: Table name is not schema-qualified. Use schema.table (e.g., public.my_table).
6+
β•­β–Έ
7+
2 β”‚ ALTER TABLE my_table ADD COLUMN name text;
8+
╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/squawk_linter/src/rules/require_table_schema.rs
3+
expression: "lint_errors(sql, Rule::RequireTableSchema)"
4+
---
5+
warning[require-table-schema]: Table name is not schema-qualified. Use schema.table (e.g., public.my_table).
6+
β•­β–Έ
7+
2 β”‚ CREATE TABLE my_table AS SELECT 1;
8+
╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/squawk_linter/src/rules/require_table_schema.rs
3+
expression: "lint_errors(sql, Rule::RequireTableSchema)"
4+
---
5+
warning[require-table-schema]: Table name is not schema-qualified. Use schema.table (e.g., public.my_table).
6+
β•­β–Έ
7+
2 β”‚ CREATE TABLE my_table (id int);
8+
╰╴━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
source: crates/squawk_linter/src/rules/require_table_schema.rs
3+
expression: "lint_errors(sql, Rule::RequireTableSchema)"
4+
---
5+
warning[require-table-schema]: Table name is not schema-qualified. Use schema.table (e.g., public.my_table).
6+
β•­β–Έ
7+
2 β”‚ DROP TABLE my_table;
8+
╰╴━━━━━━━━━━━━━━━━━━━

0 commit comments

Comments
Β (0)