Skip to main content

squawk_linter/
analyze.rs

1use squawk_syntax::ast;
2
3fn has_foreign_key_constraint(create_table: &ast::CreateTable) -> bool {
4    if let Some(table_arg_list) = create_table.table_arg_list() {
5        for arg in table_arg_list.args() {
6            match arg {
7                ast::TableArg::TableConstraint(ast::TableConstraint::ForeignKeyConstraint(_)) => {
8                    return true;
9                }
10                ast::TableArg::Column(column) => {
11                    if column
12                        .constraints()
13                        .any(|c| matches!(c, ast::ColumnConstraint::ReferencesConstraint(_)))
14                    {
15                        return true;
16                    }
17                }
18                _ => (),
19            }
20        }
21    }
22    false
23}
24
25/// Returns `true` if the statement might impede normal database queries.
26pub fn possibly_slow_stmt(stmt: &ast::Stmt) -> bool {
27    // We assume all DDL like Alter, Create, Drop could affect queries.
28    //
29    // We don't want to warn about DML style queries, like select,
30    // insert, update, delete, etc. This allows using squawk for normal SQL
31    // editing.
32    match stmt {
33        // Without a foreign key constraint, creating a new table should be fast
34        ast::Stmt::CreateTable(create_table) => has_foreign_key_constraint(create_table),
35        | ast::Stmt::AlterAggregate(_)
36        | ast::Stmt::AlterCollation(_)
37        | ast::Stmt::AlterConversion(_)
38        | ast::Stmt::AlterDatabase(_)
39        | ast::Stmt::AlterDefaultPrivileges(_)
40        | ast::Stmt::AlterDomain(_)
41        | ast::Stmt::AlterEventTrigger(_)
42        | ast::Stmt::AlterExtension(_)
43        | ast::Stmt::AlterForeignDataWrapper(_)
44        | ast::Stmt::AlterForeignTable(_)
45        | ast::Stmt::AlterFunction(_)
46        | ast::Stmt::AlterGroup(_)
47        | ast::Stmt::AlterIndex(_)
48        | ast::Stmt::AlterLanguage(_)
49        | ast::Stmt::AlterLargeObject(_)
50        | ast::Stmt::AlterMaterializedView(_)
51        | ast::Stmt::AlterOperator(_)
52        | ast::Stmt::AlterOperatorClass(_)
53        | ast::Stmt::AlterOperatorFamily(_)
54        | ast::Stmt::AlterPolicy(_)
55        | ast::Stmt::AlterPropertyGraph(_)
56        | ast::Stmt::AlterProcedure(_)
57        | ast::Stmt::AlterPublication(_)
58        | ast::Stmt::AlterRole(_)
59        | ast::Stmt::AlterRoutine(_)
60        | ast::Stmt::AlterRule(_)
61        | ast::Stmt::AlterSchema(_)
62        | ast::Stmt::AlterSequence(_)
63        | ast::Stmt::AlterServer(_)
64        | ast::Stmt::AlterStatistics(_)
65        | ast::Stmt::AlterSubscription(_)
66        | ast::Stmt::AlterSystem(_)
67        | ast::Stmt::AlterTable(_)
68        | ast::Stmt::AlterTablespace(_)
69        | ast::Stmt::AlterTextSearchConfiguration(_)
70        | ast::Stmt::AlterTextSearchDictionary(_)
71        | ast::Stmt::AlterTextSearchParser(_)
72        | ast::Stmt::AlterTextSearchTemplate(_)
73        | ast::Stmt::AlterTrigger(_)
74        | ast::Stmt::AlterType(_)
75        | ast::Stmt::AlterUser(_)
76        | ast::Stmt::AlterUserMapping(_)
77        | ast::Stmt::AlterView(_)
78        | ast::Stmt::CreateAccessMethod(_)
79        | ast::Stmt::CreateAggregate(_)
80        | ast::Stmt::CreateCast(_)
81        | ast::Stmt::CreateCollation(_)
82        | ast::Stmt::CreateConversion(_)
83        | ast::Stmt::CreateDatabase(_)
84        | ast::Stmt::CreateDomain(_)
85        | ast::Stmt::CreateEventTrigger(_)
86        | ast::Stmt::CreateExtension(_)
87        | ast::Stmt::CreateForeignDataWrapper(_)
88        | ast::Stmt::CreateForeignTable(_)
89        | ast::Stmt::CreateFunction(_)
90        | ast::Stmt::CreateGroup(_)
91        | ast::Stmt::CreateIndex(_)
92        | ast::Stmt::CreateLanguage(_)
93        | ast::Stmt::CreateMaterializedView(_)
94        | ast::Stmt::CreateOperator(_)
95        | ast::Stmt::CreateOperatorClass(_)
96        | ast::Stmt::CreateOperatorFamily(_)
97        | ast::Stmt::CreatePolicy(_)
98        | ast::Stmt::CreatePropertyGraph(_)
99        | ast::Stmt::CreateProcedure(_)
100        | ast::Stmt::CreatePublication(_)
101        | ast::Stmt::CreateRole(_)
102        | ast::Stmt::CreateRule(_)
103        | ast::Stmt::CreateSchema(_)
104        | ast::Stmt::CreateSequence(_)
105        | ast::Stmt::CreateServer(_)
106        | ast::Stmt::CreateStatistics(_)
107        | ast::Stmt::CreateSubscription(_)
108        | ast::Stmt::CreateTableAs(_)
109        | ast::Stmt::SelectInto(_)
110        | ast::Stmt::CreateTablespace(_)
111        | ast::Stmt::CreateTextSearchConfiguration(_)
112        | ast::Stmt::CreateTextSearchDictionary(_)
113        | ast::Stmt::CreateTextSearchParser(_)
114        | ast::Stmt::CreateTextSearchTemplate(_)
115        | ast::Stmt::CreateTransform(_)
116        | ast::Stmt::CreateTrigger(_)
117        | ast::Stmt::CreateType(_)
118        | ast::Stmt::CreateUser(_)
119        | ast::Stmt::CreateUserMapping(_)
120        | ast::Stmt::CreateView(_)
121        | ast::Stmt::DropAccessMethod(_)
122        | ast::Stmt::DropAggregate(_)
123        | ast::Stmt::DropCast(_)
124        | ast::Stmt::DropCollation(_)
125        | ast::Stmt::DropConversion(_)
126        | ast::Stmt::DropDatabase(_)
127        | ast::Stmt::DropDomain(_)
128        | ast::Stmt::DropEventTrigger(_)
129        | ast::Stmt::DropExtension(_)
130        | ast::Stmt::DropForeignDataWrapper(_)
131        | ast::Stmt::DropForeignTable(_)
132        | ast::Stmt::DropFunction(_)
133        | ast::Stmt::DropGroup(_)
134        | ast::Stmt::DropIndex(_)
135        | ast::Stmt::DropLanguage(_)
136        | ast::Stmt::DropMaterializedView(_)
137        | ast::Stmt::DropOperator(_)
138        | ast::Stmt::DropOperatorClass(_)
139        | ast::Stmt::DropOperatorFamily(_)
140        | ast::Stmt::DropOwned(_)
141        | ast::Stmt::DropPolicy(_)
142        | ast::Stmt::DropPropertyGraph(_)
143        | ast::Stmt::DropProcedure(_)
144        | ast::Stmt::DropPublication(_)
145        | ast::Stmt::DropRole(_)
146        | ast::Stmt::DropRoutine(_)
147        | ast::Stmt::DropRule(_)
148        | ast::Stmt::DropSchema(_)
149        | ast::Stmt::DropSequence(_)
150        | ast::Stmt::DropServer(_)
151        | ast::Stmt::DropStatistics(_)
152        | ast::Stmt::DropSubscription(_)
153        | ast::Stmt::DropTable(_)
154        | ast::Stmt::DropTablespace(_)
155        | ast::Stmt::DropTextSearchConfig(_)
156        | ast::Stmt::DropTextSearchDict(_)
157        | ast::Stmt::DropTextSearchParser(_)
158        | ast::Stmt::DropTextSearchTemplate(_)
159        | ast::Stmt::DropTransform(_)
160        | ast::Stmt::DropTrigger(_)
161        | ast::Stmt::DropType(_)
162        | ast::Stmt::DropUser(_)
163        | ast::Stmt::DropUserMapping(_)
164        | ast::Stmt::DropView(_)
165        // non-Alter, Create, Drop statements
166        | ast::Stmt::Cluster(_)
167        | ast::Stmt::CommentOn(_)
168        | ast::Stmt::ImportForeignSchema(_)
169        | ast::Stmt::Load(_)
170        | ast::Stmt::Lock(_)
171        | ast::Stmt::Refresh(_)
172        | ast::Stmt::Reindex(_)
173        | ast::Stmt::Truncate(_)
174        | ast::Stmt::Vacuum(_)
175        => true,
176        ast::Stmt::Analyze(_)
177        | ast::Stmt::Begin(_)
178        | ast::Stmt::Call(_)
179        | ast::Stmt::Checkpoint(_)
180        | ast::Stmt::Close(_)
181        | ast::Stmt::Commit(_)
182        | ast::Stmt::CompoundSelect(_)
183        | ast::Stmt::Copy(_)
184        | ast::Stmt::Deallocate(_)
185        | ast::Stmt::Declare(_)
186        | ast::Stmt::Delete(_)
187        | ast::Stmt::Discard(_)
188        | ast::Stmt::Do(_)
189        | ast::Stmt::EmptyStmt(_)
190        | ast::Stmt::Execute(_)
191        | ast::Stmt::Explain(_)
192        | ast::Stmt::Fetch(_)
193        | ast::Stmt::Grant(_)
194        | ast::Stmt::Insert(_)
195        | ast::Stmt::Listen(_)
196        | ast::Stmt::Merge(_)
197        | ast::Stmt::Move(_)
198        | ast::Stmt::Notify(_)
199        | ast::Stmt::ParenSelect(_)
200        | ast::Stmt::Prepare(_)
201        | ast::Stmt::PrepareTransaction(_)
202        | ast::Stmt::Reassign(_)
203        | ast::Stmt::ReleaseSavepoint(_)
204        | ast::Stmt::Reset(_)
205        | ast::Stmt::Revoke(_)
206        | ast::Stmt::Repack(_)
207        | ast::Stmt::Rollback(_)
208        | ast::Stmt::SavepointCreate(_)
209        | ast::Stmt::SecurityLabel(_)
210        | ast::Stmt::Select(_)
211        | ast::Stmt::Set(_)
212        | ast::Stmt::SetConstraints(_)
213        | ast::Stmt::SetRole(_)
214        | ast::Stmt::SetSessionAuth(_)
215        | ast::Stmt::ResetRole(_)
216        | ast::Stmt::ResetSessionAuth(_)
217        | ast::Stmt::SetTransaction(_)
218        | ast::Stmt::Show(_)
219        | ast::Stmt::Table(_)
220        | ast::Stmt::Unlisten(_)
221        | ast::Stmt::Update(_)
222        | ast::Stmt::Values(_) => false,
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use squawk_syntax::SourceFile;
230
231    #[test]
232    fn alter_table() {
233        let sql = "ALTER TABLE users ADD COLUMN email TEXT;";
234        let file = SourceFile::parse(sql);
235        let stmts = file.tree().stmts().next().unwrap();
236        assert!(possibly_slow_stmt(&stmts));
237    }
238
239    #[test]
240    fn select_into() {
241        let sql = "select 1 a into t;";
242        let file = SourceFile::parse(sql);
243        let stmts = file.tree().stmts().next().unwrap();
244        assert!(possibly_slow_stmt(&stmts));
245    }
246
247    #[test]
248    fn select() {
249        let sql = "select 1;";
250        let file = SourceFile::parse(sql);
251        let stmts = file.tree().stmts().next().unwrap();
252        assert!(!possibly_slow_stmt(&stmts));
253    }
254
255    #[test]
256    fn create_table_without_foreign_key() {
257        let sql = "create table foo (id integer generated by default as identity primary key);";
258        let file = SourceFile::parse(sql);
259        let stmts = file.tree().stmts().next().unwrap();
260        assert!(!possibly_slow_stmt(&stmts));
261    }
262
263    #[test]
264    fn create_table_with_foreign_key() {
265        let sql = "create table foo (id integer, user_id integer references users(id));";
266        let file = SourceFile::parse(sql);
267        let stmts = file.tree().stmts().next().unwrap();
268        assert!(possibly_slow_stmt(&stmts));
269    }
270
271    #[test]
272    fn create_table_with_table_level_foreign_key() {
273        let sql = "create table foo (id integer, user_id integer, foreign key (user_id) references users(id));";
274        let file = SourceFile::parse(sql);
275        let stmts = file.tree().stmts().next().unwrap();
276        assert!(possibly_slow_stmt(&stmts));
277    }
278}