Skip to main content

safe_migrate/ast/
visitor.rs

1// FILE: src/ast/visitor.rs
2use crate::analysis::expr_ir::ExprIr;
3use crate::analysis::facts::{
4    AlterIndexActionFact, AlterTableActionFact, AlterTypeActionFact, AlterTypeFact, ColumnFact,
5    CreateTypeFact, FkFact, PersistenceFact, SearchPathTarget, StatementFact, TableConstraintFact,
6    TypeCreationKind,
7};
8use crate::ast::identifiers::{Ident, QualifiedName};
9use squawk_syntax::ast::{
10    AlterColumnOption, AlterConstraint, AlterDomain, AlterIndex, AlterSequence, AlterTable,
11    AlterTableAction, AlterType, AstNode, AttachPartition, Column, ColumnConstraint, Constraint,
12    CreateDatabase, CreateDomain, CreateIndex, CreateMaterializedView, CreatePolicy,
13    CreateSequence, CreateTable, CreateTableAs, CreateTrigger, CreateType, CreateView,
14    DetachPartition, DropDomain, DropIndex, DropMaterializedView, DropPolicy, DropSequence,
15    DropTable, DropTrigger, DropType, DropView, FieldExpr, Grant, Name, NameRef, Path, PathSegment,
16    ReleaseSavepoint, RenameTo, Revoke, RevokeCommand, Rollback, Savepoint, Set, Stmt, TableArg,
17    TableConstraint,
18};
19use squawk_syntax::{SyntaxKind, ast};
20
21pub struct AstVisitor;
22
23impl AstVisitor {
24    fn resolve_name(n: Name) -> String {
25        Ident::new(n.text().to_string(), n.is_quoted()).resolve()
26    }
27
28    fn resolve_name_ref(nr: &NameRef) -> String {
29        Ident::new(nr.text().to_string(), nr.is_quoted()).resolve()
30    }
31
32    pub fn extract(stmt: &Stmt) -> Option<StatementFact> {
33        let syntax = stmt.syntax();
34        match stmt {
35            Stmt::CreateTable(node) => return Self::extract_create_table(node),
36            Stmt::CreateTableAs(node) => return Self::extract_create_table_as(node),
37            Stmt::CreateView(node) => return Self::extract_create_view(node),
38            Stmt::CreateMaterializedView(node) => {
39                return Self::extract_create_materialized_view(node);
40            }
41            Stmt::CreateIndex(node) => return Self::extract_create_index(node),
42            Stmt::AlterTable(node) => return Self::extract_alter_table(node),
43            Stmt::AlterIndex(node) => return Self::extract_alter_index(node),
44            Stmt::DropTable(node) => return Self::extract_drop_table(node),
45            Stmt::DropView(node) => return Self::extract_drop_view(node),
46            Stmt::DropMaterializedView(node) => return Self::extract_drop_materialized_view(node),
47            Stmt::DropIndex(node) => return Self::extract_drop_index(node),
48            Stmt::Set(node) => return Self::extract_set(node),
49            Stmt::Begin(_) => return Some(StatementFact::BeginTransaction),
50            Stmt::Commit(_) => return Some(StatementFact::CommitTransaction),
51            Stmt::Rollback(node) => return Self::extract_rollback(node),
52            Stmt::Savepoint(node) => return Some(Self::extract_savepoint(node)),
53            Stmt::ReleaseSavepoint(node) => return Some(Self::extract_release_savepoint(node)),
54            Stmt::Do(_) => return Some(StatementFact::OpaqueBlock),
55            Stmt::Execute(_) => return Some(StatementFact::Execute),
56            Stmt::Vacuum(node) => {
57                let relation = if let Some(list) = node.table_and_columns_list() {
58                    list.table_and_columnss()
59                        .next()
60                        .and_then(|tc| tc.relation_name())
61                        .and_then(|rn| rn.path())
62                        .and_then(|path| Self::path_to_qualified_name(&path))
63                } else {
64                    None
65                };
66                return Some(StatementFact::Vacuum {
67                    relation,
68                    is_full: node.is_full(),
69                });
70            }
71            _ => {}
72        }
73
74        if let Some(node) = ast::CreateSchema::cast(syntax.clone()) {
75            return Self::extract_create_schema(&node);
76        }
77        if let Some(node) = ast::AlterSchema::cast(syntax.clone()) {
78            return Self::extract_alter_schema(&node);
79        }
80        if let Some(node) = ast::DropSchema::cast(syntax.clone()) {
81            return Self::extract_drop_schema(&node);
82        }
83
84        if let Some(node) = ast::AlterView::cast(syntax.clone()) {
85            return Self::extract_alter_view(&node);
86        }
87        if let Some(node) = ast::AlterMaterializedView::cast(syntax.clone()) {
88            return Self::extract_alter_materialized_view(&node);
89        }
90        if let Some(node) = ast::Refresh::cast(syntax.clone()) {
91            return Self::extract_refresh(&node);
92        }
93
94        if let Some(node) = CreateSequence::cast(syntax.clone()) {
95            return Self::extract_create_sequence(&node);
96        }
97        if let Some(node) = AlterSequence::cast(syntax.clone()) {
98            return Self::extract_alter_sequence(&node);
99        }
100        if let Some(node) = DropSequence::cast(syntax.clone()) {
101            return Self::extract_drop_sequence(&node);
102        }
103        if let Some(node) = CreateType::cast(syntax.clone()) {
104            return Self::extract_create_type(&node);
105        }
106        if let Some(node) = AlterType::cast(syntax.clone()) {
107            return Self::extract_alter_type(&node);
108        }
109        if let Some(node) = CreateDomain::cast(syntax.clone()) {
110            return Self::extract_create_domain(&node);
111        }
112        if let Some(node) = AlterDomain::cast(syntax.clone()) {
113            return Self::extract_alter_domain(&node);
114        }
115        if let Some(node) = DropType::cast(syntax.clone()) {
116            return Self::extract_drop_type(&node);
117        }
118        if let Some(node) = DropDomain::cast(syntax.clone()) {
119            return Self::extract_drop_domain(&node);
120        }
121        if let Some(node) = CreatePolicy::cast(syntax.clone()) {
122            return Self::extract_create_policy(&node);
123        }
124        if let Some(node) = DropPolicy::cast(syntax.clone()) {
125            return Self::extract_drop_policy(&node);
126        }
127        if let Some(node) = CreateTrigger::cast(syntax.clone()) {
128            return Self::extract_create_trigger(&node);
129        }
130        if let Some(node) = DropTrigger::cast(syntax.clone()) {
131            return Self::extract_drop_trigger(&node);
132        }
133
134        if ast::PrepareTransaction::cast(syntax.clone()).is_some() {
135            let name = syntax
136                .descendants()
137                .find_map(ast::Literal::cast)
138                .map(|l| l.syntax().text().to_string().trim_matches('\'').to_string())
139                .or_else(|| {
140                    syntax
141                        .descendants()
142                        .find_map(Name::cast)
143                        .map(Self::resolve_name)
144                })
145                .unwrap_or_default();
146            return Some(StatementFact::PrepareTransaction { name });
147        }
148        if ast::SetTransaction::cast(syntax.clone()).is_some() {
149            return Some(StatementFact::SetTransaction);
150        }
151        if ast::SetConstraints::cast(syntax.clone()).is_some() {
152            return Some(StatementFact::SetConstraints);
153        }
154
155        if let Some(node) = ast::CreateFunction::cast(syntax.clone()) {
156            return Self::extract_create_function(&node);
157        }
158        if let Some(node) = ast::AlterFunction::cast(syntax.clone()) {
159            return Self::extract_alter_function(&node);
160        }
161        if let Some(node) = ast::DropFunction::cast(syntax.clone()) {
162            return Self::extract_drop_function(&node);
163        }
164        if let Some(node) = ast::CreateProcedure::cast(syntax.clone()) {
165            return Self::extract_create_procedure(&node);
166        }
167        if let Some(node) = ast::AlterProcedure::cast(syntax.clone()) {
168            return Self::extract_alter_procedure(&node);
169        }
170        if let Some(node) = ast::DropProcedure::cast(syntax.clone()) {
171            return Self::extract_drop_procedure(&node);
172        }
173        if let Some(node) = ast::CreatePublication::cast(syntax.clone()) {
174            return Self::extract_create_publication(&node);
175        }
176        if let Some(node) = ast::AlterPublication::cast(syntax.clone()) {
177            return Self::extract_alter_publication(&node);
178        }
179        if let Some(node) = ast::DropPublication::cast(syntax.clone()) {
180            return Self::extract_drop_publication(&node);
181        }
182        if let Some(node) = ast::CreateSubscription::cast(syntax.clone()) {
183            return Self::extract_create_subscription(&node);
184        }
185        if let Some(node) = ast::AlterSubscription::cast(syntax.clone()) {
186            return Self::extract_alter_subscription(&node);
187        }
188        if let Some(node) = ast::DropSubscription::cast(syntax.clone()) {
189            return Self::extract_drop_subscription(&node);
190        }
191        if let Some(node) = ast::CreateRole::cast(syntax.clone()) {
192            return Self::extract_create_role(&node);
193        }
194        if let Some(node) = ast::AlterRole::cast(syntax.clone()) {
195            return Self::extract_alter_role(&node);
196        }
197        if let Some(node) = ast::DropRole::cast(syntax.clone()) {
198            return Self::extract_drop_role(&node);
199        }
200        if let Some(node) = Grant::cast(syntax.clone()) {
201            return Self::extract_grant(&node);
202        }
203        if let Some(node) = Revoke::cast(syntax.clone()) {
204            return Self::extract_revoke(&node);
205        }
206        if let Some(node) = CreateDatabase::cast(syntax.clone()) {
207            return Self::extract_create_database(&node);
208        }
209        if let Some(node) = ast::AlterDatabase::cast(syntax.clone()) {
210            return Self::extract_alter_database(&node);
211        }
212        if let Some(node) = ast::DropDatabase::cast(syntax.clone()) {
213            return Self::extract_drop_database(&node);
214        }
215
216        None
217    }
218
219    fn extract_create_schema(node: &ast::CreateSchema) -> Option<StatementFact> {
220        let name = node.name().map(|n| {
221            Ident::new(
222                n.text().to_string().trim_matches('"').to_string(),
223                n.is_quoted(),
224            )
225        })?;
226
227        Some(StatementFact::CreateSchema {
228            name: QualifiedName::new(None, name),
229            if_not_exists: node.if_not_exists().is_some(),
230        })
231    }
232
233    fn extract_alter_schema(node: &ast::AlterSchema) -> Option<StatementFact> {
234        let nr = node.name_ref()?;
235        let name = QualifiedName::new(
236            None,
237            Ident::new(
238                nr.text().to_string().trim_matches('"').to_string(),
239                nr.is_quoted(),
240            ),
241        );
242        let new_name = node.rename_to().and_then(|rt| {
243            rt.name().map(|n| {
244                Ident::new(
245                    n.text().to_string().trim_matches('"').to_string(),
246                    n.is_quoted(),
247                )
248            })
249        });
250        Some(StatementFact::AlterSchema { name, new_name })
251    }
252
253    fn extract_drop_schema(node: &ast::DropSchema) -> Option<StatementFact> {
254        // DROP SCHEMA uses NameRef nodes (bare identifiers), NOT Path nodes.
255        // Squawk's DropSchema accessor exposes name_refs() for exactly this reason.
256        let names: Vec<QualifiedName> = node
257            .name_refs()
258            .map(|nr| {
259                let ident = Ident::new(
260                    nr.text().to_string().trim_matches('"').to_string(),
261                    nr.is_quoted(),
262                );
263                QualifiedName::new(None, ident)
264            })
265            .collect();
266
267        if names.is_empty() {
268            return None;
269        }
270
271        Some(StatementFact::DropSchema {
272            names,
273            if_exists: node.if_exists().is_some(),
274            cascade: node.cascade_token().is_some(),
275        })
276    }
277
278    fn extract_create_table(node: &CreateTable) -> Option<StatementFact> {
279        let path = node.syntax().descendants().find_map(Path::cast)?;
280        let name = Self::path_to_qualified_name(&path)?;
281
282        let persistence = match node
283            .persistence()
284            .map(|p| p.syntax().text().to_string().to_lowercase())
285            .as_deref()
286        {
287            Some("temporary") | Some("temp") => PersistenceFact::Temporary,
288            Some("unlogged") => PersistenceFact::Unlogged,
289            _ => PersistenceFact::Permanent,
290        };
291
292        let partition_by = node.partition_by().map(|p| p.syntax().text().to_string());
293        let partition_of = node
294            .partition_of()
295            .and_then(|p| p.path())
296            .and_then(|p| Self::path_to_qualified_name(&p));
297        let partition_type = node
298            .partition_type()
299            .map(|pt| pt.syntax().text().to_string());
300
301        let (columns, foreign_keys, table_constraints) = node
302            .table_arg_list()
303            .map(|tal| Self::extract_table_body(tal.args()))
304            .unwrap_or_else(|| (Vec::new(), Vec::new(), Vec::new()));
305
306        Some(StatementFact::CreateTable {
307            name,
308            if_not_exists: node.if_not_exists().is_some(),
309            as_select: false,
310            persistence,
311            columns,
312            foreign_keys,
313            table_constraints,
314            partition_by,
315            partition_of,
316            partition_type,
317        })
318    }
319
320    fn extract_create_table_as(node: &CreateTableAs) -> Option<StatementFact> {
321        let path = node.path()?;
322        let persistence = match node
323            .persistence()
324            .map(|p| p.syntax().text().to_string().to_lowercase())
325            .as_deref()
326        {
327            Some("temporary") | Some("temp") => PersistenceFact::Temporary,
328            Some("unlogged") => PersistenceFact::Unlogged,
329            _ => PersistenceFact::Permanent,
330        };
331        Some(StatementFact::CreateTable {
332            name: Self::path_to_qualified_name(&path)?,
333            if_not_exists: node.if_not_exists().is_some(),
334            as_select: true,
335            persistence,
336            columns: Vec::new(),
337            foreign_keys: Vec::new(),
338            table_constraints: Vec::new(),
339            partition_by: None,
340            partition_of: None,
341            partition_type: None,
342        })
343    }
344
345    fn extract_drop_table(node: &DropTable) -> Option<StatementFact> {
346        let path = node.paths().next()?;
347        Some(StatementFact::DropTable {
348            name: Self::path_to_qualified_name(&path)?,
349            if_exists: node.if_exists().is_some(),
350            cascade: node.cascade_token().is_some(),
351        })
352    }
353
354    fn extract_alter_table(node: &AlterTable) -> Option<StatementFact> {
355        let path = node.syntax().descendants().find_map(Path::cast)?;
356        let table_name = Self::path_to_qualified_name(&path)?;
357        let mut actions = Vec::new();
358
359        for action in node.actions() {
360            if let Some(ap) = AttachPartition::cast(action.syntax().clone()) {
361                if let Some(child_path) = ap.syntax().descendants().find_map(Path::cast)
362                    && let Some(child) = Self::path_to_qualified_name(&child_path)
363                {
364                    actions.push(AlterTableActionFact::AttachPartition { child });
365                }
366                continue;
367            }
368            if let Some(dp) = DetachPartition::cast(action.syntax().clone()) {
369                if let Some(child_path) = dp.syntax().descendants().find_map(Path::cast)
370                    && let Some(child) = Self::path_to_qualified_name(&child_path)
371                {
372                    actions.push(AlterTableActionFact::DetachPartition { child });
373                }
374                continue;
375            }
376            if let Some(ac) = AlterConstraint::cast(action.syntax().clone()) {
377                let deferrable = ac.deferrable_constraint_option().is_some();
378                actions.push(AlterTableActionFact::AlterConstraint {
379                    name: None,
380                    deferrable,
381                });
382                continue;
383            }
384            if let Some(rc) = ast::RenameConstraint::cast(action.syntax().clone()) {
385                let old_name = rc
386                    .syntax()
387                    .descendants()
388                    .find_map(NameRef::cast)
389                    .map(|nr| Self::resolve_name_ref(&nr));
390                let new_name = rc
391                    .syntax()
392                    .descendants()
393                    .find_map(Name::cast)
394                    .map(Self::resolve_name);
395                if let (Some(old_name), Some(new_name)) = (old_name, new_name) {
396                    actions.push(AlterTableActionFact::RenameConstraint { old_name, new_name });
397                }
398                continue;
399            }
400
401            match action {
402                AlterTableAction::AddColumn(add) => {
403                    if let Some(name) = add.name().map(Self::resolve_name) {
404                        let mut not_null = false;
405                        let mut default = None;
406                        for c in add.constraints() {
407                            match c {
408                                Constraint::NotNullConstraint(_) => not_null = true,
409                                Constraint::PrimaryKeyConstraint(_) => not_null = true,
410                                Constraint::DefaultConstraint(dc) => {
411                                    default = dc
412                                        .expr()
413                                        .map(crate::analysis::expr_visitor::ExprVisitor::convert)
414                                }
415                                _ => {}
416                            }
417                        }
418                        actions.push(AlterTableActionFact::AddColumn {
419                            name,
420                            ty: add.ty().map(|t| t.syntax().text().to_string()),
421                            if_not_exists: add.if_not_exists().is_some(),
422                            not_null,
423                            default,
424                        });
425                    }
426                }
427                AlterTableAction::DropColumn(drop) => {
428                    if let Some(name) = drop.name_ref().map(|nr| Self::resolve_name_ref(&nr)) {
429                        actions.push(AlterTableActionFact::DropColumn {
430                            name,
431                            if_exists: drop.if_exists().is_some(),
432                        });
433                    }
434                }
435                AlterTableAction::RenameColumn(rc) => {
436                    let from_ident = rc
437                        .from()
438                        .map(|nr| {
439                            Ident::new(
440                                nr.text().to_string().trim_matches('"').to_string(),
441                                nr.is_quoted(),
442                            )
443                        })
444                        .or_else(|| {
445                            rc.syntax().descendants().find_map(NameRef::cast).map(|nr| {
446                                Ident::new(
447                                    nr.text().to_string().trim_matches('"').to_string(),
448                                    nr.is_quoted(),
449                                )
450                            })
451                        });
452
453                    let to_ident = rc
454                        .to()
455                        .map(|nr| {
456                            Ident::new(
457                                nr.text().to_string().trim_matches('"').to_string(),
458                                nr.is_quoted(),
459                            )
460                        })
461                        .or_else(|| {
462                            rc.syntax().descendants().find_map(Name::cast).map(|n| {
463                                Ident::new(
464                                    n.text().to_string().trim_matches('"').to_string(),
465                                    n.is_quoted(),
466                                )
467                            })
468                        });
469
470                    if let (Some(from), Some(to)) = (from_ident, to_ident) {
471                        actions.push(AlterTableActionFact::RenameColumn { from, to });
472                    }
473                }
474                AlterTableAction::RenameTo(rt) => {
475                    if let Some(new_name) = rt.name() {
476                        actions.push(AlterTableActionFact::RenameTo {
477                            new_name: Ident::new(
478                                new_name.text().to_string().trim_matches('"').to_string(),
479                                new_name.is_quoted(),
480                            ),
481                        });
482                    }
483                }
484                AlterTableAction::AddConstraint(ac) => {
485                    if let Some(fact) = Self::extract_add_constraint_fact(&ac) {
486                        actions.push(fact);
487                    }
488                }
489                AlterTableAction::DropConstraint(dc) => {
490                    if let Some(name) = dc.name_ref().map(|nr| Self::resolve_name_ref(&nr)) {
491                        actions.push(AlterTableActionFact::DropConstraint { name });
492                    }
493                }
494                AlterTableAction::AlterColumn(alter_col) => {
495                    let col_ident = alter_col
496                        .syntax()
497                        .descendants()
498                        .find_map(NameRef::cast)
499                        .map(|nr| Self::resolve_name_ref(&nr))
500                        .or_else(|| {
501                            alter_col
502                                .syntax()
503                                .descendants()
504                                .find_map(Name::cast)
505                                .map(Self::resolve_name)
506                        });
507
508                    if let Some(col_name) = col_ident
509                        && let Some(opt) = alter_col.option()
510                        && let Some(fact) = Self::extract_alter_column_option(col_name, opt)
511                    {
512                        actions.push(fact);
513                    }
514                }
515                AlterTableAction::ValidateConstraint(vc) => {
516                    if let Some(constraint_name) = vc
517                        .syntax()
518                        .descendants()
519                        .find_map(NameRef::cast)
520                        .map(|nr| Self::resolve_name_ref(&nr))
521                    {
522                        actions.push(AlterTableActionFact::ValidateConstraint { constraint_name });
523                    }
524                }
525                AlterTableAction::SetAccessMethod(sam) => {
526                    if sam.name_ref().is_some() {
527                        actions.push(AlterTableActionFact::SetAccessMethod);
528                    }
529                }
530                AlterTableAction::DisableTrigger(dt) => {
531                    let trigger_name = dt
532                        .name_ref()
533                        .map(|nr| Self::resolve_name_ref(&nr))
534                        .or_else(|| {
535                            if dt.all_token().is_some() {
536                                Some("ALL".to_string())
537                            } else {
538                                None
539                            }
540                        })
541                        .or_else(|| {
542                            dt.syntax()
543                                .descendants()
544                                .find_map(NameRef::cast)
545                                .map(|nr| Self::resolve_name_ref(&nr))
546                        });
547                    actions.push(AlterTableActionFact::DisableTrigger { trigger_name });
548                }
549                AlterTableAction::EnableTrigger(et) => {
550                    let trigger_name = et
551                        .name_ref()
552                        .map(|nr| Self::resolve_name_ref(&nr))
553                        .or_else(|| {
554                            if et.all_token().is_some() {
555                                Some("ALL".to_string())
556                            } else {
557                                None
558                            }
559                        })
560                        .or_else(|| {
561                            et.syntax()
562                                .descendants()
563                                .find_map(NameRef::cast)
564                                .map(|nr| Self::resolve_name_ref(&nr))
565                        });
566                    actions.push(AlterTableActionFact::EnableTrigger { trigger_name });
567                }
568                AlterTableAction::SetSchema(ss) => {
569                    if let Some(nr) = ss.name_ref() {
570                        actions.push(AlterTableActionFact::SetSchema {
571                            new_schema: Self::resolve_name_ref(&nr),
572                        });
573                    }
574                }
575                AlterTableAction::SetTablespace(st) => {
576                    if let Some(path) = st.path()
577                        && let Some(qname) = Self::path_to_qualified_name(&path)
578                    {
579                        actions.push(AlterTableActionFact::SetTablespace {
580                            tablespace: qname.name.resolve(),
581                        });
582                    }
583                }
584                AlterTableAction::OwnerTo(ot) => {
585                    if let Some(role_ref) = ot.role_ref()
586                        && let Some(nr) = role_ref.name_ref()
587                    {
588                        actions.push(AlterTableActionFact::OwnerTo {
589                            new_owner: Self::resolve_name_ref(&nr),
590                        });
591                    } else {
592                        let nr = ot.syntax().descendants().find_map(NameRef::cast);
593                        if let Some(nr) = nr {
594                            actions.push(AlterTableActionFact::OwnerTo {
595                                new_owner: Self::resolve_name_ref(&nr),
596                            });
597                        }
598                    }
599                }
600                AlterTableAction::SetLogged(_) => {
601                    actions.push(AlterTableActionFact::SetLogged);
602                }
603                AlterTableAction::SetUnlogged(_) => {
604                    actions.push(AlterTableActionFact::SetUnlogged);
605                }
606                AlterTableAction::ReplicaIdentity(ri) => {
607                    let option = ri
608                        .name_ref()
609                        .map(|nr| Self::resolve_name_ref(&nr))
610                        .or_else(|| {
611                            if ri.default_token().is_some() {
612                                Some("DEFAULT".to_string())
613                            } else if ri.full_token().is_some() {
614                                Some("FULL".to_string())
615                            } else if ri.syntax().descendants().find_map(NameRef::cast).is_some() {
616                                ri.syntax()
617                                    .descendants()
618                                    .find_map(NameRef::cast)
619                                    .map(|nr| Self::resolve_name_ref(&nr))
620                            } else {
621                                Some("NOTHING".to_string())
622                            }
623                        })
624                        .unwrap_or_default();
625                    actions.push(AlterTableActionFact::ReplicaIdentity { option });
626                }
627                AlterTableAction::ClusterOn(co) => {
628                    let index = co
629                        .name_ref()
630                        .map(|nr| Self::resolve_name_ref(&nr))
631                        .or_else(|| {
632                            co.syntax()
633                                .descendants()
634                                .find_map(NameRef::cast)
635                                .map(|nr| Self::resolve_name_ref(&nr))
636                        })
637                        .unwrap_or_default();
638                    actions.push(AlterTableActionFact::ClusterOn { index });
639                }
640                AlterTableAction::InheritTable(it) => {
641                    if let Some(path) = it.path()
642                        && let Some(parent) = Self::path_to_qualified_name(&path)
643                    {
644                        actions.push(AlterTableActionFact::InheritTable { parent });
645                    }
646                }
647                AlterTableAction::NoInheritTable(nit) => {
648                    if let Some(path) = nit.path()
649                        && let Some(parent) = Self::path_to_qualified_name(&path)
650                    {
651                        actions.push(AlterTableActionFact::NoInheritTable { parent });
652                    }
653                }
654                AlterTableAction::MergePartitions(mp) => {
655                    if let Some(path) = mp.path()
656                        && let Some(parent) = Self::path_to_qualified_name(&path)
657                    {
658                        actions.push(AlterTableActionFact::MergePartitions { parent });
659                    }
660                }
661                AlterTableAction::SplitPartition(_sp) => {
662                    actions.push(AlterTableActionFact::SplitPartition);
663                }
664                AlterTableAction::ForceRls(_) => {
665                    actions.push(AlterTableActionFact::ForceRls);
666                }
667                AlterTableAction::EnableRls(_) => {
668                    actions.push(AlterTableActionFact::EnableRls);
669                }
670                AlterTableAction::DisableRls(_) => {
671                    actions.push(AlterTableActionFact::DisableRls);
672                }
673                AlterTableAction::EnableAlwaysTrigger(eat) => {
674                    let trigger_name = eat
675                        .name_ref()
676                        .map(|nr| Self::resolve_name_ref(&nr))
677                        .or_else(|| {
678                            eat.syntax()
679                                .descendants()
680                                .find_map(NameRef::cast)
681                                .map(|nr| Self::resolve_name_ref(&nr))
682                        });
683                    actions.push(AlterTableActionFact::EnableAlwaysTrigger { trigger_name });
684                }
685                AlterTableAction::EnableReplicaTrigger(ert) => {
686                    let trigger_name = ert
687                        .name_ref()
688                        .map(|nr| Self::resolve_name_ref(&nr))
689                        .or_else(|| {
690                            ert.syntax()
691                                .descendants()
692                                .find_map(NameRef::cast)
693                                .map(|nr| Self::resolve_name_ref(&nr))
694                        });
695                    actions.push(AlterTableActionFact::EnableReplicaTrigger { trigger_name });
696                }
697                _ => {
698                    let txt = action.syntax().text().to_string().to_lowercase();
699
700                    if txt.contains("set storage") {
701                        let parts: Vec<&str> = txt.split_whitespace().collect();
702                        if let Some(idx) = parts.iter().position(|&p| p == "column")
703                            && idx + 1 < parts.len()
704                        {
705                            let c_name = parts[idx + 1].trim_matches('"').to_string();
706                            actions.push(AlterTableActionFact::SetStorage { column: c_name });
707                        }
708                    }
709                }
710            }
711        }
712
713        Some(StatementFact::AlterTable {
714            name: table_name,
715            actions,
716        })
717    }
718
719    fn extract_table_body(
720        args: impl Iterator<Item = TableArg>,
721    ) -> (Vec<ColumnFact>, Vec<FkFact>, Vec<TableConstraintFact>) {
722        let mut columns = Vec::new();
723        let mut foreign_keys = Vec::new();
724        let mut table_constraints = Vec::new();
725
726        for arg in args {
727            match arg {
728                TableArg::Column(col) => {
729                    for fk in Self::extract_column_fk_facts(&col) {
730                        foreign_keys.push(fk);
731                    }
732                    if let Some(fact) = Self::extract_column_fact(&col) {
733                        columns.push(fact);
734                    }
735                }
736                TableArg::LikeClause(like) => {
737                    if let Some(path) = like.syntax().descendants().find_map(Path::cast)
738                        && let Some(_parent) = Self::path_to_qualified_name(&path)
739                    {
740                        // In the future, we may need to track 'Like' clauses as a specific
741                        // mutation fact to properly model schema dependency and inheritance.
742                        // For now, we omit them from the core table creation facts as
743                        // they do not create column definitions in the current AST.
744                    }
745                }
746                TableArg::TableConstraint(tc) => {
747                    if let Some(fk) = Self::extract_table_fk_fact(&tc) {
748                        foreign_keys.push(fk);
749                    }
750                    if let Some(tc_fact) = Self::extract_table_constraint_fact(&tc) {
751                        table_constraints.push(tc_fact);
752                    }
753                }
754            }
755        }
756        (columns, foreign_keys, table_constraints)
757    }
758
759    fn extract_column_fact(col: &Column) -> Option<ColumnFact> {
760        let name = Self::resolve_name(col.name()?);
761        let ty = col.ty().map(|t| t.syntax().text().to_string());
762        let not_null = col
763            .constraints()
764            .any(|c| matches!(c, ColumnConstraint::NotNullConstraint(_)));
765        let is_primary_key = col
766            .constraints()
767            .any(|c| matches!(c, ColumnConstraint::PrimaryKeyConstraint(_)));
768        let default = col.constraints().find_map(|c| {
769            if let ColumnConstraint::DefaultConstraint(dc) = c {
770                Some(crate::analysis::expr_visitor::ExprVisitor::convert(
771                    dc.expr()?,
772                ))
773            } else {
774                None
775            }
776        });
777        Some(ColumnFact {
778            name,
779            ty,
780            not_null,
781            is_primary_key,
782            default,
783        })
784    }
785
786    fn extract_alter_column_option(
787        col_name: String,
788        opt: AlterColumnOption,
789    ) -> Option<AlterTableActionFact> {
790        match opt {
791            AlterColumnOption::SetStorage(_) => {
792                Some(AlterTableActionFact::SetStorage { column: col_name })
793            }
794            AlterColumnOption::SetNotNull(_) => {
795                Some(AlterTableActionFact::SetNotNull { column: col_name })
796            }
797            AlterColumnOption::DropNotNull(_) => {
798                Some(AlterTableActionFact::DropNotNull { column: col_name })
799            }
800            AlterColumnOption::SetType(st) => {
801                let has_using = st
802                    .syntax()
803                    .descendants()
804                    .any(|t| t.kind() == SyntaxKind::USING_KW);
805                Some(AlterTableActionFact::SetType {
806                    column: col_name,
807                    ty: st.ty()?.syntax().text().to_string(),
808                    has_using,
809                })
810            }
811            AlterColumnOption::SetDefault(sd) => Some(AlterTableActionFact::SetDefault {
812                column: col_name,
813                default: sd
814                    .expr()
815                    .map(crate::analysis::expr_visitor::ExprVisitor::convert),
816            }),
817            AlterColumnOption::SetExpression(se) => Some(AlterTableActionFact::SetExpression {
818                column: col_name,
819                expr: se
820                    .expr()
821                    .map(crate::analysis::expr_visitor::ExprVisitor::convert)
822                    .unwrap_or(ExprIr::Omitted),
823            }),
824            AlterColumnOption::SetOptions(so) => Some(AlterTableActionFact::SetOptions {
825                column: col_name,
826                attributes: so
827                    .attribute_list()
828                    .map(|al| {
829                        al.attribute_options()
830                            .map(|ao| crate::analysis::facts::AttributeFact {
831                                name: ao.name().map(|n| n.text().to_string()).unwrap_or_default(),
832                                value: ao
833                                    .syntax()
834                                    .descendants()
835                                    .find_map(ast::Literal::cast)
836                                    .map(|l| l.syntax().text().to_string())
837                                    .unwrap_or_default(),
838                            })
839                            .collect()
840                    })
841                    .unwrap_or_default(),
842            }),
843            AlterColumnOption::Inherit(i) => Some(AlterTableActionFact::Inherit {
844                column: col_name,
845                parent: i
846                    .syntax()
847                    .descendants()
848                    .find_map(Path::cast)
849                    .and_then(|p| Self::path_to_qualified_name(&p))
850                    .unwrap_or_else(|| {
851                        QualifiedName::new(None, Ident::new("unknown".to_string(), false))
852                    }),
853            }),
854            AlterColumnOption::NoInherit(ni) => Some(AlterTableActionFact::NoInherit {
855                column: col_name,
856                parent: ni
857                    .syntax()
858                    .descendants()
859                    .find_map(Path::cast)
860                    .and_then(|p| Self::path_to_qualified_name(&p))
861                    .unwrap_or_else(|| {
862                        QualifiedName::new(None, Ident::new("unknown".to_string(), false))
863                    }),
864            }),
865            _ => None,
866        }
867    }
868
869    fn extract_add_constraint_fact(
870        ac: &squawk_syntax::ast::AddConstraint,
871    ) -> Option<AlterTableActionFact> {
872        let not_valid = ac.not_valid().is_some();
873
874        if let Some(fkc) = ac
875            .syntax()
876            .descendants()
877            .find_map(ast::ForeignKeyConstraint::cast)
878        {
879            let constraint_name = fkc
880                .constraint_name()
881                .and_then(|cn| cn.name())
882                .map(Self::resolve_name)
883                .or_else(|| {
884                    ac.syntax()
885                        .descendants()
886                        .find_map(ast::ConstraintName::cast)
887                        .and_then(|cn| cn.name())
888                        .map(Self::resolve_name)
889                });
890            let path = fkc.syntax().descendants().find_map(Path::cast)?;
891            let references = Self::path_to_qualified_name(&path)?;
892            return Some(AlterTableActionFact::AddForeignKey {
893                constraint_name,
894                references,
895                from_columns: fkc
896                    .from_columns()
897                    .map(Self::extract_column_list_names)
898                    .unwrap_or_default(),
899                to_columns: fkc
900                    .to_columns()
901                    .map(Self::extract_column_list_names)
902                    .unwrap_or_default(),
903                not_valid,
904            });
905        }
906
907        if let Some(cc) = ac
908            .syntax()
909            .descendants()
910            .find_map(ast::CheckConstraint::cast)
911        {
912            let constraint_name = cc
913                .constraint_name()
914                .and_then(|cn| cn.name())
915                .map(Self::resolve_name)
916                .or_else(|| {
917                    ac.syntax()
918                        .descendants()
919                        .find_map(ast::ConstraintName::cast)
920                        .and_then(|cn| cn.name())
921                        .map(Self::resolve_name)
922                });
923            return Some(AlterTableActionFact::AddCheckConstraint {
924                constraint_name,
925                not_valid,
926            });
927        }
928
929        if ac
930            .syntax()
931            .descendants()
932            .any(|n| ast::UniqueConstraint::can_cast(n.kind()))
933        {
934            return Some(AlterTableActionFact::AddUniqueConstraint);
935        }
936
937        if ac
938            .syntax()
939            .descendants()
940            .any(|n| ast::PrimaryKeyConstraint::can_cast(n.kind()))
941        {
942            return Some(AlterTableActionFact::AddPrimaryKeyConstraint);
943        }
944
945        None
946    }
947
948    fn extract_table_constraint_fact(tc: &TableConstraint) -> Option<TableConstraintFact> {
949        match tc {
950            TableConstraint::PrimaryKeyConstraint(pkc) => Some(TableConstraintFact::PrimaryKey {
951                columns: Self::extract_column_list_names(pkc.column_list()?),
952            }),
953            TableConstraint::UniqueConstraint(uc) => Some(TableConstraintFact::Unique {
954                columns: Self::extract_column_list_names(uc.column_list()?),
955            }),
956            TableConstraint::CheckConstraint(_) => Some(TableConstraintFact::Check),
957            _ => None,
958        }
959    }
960
961    fn extract_column_fk_facts(col: &Column) -> Vec<FkFact> {
962        let col_name = col.name().map(Self::resolve_name);
963        col.constraints()
964            .filter_map(|c| {
965                if let ColumnConstraint::ReferencesConstraint(rc) = c {
966                    let ref_path = rc.syntax().descendants().find_map(Path::cast)?;
967                    Some(FkFact {
968                        constraint_name: None,
969                        references: Self::path_to_qualified_name(&ref_path)?,
970                        from_columns: col_name.iter().cloned().collect(),
971                        to_columns: Vec::new(),
972                    })
973                } else {
974                    None
975                }
976            })
977            .collect()
978    }
979
980    fn extract_table_fk_fact(tc: &TableConstraint) -> Option<FkFact> {
981        if let TableConstraint::ForeignKeyConstraint(fkc) = tc {
982            let constraint_name = fkc
983                .constraint_name()
984                .and_then(|cn| cn.name())
985                .map(Self::resolve_name);
986            let path = fkc.path()?;
987            let references = Self::path_to_qualified_name(&path)?;
988            let from_columns = fkc
989                .from_columns()
990                .map(Self::extract_column_list_names)
991                .unwrap_or_default();
992            let to_columns = fkc
993                .to_columns()
994                .map(Self::extract_column_list_names)
995                .unwrap_or_default();
996            Some(FkFact {
997                constraint_name,
998                references,
999                from_columns,
1000                to_columns,
1001            })
1002        } else {
1003            None
1004        }
1005    }
1006
1007    fn extract_column_list_names(cl: ast::ColumnList) -> Vec<String> {
1008        cl.columns()
1009            .filter_map(|col| col.name_ref().map(|nr| Self::resolve_name_ref(&nr)))
1010            .collect()
1011    }
1012
1013    fn extract_create_index(node: &CreateIndex) -> Option<StatementFact> {
1014        let relation_path = node.syntax().descendants().find_map(Path::cast)?;
1015        let relation = Self::path_to_qualified_name(&relation_path)?;
1016
1017        let index_ident = if let Some(name) = node.name() {
1018            Ident::new(
1019                name.text().to_string().trim_matches('"').to_string(),
1020                name.is_quoted(),
1021            )
1022        } else {
1023            Ident::new(
1024                format!("<unnamed_idx_on_{}>", relation.name.resolve()),
1025                false,
1026            )
1027        };
1028
1029        let using_method = node.using_method().map(|um| {
1030            um.name_ref()
1031                .map(|nr| nr.text().to_string().to_uppercase())
1032                .unwrap_or_default()
1033        });
1034
1035        let has_predicate = node.where_clause().is_some();
1036        let unique = node.unique_token().is_some();
1037
1038        Some(StatementFact::CreateIndex {
1039            name: QualifiedName::new(None, index_ident),
1040            relation,
1041            if_not_exists: node.if_not_exists().is_some(),
1042            concurrently: node.concurrently_token().is_some(),
1043            using_method,
1044            has_predicate,
1045            unique,
1046        })
1047    }
1048
1049    fn extract_alter_index(node: &AlterIndex) -> Option<StatementFact> {
1050        let path = node.path()?;
1051        let name = Self::path_to_qualified_name(&path)?;
1052        let mut actions = Vec::new();
1053
1054        if let Some(rt) = node.syntax().descendants().find_map(RenameTo::cast)
1055            && let Some(new_name) = rt.name()
1056        {
1057            actions.push(AlterIndexActionFact::RenameTo {
1058                new_name: Ident::new(
1059                    new_name.text().to_string().trim_matches('"').to_string(),
1060                    new_name.is_quoted(),
1061                ),
1062            });
1063        }
1064
1065        if actions.is_empty() {
1066            return None;
1067        }
1068        Some(StatementFact::AlterIndex { name, actions })
1069    }
1070
1071    fn extract_drop_index(node: &DropIndex) -> Option<StatementFact> {
1072        let names: Vec<QualifiedName> = node
1073            .paths()
1074            .filter_map(|p| Self::path_to_qualified_name(&p))
1075            .collect();
1076        if names.is_empty() {
1077            return None;
1078        }
1079        Some(StatementFact::DropIndex {
1080            names,
1081            if_exists: node.if_exists().is_some(),
1082            concurrently: node.concurrently_token().is_some(),
1083        })
1084    }
1085
1086    fn extract_create_view(node: &CreateView) -> Option<StatementFact> {
1087        let path = node.path()?;
1088        Some(StatementFact::CreateView {
1089            name: Self::path_to_qualified_name(&path)?,
1090            or_replace: node.or_replace().is_some(),
1091            depends_on: Self::extract_view_dependencies(node.syntax()),
1092        })
1093    }
1094
1095    fn extract_alter_view(node: &ast::AlterView) -> Option<StatementFact> {
1096        let path = node.path()?;
1097        let name = Self::path_to_qualified_name(&path)?;
1098
1099        // Check for RenameTo
1100        if let Some(rt) = node.syntax().descendants().find_map(RenameTo::cast)
1101            && let Some(new_name_node) = rt.name()
1102        {
1103            return Some(StatementFact::AlterView {
1104                name,
1105                action: crate::analysis::facts::AlterViewAction::RenameTo {
1106                    new_name: Ident::new(
1107                        new_name_node
1108                            .text()
1109                            .to_string()
1110                            .trim_matches('"')
1111                            .to_string(),
1112                        new_name_node.is_quoted(),
1113                    ),
1114                },
1115            });
1116        }
1117
1118        // Check for OwnerTo
1119        if let Some(ot) = node.owner_to()
1120            && let Some(role_ref) = ot.syntax().descendants().find_map(ast::RoleRef::cast)
1121            && let Some(nr) = role_ref.name_ref()
1122        {
1123            return Some(StatementFact::AlterView {
1124                name,
1125                action: crate::analysis::facts::AlterViewAction::OwnerTo {
1126                    new_owner: Self::resolve_name_ref(&nr),
1127                },
1128            });
1129        }
1130
1131        // Check for SetSchema
1132        if let Some(ss) = node.set_schema()
1133            && let Some(nr) = ss.name_ref()
1134        {
1135            return Some(StatementFact::AlterView {
1136                name,
1137                action: crate::analysis::facts::AlterViewAction::SetSchema {
1138                    new_schema: Self::resolve_name_ref(&nr),
1139                },
1140            });
1141        }
1142
1143        // Check for column default modifications (SET DEFAULT / DROP DEFAULT)
1144        // Grammar: 'alter' 'column'? NameRef (('set' 'default' Expr) | ('drop' 'default'))
1145        if node.column_token().is_some()
1146            && node.default_token().is_some()
1147            && (node.set_token().is_some() || node.drop_token().is_some())
1148        {
1149            let col_name = node
1150                .name_ref()
1151                .map(|nr| Self::resolve_name_ref(&nr))
1152                .unwrap_or_default();
1153
1154            if node.drop_token().is_some() {
1155                return Some(StatementFact::AlterView {
1156                    name,
1157                    action: crate::analysis::facts::AlterViewAction::DropDefault {
1158                        column: col_name,
1159                    },
1160                });
1161            }
1162
1163            // SET DEFAULT
1164            if let Some(expr) = node.expr() {
1165                return Some(StatementFact::AlterView {
1166                    name,
1167                    action: crate::analysis::facts::AlterViewAction::SetDefault {
1168                        column: col_name,
1169                        default: Some(crate::analysis::expr_visitor::ExprVisitor::convert(expr)),
1170                    },
1171                });
1172            }
1173        }
1174
1175        // Check for RENAME COLUMN
1176        if node.rename_token().is_some() && node.column_token().is_some() {
1177            let from = node
1178                .name_ref()
1179                .map(|nr| {
1180                    Ident::new(
1181                        nr.text().to_string().trim_matches('"').to_string(),
1182                        nr.is_quoted(),
1183                    )
1184                })
1185                .unwrap_or_else(|| Ident::new("unknown".to_string(), false));
1186            let to = node
1187                .name()
1188                .map(|n| {
1189                    Ident::new(
1190                        n.text().to_string().trim_matches('"').to_string(),
1191                        n.is_quoted(),
1192                    )
1193                })
1194                .unwrap_or_else(|| Ident::new("unknown".to_string(), false));
1195            return Some(StatementFact::AlterView {
1196                name,
1197                action: crate::analysis::facts::AlterViewAction::RenameColumn { from, to },
1198            });
1199        }
1200
1201        // Check for SET OPTIONS
1202        if let Some(so) = node.set_options() {
1203            let options: Vec<String> = so
1204                .syntax()
1205                .descendants()
1206                .filter_map(ast::NameRef::cast)
1207                .map(|nr| Self::resolve_name_ref(&nr))
1208                .collect();
1209            return Some(StatementFact::AlterView {
1210                name,
1211                action: crate::analysis::facts::AlterViewAction::SetOptions { options },
1212            });
1213        }
1214
1215        // Check for RESET OPTIONS
1216        if let Some(ro) = node.reset_options() {
1217            let options: Vec<String> = ro
1218                .syntax()
1219                .descendants()
1220                .filter_map(ast::NameRef::cast)
1221                .map(|nr| Self::resolve_name_ref(&nr))
1222                .collect();
1223            return Some(StatementFact::AlterView {
1224                name,
1225                action: crate::analysis::facts::AlterViewAction::ResetOptions { options },
1226            });
1227        }
1228
1229        // Fallback: emit as opaque if we can't determine the action
1230        None
1231    }
1232
1233    fn extract_create_materialized_view(node: &CreateMaterializedView) -> Option<StatementFact> {
1234        let path = node.path()?;
1235        Some(StatementFact::CreateMaterializedView {
1236            name: Self::path_to_qualified_name(&path)?,
1237            depends_on: Self::extract_view_dependencies(node.syntax()),
1238        })
1239    }
1240
1241    fn extract_alter_materialized_view(node: &ast::AlterMaterializedView) -> Option<StatementFact> {
1242        let path = node.path()?;
1243        let new_name = node.action().find_map(|action| {
1244            if let squawk_syntax::ast::AlterMaterializedViewAction::RenameTo(rt) = action {
1245                rt.name().map(|n| {
1246                    Ident::new(
1247                        n.text().to_string().trim_matches('"').to_string(),
1248                        n.is_quoted(),
1249                    )
1250                })
1251            } else {
1252                None
1253            }
1254        });
1255        Some(StatementFact::AlterMaterializedView {
1256            name: Self::path_to_qualified_name(&path)?,
1257            new_name,
1258        })
1259    }
1260
1261    fn extract_refresh(node: &ast::Refresh) -> Option<StatementFact> {
1262        let path = node.path()?;
1263        Some(StatementFact::RefreshMaterializedView {
1264            name: Self::path_to_qualified_name(&path)?,
1265            concurrently: node.concurrently_token().is_some(),
1266        })
1267    }
1268
1269    fn extract_drop_view(node: &DropView) -> Option<StatementFact> {
1270        let path = node.paths().next()?;
1271        Some(StatementFact::DropView {
1272            name: Self::path_to_qualified_name(&path)?,
1273            if_exists: node.if_exists().is_some(),
1274            cascade: node.cascade_token().is_some(),
1275        })
1276    }
1277
1278    fn extract_drop_materialized_view(node: &DropMaterializedView) -> Option<StatementFact> {
1279        let names: Vec<QualifiedName> = node
1280            .paths()
1281            .filter_map(|p| Self::path_to_qualified_name(&p))
1282            .collect();
1283        if names.is_empty() {
1284            return None;
1285        }
1286        Some(StatementFact::DropMaterializedView {
1287            names,
1288            if_exists: node.if_exists().is_some(),
1289            cascade: node.cascade_token().is_some(),
1290        })
1291    }
1292
1293    fn extract_view_dependencies(syntax: &squawk_syntax::SyntaxNode) -> Vec<QualifiedName> {
1294        let mut depends_on = Vec::new();
1295        let keywords = [
1296            "SELECT",
1297            "FROM",
1298            "WHERE",
1299            "JOIN",
1300            "ON",
1301            "AND",
1302            "OR",
1303            "AS",
1304            "WITH",
1305            "GROUP",
1306            "BY",
1307            "HAVING",
1308            "LIMIT",
1309            "OFFSET",
1310            "ORDER",
1311            "ASC",
1312            "DESC",
1313            "IN",
1314            "NOT",
1315            "IS",
1316            "NULL",
1317            "UNION",
1318            "ALL",
1319            "EXCEPT",
1320            "INTERSECT",
1321            "TRUE",
1322            "FALSE",
1323        ];
1324
1325        let mut local_declarations = Vec::new();
1326        for name_node in syntax.descendants().filter_map(Name::cast) {
1327            local_declarations.push(name_node.text().to_string().trim_matches('"').to_string());
1328        }
1329
1330        for n in syntax.descendants().filter_map(NameRef::cast) {
1331            let text = n.text().to_string();
1332            let upper = text.to_uppercase();
1333            let is_quoted = n.is_quoted();
1334            let clean_text = text.trim_matches('"').to_string();
1335
1336            if !is_quoted && keywords.contains(&upper.as_str()) {
1337                continue;
1338            }
1339
1340            if local_declarations.contains(&clean_text) {
1341                continue;
1342            }
1343
1344            let field_expr = n.syntax().ancestors().skip(1).find_map(FieldExpr::cast);
1345
1346            let qname = if let Some(fe) = field_expr {
1347                let name_refs: Vec<NameRef> =
1348                    fe.syntax().children().filter_map(NameRef::cast).collect();
1349
1350                if name_refs.len() >= 2 {
1351                    let is_last = name_refs
1352                        .last()
1353                        .is_some_and(|last| last.syntax().text_range() == n.syntax().text_range());
1354
1355                    if is_last {
1356                        let schema_text = name_refs[0].text().to_string();
1357                        QualifiedName::new(
1358                            Some(Ident::new(
1359                                schema_text.trim_matches('"').to_string(),
1360                                name_refs[0].is_quoted(),
1361                            )),
1362                            Ident::new(clean_text.clone(), is_quoted),
1363                        )
1364                    } else {
1365                        continue;
1366                    }
1367                } else {
1368                    QualifiedName::new(None, Ident::new(clean_text.clone(), is_quoted))
1369                }
1370            } else {
1371                QualifiedName::new(None, Ident::new(clean_text.clone(), is_quoted))
1372            };
1373
1374            if !depends_on.contains(&qname) {
1375                depends_on.push(qname);
1376            }
1377        }
1378        depends_on
1379    }
1380
1381    fn extract_create_sequence(node: &CreateSequence) -> Option<StatementFact> {
1382        let path = node.path()?;
1383        let name = Self::path_to_qualified_name(&path)?;
1384        Some(StatementFact::CreateSequence {
1385            name,
1386            if_not_exists: node.if_not_exists().is_some(),
1387            owned_by: Self::extract_owned_by(node.syntax()),
1388        })
1389    }
1390
1391    fn extract_alter_sequence(node: &AlterSequence) -> Option<StatementFact> {
1392        let path = node.path()?;
1393        let name = Self::path_to_qualified_name(&path)?;
1394        Some(StatementFact::AlterSequence {
1395            name,
1396            owned_by: Self::extract_owned_by(node.syntax()),
1397        })
1398    }
1399
1400    fn extract_drop_sequence(node: &DropSequence) -> Option<StatementFact> {
1401        let names: Vec<QualifiedName> = node
1402            .paths()
1403            .filter_map(|p| Self::path_to_qualified_name(&p))
1404            .collect();
1405
1406        Some(StatementFact::DropSequence {
1407            names,
1408            if_exists: node.if_exists().is_some(),
1409            cascade: node.cascade_token().is_some(),
1410        })
1411    }
1412
1413    fn extract_owned_by(node: &squawk_syntax::SyntaxNode) -> Option<(QualifiedName, String)> {
1414        for opt in node.descendants().filter_map(ast::SequenceOption::cast) {
1415            if opt.owned_token().is_some() {
1416                let path = opt.path()?;
1417
1418                let segments: Vec<PathSegment> = path
1419                    .syntax()
1420                    .descendants()
1421                    .filter_map(PathSegment::cast)
1422                    .collect();
1423                if segments.len() >= 2 {
1424                    let col_ident = Self::segment_ident(segments.last().unwrap().clone())?;
1425                    let col_name = col_ident.resolve();
1426
1427                    let table_len = segments.len() - 1;
1428                    let table_name = if table_len == 1 {
1429                        QualifiedName::new(None, Self::segment_ident(segments[0].clone())?)
1430                    } else {
1431                        QualifiedName::new(
1432                            Some(Self::segment_ident(segments[table_len - 2].clone())?),
1433                            Self::segment_ident(segments[table_len - 1].clone())?,
1434                        )
1435                    };
1436                    return Some((table_name, col_name));
1437                }
1438            }
1439        }
1440        None
1441    }
1442
1443    fn extract_create_domain(node: &CreateDomain) -> Option<StatementFact> {
1444        let path = node.path()?;
1445        let base_type = node
1446            .ty()
1447            .map(|t| t.syntax().text().to_string())
1448            .unwrap_or_else(|| "<domain>".to_string());
1449        Some(StatementFact::CreateDomain {
1450            name: Self::path_to_qualified_name(&path)?,
1451            base_type,
1452        })
1453    }
1454
1455    fn extract_alter_domain(node: &AlterDomain) -> Option<StatementFact> {
1456        let path = node.path()?;
1457        let action = node.action().map(|a| match a {
1458            squawk_syntax::ast::AlterDomainAction::AddConstraint(_) => {
1459                crate::analysis::facts::AlterDomainActionFact::AddConstraint
1460            }
1461            squawk_syntax::ast::AlterDomainAction::DropConstraint(_) => {
1462                crate::analysis::facts::AlterDomainActionFact::DropConstraint
1463            }
1464            squawk_syntax::ast::AlterDomainAction::DropDefault(_) => {
1465                crate::analysis::facts::AlterDomainActionFact::DropDefault
1466            }
1467            squawk_syntax::ast::AlterDomainAction::DropNotNull(_) => {
1468                crate::analysis::facts::AlterDomainActionFact::DropNotNull
1469            }
1470            squawk_syntax::ast::AlterDomainAction::OwnerTo(_) => {
1471                crate::analysis::facts::AlterDomainActionFact::OwnerChange
1472            }
1473            squawk_syntax::ast::AlterDomainAction::RenameConstraint(_) => {
1474                crate::analysis::facts::AlterDomainActionFact::RenameConstraint
1475            }
1476            squawk_syntax::ast::AlterDomainAction::RenameTo(_) => {
1477                crate::analysis::facts::AlterDomainActionFact::RenameTo
1478            }
1479            squawk_syntax::ast::AlterDomainAction::SetDefault(_) => {
1480                crate::analysis::facts::AlterDomainActionFact::SetDefault
1481            }
1482            squawk_syntax::ast::AlterDomainAction::SetNotNull(_) => {
1483                crate::analysis::facts::AlterDomainActionFact::SetNotNull
1484            }
1485            squawk_syntax::ast::AlterDomainAction::SetSchema(_) => {
1486                crate::analysis::facts::AlterDomainActionFact::SetSchema
1487            }
1488            squawk_syntax::ast::AlterDomainAction::ValidateConstraint(_) => {
1489                crate::analysis::facts::AlterDomainActionFact::ValidateConstraint
1490            }
1491        });
1492        Some(StatementFact::AlterDomain {
1493            name: Self::path_to_qualified_name(&path)?,
1494            action,
1495        })
1496    }
1497
1498    fn extract_drop_type(node: &DropType) -> Option<StatementFact> {
1499        println!("EXTRACT DROP TYPE!");
1500        let names: Vec<QualifiedName> = node
1501            .paths()
1502            .filter_map(|p| Self::path_to_qualified_name(&p))
1503            .collect();
1504
1505        Some(StatementFact::DropType {
1506            names,
1507            if_exists: node.if_exists().is_some(),
1508            cascade: node.cascade_token().is_some(),
1509        })
1510    }
1511    fn extract_drop_domain(node: &DropDomain) -> Option<StatementFact> {
1512        let names: Vec<QualifiedName> = node
1513            .paths()
1514            .filter_map(|p| Self::path_to_qualified_name(&p))
1515            .collect();
1516
1517        Some(StatementFact::DropDomain {
1518            names,
1519            if_exists: node.if_exists().is_some(),
1520            cascade: node.cascade_token().is_some(),
1521        })
1522    }
1523
1524    fn extract_create_type(node: &CreateType) -> Option<StatementFact> {
1525        let path = node.path()?;
1526        let name = Self::path_to_qualified_name(&path)?;
1527
1528        let kind = if node.enum_token().is_some() {
1529            TypeCreationKind::Enum
1530        } else if node.range_token().is_some() {
1531            TypeCreationKind::Range
1532        } else if node.attribute_list().is_some() {
1533            TypeCreationKind::Composite
1534        } else {
1535            TypeCreationKind::Base
1536        };
1537
1538        Some(StatementFact::CreateType(CreateTypeFact { name, kind }))
1539    }
1540
1541    fn extract_alter_type(node: &AlterType) -> Option<StatementFact> {
1542        let path = node.path()?;
1543        let name = Self::path_to_qualified_name(&path)?;
1544        let mut actions = Vec::new();
1545
1546        if let Some(av) = node.add_value()
1547            && let Some(lit) = av.literal()
1548        {
1549            actions.push(AlterTypeActionFact::AddValue {
1550                new_value: lit
1551                    .syntax()
1552                    .text()
1553                    .to_string()
1554                    .trim_matches('\'')
1555                    .to_string(),
1556            });
1557        }
1558
1559        Some(StatementFact::AlterType(AlterTypeFact { name, actions }))
1560    }
1561
1562    fn extract_create_policy(node: &CreatePolicy) -> Option<StatementFact> {
1563        let name = node.name().map(Self::resolve_name)?;
1564        let path = node.syntax().descendants().find_map(Path::cast)?;
1565        let table = Self::path_to_qualified_name(&path)?;
1566
1567        let permissive = if let Some(as_type) = node.as_policy_type() {
1568            as_type
1569                .ident_token()
1570                .map(|t| t.text().to_lowercase())
1571                .map(|t| t == "permissive")
1572                .unwrap_or(true)
1573        } else {
1574            true
1575        };
1576
1577        let command = if node.all_token().is_some() {
1578            crate::analysis::facts::PolicyCommand::All
1579        } else if node.select_token().is_some() {
1580            crate::analysis::facts::PolicyCommand::Select
1581        } else if node.insert_token().is_some() {
1582            crate::analysis::facts::PolicyCommand::Insert
1583        } else if node.update_token().is_some() {
1584            crate::analysis::facts::PolicyCommand::Update
1585        } else if node.delete_token().is_some() {
1586            crate::analysis::facts::PolicyCommand::Delete
1587        } else {
1588            crate::analysis::facts::PolicyCommand::All
1589        };
1590
1591        Some(StatementFact::CreatePolicy {
1592            name,
1593            table,
1594            permissive,
1595            command,
1596        })
1597    }
1598
1599    fn extract_drop_policy(node: &DropPolicy) -> Option<StatementFact> {
1600        let path = node.syntax().descendants().find_map(Path::cast)?;
1601        let table = Self::path_to_qualified_name(&path)?;
1602        let name = Self::resolve_name_ref(&node.name_ref()?);
1603        Some(StatementFact::DropPolicy {
1604            name,
1605            table,
1606            if_exists: node.if_exists().is_some(),
1607        })
1608    }
1609
1610    fn extract_create_trigger(node: &CreateTrigger) -> Option<StatementFact> {
1611        let name = node.name().map(Self::resolve_name)?;
1612        // The ON table path is the LAST path descendant (after the trigger events clause)
1613        let table = node
1614            .on_table()
1615            .and_then(|on| on.path())
1616            .and_then(|p| Self::path_to_qualified_name(&p))?;
1617        let function = node.call_expr().and_then(|call| {
1618            let node_ref = call.syntax();
1619            let fn_name = node_ref.descendants().find_map(Name::cast).map(|n| {
1620                let ident = Ident::new(
1621                    n.text().to_string().trim_matches('"').to_string(),
1622                    n.is_quoted(),
1623                );
1624                QualifiedName::new(None, ident)
1625            });
1626            if fn_name.is_some() {
1627                return fn_name;
1628            }
1629            node_ref.descendants().find_map(NameRef::cast).map(|n| {
1630                let ident = Ident::new(
1631                    n.text().to_string().trim_matches('"').to_string(),
1632                    n.is_quoted(),
1633                );
1634                QualifiedName::new(None, ident)
1635            })
1636        });
1637        Some(StatementFact::CreateTrigger {
1638            name,
1639            table,
1640            function,
1641        })
1642    }
1643
1644    fn extract_drop_trigger(node: &DropTrigger) -> Option<StatementFact> {
1645        let trigger_path = node.path()?;
1646        let trigger_name = Self::path_to_qualified_name(&trigger_path)?.name.resolve();
1647        let table_path = node.on_table()?.path()?;
1648        let table = Self::path_to_qualified_name(&table_path)?;
1649        Some(StatementFact::DropTrigger {
1650            name: trigger_name,
1651            table,
1652            if_exists: node.if_exists().is_some(),
1653        })
1654    }
1655
1656    fn extract_param(param: &squawk_syntax::ast::Param) -> crate::analysis::facts::ParamFact {
1657        crate::analysis::facts::ParamFact {
1658            mode: match param.mode() {
1659                Some(ast::ParamMode::ParamVariadic(_)) => {
1660                    crate::analysis::facts::ParamModeFact::Variadic
1661                }
1662                Some(ast::ParamMode::ParamInOut(_)) => crate::analysis::facts::ParamModeFact::InOut,
1663                Some(ast::ParamMode::ParamOut(_)) => crate::analysis::facts::ParamModeFact::Out,
1664                _ => crate::analysis::facts::ParamModeFact::In,
1665            },
1666            name: param.name().map(Self::resolve_name),
1667            ty: param
1668                .ty()
1669                .map(|t| t.syntax().text().to_string())
1670                .unwrap_or_else(|| "unknown".into()),
1671            default: param.param_default().and_then(|pd| {
1672                pd.expr()
1673                    .map(crate::analysis::expr_visitor::ExprVisitor::convert)
1674            }),
1675        }
1676    }
1677
1678    fn extract_ret_type(ret: &squawk_syntax::ast::RetType) -> crate::analysis::facts::RetTypeFact {
1679        if let Some(tal) = ret.table_arg_list() {
1680            let cols = tal
1681                .args()
1682                .filter_map(|arg| match arg {
1683                    TableArg::Column(col) => Self::extract_column_fact(&col),
1684                    _ => None,
1685                })
1686                .collect();
1687            crate::analysis::facts::RetTypeFact::Table(cols)
1688        } else {
1689            let ty = ret
1690                .ty()
1691                .map(|t| t.syntax().text().to_string())
1692                .unwrap_or_else(|| "unknown".into());
1693            crate::analysis::facts::RetTypeFact::Scalar(ty)
1694        }
1695    }
1696
1697    fn extract_func_option(
1698        opt: &squawk_syntax::ast::FuncOption,
1699    ) -> crate::analysis::facts::FuncOptionFact {
1700        match opt {
1701            ast::FuncOption::LanguageFuncOption(f) => {
1702                crate::analysis::facts::FuncOptionFact::Language(
1703                    f.name_ref()
1704                        .map(|n| n.text().to_string())
1705                        .unwrap_or_default(),
1706                )
1707            }
1708            ast::FuncOption::VolatilityFuncOption(f) => {
1709                let vol = if f.immutable_token().is_some() {
1710                    crate::analysis::facts::VolatilityKind::Immutable
1711                } else if f.stable_token().is_some() {
1712                    crate::analysis::facts::VolatilityKind::Stable
1713                } else {
1714                    crate::analysis::facts::VolatilityKind::Volatile
1715                };
1716                crate::analysis::facts::FuncOptionFact::Volatility(vol)
1717            }
1718            ast::FuncOption::SecurityFuncOption(f) => {
1719                let sec = if f.invoker_token().is_some() {
1720                    crate::analysis::facts::SecurityKind::Invoker
1721                } else {
1722                    crate::analysis::facts::SecurityKind::Definer
1723                };
1724                crate::analysis::facts::FuncOptionFact::Security(sec)
1725            }
1726            ast::FuncOption::StrictFuncOption(f) => {
1727                let strct = if f.called_token().is_some() {
1728                    crate::analysis::facts::StrictKind::CalledOnNull
1729                } else if f.returns_token().is_some() {
1730                    crate::analysis::facts::StrictKind::ReturnsNullOnNull
1731                } else {
1732                    crate::analysis::facts::StrictKind::Strict
1733                };
1734                crate::analysis::facts::FuncOptionFact::Strict(strct)
1735            }
1736            ast::FuncOption::LeakproofFuncOption(f) => {
1737                let is_leakproof = f.leakproof_token().is_some();
1738                crate::analysis::facts::FuncOptionFact::Leakproof(is_leakproof)
1739            }
1740            ast::FuncOption::ParallelFuncOption(f) => {
1741                crate::analysis::facts::FuncOptionFact::Parallel(
1742                    f.syntax()
1743                        .descendants()
1744                        .find_map(ast::NameRef::cast)
1745                        .map(|n| n.text())
1746                        .unwrap_or_default(),
1747                )
1748            }
1749            ast::FuncOption::CostFuncOption(_) => crate::analysis::facts::FuncOptionFact::Cost,
1750            ast::FuncOption::RowsFuncOption(_) => crate::analysis::facts::FuncOptionFact::Rows,
1751            ast::FuncOption::ResetFuncOption(f) => crate::analysis::facts::FuncOptionFact::Reset(
1752                f.name_ref()
1753                    .map(|n| n.text().to_string())
1754                    .unwrap_or_default(),
1755            ),
1756            ast::FuncOption::AsFuncOption(f) => {
1757                let lit = f
1758                    .syntax()
1759                    .descendants()
1760                    .find_map(ast::Literal::cast)
1761                    .map(|l| l.syntax().text().to_string().trim_matches('\'').to_string());
1762                crate::analysis::facts::FuncOptionFact::As {
1763                    definition: lit,
1764                    obj_file: None,
1765                    link_symbol: None,
1766                }
1767            }
1768            ast::FuncOption::TransformFuncOption(_) => {
1769                crate::analysis::facts::FuncOptionFact::Transform
1770            }
1771            ast::FuncOption::WindowFuncOption(_) => crate::analysis::facts::FuncOptionFact::Window,
1772            ast::FuncOption::SupportFuncOption(_) => {
1773                crate::analysis::facts::FuncOptionFact::Support
1774            }
1775            _ => crate::analysis::facts::FuncOptionFact::Unknown,
1776        }
1777    }
1778
1779    fn extract_create_function(node: &squawk_syntax::ast::CreateFunction) -> Option<StatementFact> {
1780        let path = node.path()?;
1781        let name = Self::path_to_qualified_name(&path)?;
1782        let or_replace = node.or_replace().is_some();
1783        let params = node
1784            .param_list()
1785            .map(|pl| pl.params().map(|p| Self::extract_param(&p)).collect())
1786            .unwrap_or_default();
1787        let return_type = node.ret_type().map(|r| Self::extract_ret_type(&r));
1788        let options = node
1789            .option_list()
1790            .map(|ol| {
1791                ol.options()
1792                    .map(|o| Self::extract_func_option(&o))
1793                    .collect()
1794            })
1795            .unwrap_or_default();
1796
1797        Some(StatementFact::CreateFunction(
1798            crate::analysis::facts::CreateFunctionFact {
1799                name,
1800                or_replace,
1801                params,
1802                return_type,
1803                options,
1804            },
1805        ))
1806    }
1807
1808    fn extract_alter_function(node: &ast::AlterFunction) -> Option<StatementFact> {
1809        let path = node.function_sig().and_then(|sig| sig.path())?;
1810        let name = Self::path_to_qualified_name(&path)?;
1811        let params = node
1812            .function_sig()
1813            .and_then(|sig| sig.param_list())
1814            .map(|pl| pl.params().map(|p| p.syntax().text().to_string()).collect())
1815            .unwrap_or_default();
1816        let action = if let Some(rt) = node.rename_to() {
1817            crate::analysis::facts::AlterFunctionAction::Rename {
1818                from: name.name.resolve(),
1819                to: rt.name().map(|n| n.text()).unwrap_or_default(),
1820            }
1821        } else if let Some(ot) = node.owner_to() {
1822            crate::analysis::facts::AlterFunctionAction::OwnerChange(Self::extract_role(
1823                &ot.role_ref().unwrap(),
1824            ))
1825        } else if let Some(ss) = node.set_schema() {
1826            crate::analysis::facts::AlterFunctionAction::SchemaChange {
1827                new_schema: ss.name_ref().map(|n| n.text()).unwrap_or_default(),
1828            }
1829        } else if let Some(de) = node.depends_on_extension() {
1830            crate::analysis::facts::AlterFunctionAction::DependsOnExtension {
1831                extension: de
1832                    .name_ref()
1833                    .map(|n| n.text().to_string())
1834                    .unwrap_or_default(),
1835            }
1836        } else if let Some(nde) = node.no_depends_on_extension() {
1837            crate::analysis::facts::AlterFunctionAction::NoDependsOnExtension {
1838                extension: nde
1839                    .name_ref()
1840                    .map(|n| n.text().to_string())
1841                    .unwrap_or_default(),
1842            }
1843        } else {
1844            let ol = node.func_option_list()?;
1845            crate::analysis::facts::AlterFunctionAction::OptionsChange(
1846                ol.options()
1847                    .map(|o| Self::extract_func_option(&o))
1848                    .collect(),
1849            )
1850        };
1851
1852        Some(StatementFact::AlterFunction(
1853            crate::analysis::facts::AlterFunctionFact {
1854                name,
1855                params,
1856                action,
1857            },
1858        ))
1859    }
1860
1861    fn extract_drop_function(node: &squawk_syntax::ast::DropFunction) -> Option<StatementFact> {
1862        let sigs = node
1863            .function_sig_list()
1864            .map(|sl| {
1865                sl.function_sigs()
1866                    .filter_map(|sig| {
1867                        let path = sig.path()?;
1868                        Some(crate::analysis::facts::FunctionSigFact {
1869                            name: Self::path_to_qualified_name(&path).unwrap_or_else(|| {
1870                                QualifiedName::new(None, Ident::new("unknown".to_string(), false))
1871                            }),
1872                            params: sig
1873                                .param_list()
1874                                .map(|pl| {
1875                                    pl.params()
1876                                        .map(|p| {
1877                                            p.ty()
1878                                                .map(|t| t.syntax().text().to_string())
1879                                                .unwrap_or_else(|| "unknown".into())
1880                                        })
1881                                        .collect()
1882                                })
1883                                .unwrap_or_default(),
1884                        })
1885                    })
1886                    .collect::<Vec<_>>()
1887            })
1888            .unwrap_or_default();
1889
1890        Some(StatementFact::DropFunction(
1891            crate::analysis::facts::DropFunctionFact {
1892                signatures: sigs,
1893                if_exists: node.if_exists().is_some(),
1894                cascade: node.cascade_token().is_some(),
1895            },
1896        ))
1897    }
1898
1899    fn extract_create_procedure(
1900        node: &squawk_syntax::ast::CreateProcedure,
1901    ) -> Option<StatementFact> {
1902        let path = node.path()?;
1903        let name = Self::path_to_qualified_name(&path)?;
1904        let or_replace = node.or_replace().is_some();
1905        let params = node
1906            .param_list()
1907            .map(|pl| pl.params().map(|p| Self::extract_param(&p)).collect())
1908            .unwrap_or_default();
1909        let options = node
1910            .option_list()
1911            .map(|ol| {
1912                ol.options()
1913                    .map(|o| Self::extract_func_option(&o))
1914                    .collect()
1915            })
1916            .unwrap_or_default();
1917
1918        Some(StatementFact::CreateProcedure(
1919            crate::analysis::facts::CreateProcedureFact {
1920                name,
1921                or_replace,
1922                params,
1923                options,
1924            },
1925        ))
1926    }
1927
1928    fn extract_alter_procedure(node: &squawk_syntax::ast::AlterProcedure) -> Option<StatementFact> {
1929        let sig = node.function_sig()?;
1930        let path = sig.path()?;
1931        let name = Self::path_to_qualified_name(&path)?;
1932        let params = sig
1933            .param_list()
1934            .map(|pl| pl.params().map(|p| p.syntax().text().to_string()).collect())
1935            .unwrap_or_default();
1936
1937        let action = if let Some(rt) = node.rename_to() {
1938            crate::analysis::facts::AlterFunctionAction::Rename {
1939                from: name.name.resolve(),
1940                to: rt.name().map(|n| n.text()).unwrap_or_default(),
1941            }
1942        } else if let Some(ot) = node.owner_to() {
1943            crate::analysis::facts::AlterFunctionAction::OwnerChange(Self::extract_role(
1944                &ot.role_ref().unwrap(),
1945            ))
1946        } else if let Some(ss) = node.set_schema() {
1947            crate::analysis::facts::AlterFunctionAction::SchemaChange {
1948                new_schema: ss.name_ref().map(|n| n.text()).unwrap_or_default(),
1949            }
1950        } else if let Some(de) = node.depends_on_extension() {
1951            crate::analysis::facts::AlterFunctionAction::DependsOnExtension {
1952                extension: de
1953                    .name_ref()
1954                    .map(|n| n.text().to_string())
1955                    .unwrap_or_default(),
1956            }
1957        } else if let Some(nde) = node.no_depends_on_extension() {
1958            crate::analysis::facts::AlterFunctionAction::NoDependsOnExtension {
1959                extension: nde
1960                    .name_ref()
1961                    .map(|n| n.text().to_string())
1962                    .unwrap_or_default(),
1963            }
1964        } else {
1965            let ol = node.func_option_list()?;
1966            crate::analysis::facts::AlterFunctionAction::OptionsChange(
1967                ol.options()
1968                    .map(|o| Self::extract_func_option(&o))
1969                    .collect(),
1970            )
1971        };
1972
1973        Some(StatementFact::AlterProcedure(
1974            crate::analysis::facts::AlterProcedureFact {
1975                name,
1976                params,
1977                action,
1978            },
1979        ))
1980    }
1981
1982    fn extract_drop_procedure(node: &squawk_syntax::ast::DropProcedure) -> Option<StatementFact> {
1983        let sigs = node
1984            .function_sig_list()
1985            .map(|sl| {
1986                sl.function_sigs()
1987                    .filter_map(|sig| {
1988                        let path = sig.path()?;
1989                        Some(crate::analysis::facts::FunctionSigFact {
1990                            name: Self::path_to_qualified_name(&path).unwrap_or_else(|| {
1991                                QualifiedName::new(None, Ident::new("unknown".to_string(), false))
1992                            }),
1993                            params: sig
1994                                .param_list()
1995                                .map(|pl| {
1996                                    pl.params()
1997                                        .map(|p| {
1998                                            p.ty()
1999                                                .map(|t| t.syntax().text().to_string())
2000                                                .unwrap_or_else(|| "unknown".into())
2001                                        })
2002                                        .collect()
2003                                })
2004                                .unwrap_or_default(),
2005                        })
2006                    })
2007                    .collect::<Vec<_>>()
2008            })
2009            .unwrap_or_default();
2010
2011        Some(StatementFact::DropProcedure(
2012            crate::analysis::facts::DropProcedureFact {
2013                signatures: sigs,
2014                if_exists: node.if_exists().is_some(),
2015                cascade: node.cascade_token().is_some(),
2016            },
2017        ))
2018    }
2019
2020    fn extract_create_publication(
2021        node: &squawk_syntax::ast::CreatePublication,
2022    ) -> Option<StatementFact> {
2023        let name = node.name().map(Self::resolve_name).unwrap_or_default();
2024        let scope = if node.all_token().is_some() && node.tables_token().is_some() {
2025            crate::analysis::facts::PublicationScope::AllTables {
2026                except: node
2027                    .except_table_clause()
2028                    .map(|c| {
2029                        c.syntax()
2030                            .descendants()
2031                            .filter_map(NameRef::cast)
2032                            .map(|nr| Self::resolve_name_ref(&nr))
2033                            .collect()
2034                    })
2035                    .unwrap_or_default(),
2036            }
2037        } else {
2038            let objects = node
2039                .publication_objects()
2040                .map(|obj| {
2041                    if let Some(path) = obj.path() {
2042                        crate::analysis::facts::PublicationObjectFact::Table {
2043                            name: Self::path_to_qualified_name(&path).unwrap_or_else(|| {
2044                                QualifiedName::new(None, Ident::new("unknown".to_string(), false))
2045                            }),
2046                            only: obj.only_token().is_some(),
2047                            include_partitions: obj.star_token().is_some(),
2048                            columns: obj.column_list().map(|cl| {
2049                                cl.columns()
2050                                    .filter_map(|c| c.name().map(Self::resolve_name))
2051                                    .collect()
2052                            }),
2053                            row_filter: obj.where_condition_clause().and_then(|w| {
2054                                w.expr()
2055                                    .map(crate::analysis::expr_visitor::ExprVisitor::convert)
2056                            }),
2057                        }
2058                    } else if obj.in_token().is_some() {
2059                        crate::analysis::facts::PublicationObjectFact::SchemaTables {
2060                            schema: obj
2061                                .name_ref()
2062                                .map(|n| n.text().to_string())
2063                                .or_else(|| {
2064                                    obj.current_schema_token()
2065                                        .map(|_| "CURRENT_SCHEMA".to_string())
2066                                })
2067                                .unwrap_or_default(),
2068                            row_filter: obj.where_condition_clause().and_then(|w| {
2069                                w.expr()
2070                                    .map(crate::analysis::expr_visitor::ExprVisitor::convert)
2071                            }),
2072                        }
2073                    } else if obj.current_schema_token().is_some() {
2074                        crate::analysis::facts::PublicationObjectFact::CurrentSchemaShorthand
2075                    } else {
2076                        crate::analysis::facts::PublicationObjectFact::Unknown
2077                    }
2078                })
2079                .collect();
2080            crate::analysis::facts::PublicationScope::Explicit(objects)
2081        };
2082        let params = node
2083            .with_params()
2084            .map(|wp| {
2085                wp.attribute_list()
2086                    .map(|al| {
2087                        al.attribute_options()
2088                            .map(|p| crate::analysis::facts::AttributeFact {
2089                                name: p.name().map(|n| n.text().to_string()).unwrap_or_default(),
2090                                value: p
2091                                    .syntax()
2092                                    .descendants()
2093                                    .find_map(ast::Literal::cast)
2094                                    .map(|l| l.syntax().text().to_string())
2095                                    .unwrap_or_default(),
2096                            })
2097                            .collect()
2098                    })
2099                    .unwrap_or_default()
2100            })
2101            .unwrap_or_default();
2102
2103        Some(StatementFact::CreatePublication(
2104            crate::analysis::facts::CreatePublicationFact {
2105                name,
2106                scope,
2107                params,
2108            },
2109        ))
2110    }
2111
2112    fn extract_alter_publication(
2113        node: &squawk_syntax::ast::AlterPublication,
2114    ) -> Option<StatementFact> {
2115        let name = node
2116            .name_ref()
2117            .map(|nr| Self::resolve_name_ref(&nr))
2118            .unwrap_or_default();
2119        Some(StatementFact::AlterPublication(
2120            crate::analysis::facts::AlterPublicationFact { name },
2121        ))
2122    }
2123
2124    fn extract_drop_publication(
2125        node: &squawk_syntax::ast::DropPublication,
2126    ) -> Option<StatementFact> {
2127        let names = node
2128            .name_refs()
2129            .map(|nr| Self::resolve_name_ref(&nr))
2130            .collect();
2131        Some(StatementFact::DropPublication(
2132            crate::analysis::facts::DropPublicationFact {
2133                names,
2134                if_exists: node.if_exists().is_some(),
2135                cascade: node.cascade_token().is_some(),
2136            },
2137        ))
2138    }
2139
2140    fn extract_create_subscription(
2141        node: &squawk_syntax::ast::CreateSubscription,
2142    ) -> Option<StatementFact> {
2143        let name = node.name().map(Self::resolve_name);
2144        let connection = if node.server_token().is_some() {
2145            crate::analysis::facts::ConnectionTarget::Server(node.name_ref().map(|n| n.text()))
2146        } else {
2147            crate::analysis::facts::ConnectionTarget::Literal(
2148                node.literal()
2149                    .map(|l| l.syntax().text().to_string().trim_matches('\'').to_string()),
2150            )
2151        };
2152        let publications = node
2153            .name_refs()
2154            .map(|nr| Self::resolve_name_ref(&nr))
2155            .collect();
2156        let params = node.with_params().map(|wp| {
2157            wp.attribute_list()
2158                .map(|al| {
2159                    al.attribute_options()
2160                        .map(|p| crate::analysis::facts::AttributeFact {
2161                            name: p.name().map(|n| n.text().to_string()).unwrap_or_default(),
2162                            value: p
2163                                .syntax()
2164                                .descendants()
2165                                .find_map(ast::Literal::cast)
2166                                .map(|l| l.syntax().text().to_string())
2167                                .unwrap_or_default(),
2168                        })
2169                        .collect()
2170                })
2171                .unwrap_or_default()
2172        });
2173
2174        Some(StatementFact::CreateSubscription(
2175            crate::analysis::facts::CreateSubscriptionFact {
2176                name,
2177                connection,
2178                publications,
2179                params,
2180            },
2181        ))
2182    }
2183
2184    fn extract_alter_subscription(
2185        node: &squawk_syntax::ast::AlterSubscription,
2186    ) -> Option<StatementFact> {
2187        let name = node
2188            .name_ref()
2189            .map(|nr| Self::resolve_name_ref(&nr))
2190            .unwrap_or_default();
2191        Some(StatementFact::AlterSubscription(
2192            crate::analysis::facts::AlterSubscriptionFact { name },
2193        ))
2194    }
2195
2196    fn extract_drop_subscription(
2197        node: &squawk_syntax::ast::DropSubscription,
2198    ) -> Option<StatementFact> {
2199        let name = Self::resolve_name_ref(&node.name_ref()?);
2200        Some(StatementFact::DropSubscription(
2201            crate::analysis::facts::DropSubscriptionFact {
2202                name,
2203                if_exists: node.if_exists().is_some(),
2204            },
2205        ))
2206    }
2207
2208    fn extract_role(role_ref: &squawk_syntax::ast::RoleRef) -> crate::analysis::facts::RoleFact {
2209        if let Some(name) = role_ref.name_ref() {
2210            crate::analysis::facts::RoleFact::Named {
2211                name: Self::resolve_name_ref(&name),
2212                via_legacy_group_syntax: role_ref.group_token().is_some(),
2213            }
2214        } else if role_ref.current_role_token().is_some() {
2215            crate::analysis::facts::RoleFact::CurrentRole
2216        } else if role_ref.current_user_token().is_some() {
2217            crate::analysis::facts::RoleFact::CurrentUser
2218        } else if role_ref.session_user_token().is_some() {
2219            crate::analysis::facts::RoleFact::SessionUser
2220        } else {
2221            crate::analysis::facts::RoleFact::Unknown
2222        }
2223    }
2224
2225    fn extract_create_role(node: &squawk_syntax::ast::CreateRole) -> Option<StatementFact> {
2226        let name = Self::resolve_name(node.name()?);
2227        let inherits = node
2228            .role_option_list()
2229            .map(|ol| ol.role_options().any(|o| o.inherit_token().is_some()))
2230            .unwrap_or(false);
2231        Some(StatementFact::CreateRole(
2232            crate::analysis::facts::CreateRoleFact { name, inherits },
2233        ))
2234    }
2235
2236    fn extract_alter_role(node: &squawk_syntax::ast::AlterRole) -> Option<StatementFact> {
2237        let name = Self::extract_role(&node.role_ref()?);
2238        let inherits = node.role_option_list().and_then(|ol| {
2239            let mut found = None;
2240            for o in ol.role_options() {
2241                if o.inherit_token().is_some() {
2242                    found = Some(true);
2243                }
2244            }
2245            found
2246        });
2247        Some(StatementFact::AlterRole(
2248            crate::analysis::facts::AlterRoleFact { name, inherits },
2249        ))
2250    }
2251
2252    fn extract_drop_role(node: &squawk_syntax::ast::DropRole) -> Option<StatementFact> {
2253        let names = node
2254            .name_refs()
2255            .map(|nr| Self::resolve_name_ref(&nr))
2256            .collect();
2257        Some(StatementFact::DropRole(
2258            crate::analysis::facts::DropRoleFact {
2259                names,
2260                if_exists: node.if_exists().is_some(),
2261            },
2262        ))
2263    }
2264
2265    fn extract_privilege_from_revoke_command(
2266        cmd: &RevokeCommand,
2267    ) -> crate::analysis::facts::PrivilegeFact {
2268        if let Some(role_ref) = cmd.role_ref() {
2269            crate::analysis::facts::PrivilegeFact::RoleMembership(Self::resolve_name_ref(
2270                &role_ref.name_ref().unwrap(),
2271            ))
2272        } else if cmd.select_token().is_some() {
2273            crate::analysis::facts::PrivilegeFact::Select
2274        } else if cmd.insert_token().is_some() {
2275            crate::analysis::facts::PrivilegeFact::Insert
2276        } else if cmd.update_token().is_some() {
2277            crate::analysis::facts::PrivilegeFact::Update
2278        } else if cmd.delete_token().is_some() {
2279            crate::analysis::facts::PrivilegeFact::Delete
2280        } else if cmd.truncate_token().is_some() {
2281            crate::analysis::facts::PrivilegeFact::Truncate
2282        } else if cmd.references_token().is_some() {
2283            crate::analysis::facts::PrivilegeFact::References
2284        } else if cmd.trigger_token().is_some() {
2285            crate::analysis::facts::PrivilegeFact::Trigger
2286        } else if cmd.execute_token().is_some() {
2287            crate::analysis::facts::PrivilegeFact::Execute
2288        } else if cmd.create_token().is_some() {
2289            crate::analysis::facts::PrivilegeFact::Create
2290        } else if cmd.temp_token().is_some() || cmd.temporary_token().is_some() {
2291            crate::analysis::facts::PrivilegeFact::Temporary
2292        } else if cmd.alter_token().is_some() && cmd.system_token().is_some() {
2293            crate::analysis::facts::PrivilegeFact::AlterSystem
2294        } else if cmd.all_token().is_some() {
2295            crate::analysis::facts::PrivilegeFact::All
2296        } else if let Some(ident) = cmd.syntax().descendants().find_map(ast::Name::cast) {
2297            crate::analysis::facts::PrivilegeFact::Named(ident.text().to_string())
2298        } else {
2299            crate::analysis::facts::PrivilegeFact::Unknown
2300        }
2301    }
2302
2303    fn extract_grant_target_from_privilege_objects(
2304        po: Option<squawk_syntax::ast::PrivilegeObjects>,
2305        syntax: &squawk_syntax::SyntaxNode,
2306    ) -> Option<crate::analysis::facts::GrantTarget> {
2307        if let Some(po) = po {
2308            if po.table_token().is_some() || po.paths().next().is_some() {
2309                let paths: Vec<_> = po
2310                    .paths()
2311                    .filter_map(|p| Self::path_to_qualified_name(&p))
2312                    .collect();
2313                if paths.is_empty() {
2314                    None
2315                } else {
2316                    Some(crate::analysis::facts::GrantTarget::Tables(paths))
2317                }
2318            } else if po.tables_token().is_some()
2319                && po.in_token().is_some()
2320                && po.schema_token().is_some()
2321            {
2322                let schemas: Vec<_> = po
2323                    .name_refs()
2324                    .map(|nr| Self::resolve_name_ref(&nr))
2325                    .collect();
2326                if schemas.is_empty() {
2327                    None
2328                } else {
2329                    Some(crate::analysis::facts::GrantTarget::AllTablesInSchema(
2330                        schemas,
2331                    ))
2332                }
2333            } else {
2334                None
2335            }
2336        } else {
2337            let paths: Vec<_> = syntax.descendants().filter_map(Path::cast).collect();
2338            let name_refs: Vec<_> = syntax.descendants().filter_map(NameRef::cast).collect();
2339            if !paths.is_empty() {
2340                Some(crate::analysis::facts::GrantTarget::Tables(
2341                    paths
2342                        .iter()
2343                        .filter_map(Self::path_to_qualified_name)
2344                        .collect(),
2345                ))
2346            } else if !name_refs.is_empty() {
2347                Some(crate::analysis::facts::GrantTarget::AllTablesInSchema(
2348                    name_refs.iter().map(Self::resolve_name_ref).collect(),
2349                ))
2350            } else {
2351                None
2352            }
2353        }
2354    }
2355
2356    fn extract_grant(node: &Grant) -> Option<StatementFact> {
2357        let privileges = if node.all_token().is_some() {
2358            crate::analysis::facts::PrivilegeSpec::All
2359        } else {
2360            crate::analysis::facts::PrivilegeSpec::List(
2361                node.revoke_command_list()
2362                    .map(|rcl| {
2363                        rcl.revoke_commands()
2364                            .map(|rc| Self::extract_privilege_from_revoke_command(&rc))
2365                            .collect()
2366                    })
2367                    .unwrap_or_default(),
2368            )
2369        };
2370
2371        let target = Self::extract_grant_target_from_privilege_objects(
2372            node.privilege_objects(),
2373            node.syntax(),
2374        )?;
2375
2376        let grantees = node
2377            .role_ref_list()
2378            .map(|rrl| rrl.role_refs().map(|r| Self::extract_role(&r)).collect())
2379            .unwrap_or_default();
2380        let with_grant_option = node.grant_with_clause().is_some();
2381        let granted_by = node.role_ref().map(|r| Self::extract_role(&r));
2382
2383        Some(StatementFact::Grant(crate::analysis::facts::GrantFact {
2384            privileges,
2385            target,
2386            grantees,
2387            with_grant_option,
2388            granted_by,
2389        }))
2390    }
2391
2392    fn extract_revoke(node: &Revoke) -> Option<StatementFact> {
2393        let grant_option_only = node.for_token().is_some()
2394            && node.grant_token().is_some()
2395            && node.option_token().is_some();
2396
2397        let privileges = if let Some(p) = node.privileges() {
2398            if p.all_token().is_some() {
2399                crate::analysis::facts::PrivilegeSpec::All
2400            } else {
2401                crate::analysis::facts::PrivilegeSpec::List(
2402                    p.revoke_command_list()
2403                        .map(|rcl| {
2404                            rcl.revoke_commands()
2405                                .map(|rc| Self::extract_privilege_from_revoke_command(&rc))
2406                                .collect()
2407                        })
2408                        .unwrap_or_default(),
2409                )
2410            }
2411        } else {
2412            crate::analysis::facts::PrivilegeSpec::List(vec![])
2413        };
2414
2415        let target = Self::extract_grant_target_from_privilege_objects(
2416            node.privilege_objects(),
2417            node.syntax(),
2418        )?;
2419
2420        let revokees = node
2421            .role_ref_list()
2422            .map(|rrl| rrl.role_refs().map(|r| Self::extract_role(&r)).collect())
2423            .unwrap_or_default();
2424        let granted_by = node.role_ref().map(|r| Self::extract_role(&r));
2425        let cascade = node.cascade_token().is_some();
2426
2427        Some(StatementFact::Revoke(crate::analysis::facts::RevokeFact {
2428            grant_option_only,
2429            privileges,
2430            target,
2431            revokees,
2432            granted_by,
2433            cascade,
2434        }))
2435    }
2436
2437    fn extract_db_option(
2438        opt: squawk_syntax::ast::DatabaseOption,
2439    ) -> crate::analysis::facts::DatabaseOptionFact {
2440        let value = if opt.default_token().is_some() {
2441            crate::analysis::facts::DatabaseOptionValue::Default
2442        } else {
2443            crate::analysis::facts::DatabaseOptionValue::Literal(
2444                opt.literal()
2445                    .map(|l| l.syntax().text().to_string().trim_matches('\'').to_string()),
2446            )
2447        };
2448
2449        if opt.owner_token().is_some() {
2450            crate::analysis::facts::DatabaseOptionFact::Owner(value)
2451        } else if opt.template_token().is_some() {
2452            crate::analysis::facts::DatabaseOptionFact::Template(value)
2453        } else if opt.encoding_token().is_some() {
2454            crate::analysis::facts::DatabaseOptionFact::Encoding(value)
2455        } else if opt.tablespace_token().is_some() {
2456            crate::analysis::facts::DatabaseOptionFact::Tablespace(value)
2457        } else if opt.connection_token().is_some() && opt.limit_token().is_some() {
2458            crate::analysis::facts::DatabaseOptionFact::ConnectionLimit(value)
2459        } else if let Some(ident) = opt.ident_token() {
2460            crate::analysis::facts::DatabaseOptionFact::Named(ident.text().to_string(), value)
2461        } else {
2462            crate::analysis::facts::DatabaseOptionFact::Unknown(value)
2463        }
2464    }
2465
2466    fn extract_create_database(node: &CreateDatabase) -> Option<StatementFact> {
2467        let name = node.name().map(Self::resolve_name).unwrap_or_default();
2468        let options = node
2469            .database_option_list()
2470            .map(|ol| ol.database_options().map(Self::extract_db_option).collect())
2471            .unwrap_or_default();
2472        Some(StatementFact::CreateDatabase(
2473            crate::analysis::facts::CreateDatabaseFact { name, options },
2474        ))
2475    }
2476
2477    fn extract_alter_database(node: &ast::AlterDatabase) -> Option<StatementFact> {
2478        let name_ref = node.name_ref()?;
2479        let name = QualifiedName::new(
2480            None,
2481            Ident::new(name_ref.text().to_string(), name_ref.is_quoted()),
2482        );
2483
2484        let action = if let Some(rt) = node.rename_to() {
2485            crate::analysis::facts::AlterDatabaseAction::Rename {
2486                to: rt.name().map(|n| n.text()).unwrap_or_default(),
2487            }
2488        } else if let Some(ot) = node.owner_to() {
2489            crate::analysis::facts::AlterDatabaseAction::OwnerChange(Self::extract_role(
2490                &ot.role_ref().unwrap(),
2491            ))
2492        } else if let Some(st) = node.set_tablespace() {
2493            crate::analysis::facts::AlterDatabaseAction::TablespaceChange {
2494                new_tablespace: st
2495                    .path()
2496                    .map(|p| p.syntax().text().to_string())
2497                    .unwrap_or_default(),
2498            }
2499        } else if let Some(scp) = node.set_config_param() {
2500            crate::analysis::facts::AlterDatabaseAction::SetConfigParam {
2501                param: scp
2502                    .path()
2503                    .map(|p| p.syntax().text().to_string())
2504                    .unwrap_or_default(),
2505            }
2506        } else if let Some(rcp) = node.reset_config_param() {
2507            crate::analysis::facts::AlterDatabaseAction::ResetConfigParam {
2508                param: rcp.path().map(|p| p.syntax().text().to_string()),
2509            }
2510        } else if node.refresh_collation_version().is_some() {
2511            crate::analysis::facts::AlterDatabaseAction::RefreshCollationVersion
2512        } else {
2513            let ol = node.database_option_list()?;
2514            crate::analysis::facts::AlterDatabaseAction::OptionChanges(
2515                ol.database_options().map(Self::extract_db_option).collect(),
2516            )
2517        };
2518
2519        Some(StatementFact::AlterDatabase(
2520            crate::analysis::facts::AlterDatabaseFact { name, action },
2521        ))
2522    }
2523
2524    fn extract_drop_database(node: &ast::DropDatabase) -> Option<StatementFact> {
2525        let name_ref = node.name_ref()?;
2526        let name = QualifiedName::new(
2527            None,
2528            Ident::new(name_ref.text().to_string(), name_ref.is_quoted()),
2529        );
2530        Some(StatementFact::DropDatabase(
2531            crate::analysis::facts::DropDatabaseFact {
2532                name,
2533                if_exists: node.if_exists().is_some(),
2534            },
2535        ))
2536    }
2537
2538    fn extract_set(node: &Set) -> Option<StatementFact> {
2539        let setting_name = node.path()?.syntax().text().to_string().to_lowercase();
2540        if setting_name != "search_path" {
2541            return None;
2542        }
2543
2544        let schemas: Vec<String> = node
2545            .config_values()
2546            .filter_map(|cv| match cv {
2547                ast::ConfigValue::NameRef(nr) => Some(Self::resolve_name_ref(&nr)),
2548                ast::ConfigValue::Literal(_) => None,
2549            })
2550            .filter(|s| s.to_lowercase() != "default")
2551            .collect();
2552
2553        let is_default = node.config_value().is_some_and(|cv| {
2554            matches!(&cv, ast::ConfigValue::NameRef(nr) if nr.text().to_uppercase() == "DEFAULT")
2555        });
2556
2557        if is_default {
2558            return Some(StatementFact::SetSearchPath {
2559                target: SearchPathTarget::Default,
2560            });
2561        }
2562
2563        Some(StatementFact::SetSearchPath {
2564            target: SearchPathTarget::Schemas(schemas),
2565        })
2566    }
2567
2568    fn extract_rollback(node: &Rollback) -> Option<StatementFact> {
2569        if node.prepared_token().is_some() {
2570            return Some(StatementFact::OpaqueBlock);
2571        }
2572
2573        match node.name_ref().map(|nr| Self::resolve_name_ref(&nr)) {
2574            Some(name) => Some(StatementFact::RollbackToSavepoint { name }),
2575            None => Some(StatementFact::RollbackTransaction),
2576        }
2577    }
2578
2579    fn extract_savepoint(node: &Savepoint) -> StatementFact {
2580        StatementFact::Savepoint {
2581            name: node
2582                .name()
2583                .map(|n| n.text().to_string())
2584                .unwrap_or_default(),
2585        }
2586    }
2587
2588    fn extract_release_savepoint(node: &ReleaseSavepoint) -> StatementFact {
2589        StatementFact::ReleaseSavepoint {
2590            name: node
2591                .name_ref()
2592                .map(|n| n.text().to_string())
2593                .unwrap_or_default(),
2594        }
2595    }
2596
2597    fn segment_ident(segment: PathSegment) -> Option<Ident> {
2598        if let Some(nr) = segment.syntax().descendants().find_map(NameRef::cast) {
2599            Some(Ident::new(nr.text().to_string(), nr.is_quoted()))
2600        } else {
2601            segment
2602                .syntax()
2603                .descendants()
2604                .find_map(Name::cast)
2605                .map(|n| Ident::new(n.text().to_string(), n.is_quoted()))
2606        }
2607    }
2608
2609    fn path_to_qualified_name(path: &Path) -> Option<QualifiedName> {
2610        let segments: Vec<PathSegment> = path
2611            .syntax()
2612            .descendants()
2613            .filter_map(PathSegment::cast)
2614            .collect();
2615        if segments.is_empty() {
2616            return None;
2617        }
2618
2619        if segments.len() >= 2 {
2620            let schema = Self::segment_ident(segments[0].clone());
2621            let name = Self::segment_ident(segments[1].clone())?;
2622            Some(QualifiedName::new(schema, name))
2623        } else {
2624            let name = Self::segment_ident(segments[0].clone())?;
2625            Some(QualifiedName::new(None, name))
2626        }
2627    }
2628}