Skip to main content

safe_migrate/analysis/
resolver.rs

1// FILE: src/analysis/resolver.rs
2use crate::analysis::facts::{
3    AlterIndexActionFact, AlterTableActionFact, PersistenceFact, StatementFact, TypeCreationKind,
4};
5use crate::analysis::mutations::{
6    AlterDatabaseMutation, AlterDomainMutation, AlterFunctionMutation, AlterProcedureMutation,
7    AlterPublicationMutation, AlterRoleMutation, AlterSchemaMutation, AlterSequenceActionMutation,
8    AlterSequenceMutation, AlterSubscriptionMutation, AlterTable, AlterTableActionMutation,
9    AlterTypeActionMutation, AlterTypeMutation, ColumnMutation, CreateDatabaseMutation,
10    CreateDomainMutation, CreateFunctionMutation, CreateIndex, CreateMaterializedView,
11    CreatePolicyMutation, CreateProcedureMutation, CreatePublicationMutation, CreateRoleMutation,
12    CreateSchemaMutation, CreateSequenceMutation, CreateSubscriptionMutation, CreateTable,
13    CreateTriggerMutation, CreateTypeMutation, CreateView, DropDatabaseMutation,
14    DropDomainMutation, DropFunctionMutation, DropIndex, DropMaterializedViewMutation,
15    DropPolicyMutation, DropProcedureMutation, DropPublicationMutation, DropRoleMutation,
16    DropSchemaMutation, DropSequenceMutation, DropSubscriptionMutation, DropTable,
17    DropTriggerMutation, DropTypeMutation, DropViewMutation, FkMutation, GrantMutation, Mutation,
18    OpaqueMutation, PersistenceMutation, RefreshMaterializedViewMutation, ReleaseSavepointMutation,
19    Rename, RenameTriggerMutation, ResolvedGrantTarget, RevokeMutation,
20    RollbackToSavepointMutation, SavepointMutation, SearchPathChange,
21};
22use crate::analysis::state::AnalysisState;
23use crate::ast::identifiers::{ObjectId, QualifiedName};
24use crate::model::types::TypeKind;
25
26pub struct Resolver;
27
28impl Resolver {
29    fn resolve_creation_name(name: &QualifiedName, state: &AnalysisState) -> ObjectId {
30        let schema = name
31            .schema
32            .as_ref()
33            .map(|i| i.resolve())
34            .unwrap_or_else(|| {
35                state
36                    .local
37                    .search_path
38                    .first()
39                    .map(|s| s.as_str())
40                    .unwrap_or("public")
41                    .to_string()
42            });
43
44        ObjectId::new(schema, name.name.resolve())
45    }
46
47    fn resolve_lookup_name(name: &QualifiedName, state: &AnalysisState) -> ObjectId {
48        if let Some(schema_ident) = &name.schema {
49            return ObjectId::new(schema_ident.resolve(), name.name.resolve());
50        }
51
52        let resolved_name = name.name.resolve();
53
54        for schema in &state.local.search_path {
55            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
56            if state.local.relations.contains_key(&candidate)
57                || state.local.types.contains_key(&candidate)
58                || state.local.sequences.contains_key(&candidate)
59                || state.local.functions.keys().any(|k| {
60                    k.schema == candidate.schema
61                        && (k.name == candidate.name
62                            || k.name.starts_with(&format!("{}(", candidate.name)))
63                })
64            {
65                candidate.inferred_schema = true;
66                return candidate;
67            }
68        }
69
70        let schema = state
71            .local
72            .search_path
73            .first()
74            .map(|s| s.as_str())
75            .unwrap_or("public")
76            .to_string();
77        let mut id = ObjectId::new(schema, resolved_name);
78        id.inferred_schema = true;
79        id
80    }
81
82    fn resolve_type_lookup_name(name: &QualifiedName, state: &AnalysisState) -> ObjectId {
83        if let Some(schema_ident) = &name.schema {
84            return ObjectId::new(schema_ident.resolve(), name.name.resolve());
85        }
86
87        let resolved_name = name.name.resolve();
88        for schema in &state.local.search_path {
89            let mut candidate = ObjectId::new(schema.clone(), resolved_name.clone());
90            if matches!(
91                state.local.types.get(&candidate),
92                Some(crate::model::types::TypeOverlay::Present(_))
93            ) {
94                candidate.inferred_schema = true;
95                return candidate;
96            }
97        }
98
99        let schema = state
100            .local
101            .search_path
102            .first()
103            .cloned()
104            .unwrap_or_else(|| "public".to_string());
105        let mut id = ObjectId::new(schema, resolved_name);
106        id.inferred_schema = true;
107        id
108    }
109
110    fn resolve_constraint_index_name(name: &QualifiedName, table: &ObjectId) -> ObjectId {
111        let schema = name
112            .schema
113            .as_ref()
114            .map(|schema| schema.resolve())
115            .unwrap_or_else(|| table.schema.clone());
116        ObjectId::new(schema, name.name.resolve())
117    }
118
119    fn resolve_function_id(
120        name: &QualifiedName,
121        params: &[crate::analysis::facts::ParamFact],
122        state: &AnalysisState,
123    ) -> ObjectId {
124        let base_id = Self::resolve_creation_name(name, state);
125        let sig = params
126            .iter()
127            .map(|p| p.ty.clone())
128            .collect::<Vec<_>>()
129            .join(",");
130        Self::resolve_function_id_by_sig(&base_id, &sig)
131    }
132
133    fn resolve_function_id_by_sig(base_id: &ObjectId, sig: &str) -> ObjectId {
134        // Normalize types in signature to match pg_proc standard names
135        let normalized_sig = sig
136            .split(',')
137            .map(Self::normalize_function_arg_type)
138            .collect::<Vec<_>>()
139            .join(",");
140
141        let mut id = ObjectId::new(
142            base_id.schema.clone(),
143            format!("{}({})", base_id.name, normalized_sig),
144        );
145        id.inferred_schema = base_id.inferred_schema;
146        id
147    }
148
149    pub(crate) fn normalize_function_arg_type(raw: &str) -> String {
150        let normalized = Self::fold_unquoted_identifier_case(raw.trim());
151        if let Some(element_type) = normalized.strip_suffix("[]") {
152            return format!("{}[]", Self::normalize_function_arg_type(element_type));
153        }
154        match normalized.as_str() {
155            "int" | "int4" => "integer".to_string(),
156            "int8" => "bigint".to_string(),
157            "int2" => "smallint".to_string(),
158            "float8" => "double precision".to_string(),
159            "float4" => "real".to_string(),
160            "bool" => "boolean".to_string(),
161            "varchar" => "character varying".to_string(),
162            "char" => "character".to_string(),
163            "time" => "time without time zone".to_string(),
164            "timestamp" => "timestamp without time zone".to_string(),
165            "timestamptz" => "timestamp with time zone".to_string(),
166            "decimal" => "numeric".to_string(),
167            _ => normalized,
168        }
169    }
170
171    fn fold_unquoted_identifier_case(raw: &str) -> String {
172        let mut folded = String::with_capacity(raw.len());
173        let mut quoted = false;
174        let mut chars = raw.chars().peekable();
175        while let Some(character) = chars.next() {
176            match character {
177                '"' if quoted && chars.peek() == Some(&'"') => {
178                    folded.push('"');
179                    folded.push('"');
180                    chars.next();
181                }
182                '"' => {
183                    quoted = !quoted;
184                    folded.push(character);
185                }
186                character if quoted => folded.push(character),
187                character => folded.extend(character.to_lowercase()),
188            }
189        }
190        folded
191    }
192
193    fn resolve_grant_target(
194        target: &crate::analysis::facts::GrantTarget,
195        state: &AnalysisState,
196    ) -> ResolvedGrantTarget {
197        match target {
198            crate::analysis::facts::GrantTarget::Tables(names) => ResolvedGrantTarget::Tables(
199                names
200                    .iter()
201                    .map(|n| Self::resolve_lookup_name(n, state))
202                    .collect(),
203            ),
204            crate::analysis::facts::GrantTarget::AllTablesInSchema(schemas) => {
205                ResolvedGrantTarget::AllTablesInSchema(schemas.clone())
206            }
207        }
208    }
209
210    pub fn resolve(fact: &StatementFact, state: &AnalysisState) -> Vec<Mutation> {
211        let mut mutations = Vec::new();
212        match fact {
213            StatementFact::CreateSchema {
214                name,
215                if_not_exists,
216                authorization,
217            } => {
218                mutations.push(Mutation::CreateSchema(CreateSchemaMutation {
219                    name: name.name.resolve(),
220                    if_not_exists: *if_not_exists,
221                    authorization: authorization.clone(),
222                }));
223            }
224            StatementFact::SchemaNeutralNoop => {}
225            StatementFact::AlterSchema { name, action } => {
226                let name = name.name.resolve();
227                let action = match action {
228                    crate::analysis::facts::AlterSchemaActionFact::RenameTo { new_name } => {
229                        AlterSchemaMutation::Rename {
230                            old_name: name,
231                            new_name: new_name.resolve(),
232                        }
233                    }
234                    crate::analysis::facts::AlterSchemaActionFact::OwnerTo { new_owner } => {
235                        AlterSchemaMutation::OwnerTo {
236                            name,
237                            new_owner: new_owner.clone(),
238                        }
239                    }
240                };
241                mutations.push(Mutation::AlterSchema(action));
242            }
243            StatementFact::DropSchema {
244                names,
245                if_exists,
246                cascade,
247            } => {
248                mutations.push(Mutation::DropSchema(DropSchemaMutation {
249                    names: names.iter().map(|n| n.name.resolve()).collect(),
250                    if_exists: *if_exists,
251                    cascade: *cascade,
252                }));
253            }
254            StatementFact::CreateTable {
255                name,
256                if_not_exists,
257                as_select,
258                persistence,
259                columns,
260                foreign_keys,
261                table_constraints,
262                partition_by,
263                partition_of,
264                partition_type,
265            } => {
266                let id = Self::resolve_creation_name(name, state);
267
268                let resolved_persistence = match persistence {
269                    PersistenceFact::Permanent => PersistenceMutation::Permanent,
270                    PersistenceFact::Temporary => PersistenceMutation::Temporary,
271                    PersistenceFact::Unlogged => PersistenceMutation::Unlogged,
272                };
273
274                let col_mutations: Vec<ColumnMutation> = columns
275                    .iter()
276                    .map(|c| ColumnMutation {
277                        name: c.name.clone(),
278                        ty: c.ty.clone(),
279                        not_null: c.not_null,
280                        is_primary_key: c.is_primary_key,
281                        primary_key_constraint_name: c.primary_key_constraint_name.clone(),
282                        is_unique: c.is_unique,
283                        unique_constraint_name: c.unique_constraint_name.clone(),
284                        default: c.default.clone(),
285                        generation: c.generation,
286                    })
287                    .collect();
288
289                let mut fk_mutations = Vec::new();
290                for fk in foreign_keys {
291                    let to_table = Self::resolve_lookup_name(&fk.references, state);
292
293                    fk_mutations.push(FkMutation {
294                        constraint_name: fk.constraint_name.clone(),
295                        to_table,
296                        from_columns: fk.from_columns.clone(),
297                        to_columns: fk.to_columns.clone(),
298                    });
299                }
300
301                let partition_of_id = partition_of
302                    .as_ref()
303                    .map(|n| Self::resolve_lookup_name(n, state));
304
305                mutations.push(Mutation::CreateTable(CreateTable {
306                    id,
307                    if_not_exists: *if_not_exists,
308                    as_select: *as_select,
309                    persistence: resolved_persistence,
310                    columns: col_mutations,
311                    foreign_keys: fk_mutations,
312                    table_constraints: table_constraints.clone(),
313                    partition_by: partition_by.clone(),
314                    partition_of: partition_of_id,
315                    partition_type: partition_type.clone(),
316                }));
317            }
318            StatementFact::CreateView {
319                name,
320                or_replace,
321                depends_on,
322            } => {
323                let id = Self::resolve_creation_name(name, state);
324
325                let resolved_depends = depends_on
326                    .iter()
327                    .map(|n| Self::resolve_lookup_name(n, state))
328                    .collect();
329
330                mutations.push(Mutation::CreateView(CreateView {
331                    id,
332                    or_replace: *or_replace,
333                    depends_on: resolved_depends,
334                }));
335            }
336            StatementFact::AlterView { name, action } => {
337                match action {
338                    crate::analysis::facts::AlterViewAction::RenameTo { new_name } => {
339                        let id = Self::resolve_lookup_name(name, state);
340                        let mut new_id = ObjectId::new(id.schema.clone(), new_name.resolve());
341                        new_id.inferred_schema = id.inferred_schema;
342                        mutations.push(Mutation::Rename(Rename { old_id: id, new_id }));
343                    }
344                    crate::analysis::facts::AlterViewAction::SetSchema { new_schema } => {
345                        let id = Self::resolve_lookup_name(name, state);
346                        let new_id = ObjectId::new(new_schema, &id.name);
347                        mutations.push(Mutation::Rename(Rename { old_id: id, new_id }));
348                    }
349                    crate::analysis::facts::AlterViewAction::OwnerTo { new_owner } => {
350                        mutations.push(Mutation::ChangeRelationOwner {
351                            id: Self::resolve_lookup_name(name, state),
352                            new_owner: new_owner.clone(),
353                        });
354                    }
355                    crate::analysis::facts::AlterViewAction::SetDefault { .. }
356                    | crate::analysis::facts::AlterViewAction::DropDefault { .. }
357                    | crate::analysis::facts::AlterViewAction::RenameColumn { .. }
358                    | crate::analysis::facts::AlterViewAction::SetOptions { .. }
359                    | crate::analysis::facts::AlterViewAction::ResetOptions { .. } => {
360                        // These are opaque from the state machine's perspective —
361                        // they don't create or destroy objects, just modify metadata.
362                        // No mutation emitted; rules can still check the StatementFact.
363                    }
364                }
365            }
366            StatementFact::CreateMaterializedView { name, depends_on } => {
367                let id = Self::resolve_creation_name(name, state);
368
369                let resolved_depends = depends_on
370                    .iter()
371                    .map(|n| Self::resolve_lookup_name(n, state))
372                    .collect();
373
374                mutations.push(Mutation::CreateMaterializedView(CreateMaterializedView {
375                    id,
376                    depends_on: resolved_depends,
377                }));
378            }
379            StatementFact::AlterMaterializedView { name, new_name } => {
380                if let Some(new_name) = new_name {
381                    let id = Self::resolve_lookup_name(name, state);
382                    let mut new_id = ObjectId::new(id.schema.clone(), new_name.resolve());
383                    new_id.inferred_schema = id.inferred_schema;
384                    mutations.push(Mutation::Rename(Rename { old_id: id, new_id }));
385                }
386            }
387            StatementFact::RefreshMaterializedView { name, concurrently } => {
388                mutations.push(Mutation::RefreshMaterializedView(
389                    RefreshMaterializedViewMutation {
390                        id: Self::resolve_lookup_name(name, state),
391                        concurrently: *concurrently,
392                    },
393                ));
394            }
395            StatementFact::CreateIndex {
396                name,
397                relation,
398                if_not_exists,
399                concurrently,
400                using_method,
401                has_predicate,
402                unique,
403            } => {
404                let table = Self::resolve_lookup_name(relation, state);
405                // PostgreSQL places an unqualified index in the indexed
406                // relation's schema, not the first schema in search_path.
407                let id = if name.schema.is_some() {
408                    Self::resolve_creation_name(name, state)
409                } else {
410                    ObjectId::new(table.schema.clone(), name.name.resolve())
411                };
412
413                mutations.push(Mutation::CreateIndex(CreateIndex {
414                    id,
415                    table,
416                    if_not_exists: *if_not_exists,
417                    concurrently: *concurrently,
418                    using_method: using_method.clone(),
419                    has_predicate: *has_predicate,
420                    unique: *unique,
421                }));
422            }
423            StatementFact::CreatePolicy {
424                name,
425                table,
426                permissive,
427                command,
428            } => {
429                mutations.push(Mutation::CreatePolicy(CreatePolicyMutation {
430                    name: name.clone(),
431                    table: Self::resolve_lookup_name(table, state),
432                    permissive: *permissive,
433                    command: command.clone(),
434                }));
435            }
436            StatementFact::DropPolicy {
437                name,
438                table,
439                if_exists,
440            } => {
441                mutations.push(Mutation::DropPolicy(DropPolicyMutation {
442                    name: name.clone(),
443                    table: Self::resolve_lookup_name(table, state),
444                    if_exists: *if_exists,
445                }));
446            }
447            StatementFact::CreateTrigger {
448                name,
449                table,
450                function,
451            } => {
452                // Function references in triggers are bare names (e.g., "notify_func")
453                // but functions are stored with signature (e.g., "notify_func()").
454                // Use resolve_function_id_by_sig with empty params for consistent lookup.
455                let function_base = function
456                    .as_ref()
457                    .map(|f| Self::resolve_lookup_name(f, state))
458                    .unwrap_or_else(|| ObjectId::new("public", "unknown_function"));
459                let function_id = Self::resolve_function_id_by_sig(&function_base, "");
460                mutations.push(Mutation::CreateTrigger(CreateTriggerMutation {
461                    name: name.clone(),
462                    table: Self::resolve_lookup_name(table, state),
463                    function_id,
464                }));
465            }
466            StatementFact::DropTrigger {
467                name,
468                table,
469                if_exists,
470            } => {
471                mutations.push(Mutation::DropTrigger(DropTriggerMutation {
472                    name: name.clone(),
473                    table: Self::resolve_lookup_name(table, state),
474                    if_exists: *if_exists,
475                }));
476            }
477            StatementFact::AlterTrigger {
478                name,
479                table,
480                new_name,
481            } => mutations.push(Mutation::RenameTrigger(RenameTriggerMutation {
482                name: name.clone(),
483                table: Self::resolve_lookup_name(table, state),
484                new_name: new_name.clone(),
485            })),
486            StatementFact::AlterIndex { name, actions } => {
487                let id = Self::resolve_lookup_name(name, state);
488                for action in actions {
489                    match action {
490                        AlterIndexActionFact::RenameTo { new_name } => {
491                            let mut new_id = ObjectId::new(id.schema.clone(), new_name.resolve());
492                            new_id.inferred_schema = id.inferred_schema;
493                            mutations.push(Mutation::Rename(Rename {
494                                old_id: id.clone(),
495                                new_id,
496                            }));
497                        }
498                    }
499                }
500            }
501            StatementFact::CreateType(create_type) => {
502                let id = Self::resolve_creation_name(&create_type.name, state);
503
504                let mapped_kind = match &create_type.kind {
505                    TypeCreationKind::Enum { variants } => TypeKind::Enum {
506                        variants: variants.clone(),
507                    },
508                    TypeCreationKind::Range => TypeKind::Range,
509                    TypeCreationKind::Composite => TypeKind::Composite,
510                    TypeCreationKind::Base => TypeKind::Base,
511                };
512
513                mutations.push(Mutation::CreateType(CreateTypeMutation {
514                    id,
515                    kind: mapped_kind,
516                }));
517            }
518            StatementFact::AlterType(alter_type) => {
519                let id = Self::resolve_type_lookup_name(&alter_type.name, state);
520                for action_fact in &alter_type.actions {
521                    match action_fact {
522                        crate::analysis::facts::AlterTypeActionFact::RenameTo { new_name } => {
523                            let mut new_id = ObjectId::new(id.schema.clone(), new_name.resolve());
524                            new_id.inferred_schema = id.inferred_schema;
525                            mutations.push(Mutation::RenameType(Rename {
526                                old_id: id.clone(),
527                                new_id,
528                            }));
529                        }
530                        crate::analysis::facts::AlterTypeActionFact::SetSchema { new_schema } => {
531                            mutations.push(Mutation::RenameType(Rename {
532                                old_id: id.clone(),
533                                new_id: ObjectId::new(new_schema, &id.name),
534                            }));
535                        }
536                        crate::analysis::facts::AlterTypeActionFact::AddValue {
537                            new_value,
538                            neighbor,
539                            before,
540                        } => {
541                            mutations.push(Mutation::AlterType(AlterTypeMutation {
542                                id: id.clone(),
543                                action: AlterTypeActionMutation::AddValue {
544                                    new_value: new_value.clone(),
545                                    neighbor: neighbor.clone(),
546                                    before: *before,
547                                },
548                            }));
549                        }
550                        crate::analysis::facts::AlterTypeActionFact::RenameValue {
551                            old_value,
552                            new_value,
553                        } => {
554                            mutations.push(Mutation::AlterType(AlterTypeMutation {
555                                id: id.clone(),
556                                action: AlterTypeActionMutation::RenameValue {
557                                    old_value: old_value.clone(),
558                                    new_value: new_value.clone(),
559                                },
560                            }));
561                        }
562                    }
563                }
564            }
565            StatementFact::CreateDomain { name, base_type } => {
566                let id = Self::resolve_creation_name(name, state);
567
568                mutations.push(Mutation::CreateDomain(CreateDomainMutation {
569                    id,
570                    base_type: base_type.clone(),
571                }));
572            }
573            StatementFact::AlterDomain { name, action } => {
574                mutations.push(Mutation::AlterDomain(AlterDomainMutation {
575                    id: Self::resolve_lookup_name(name, state),
576                    action: action.clone(),
577                }));
578            }
579            StatementFact::DropDomain {
580                names,
581                if_exists,
582                cascade,
583            } => {
584                let ids = names
585                    .iter()
586                    .map(|n| Self::resolve_lookup_name(n, state))
587                    .collect();
588                mutations.push(Mutation::DropDomain(DropDomainMutation {
589                    ids,
590                    if_exists: *if_exists,
591                    cascade: *cascade,
592                }));
593            }
594            StatementFact::DropType {
595                names,
596                if_exists,
597                cascade,
598            } => {
599                let ids = names
600                    .iter()
601                    .map(|n| Self::resolve_lookup_name(n, state))
602                    .collect();
603                mutations.push(Mutation::DropType(DropTypeMutation {
604                    ids,
605                    if_exists: *if_exists,
606                    cascade: *cascade,
607                }));
608            }
609            StatementFact::CreateSequence {
610                name,
611                if_not_exists,
612                owned_by,
613            } => {
614                let id = Self::resolve_creation_name(name, state);
615
616                let resolved_owned_by = owned_by.as_ref().map(|(table_name, col)| {
617                    (Self::resolve_lookup_name(table_name, state), col.clone())
618                });
619                mutations.push(Mutation::CreateSequence(CreateSequenceMutation {
620                    id,
621                    if_not_exists: *if_not_exists,
622                    owned_by: resolved_owned_by,
623                }));
624            }
625            StatementFact::AlterSequence {
626                name,
627                if_exists,
628                action,
629            } => {
630                let id = Self::resolve_lookup_name(name, state);
631                let action = match action {
632                    crate::analysis::facts::AlterSequenceActionFact::OwnedBy(owned_by) => {
633                        AlterSequenceActionMutation::OwnedBy(owned_by.as_ref().map(
634                            |(table_name, col)| {
635                                (Self::resolve_lookup_name(table_name, state), col.clone())
636                            },
637                        ))
638                    }
639                    crate::analysis::facts::AlterSequenceActionFact::OwnerTo(owner) => {
640                        AlterSequenceActionMutation::OwnerTo(owner.clone())
641                    }
642                    crate::analysis::facts::AlterSequenceActionFact::RenameTo(new_name) => {
643                        AlterSequenceActionMutation::RenameTo(ObjectId::new(
644                            &id.schema,
645                            new_name.resolve(),
646                        ))
647                    }
648                    crate::analysis::facts::AlterSequenceActionFact::SetSchema(schema) => {
649                        AlterSequenceActionMutation::SetSchema(ObjectId::new(schema, &id.name))
650                    }
651                    crate::analysis::facts::AlterSequenceActionFact::Other => {
652                        AlterSequenceActionMutation::Other
653                    }
654                };
655                mutations.push(Mutation::AlterSequence(AlterSequenceMutation {
656                    id,
657                    if_exists: *if_exists,
658                    action,
659                }));
660            }
661            StatementFact::DropSequence {
662                names,
663                if_exists,
664                cascade,
665            } => {
666                let ids = names
667                    .iter()
668                    .map(|n| Self::resolve_lookup_name(n, state))
669                    .collect();
670                mutations.push(Mutation::DropSequence(DropSequenceMutation {
671                    ids,
672                    if_exists: *if_exists,
673                    cascade: *cascade,
674                }));
675            }
676            StatementFact::AlterTable { name, actions } => {
677                let id = Self::resolve_lookup_name(name, state);
678                for action_fact in actions {
679                    let action = match action_fact {
680                        AlterTableActionFact::AddColumn {
681                            name: col_name,
682                            ty,
683                            if_not_exists,
684                            not_null,
685                            default,
686                            generation,
687                        } => AlterTableActionMutation::AddColumn {
688                            name: col_name.clone(),
689                            ty: ty.clone(),
690                            if_not_exists: *if_not_exists,
691                            not_null: *not_null,
692                            default: default.clone(),
693                            depends_on: None, // Logic for extraction can be added later if needed
694                            generation: *generation,
695                        },
696                        AlterTableActionFact::DropColumn {
697                            name: col_name,
698                            if_exists,
699                        } => AlterTableActionMutation::DropColumn {
700                            name: col_name.clone(),
701                            if_exists: *if_exists,
702                        },
703                        AlterTableActionFact::RenameColumn { from, to } => {
704                            AlterTableActionMutation::RenameColumn {
705                                from: from.resolve(),
706                                to: to.resolve(),
707                            }
708                        }
709                        AlterTableActionFact::RenameTo { new_name } => {
710                            let mut new_id = ObjectId::new(id.schema.clone(), new_name.resolve());
711                            new_id.inferred_schema = id.inferred_schema;
712                            mutations.push(Mutation::Rename(Rename {
713                                old_id: id.clone(),
714                                new_id,
715                            }));
716                            continue;
717                        }
718                        AlterTableActionFact::SetSchema { new_schema } => {
719                            let new_id = ObjectId::new(new_schema, &id.name);
720                            mutations.push(Mutation::Rename(Rename {
721                                old_id: id.clone(),
722                                new_id,
723                            }));
724                            continue;
725                        }
726                        AlterTableActionFact::AddForeignKey {
727                            constraint_name,
728                            references,
729                            from_columns,
730                            to_columns,
731                            not_valid,
732                        } => {
733                            let to_table = Self::resolve_lookup_name(references, state);
734                            if !state.relation_is_present(&to_table) {
735                                return vec![Mutation::Opaque(
736                                    OpaqueMutation::UnresolvedReference {
737                                        object_kind: crate::report::violations::ObjectKind::Table,
738                                        object_name: to_table.to_string(),
739                                    },
740                                )];
741                            }
742                            AlterTableActionMutation::AddForeignKey {
743                                constraint_name: constraint_name.clone(),
744                                to_table,
745                                from_columns: from_columns.clone(),
746                                to_columns: to_columns.clone(),
747                                not_valid: *not_valid,
748                            }
749                        }
750                        AlterTableActionFact::AlterConstraint {
751                            name: c_name,
752                            deferrable,
753                        } => AlterTableActionMutation::AlterConstraint {
754                            name: c_name.clone(),
755                            deferrable: *deferrable,
756                        },
757                        AlterTableActionFact::RenameConstraint { old_name, new_name } => {
758                            AlterTableActionMutation::RenameConstraint {
759                                old_name: old_name.clone(),
760                                new_name: new_name.clone(),
761                            }
762                        }
763                        AlterTableActionFact::DropConstraint { name: c_name } => {
764                            AlterTableActionMutation::DropConstraint {
765                                name: c_name.clone(),
766                            }
767                        }
768                        AlterTableActionFact::AddCheckConstraint {
769                            constraint_name,
770                            not_valid,
771                        } => AlterTableActionMutation::AddCheckConstraint {
772                            constraint_name: constraint_name.clone(),
773                            not_valid: *not_valid,
774                        },
775                        AlterTableActionFact::AddUniqueConstraint {
776                            constraint_name,
777                            using_index,
778                        } => AlterTableActionMutation::AddUniqueConstraint {
779                            constraint_name: constraint_name.clone(),
780                            using_index: using_index
781                                .as_ref()
782                                .map(|name| Self::resolve_constraint_index_name(name, &id)),
783                        },
784                        AlterTableActionFact::AddPrimaryKeyConstraint {
785                            constraint_name,
786                            using_index,
787                        } => AlterTableActionMutation::AddPrimaryKeyConstraint {
788                            constraint_name: constraint_name.clone(),
789                            using_index: using_index
790                                .as_ref()
791                                .map(|name| Self::resolve_constraint_index_name(name, &id)),
792                        },
793                        AlterTableActionFact::AddExcludeConstraint { constraint_name } => {
794                            AlterTableActionMutation::AddExcludeConstraint {
795                                constraint_name: constraint_name.clone(),
796                            }
797                        }
798                        AlterTableActionFact::SetNotNull { column } => {
799                            AlterTableActionMutation::SetNotNull {
800                                column: column.clone(),
801                            }
802                        }
803                        AlterTableActionFact::DropNotNull { column } => {
804                            AlterTableActionMutation::DropNotNull {
805                                column: column.clone(),
806                            }
807                        }
808                        AlterTableActionFact::SetType {
809                            column,
810                            ty,
811                            has_using,
812                        } => AlterTableActionMutation::SetType {
813                            column: column.clone(),
814                            ty: ty.clone(),
815                            has_using: *has_using,
816                        },
817                        AlterTableActionFact::SetDefault { column, default } => {
818                            AlterTableActionMutation::SetDefault {
819                                column: column.clone(),
820                                default: default.clone(),
821                            }
822                        }
823                        AlterTableActionFact::ValidateConstraint { constraint_name } => {
824                            AlterTableActionMutation::ValidateConstraint {
825                                constraint_name: constraint_name.clone(),
826                            }
827                        }
828                        AlterTableActionFact::AttachPartition { child, strategy } => {
829                            let child_id = Self::resolve_lookup_name(child, state);
830
831                            AlterTableActionMutation::AttachPartition {
832                                child: child_id,
833                                strategy: strategy.clone(),
834                            }
835                        }
836                        AlterTableActionFact::DetachPartition { child } => {
837                            AlterTableActionMutation::DetachPartition {
838                                child: Self::resolve_lookup_name(child, state),
839                            }
840                        }
841                        AlterTableActionFact::SetStorage { column } => {
842                            AlterTableActionMutation::SetStorage {
843                                column: column.clone(),
844                            }
845                        }
846                        AlterTableActionFact::SetAccessMethod => {
847                            AlterTableActionMutation::SetAccessMethod
848                        }
849                        AlterTableActionFact::DisableTrigger { trigger_name } => {
850                            AlterTableActionMutation::DisableTrigger {
851                                trigger_name: trigger_name.clone(),
852                            }
853                        }
854                        AlterTableActionFact::EnableTrigger { trigger_name } => {
855                            AlterTableActionMutation::EnableTrigger {
856                                trigger_name: trigger_name.clone(),
857                            }
858                        }
859                        AlterTableActionFact::SetExpression { .. }
860                        | AlterTableActionFact::SetOptions { .. }
861                        | AlterTableActionFact::Inherit { .. }
862                        | AlterTableActionFact::NoInherit { .. }
863                        | AlterTableActionFact::ClusterOn { .. }
864                        | AlterTableActionFact::InheritTable { .. }
865                        | AlterTableActionFact::NoInheritTable { .. }
866                        | AlterTableActionFact::MergePartitions { .. }
867                        | AlterTableActionFact::SplitPartition
868                        | AlterTableActionFact::SetTablespace { .. }
869                        | AlterTableActionFact::SetLogged
870                        | AlterTableActionFact::SetUnlogged
871                        | AlterTableActionFact::ReplicaIdentity { .. }
872                        | AlterTableActionFact::ForceRls
873                        | AlterTableActionFact::EnableRls
874                        | AlterTableActionFact::DisableRls
875                        | AlterTableActionFact::EnableAlwaysTrigger { .. }
876                        | AlterTableActionFact::EnableReplicaTrigger { .. } => {
877                            AlterTableActionMutation::Opaque
878                        }
879                        AlterTableActionFact::OwnerTo { new_owner } => {
880                            AlterTableActionMutation::OwnerTo {
881                                new_owner: new_owner.clone(),
882                            }
883                        }
884                    };
885                    mutations.push(Mutation::AlterTable(AlterTable {
886                        id: id.clone(),
887                        action,
888                    }));
889                }
890            }
891            StatementFact::DropTable {
892                name,
893                if_exists,
894                cascade,
895            } => {
896                let id = Self::resolve_lookup_name(name, state);
897
898                // Still emit a DropTable mutation for rule evaluation (e.g. DriftDetectionRule)
899                // even when the table is not present locally. The state machine will handle
900                // tainting confidence in apply().
901                mutations.push(Mutation::DropTable(DropTable {
902                    id,
903                    if_exists: *if_exists,
904                    cascade: *cascade,
905                }));
906            }
907            StatementFact::DropView {
908                name,
909                if_exists,
910                cascade,
911            } => {
912                mutations.push(Mutation::DropView(DropViewMutation {
913                    ids: vec![Self::resolve_lookup_name(name, state)],
914                    if_exists: *if_exists,
915                    cascade: *cascade,
916                }));
917            }
918            StatementFact::DropMaterializedView {
919                names,
920                if_exists,
921                cascade,
922            } => {
923                let ids = names
924                    .iter()
925                    .map(|n| Self::resolve_lookup_name(n, state))
926                    .collect();
927                mutations.push(Mutation::DropMaterializedView(
928                    DropMaterializedViewMutation {
929                        ids,
930                        if_exists: *if_exists,
931                        cascade: *cascade,
932                    },
933                ));
934            }
935            StatementFact::DropIndex {
936                names,
937                if_exists,
938                concurrently,
939            } => {
940                for name in names {
941                    mutations.push(Mutation::DropIndex(DropIndex {
942                        id: Self::resolve_lookup_name(name, state),
943                        if_exists: *if_exists,
944                        concurrently: *concurrently,
945                    }));
946                }
947            }
948            StatementFact::SetSearchPath { target } => {
949                mutations.push(Mutation::SearchPath(SearchPathChange {
950                    target: target.clone(),
951                }))
952            }
953            StatementFact::BeginTransaction => mutations.push(Mutation::BeginTransaction),
954            StatementFact::CommitTransaction => mutations.push(Mutation::CommitTransaction),
955            StatementFact::CommitAndChain => mutations.push(Mutation::CommitAndChain),
956            StatementFact::RollbackTransaction => mutations.push(Mutation::RollbackTransaction),
957            StatementFact::RollbackAndChain => mutations.push(Mutation::RollbackAndChain),
958            StatementFact::RollbackToSavepoint { name } => {
959                mutations.push(Mutation::RollbackToSavepoint(RollbackToSavepointMutation {
960                    name: name.clone(),
961                }))
962            }
963            StatementFact::Savepoint { name } => {
964                mutations.push(Mutation::Savepoint(SavepointMutation {
965                    name: name.clone(),
966                }))
967            }
968            StatementFact::ReleaseSavepoint { name } => {
969                mutations.push(Mutation::ReleaseSavepoint(ReleaseSavepointMutation {
970                    name: name.clone(),
971                }))
972            }
973            StatementFact::PrepareTransaction { .. } => {
974                mutations.push(Mutation::Opaque(OpaqueMutation::PrepareTransaction))
975            }
976            StatementFact::SetTransaction => {
977                mutations.push(Mutation::Opaque(OpaqueMutation::SetTransaction))
978            }
979            StatementFact::SetConstraints => {
980                mutations.push(Mutation::Opaque(OpaqueMutation::SetConstraints))
981            }
982            StatementFact::OpaqueBlock => mutations.push(Mutation::Opaque(OpaqueMutation::DoBlock)),
983            StatementFact::Execute => mutations.push(Mutation::Opaque(OpaqueMutation::Execute)),
984            StatementFact::Vacuum { relation, is_full } => {
985                let table_id = relation
986                    .as_ref()
987                    .map(|r| Self::resolve_lookup_name(r, state));
988                mutations.push(Mutation::Vacuum {
989                    table_id,
990                    is_full: *is_full,
991                })
992            }
993            StatementFact::CreateFunction(f) => {
994                let id = Self::resolve_function_id(&f.name, &f.params, state);
995                mutations.push(Mutation::CreateFunction(CreateFunctionMutation {
996                    id,
997                    or_replace: f.or_replace,
998                    params: f.params.clone(),
999                    return_type: f.return_type.clone(),
1000                    options: f.options.clone(),
1001                }));
1002            }
1003            StatementFact::AlterFunction(f) => {
1004                let base_id = Self::resolve_lookup_name(&f.name, state);
1005                let sig = f.params.join(",");
1006                let id = Self::resolve_function_id_by_sig(&base_id, &sig);
1007                mutations.push(Mutation::AlterFunction(AlterFunctionMutation {
1008                    id,
1009                    action: f.action.clone(),
1010                }));
1011            }
1012            StatementFact::DropFunction(f) => {
1013                let mut signatures = Vec::new();
1014                for sig in &f.signatures {
1015                    let mut normalized_sig = sig.clone();
1016                    normalized_sig.params = normalized_sig
1017                        .params
1018                        .into_iter()
1019                        .map(|p| Self::normalize_function_arg_type(&p))
1020                        .collect();
1021                    signatures.push(normalized_sig);
1022                }
1023                mutations.push(Mutation::DropFunction(DropFunctionMutation {
1024                    signatures,
1025                    if_exists: f.if_exists,
1026                    cascade: f.cascade,
1027                }));
1028            }
1029            StatementFact::CreateProcedure(p) => {
1030                let id = Self::resolve_function_id(&p.name, &p.params, state);
1031                mutations.push(Mutation::CreateProcedure(CreateProcedureMutation {
1032                    id,
1033                    or_replace: p.or_replace,
1034                    params: p.params.clone(),
1035                    options: p.options.clone(),
1036                }));
1037            }
1038            StatementFact::AlterProcedure(p) => {
1039                let base_id = Self::resolve_lookup_name(&p.name, state);
1040                let sig = p
1041                    .params
1042                    .iter()
1043                    .map(|p| p.to_string())
1044                    .collect::<Vec<_>>()
1045                    .join(",");
1046                let id = Self::resolve_function_id_by_sig(&base_id, &sig);
1047                mutations.push(Mutation::AlterProcedure(AlterProcedureMutation {
1048                    id,
1049                    action: p.action.clone(),
1050                }));
1051            }
1052            StatementFact::DropProcedure(p) => {
1053                mutations.push(Mutation::DropProcedure(DropProcedureMutation {
1054                    signatures: p.signatures.clone(),
1055                    if_exists: p.if_exists,
1056                    cascade: p.cascade,
1057                }));
1058            }
1059            StatementFact::CreatePublication(p) => {
1060                mutations.push(Mutation::CreatePublication(CreatePublicationMutation {
1061                    name: p.name.clone(),
1062                    scope: p.scope.clone(),
1063                    params: p.params.clone(),
1064                }));
1065            }
1066            StatementFact::AlterPublication(p) => {
1067                mutations.push(Mutation::AlterPublication(AlterPublicationMutation {
1068                    name: p.name.clone(),
1069                }));
1070            }
1071            StatementFact::DropPublication(p) => {
1072                mutations.push(Mutation::DropPublication(DropPublicationMutation {
1073                    names: p.names.clone(),
1074                    if_exists: p.if_exists,
1075                    cascade: p.cascade,
1076                }));
1077            }
1078            StatementFact::CreateSubscription(s) => {
1079                mutations.push(Mutation::CreateSubscription(CreateSubscriptionMutation {
1080                    name: s.name.clone(),
1081                    connection: s.connection.clone(),
1082                    publications: s.publications.clone(),
1083                    params: s.params.clone(),
1084                }));
1085            }
1086            StatementFact::AlterSubscription(s) => {
1087                mutations.push(Mutation::AlterSubscription(AlterSubscriptionMutation {
1088                    name: s.name.clone(),
1089                }));
1090            }
1091            StatementFact::DropSubscription(s) => {
1092                mutations.push(Mutation::DropSubscription(DropSubscriptionMutation {
1093                    name: s.name.clone(),
1094                    if_exists: s.if_exists,
1095                }));
1096            }
1097            StatementFact::CreateRole(r) => {
1098                mutations.push(Mutation::CreateRole(CreateRoleMutation {
1099                    name: r.name.clone(),
1100                    inherits: r.inherits,
1101                }));
1102            }
1103            StatementFact::AlterRole(r) => {
1104                mutations.push(Mutation::AlterRole(AlterRoleMutation {
1105                    name: r.name.clone(),
1106                    inherits: r.inherits,
1107                }));
1108            }
1109            StatementFact::DropRole(r) => {
1110                mutations.push(Mutation::DropRole(DropRoleMutation {
1111                    names: r.names.clone(),
1112                    if_exists: r.if_exists,
1113                }));
1114            }
1115            StatementFact::Grant(g) => {
1116                mutations.push(Mutation::Grant(GrantMutation {
1117                    privileges: g.privileges.clone(),
1118                    target: Self::resolve_grant_target(&g.target, state),
1119                    grantees: g.grantees.clone(),
1120                    with_grant_option: g.with_grant_option,
1121                    granted_by: g.granted_by.clone(),
1122                }));
1123            }
1124            StatementFact::Revoke(r) => {
1125                mutations.push(Mutation::Revoke(RevokeMutation {
1126                    grant_option_only: r.grant_option_only,
1127                    privileges: r.privileges.clone(),
1128                    target: Self::resolve_grant_target(&r.target, state),
1129                    revokees: r.revokees.clone(),
1130                    granted_by: r.granted_by.clone(),
1131                    cascade: r.cascade,
1132                }));
1133            }
1134            StatementFact::CreateDatabase(d) => {
1135                mutations.push(Mutation::CreateDatabase(CreateDatabaseMutation {
1136                    name: d.name.clone(),
1137                    options: d.options.clone(),
1138                }));
1139            }
1140            StatementFact::AlterDatabase(d) => {
1141                let id = Self::resolve_lookup_name(&d.name, state);
1142                mutations.push(Mutation::AlterDatabase(AlterDatabaseMutation {
1143                    id,
1144                    action: d.action.clone(),
1145                }));
1146            }
1147            StatementFact::DropDatabase(d) => {
1148                let id = Self::resolve_lookup_name(&d.name, state);
1149                mutations.push(Mutation::DropDatabase(DropDatabaseMutation {
1150                    id,
1151                    if_exists: d.if_exists,
1152                }));
1153            }
1154            StatementFact::SetRole {
1155                role,
1156                local,
1157                is_session_auth,
1158            } => {
1159                mutations.push(Mutation::SwitchRole {
1160                    role: role.clone(),
1161                    local: *local,
1162                    is_session_auth: *is_session_auth,
1163                });
1164            }
1165        }
1166        mutations
1167    }
1168}