Skip to main content

pgrx_sql_entity_graph/
pgrx_sql.rs

1//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2//LICENSE
3//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4//LICENSE
5//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6//LICENSE
7//LICENSE All rights reserved.
8//LICENSE
9//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10/*!
11
12Rust to SQL mapping support.
13
14> Like all of the [`sql_entity_graph`][crate] APIs, this is considered **internal**
15> to the `pgrx` framework and very subject to change between versions. While you may use this, please do it with caution.
16
17*/
18
19use eyre::eyre;
20use petgraph::Direction;
21use petgraph::dot::Dot;
22use petgraph::graph::NodeIndex;
23use petgraph::stable_graph::StableGraph;
24use petgraph::visit::EdgeRef;
25use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
26use std::fmt::Debug;
27use std::path::Path;
28
29use crate::aggregate::entity::PgAggregateEntity;
30use crate::control_file::ControlFile;
31use crate::extension_sql::SqlDeclared;
32use crate::extension_sql::entity::{ExtensionSqlEntity, SqlDeclaredEntity};
33use crate::metadata::TypeOrigin;
34use crate::pg_extern::entity::PgExternEntity;
35use crate::pg_trigger::entity::PgTriggerEntity;
36use crate::positioning_ref::PositioningRef;
37use crate::postgres_enum::entity::PostgresEnumEntity;
38use crate::postgres_hash::entity::PostgresHashEntity;
39use crate::postgres_ord::entity::PostgresOrdEntity;
40use crate::postgres_type::entity::PostgresTypeEntity;
41use crate::schema::entity::SchemaEntity;
42use crate::to_sql::ToSql;
43use crate::type_keyed;
44use crate::{SqlGraphEntity, SqlGraphIdentifier, UsedTypeEntity};
45
46use super::{PgExternReturnEntity, PgExternReturnEntityIteratedItem};
47
48#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
49pub enum SqlGraphRequires {
50    By,
51    ByArg,
52    ByReturn,
53}
54
55/// A generator for SQL.
56///
57/// Consumes a base mapping of types (typically `pgrx::DEFAULT_TYPEID_SQL_MAPPING`), a
58/// [`ControlFile`], and collections of each SQL entity.
59///
60/// During construction, a Directed Acyclic Graph is formed out the dependencies. For example,
61/// an item `detect_dog(x: &[u8]) -> animals::Dog` would have have a relationship with
62/// `animals::Dog`.
63///
64/// Typically, [`PgrxSql`] types are constructed in a `pgrx::pg_binary_magic!()` call in a binary
65/// out of entities collected during a `pgrx::pg_module_magic!()` call in a library.
66#[derive(Debug, Clone)]
67pub struct PgrxSql<'a> {
68    pub control: ControlFile,
69    pub graph: StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
70    pub graph_root: NodeIndex,
71    pub graph_bootstrap: Option<NodeIndex>,
72    pub graph_finalize: Option<NodeIndex>,
73    pub schemas: HashMap<SchemaEntity<'a>, NodeIndex>,
74    pub extension_sqls: HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
75    pub externs: HashMap<PgExternEntity<'a>, NodeIndex>,
76    pub types: HashMap<PostgresTypeEntity<'a>, NodeIndex>,
77    pub builtin_types: HashMap<String, NodeIndex>,
78    pub enums: HashMap<PostgresEnumEntity<'a>, NodeIndex>,
79    pub ords: HashMap<PostgresOrdEntity<'a>, NodeIndex>,
80    pub hashes: HashMap<PostgresHashEntity<'a>, NodeIndex>,
81    pub aggregates: HashMap<PgAggregateEntity<'a>, NodeIndex>,
82    pub triggers: HashMap<PgTriggerEntity<'a>, NodeIndex>,
83    pub extension_name: String,
84    pub versioned_so: bool,
85}
86
87impl<'a> PgrxSql<'a> {
88    pub fn build(
89        entities: impl Iterator<Item = SqlGraphEntity<'a>>,
90        extension_name: String,
91        versioned_so: bool,
92    ) -> eyre::Result<Self> {
93        let mut graph = StableGraph::new();
94
95        let mut entities = entities.collect::<Vec<_>>();
96        entities.sort();
97        // Split up things into their specific types:
98        let mut control: Option<ControlFile> = None;
99        let mut schemas: Vec<SchemaEntity<'a>> = Vec::default();
100        let mut extension_sqls: Vec<ExtensionSqlEntity<'a>> = Vec::default();
101        let mut externs: Vec<PgExternEntity<'a>> = Vec::default();
102        let mut types: Vec<PostgresTypeEntity<'a>> = Vec::default();
103        let mut enums: Vec<PostgresEnumEntity<'a>> = Vec::default();
104        let mut ords: Vec<PostgresOrdEntity<'a>> = Vec::default();
105        let mut hashes: Vec<PostgresHashEntity<'a>> = Vec::default();
106        let mut aggregates: Vec<PgAggregateEntity<'a>> = Vec::default();
107        let mut triggers: Vec<PgTriggerEntity<'a>> = Vec::default();
108        for entity in entities {
109            match entity {
110                SqlGraphEntity::ExtensionRoot(input_control) => {
111                    control = Some(input_control);
112                }
113                SqlGraphEntity::Schema(input_schema) => {
114                    schemas.push(input_schema);
115                }
116                SqlGraphEntity::CustomSql(input_sql) => {
117                    extension_sqls.push(input_sql);
118                }
119                SqlGraphEntity::Function(input_function) => {
120                    externs.push(input_function);
121                }
122                SqlGraphEntity::Type(input_type) => {
123                    types.push(input_type);
124                }
125                SqlGraphEntity::BuiltinType(_) => (),
126                SqlGraphEntity::Enum(input_enum) => {
127                    enums.push(input_enum);
128                }
129                SqlGraphEntity::Ord(input_ord) => {
130                    ords.push(input_ord);
131                }
132                SqlGraphEntity::Hash(input_hash) => {
133                    hashes.push(input_hash);
134                }
135                SqlGraphEntity::Aggregate(input_aggregate) => {
136                    aggregates.push(input_aggregate);
137                }
138                SqlGraphEntity::Trigger(input_trigger) => {
139                    triggers.push(input_trigger);
140                }
141            }
142        }
143
144        let control: ControlFile = control.expect("No control file found");
145        let root = graph.add_node(SqlGraphEntity::ExtensionRoot(control.clone()));
146
147        // The initial build phase.
148        //
149        // Notably, we do not set non-root edges here. We do that in a second step. This is
150        // primarily because externs, types, operators, and the like tend to intertwine. If we tried
151        // to do it here, we'd find ourselves trying to create edges to non-existing entities.
152
153        // Both of these must be unique, so we can only hold one.
154        // Populate nodes, but don't build edges until we know if there is a bootstrap/finalize.
155        let (mapped_extension_sqls, bootstrap, finalize) =
156            initialize_extension_sqls(&mut graph, root, extension_sqls)?;
157        let mapped_schemas = initialize_schemas(&mut graph, bootstrap, finalize, schemas)?;
158        let mapped_enums = initialize_enums(&mut graph, root, bootstrap, finalize, enums)?;
159        let mapped_types = initialize_types(&mut graph, root, bootstrap, finalize, types)?;
160        ensure_unique_type_targets(&mapped_types, &mapped_enums, &mapped_extension_sqls)?;
161        let (mapped_externs, mut mapped_builtin_types) = initialize_externs(
162            &mut graph,
163            root,
164            bootstrap,
165            finalize,
166            externs,
167            &mapped_types,
168            &mapped_enums,
169            &mapped_extension_sqls,
170        )?;
171        let mapped_ords = initialize_ords(&mut graph, root, bootstrap, finalize, ords)?;
172        let mapped_hashes = initialize_hashes(&mut graph, root, bootstrap, finalize, hashes)?;
173        let mapped_aggregates = initialize_aggregates(
174            &mut graph,
175            root,
176            bootstrap,
177            finalize,
178            aggregates,
179            &mut mapped_builtin_types,
180            &mapped_enums,
181            &mapped_types,
182            &mapped_extension_sqls,
183        )?;
184        let mapped_triggers = initialize_triggers(&mut graph, root, bootstrap, finalize, triggers)?;
185
186        // Now we can circle back and build up the edge sets.
187        connect_schemas(&mut graph, &mapped_schemas, root);
188        connect_extension_sqls(
189            &mut graph,
190            &mapped_extension_sqls,
191            &mapped_schemas,
192            &mapped_types,
193            &mapped_enums,
194            &mapped_externs,
195            &mapped_triggers,
196        )?;
197        connect_enums(&mut graph, &mapped_enums, &mapped_schemas);
198        connect_types(&mut graph, &mapped_types, &mapped_schemas, &mapped_externs)?;
199        connect_externs(
200            &mut graph,
201            &mapped_externs,
202            &mapped_hashes,
203            &mapped_schemas,
204            &mapped_types,
205            &mapped_enums,
206            &mapped_builtin_types,
207            &mapped_extension_sqls,
208            &mapped_triggers,
209        )?;
210        connect_ords(
211            &mut graph,
212            &mapped_ords,
213            &mapped_schemas,
214            &mapped_types,
215            &mapped_enums,
216            &mapped_externs,
217        );
218        connect_hashes(
219            &mut graph,
220            &mapped_hashes,
221            &mapped_schemas,
222            &mapped_types,
223            &mapped_enums,
224            &mapped_externs,
225        );
226        connect_aggregates(
227            &mut graph,
228            &mapped_aggregates,
229            &mapped_schemas,
230            &mapped_types,
231            &mapped_enums,
232            &mapped_builtin_types,
233            &mapped_externs,
234            &mapped_extension_sqls,
235        )?;
236        connect_triggers(&mut graph, &mapped_triggers, &mapped_schemas);
237
238        let this = Self {
239            control,
240            schemas: mapped_schemas,
241            extension_sqls: mapped_extension_sqls,
242            externs: mapped_externs,
243            types: mapped_types,
244            builtin_types: mapped_builtin_types,
245            enums: mapped_enums,
246            ords: mapped_ords,
247            hashes: mapped_hashes,
248            aggregates: mapped_aggregates,
249            triggers: mapped_triggers,
250            graph,
251            graph_root: root,
252            graph_bootstrap: bootstrap,
253            graph_finalize: finalize,
254            extension_name,
255            versioned_so,
256        };
257        Ok(this)
258    }
259
260    // NOTE: this signature is demanded by the codegen we embed via cargo-pgrx
261    pub fn to_file(&self, file: impl AsRef<Path> + Debug) -> eyre::Result<()> {
262        use std::fs::{File, create_dir_all};
263        use std::io::Write;
264        let generated = self.to_sql()?;
265        let path = Path::new(file.as_ref());
266
267        let parent = path.parent();
268        if let Some(parent) = parent {
269            create_dir_all(parent)?;
270        }
271        let mut out = File::create(path)?;
272        write!(out, "{generated}")?;
273        Ok(())
274    }
275
276    pub fn write(&self, out: &mut impl std::io::Write) -> eyre::Result<()> {
277        let generated = self.to_sql()?;
278
279        #[cfg(feature = "syntax-highlighting")]
280        {
281            use std::io::{IsTerminal, stdout};
282            if stdout().is_terminal() {
283                self.write_highlighted(out, &generated)?;
284            } else {
285                write!(*out, "{}", generated)?;
286            }
287        }
288
289        #[cfg(not(feature = "syntax-highlighting"))]
290        {
291            write!(*out, "{generated}")?;
292        }
293
294        Ok(())
295    }
296
297    #[cfg(feature = "syntax-highlighting")]
298    fn write_highlighted(&self, out: &mut dyn std::io::Write, generated: &str) -> eyre::Result<()> {
299        use eyre::WrapErr as _;
300        use owo_colors::{OwoColorize, XtermColors};
301        use syntect::easy::HighlightLines;
302        use syntect::highlighting::{Style, ThemeSet};
303        use syntect::parsing::SyntaxSet;
304        use syntect::util::LinesWithEndings;
305        let ps = SyntaxSet::load_defaults_newlines();
306        let theme_bytes = include_str!("../assets/ansi.tmTheme").as_bytes();
307        let mut theme_reader = std::io::Cursor::new(theme_bytes);
308        let theme = ThemeSet::load_from_reader(&mut theme_reader)
309            .wrap_err("Couldn't parse theme for SQL highlighting, try piping to a file")?;
310
311        if let Some(syntax) = ps.find_syntax_by_extension("sql") {
312            let mut h = HighlightLines::new(syntax, &theme);
313            for line in LinesWithEndings::from(&generated) {
314                let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps)?;
315                // Concept from https://github.com/sharkdp/bat/blob/1b030dc03b906aa345f44b8266bffeea77d763fe/src/terminal.rs#L6
316                for (style, content) in ranges {
317                    if style.foreground.a == 0x01 {
318                        write!(*out, "{}", content)?;
319                    } else {
320                        write!(*out, "{}", content.color(XtermColors::from(style.foreground.r)))?;
321                    }
322                }
323                write!(*out, "\x1b[0m")?;
324            }
325        } else {
326            write!(*out, "{}", generated)?;
327        }
328        Ok(())
329    }
330
331    // NOTE: this signature is demanded by the codegen we embed via cargo-pgrx
332    pub fn to_dot(&self, file: impl AsRef<Path> + Debug) -> eyre::Result<()> {
333        use std::fs::{File, create_dir_all};
334        use std::io::Write;
335        let generated = Dot::with_attr_getters(
336            &self.graph,
337            &[petgraph::dot::Config::EdgeNoLabel, petgraph::dot::Config::NodeNoLabel],
338            &|_graph, edge| {
339                match edge.weight() {
340                    SqlGraphRequires::By => r#"color = "gray""#,
341                    SqlGraphRequires::ByArg => r#"color = "black""#,
342                    SqlGraphRequires::ByReturn => r#"dir = "back", color = "black""#,
343                }
344                .to_owned()
345            },
346            &|_graph, (_index, node)| {
347                let dot_id = node.dot_identifier();
348                match node {
349                    // Colors derived from https://www.schemecolor.com/touch-of-creativity.php
350                    SqlGraphEntity::Schema(_item) => {
351                        format!("label = \"{dot_id}\", weight = 6, shape = \"tab\"")
352                    }
353                    SqlGraphEntity::Function(_item) => format!(
354                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#ADC7C6\", weight = 4, shape = \"box\"",
355                    ),
356                    SqlGraphEntity::Type(_item) => format!(
357                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#AE9BBD\", weight = 5, shape = \"oval\"",
358                    ),
359                    SqlGraphEntity::BuiltinType(_item) => {
360                        format!("label = \"{dot_id}\", shape = \"plain\"")
361                    }
362                    SqlGraphEntity::Enum(_item) => format!(
363                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#C9A7C8\", weight = 5, shape = \"oval\""
364                    ),
365                    SqlGraphEntity::Ord(_item) => format!(
366                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#FFCFD3\", weight = 5, shape = \"diamond\""
367                    ),
368                    SqlGraphEntity::Hash(_item) => format!(
369                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#FFE4E0\", weight = 5, shape = \"diamond\""
370                    ),
371                    SqlGraphEntity::Aggregate(_item) => format!(
372                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#FFE4E0\", weight = 5, shape = \"diamond\""
373                    ),
374                    SqlGraphEntity::Trigger(_item) => format!(
375                        "label = \"{dot_id}\", penwidth = 0, style = \"filled\", fillcolor = \"#FFE4E0\", weight = 5, shape = \"diamond\""
376                    ),
377                    SqlGraphEntity::CustomSql(_item) => {
378                        format!("label = \"{dot_id}\", weight = 3, shape = \"signature\"")
379                    }
380                    SqlGraphEntity::ExtensionRoot(_item) => {
381                        format!("label = \"{dot_id}\", shape = \"cylinder\"")
382                    }
383                }
384            },
385        );
386        let path = Path::new(file.as_ref());
387
388        let parent = path.parent();
389        if let Some(parent) = parent {
390            create_dir_all(parent)?;
391        }
392        let mut out = File::create(path)?;
393        write!(out, "{generated:?}")?;
394        Ok(())
395    }
396
397    pub fn schema_alias_of(&self, item_index: &NodeIndex) -> Option<String> {
398        self.graph
399            .neighbors_undirected(*item_index)
400            .flat_map(|neighbor_index| match &self.graph[neighbor_index] {
401                SqlGraphEntity::Schema(s) => Some(String::from(s.name)),
402                SqlGraphEntity::ExtensionRoot(_control) => None,
403                _ => None,
404            })
405            .next()
406    }
407
408    pub fn schema_prefix_for(&self, target: &NodeIndex) -> String {
409        self.schema_alias_of(target).map(|v| (v + ".").to_string()).unwrap_or_default()
410    }
411
412    pub fn find_type_dependency(
413        &self,
414        owner: &NodeIndex,
415        ty: &dyn crate::TypeIdentifiable,
416    ) -> Option<NodeIndex> {
417        self.graph
418            .neighbors_undirected(*owner)
419            .find(|neighbor| self.graph[*neighbor].type_matches(ty))
420    }
421
422    pub fn schema_prefix_for_used_type(
423        &self,
424        owner: &NodeIndex,
425        slot: &str,
426        used_ty: &UsedTypeEntity<'_>,
427    ) -> eyre::Result<String> {
428        if !used_ty.needs_type_resolution() {
429            return Ok(String::new());
430        }
431
432        let graph_index = self
433            .find_type_dependency(owner, used_ty)
434            .ok_or_else(|| eyre!("Could not find {slot} in graph. Got: {used_ty:?}"))?;
435        Ok(self.schema_prefix_for(&graph_index))
436    }
437
438    pub fn to_sql(&self) -> eyre::Result<String> {
439        let mut full_sql = String::new();
440
441        // NB:  A properly we'd *like* to maintain is that the schema generator outputs
442        // consistent results from run-to-run when there are no changes to the schema.
443        // This is to improve change detection using simple tools like `diff`.
444        //
445        // Historically, we used [`petgraph::algo:toposort`] but its ordering is not at all
446        // consistent.
447        //
448        // [`petgraph::algo::tarjan_scc`] appears to be consistent, although it's not exactly
449        // clear if this is due to an implementation detail or specifics of the algorithm itself.
450        // (I, eeeebbbbrrrr, am not a graph theory expert)
451        //
452        // In any event, if in the future schema generation stops being consistent, this is the
453        // place to look.
454        //
455        // We have no tests around this as it's really just a property we'd like to have, and
456        // it does seem ensuring it is a bit of black magic.
457        for nodes in petgraph::algo::tarjan_scc(&self.graph).iter().rev() {
458            let mut inner_sql = Vec::with_capacity(nodes.len());
459
460            for node in self.connected_component_emit_order(nodes) {
461                let step = &self.graph[node];
462                let sql = step.to_sql(self)?;
463
464                let trimmed = sql.trim();
465                if !trimmed.is_empty() {
466                    inner_sql.push(format!("{trimmed}\n"))
467                }
468            }
469
470            if !inner_sql.is_empty() {
471                full_sql.push_str("/* <begin connected objects> */\n");
472                full_sql.push_str(&inner_sql.join("\n\n"));
473                full_sql.push_str("/* </end connected objects> */\n\n");
474            }
475        }
476
477        Ok(full_sql)
478    }
479
480    fn connected_component_emit_order(&self, nodes: &[NodeIndex]) -> Vec<NodeIndex> {
481        if nodes.len() <= 1 {
482            return nodes.to_vec();
483        }
484
485        // When a connected component contains a cycle, user-authored `requires = [...]`
486        // edges are the strongest ordering signal we have. Type-resolution edges may still
487        // point back into the declaration that ultimately creates the type, such as shell-type
488        // bootstrap patterns for manual `extension_sql!()` types.
489        let mut explicit_dependents = HashMap::<NodeIndex, Vec<NodeIndex>>::new();
490        let mut remaining_explicit_dependencies = HashMap::<NodeIndex, usize>::new();
491        let mut has_explicit_edges = false;
492
493        for &node in nodes {
494            explicit_dependents.insert(node, Vec::new());
495            remaining_explicit_dependencies.insert(node, 0);
496        }
497
498        for &node in nodes {
499            for edge in self.graph.edges(node) {
500                if edge.weight() != &SqlGraphRequires::By {
501                    continue;
502                }
503
504                let dependent = edge.target();
505                if !remaining_explicit_dependencies.contains_key(&dependent) {
506                    continue;
507                }
508
509                has_explicit_edges = true;
510                explicit_dependents
511                    .get_mut(&node)
512                    .expect("component members should be initialized")
513                    .push(dependent);
514                *remaining_explicit_dependencies
515                    .get_mut(&dependent)
516                    .expect("component members should be initialized") += 1;
517            }
518        }
519
520        if !has_explicit_edges {
521            return nodes.to_vec();
522        }
523
524        let mut ready = remaining_explicit_dependencies
525            .iter()
526            .filter_map(|(node, count)| (*count == 0).then_some(*node))
527            .collect::<Vec<_>>();
528        let mut ordered = Vec::with_capacity(nodes.len());
529
530        while !ready.is_empty() {
531            ready.sort_unstable_by(|left, right| {
532                self.graph[*left]
533                    .cmp(&self.graph[*right])
534                    .then_with(|| left.index().cmp(&right.index()))
535            });
536            let next = ready.remove(0);
537            ordered.push(next);
538
539            if let Some(dependents) = explicit_dependents.get(&next) {
540                for dependent in dependents {
541                    let remaining = remaining_explicit_dependencies
542                        .get_mut(dependent)
543                        .expect("component members should be initialized");
544                    *remaining -= 1;
545                    if *remaining == 0 {
546                        ready.push(*dependent);
547                    }
548                }
549            }
550        }
551
552        if ordered.len() == nodes.len() { ordered } else { nodes.to_vec() }
553    }
554
555    pub fn has_sql_declared_entity(&self, identifier: &SqlDeclared) -> Option<&SqlDeclaredEntity> {
556        self.extension_sqls.iter().find_map(|(item, _index)| {
557            item.creates
558                .iter()
559                .find(|create_entity| create_entity.has_sql_declared_entity(identifier))
560        })
561    }
562
563    pub fn get_module_pathname(&self) -> String {
564        if self.versioned_so {
565            let extname = &self.extension_name;
566            let extver = &self.control.default_version;
567            // Note: versioned so-name format must agree with cargo pgrx
568            format!("{extname}-{extver}")
569        } else {
570            String::from("MODULE_PATHNAME")
571        }
572    }
573
574    pub fn find_matching_fn(&self, name: &str) -> Option<&PgExternEntity<'a>> {
575        self.externs.keys().find(|key| key.full_path.ends_with(name))
576    }
577
578    /// Resolve a single user-supplied item name to one graph node.
579    ///
580    /// A match is any entity whose SQL-visible name, Rust path, or operator
581    /// symbol equals `name` exactly. A `::`-bearing argument is treated as a
582    /// Rust path (matched only against `full_path`). Ambiguous hits are a
583    /// hard error.
584    pub fn resolve_item(&self, name: &str) -> eyre::Result<NodeIndex> {
585        let by_path = name.contains("::");
586        let mut matches: Vec<(NodeIndex, String)> = Vec::new();
587
588        for (entity, &idx) in &self.externs {
589            let fn_hit = if by_path {
590                entity.full_path == name
591            } else {
592                entity.name == name || entity.unaliased_name == name
593            };
594            if fn_hit {
595                matches.push((idx, format!("function `{}`", entity.full_path)));
596            }
597            if !by_path
598                && let Some(op) = &entity.operator
599                && op.opname == Some(name)
600                && !matches.iter().any(|(existing, _)| *existing == idx)
601            {
602                matches.push((idx, format!("operator `{}` on `{}`", name, entity.full_path)));
603            }
604        }
605
606        for (entity, &idx) in &self.types {
607            let hit = if by_path { entity.full_path == name } else { entity.name == name };
608            if hit {
609                matches.push((idx, format!("type `{}`", entity.full_path)));
610            }
611        }
612
613        for (entity, &idx) in &self.enums {
614            let hit = if by_path { entity.full_path == name } else { entity.name == name };
615            if hit {
616                matches.push((idx, format!("enum `{}`", entity.full_path)));
617            }
618        }
619
620        for (entity, &idx) in &self.aggregates {
621            let hit = if by_path { entity.full_path == name } else { entity.name == name };
622            if hit {
623                matches.push((idx, format!("aggregate `{}`", entity.full_path)));
624            }
625        }
626
627        for (entity, &idx) in &self.triggers {
628            let hit = if by_path { entity.full_path == name } else { entity.function_name == name };
629            if hit {
630                matches.push((idx, format!("trigger `{}`", entity.full_path)));
631            }
632        }
633
634        for (entity, &idx) in &self.extension_sqls {
635            if !by_path && entity.name == name {
636                matches.push((idx, format!("extension_sql `{}`", entity.name)));
637                continue;
638            }
639            for declared in &entity.creates {
640                let declared_name = match declared {
641                    SqlDeclaredEntity::Type(data) | SqlDeclaredEntity::Enum(data) => {
642                        data.name.as_str()
643                    }
644                    SqlDeclaredEntity::Function(data) => data.name.as_str(),
645                };
646                if declared_name == name {
647                    matches.push((
648                        idx,
649                        format!("extension_sql `{}` (declares `{declared_name}`)", entity.name),
650                    ));
651                    break;
652                }
653            }
654        }
655
656        for (entity, &idx) in &self.schemas {
657            if !by_path && entity.name == name {
658                matches.push((idx, format!("schema `{}`", entity.name)));
659            }
660        }
661
662        match matches.len() {
663            0 => Err(eyre!("no SQL entity matches `{name}`")),
664            1 => Ok(matches.remove(0).0),
665            _ => {
666                let labels = matches.iter().map(|(_, l)| l.as_str()).collect::<Vec<_>>().join(", ");
667                Err(eyre!(
668                    "`{name}` is ambiguous; matched: {labels}. Disambiguate with a `::`-qualified Rust path."
669                ))
670            }
671        }
672    }
673
674    /// Emit SQL for the given item names plus all transitive dependencies, in
675    /// dependency order, and substitute `'MODULE_PATHNAME'` with
676    /// `'$libdir/<lib_name>'` so the output can be replayed directly into a
677    /// database.
678    ///
679    /// When `extension_name` is `Some(name)`, the emitted slice is wrapped in
680    /// `BEGIN;`/`COMMIT;` and each created object is followed by an
681    /// `ALTER EXTENSION "<name>" ADD …` clause so that piping the output into
682    /// a database where the extension is already installed attaches the new
683    /// objects to the extension. When `None`, the pre-feature behavior is
684    /// used (no transaction wrapping, no ADD clauses).
685    ///
686    /// Warnings (e.g. for `extension_sql!()` blocks without `creates = [...]`)
687    /// are written to stderr. Use `emit_slice_with_warnings` directly if you
688    /// need to capture them.
689    pub fn to_sql_for_items(
690        &self,
691        item_names: &[String],
692        lib_name: &str,
693        extension_name: Option<&str>,
694    ) -> eyre::Result<String> {
695        self.emit_slice_with_warnings(item_names, lib_name, extension_name, |msg| {
696            eprintln!("{msg}");
697        })
698    }
699
700    /// Core of [`Self::to_sql_for_items`]. Takes a warning sink so tests
701    /// (and future non-stderr callers) can observe the diagnostics that
702    /// would otherwise go to stderr.
703    pub(crate) fn emit_slice_with_warnings<W: FnMut(String)>(
704        &self,
705        item_names: &[String],
706        lib_name: &str,
707        extension_name: Option<&str>,
708        warn: W,
709    ) -> eyre::Result<String> {
710        let mut targets = Vec::with_capacity(item_names.len());
711        for name in item_names {
712            targets.push(self.resolve_item(name)?);
713        }
714        self.emit_slice_from_nodes(&targets, lib_name, extension_name, warn)
715    }
716
717    /// Same as [`Self::emit_slice_with_warnings`] but takes already-resolved
718    /// node indices. Used by tests that need to target entities whose
719    /// resolution is ambiguous or not supported by [`Self::resolve_item`]
720    /// (e.g. `Ord` and `Hash` derives).
721    pub(crate) fn emit_slice_from_nodes<W: FnMut(String)>(
722        &self,
723        targets: &[NodeIndex],
724        lib_name: &str,
725        extension_name: Option<&str>,
726        mut warn: W,
727    ) -> eyre::Result<String> {
728        let keep = self.collect_transitive_deps(targets);
729
730        let mut body = String::new();
731        for nodes in petgraph::algo::tarjan_scc(&self.graph).iter().rev() {
732            let ordered = self.connected_component_emit_order(nodes);
733            let mut block = Vec::new();
734
735            for node in ordered {
736                if !keep.contains(&node) {
737                    continue;
738                }
739                let ent = &self.graph[node];
740
741                // The ExtensionRoot's CREATE-phase output is a trivial comment
742                // block that reads "auto generated by pgrx". Inside a slice
743                // aimed at an already-installed extension it would be strange
744                // and confusing, so skip it.
745                if matches!(ent, SqlGraphEntity::ExtensionRoot(_)) {
746                    continue;
747                }
748
749                let create_sql = ent.to_sql(self)?;
750                let create_sql = create_sql.trim();
751
752                let mut piece = String::new();
753                if !create_sql.is_empty() {
754                    piece.push_str(create_sql);
755                    piece.push('\n');
756                }
757
758                if let Some(ext) = extension_name {
759                    match self.render_alter_extension_for_node(node, ext)? {
760                        Some(alter_sql) => {
761                            piece.push_str(&alter_sql);
762                            if !alter_sql.ends_with('\n') {
763                                piece.push('\n');
764                            }
765                        }
766                        None => {
767                            if let SqlGraphEntity::CustomSql(c) = ent
768                                && c.creates.is_empty()
769                            {
770                                warn(format!(
771                                    "warning: extension_sql block at {}:{} does not declare `creates = [...]`; its objects won't be attached to the extension automatically",
772                                    c.file, c.line,
773                                ));
774                            }
775                        }
776                    }
777                }
778
779                if !piece.is_empty() {
780                    block.push(piece);
781                }
782            }
783
784            if !block.is_empty() {
785                body.push_str("/* <begin connected objects> */\n");
786                body.push_str(&block.join("\n"));
787                body.push_str("/* </end connected objects> */\n\n");
788            }
789        }
790
791        let replacement = format!("'$libdir/{lib_name}'");
792        let body = body.replace("'MODULE_PATHNAME'", &replacement);
793
794        Ok(match extension_name {
795            Some(_) => format!("BEGIN;\n\n{body}\nCOMMIT;\n"),
796            None => body,
797        })
798    }
799
800    /// Produce the `ALTER EXTENSION "<ext>" ADD …;` clauses for `node`, or
801    /// `Ok(None)` when the node is not an extension-attachable object
802    /// (builtin type, extension root, or a free-form `extension_sql!()`
803    /// block that didn't declare `creates = [...]`).
804    fn render_alter_extension_for_node(
805        &self,
806        node: NodeIndex,
807        extension_name: &str,
808    ) -> eyre::Result<Option<String>> {
809        let ent = &self.graph[node];
810        let ext = extension_name;
811
812        match ent {
813            SqlGraphEntity::Function(f) => {
814                let schema = f
815                    .schema
816                    .map(|s| format!("{s}."))
817                    .unwrap_or_else(|| self.schema_prefix_for(&node));
818                let argtypes = crate::pg_extern::entity::render_function_argtypes(self, node, f)?;
819                let mut out = format!(
820                    "ALTER EXTENSION \"{ext}\" ADD FUNCTION {schema}\"{name}\"({argtypes});",
821                    name = f.name
822                );
823
824                if let Some(op) = &f.operator
825                    && let Some(opname) = op.opname
826                {
827                    let left = f
828                        .fn_args
829                        .first()
830                        .ok_or_else(|| eyre!("operator `{}` missing left argument", f.name))?;
831                    let right = f
832                        .fn_args
833                        .get(1)
834                        .ok_or_else(|| eyre!("operator `{}` missing right argument", f.name))?;
835                    let left_sql = crate::pg_extern::entity::render_used_type_sql(
836                        self,
837                        node,
838                        "operator left argument",
839                        &left.used_ty,
840                    )?;
841                    let right_sql = crate::pg_extern::entity::render_used_type_sql(
842                        self,
843                        node,
844                        "operator right argument",
845                        &right.used_ty,
846                    )?;
847                    out.push('\n');
848                    out.push_str(&format!(
849                        "ALTER EXTENSION \"{ext}\" ADD OPERATOR {schema}{opname}({left_sql}, {right_sql});"
850                    ));
851                }
852
853                if f.cast.is_some() {
854                    let source = f
855                        .fn_args
856                        .first()
857                        .ok_or_else(|| eyre!("cast `{}` missing source argument", f.name))?;
858                    let source_sql = crate::pg_extern::entity::render_used_type_sql(
859                        self,
860                        node,
861                        "cast source type",
862                        &source.used_ty,
863                    )?;
864                    let target_sql =
865                        crate::pg_extern::entity::render_function_return_type(self, node, f)?;
866                    out.push('\n');
867                    out.push_str(&format!(
868                        "ALTER EXTENSION \"{ext}\" ADD CAST ({source_sql} AS {target_sql});"
869                    ));
870                }
871
872                Ok(Some(out))
873            }
874            SqlGraphEntity::Type(t) => {
875                let schema = self.schema_prefix_for(&node);
876                Ok(Some(format!(
877                    "ALTER EXTENSION \"{ext}\" ADD TYPE {schema}{name};",
878                    name = t.name
879                )))
880            }
881            SqlGraphEntity::Enum(e) => {
882                let schema = self.schema_prefix_for(&node);
883                Ok(Some(format!(
884                    "ALTER EXTENSION \"{ext}\" ADD TYPE {schema}{name};",
885                    name = e.name
886                )))
887            }
888            SqlGraphEntity::Aggregate(a) => {
889                let schema = self.schema_prefix_for(&node);
890                let argtypes = crate::aggregate::entity::render_aggregate_argtypes(self, node, a)?;
891                Ok(Some(format!(
892                    "ALTER EXTENSION \"{ext}\" ADD AGGREGATE {schema}\"{name}\"{argtypes};",
893                    name = a.name
894                )))
895            }
896            SqlGraphEntity::Trigger(t) => {
897                let schema = self.schema_prefix_for(&node);
898                Ok(Some(format!(
899                    "ALTER EXTENSION \"{ext}\" ADD FUNCTION {schema}\"{name}\"();",
900                    name = t.function_name
901                )))
902            }
903            SqlGraphEntity::Ord(o) => {
904                // Unqualified names: matches `PostgresOrdEntity::to_sql`, which
905                // also emits `{name}_btree_ops` without a schema prefix.
906                Ok(Some(format!(
907                    "ALTER EXTENSION \"{ext}\" ADD OPERATOR FAMILY {name}_btree_ops USING btree;\n\
908                     ALTER EXTENSION \"{ext}\" ADD OPERATOR CLASS {name}_btree_ops USING btree;",
909                    name = o.name
910                )))
911            }
912            SqlGraphEntity::Hash(h) => {
913                // Same unqualified-name rationale as Ord.
914                Ok(Some(format!(
915                    "ALTER EXTENSION \"{ext}\" ADD OPERATOR FAMILY {name}_hash_ops USING hash;\n\
916                     ALTER EXTENSION \"{ext}\" ADD OPERATOR CLASS {name}_hash_ops USING hash;",
917                    name = h.name
918                )))
919            }
920            SqlGraphEntity::Schema(s) => {
921                if matches!(s.name, "public" | "pg_catalog") {
922                    return Ok(None);
923                }
924                Ok(Some(format!("ALTER EXTENSION \"{ext}\" ADD SCHEMA {name};", name = s.name)))
925            }
926            SqlGraphEntity::CustomSql(c) => {
927                if c.creates.is_empty() {
928                    return Ok(None);
929                }
930                let mut out = String::new();
931                for (idx, declared) in c.creates.iter().enumerate() {
932                    if idx > 0 {
933                        out.push('\n');
934                    }
935                    match declared {
936                        SqlDeclaredEntity::Type(data) => {
937                            out.push_str(&format!(
938                                "ALTER EXTENSION \"{ext}\" ADD TYPE {};",
939                                data.sql
940                            ));
941                        }
942                        SqlDeclaredEntity::Enum(data) => {
943                            out.push_str(&format!(
944                                "ALTER EXTENSION \"{ext}\" ADD TYPE {};",
945                                data.sql
946                            ));
947                        }
948                        SqlDeclaredEntity::Function(data) => {
949                            out.push_str(&format!(
950                                "ALTER EXTENSION \"{ext}\" ADD FUNCTION {};",
951                                data.sql
952                            ));
953                        }
954                    }
955                }
956                Ok(Some(out))
957            }
958            SqlGraphEntity::BuiltinType(_) | SqlGraphEntity::ExtensionRoot(_) => Ok(None),
959        }
960    }
961
962    /// Collect every node reachable from `targets` by walking edges backward
963    /// (i.e. every dependency that must exist before the targets can be
964    /// created). The returned set always contains the targets themselves.
965    fn collect_transitive_deps(&self, targets: &[NodeIndex]) -> HashSet<NodeIndex> {
966        let mut visited = HashSet::new();
967        let mut queue = VecDeque::new();
968        for &t in targets {
969            if visited.insert(t) {
970                queue.push_back(t);
971            }
972        }
973        while let Some(node) = queue.pop_front() {
974            for predecessor in self.graph.neighbors_directed(node, Direction::Incoming) {
975                if visited.insert(predecessor) {
976                    queue.push_back(predecessor);
977                }
978            }
979        }
980        visited
981    }
982}
983
984fn build_base_edges<'a>(
985    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
986    index: NodeIndex,
987    root: NodeIndex,
988    bootstrap: Option<NodeIndex>,
989    finalize: Option<NodeIndex>,
990) {
991    graph.add_edge(root, index, SqlGraphRequires::By);
992    if let Some(bootstrap) = bootstrap {
993        graph.add_edge(bootstrap, index, SqlGraphRequires::By);
994    }
995    if let Some(finalize) = finalize {
996        graph.add_edge(index, finalize, SqlGraphRequires::By);
997    }
998}
999
1000#[allow(clippy::type_complexity)]
1001fn initialize_extension_sqls<'a>(
1002    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1003    root: NodeIndex,
1004    extension_sqls: Vec<ExtensionSqlEntity<'a>>,
1005) -> eyre::Result<(HashMap<ExtensionSqlEntity<'a>, NodeIndex>, Option<NodeIndex>, Option<NodeIndex>)>
1006{
1007    let mut bootstrap = None;
1008    let mut finalize = None;
1009    let mut mapped_extension_sqls = HashMap::default();
1010    for item in extension_sqls {
1011        let entity: SqlGraphEntity = item.clone().into();
1012        let index = graph.add_node(entity);
1013        mapped_extension_sqls.insert(item.clone(), index);
1014
1015        if item.bootstrap {
1016            if let Some(existing_index) = bootstrap {
1017                let existing: &SqlGraphEntity = &graph[existing_index];
1018                return Err(eyre!(
1019                    "Cannot have multiple `extension_sql!()` with `bootstrap` positioning, found `{}`, other was `{}`",
1020                    item.rust_identifier(),
1021                    existing.rust_identifier(),
1022                ));
1023            }
1024            bootstrap = Some(index)
1025        }
1026        if item.finalize {
1027            if let Some(existing_index) = finalize {
1028                let existing: &SqlGraphEntity = &graph[existing_index];
1029                return Err(eyre!(
1030                    "Cannot have multiple `extension_sql!()` with `finalize` positioning, found `{}`, other was `{}`",
1031                    item.rust_identifier(),
1032                    existing.rust_identifier(),
1033                ));
1034            }
1035            finalize = Some(index)
1036        }
1037    }
1038    for (item, index) in &mapped_extension_sqls {
1039        graph.add_edge(root, *index, SqlGraphRequires::By);
1040        if !item.bootstrap
1041            && let Some(bootstrap) = bootstrap
1042        {
1043            graph.add_edge(bootstrap, *index, SqlGraphRequires::By);
1044        }
1045        if !item.finalize
1046            && let Some(finalize) = finalize
1047        {
1048            graph.add_edge(*index, finalize, SqlGraphRequires::By);
1049        }
1050    }
1051    Ok((mapped_extension_sqls, bootstrap, finalize))
1052}
1053
1054/// A best effort attempt to find the related [`NodeIndex`] for some [`PositioningRef`].
1055pub fn find_positioning_ref_target<'a, 'b>(
1056    positioning_ref: &'b PositioningRef,
1057    types: &'b HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1058    enums: &'b HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1059    externs: &'b HashMap<PgExternEntity<'a>, NodeIndex>,
1060    schemas: &'b HashMap<SchemaEntity<'a>, NodeIndex>,
1061    extension_sqls: &'b HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1062    triggers: &'b HashMap<PgTriggerEntity<'a>, NodeIndex>,
1063) -> Option<&'b NodeIndex> {
1064    match positioning_ref {
1065        PositioningRef::FullPath(path) => {
1066            // The best we can do here is a fuzzy search.
1067            let segments = path.split("::").collect::<Vec<_>>();
1068            let last_segment = segments.last().expect("Expected at least one segment.");
1069            let rest = &segments[..segments.len() - 1];
1070            let module_path = rest.join("::");
1071
1072            for (other, other_index) in types {
1073                if *last_segment == other.name && other.module_path.ends_with(&module_path) {
1074                    return Some(other_index);
1075                }
1076            }
1077            for (other, other_index) in enums {
1078                if last_segment == &other.name && other.module_path.ends_with(&module_path) {
1079                    return Some(other_index);
1080                }
1081            }
1082            for (other, other_index) in externs {
1083                if *last_segment == other.unaliased_name
1084                    && other.module_path.ends_with(&module_path)
1085                {
1086                    return Some(other_index);
1087                }
1088            }
1089            for (other, other_index) in schemas {
1090                if other.module_path.ends_with(path) {
1091                    return Some(other_index);
1092                }
1093            }
1094
1095            for (other, other_index) in triggers {
1096                if last_segment == &other.function_name && other.module_path.ends_with(&module_path)
1097                {
1098                    return Some(other_index);
1099                }
1100            }
1101        }
1102        PositioningRef::Name(name) => {
1103            for (other, other_index) in extension_sqls {
1104                if other.name == name {
1105                    return Some(other_index);
1106                }
1107            }
1108        }
1109    };
1110    None
1111}
1112
1113fn connect_extension_sqls<'a>(
1114    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1115    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1116    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1117    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1118    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1119    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1120    triggers: &HashMap<PgTriggerEntity<'a>, NodeIndex>,
1121) -> eyre::Result<()> {
1122    for (item, &index) in extension_sqls {
1123        make_schema_connection(
1124            graph,
1125            "Extension SQL",
1126            index,
1127            &item.rust_identifier(),
1128            item.module_path,
1129            schemas,
1130        );
1131
1132        for requires in &item.requires {
1133            if let Some(target) = find_positioning_ref_target(
1134                requires,
1135                types,
1136                enums,
1137                externs,
1138                schemas,
1139                extension_sqls,
1140                triggers,
1141            ) {
1142                graph.add_edge(*target, index, SqlGraphRequires::By);
1143            } else {
1144                return Err(eyre!(
1145                    "Could not find `requires` target of `{}`{}: {}",
1146                    item.rust_identifier(),
1147                    match (item.file(), item.line()) {
1148                        (Some(file), Some(line)) => format!(" ({file}:{line})"),
1149                        _ => "".to_string(),
1150                    },
1151                    match requires {
1152                        PositioningRef::FullPath(path) => path.to_string(),
1153                        PositioningRef::Name(name) => format!(r#""{name}""#),
1154                    },
1155                ));
1156            }
1157        }
1158    }
1159    Ok(())
1160}
1161
1162fn initialize_schemas<'a>(
1163    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1164    bootstrap: Option<NodeIndex>,
1165    finalize: Option<NodeIndex>,
1166    schemas: Vec<SchemaEntity<'a>>,
1167) -> eyre::Result<HashMap<SchemaEntity<'a>, NodeIndex>> {
1168    let mut mapped_schemas = HashMap::default();
1169    for item in schemas {
1170        let entity = item.clone().into();
1171        let index = graph.add_node(entity);
1172        mapped_schemas.insert(item, index);
1173        if let Some(bootstrap) = bootstrap {
1174            graph.add_edge(bootstrap, index, SqlGraphRequires::By);
1175        }
1176        if let Some(finalize) = finalize {
1177            graph.add_edge(index, finalize, SqlGraphRequires::By);
1178        }
1179    }
1180    Ok(mapped_schemas)
1181}
1182
1183fn connect_schemas<'a>(
1184    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1185    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1186    root: NodeIndex,
1187) {
1188    for index in schemas.values().copied() {
1189        graph.add_edge(root, index, SqlGraphRequires::By);
1190    }
1191}
1192
1193fn initialize_enums<'a>(
1194    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1195    root: NodeIndex,
1196    bootstrap: Option<NodeIndex>,
1197    finalize: Option<NodeIndex>,
1198    enums: Vec<PostgresEnumEntity<'a>>,
1199) -> eyre::Result<HashMap<PostgresEnumEntity<'a>, NodeIndex>> {
1200    let mut mapped_enums = HashMap::default();
1201    for item in enums {
1202        let entity: SqlGraphEntity = item.clone().into();
1203        let index = graph.add_node(entity);
1204        mapped_enums.insert(item, index);
1205        build_base_edges(graph, index, root, bootstrap, finalize);
1206    }
1207    Ok(mapped_enums)
1208}
1209
1210fn connect_enums<'a>(
1211    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1212    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1213    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1214) {
1215    for (item, &index) in enums {
1216        make_schema_connection(
1217            graph,
1218            "Enum",
1219            index,
1220            &item.rust_identifier(),
1221            item.module_path,
1222            schemas,
1223        );
1224    }
1225}
1226
1227fn initialize_types<'a>(
1228    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1229    root: NodeIndex,
1230    bootstrap: Option<NodeIndex>,
1231    finalize: Option<NodeIndex>,
1232    types: Vec<PostgresTypeEntity<'a>>,
1233) -> eyre::Result<HashMap<PostgresTypeEntity<'a>, NodeIndex>> {
1234    let mut mapped_types = HashMap::default();
1235    for item in types {
1236        let entity = item.clone().into();
1237        let index = graph.add_node(entity);
1238        mapped_types.insert(item, index);
1239        build_base_edges(graph, index, root, bootstrap, finalize);
1240    }
1241    Ok(mapped_types)
1242}
1243
1244fn connect_types<'a>(
1245    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1246    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1247    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1248    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1249) -> eyre::Result<()> {
1250    for (item, &index) in types {
1251        make_schema_connection(
1252            graph,
1253            "Type",
1254            index,
1255            &item.rust_identifier(),
1256            item.module_path,
1257            schemas,
1258        );
1259
1260        make_extern_connection(
1261            graph,
1262            "Type",
1263            index,
1264            &item.rust_identifier(),
1265            &resolve_function_path(item.module_path, item.in_fn_path),
1266            externs,
1267        )?;
1268        make_extern_connection(
1269            graph,
1270            "Type",
1271            index,
1272            &item.rust_identifier(),
1273            &resolve_function_path(item.module_path, item.out_fn_path),
1274            externs,
1275        )?;
1276        if let Some(path) = item.receive_fn_path {
1277            make_extern_connection(
1278                graph,
1279                "Type",
1280                index,
1281                &item.rust_identifier(),
1282                &resolve_function_path(item.module_path, path),
1283                externs,
1284            )?;
1285        }
1286        if let Some(path) = item.send_fn_path {
1287            make_extern_connection(
1288                graph,
1289                "Type",
1290                index,
1291                &item.rust_identifier(),
1292                &resolve_function_path(item.module_path, path),
1293                externs,
1294            )?;
1295        }
1296    }
1297    Ok(())
1298}
1299
1300fn initialize_externs<'a>(
1301    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1302    root: NodeIndex,
1303    bootstrap: Option<NodeIndex>,
1304    finalize: Option<NodeIndex>,
1305    externs: Vec<PgExternEntity<'a>>,
1306    mapped_types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1307    mapped_enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1308    mapped_extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1309) -> eyre::Result<(HashMap<PgExternEntity<'a>, NodeIndex>, HashMap<String, NodeIndex>)> {
1310    let mut mapped_externs = HashMap::default();
1311    let mut mapped_builtin_types = HashMap::default();
1312    for item in externs {
1313        let entity: SqlGraphEntity = item.clone().into();
1314        let index = graph.add_node(entity.clone());
1315        mapped_externs.insert(item.clone(), index);
1316        build_base_edges(graph, index, root, bootstrap, finalize);
1317
1318        for arg in &item.fn_args {
1319            if !arg.used_ty.emits_argument_sql() || !arg.used_ty.needs_type_resolution() {
1320                continue;
1321            }
1322            let slot = format!("argument `{}`", arg.pattern);
1323            let (type_ident, type_origin) = arg
1324                .used_ty
1325                .resolution()
1326                .expect("SQL-visible extern arguments should carry resolution metadata");
1327            initialize_resolved_type(
1328                graph,
1329                &mut mapped_builtin_types,
1330                type_ident,
1331                type_origin,
1332                mapped_types,
1333                mapped_enums,
1334                mapped_extension_sqls,
1335                "Function",
1336                item.full_path,
1337                &slot,
1338                arg.used_ty.full_path,
1339            )?;
1340        }
1341
1342        match &item.fn_return {
1343            PgExternReturnEntity::None | PgExternReturnEntity::Trigger => (),
1344            PgExternReturnEntity::Type { ty, .. } | PgExternReturnEntity::SetOf { ty, .. } => {
1345                if let Some((type_ident, type_origin)) = ty.resolution() {
1346                    initialize_resolved_type(
1347                        graph,
1348                        &mut mapped_builtin_types,
1349                        type_ident,
1350                        type_origin,
1351                        mapped_types,
1352                        mapped_enums,
1353                        mapped_extension_sqls,
1354                        "Function",
1355                        item.full_path,
1356                        "return type",
1357                        ty.full_path,
1358                    )?;
1359                }
1360            }
1361            PgExternReturnEntity::Iterated { tys: iterated_returns, .. } => {
1362                for PgExternReturnEntityIteratedItem { ty, .. } in iterated_returns {
1363                    if let Some((type_ident, type_origin)) = ty.resolution() {
1364                        initialize_resolved_type(
1365                            graph,
1366                            &mut mapped_builtin_types,
1367                            type_ident,
1368                            type_origin,
1369                            mapped_types,
1370                            mapped_enums,
1371                            mapped_extension_sqls,
1372                            "Function",
1373                            item.full_path,
1374                            "table return column",
1375                            ty.full_path,
1376                        )?;
1377                    }
1378                }
1379            }
1380        }
1381    }
1382    Ok((mapped_externs, mapped_builtin_types))
1383}
1384
1385fn connect_externs<'a>(
1386    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1387    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1388    hashes: &HashMap<PostgresHashEntity<'a>, NodeIndex>,
1389    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1390    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1391    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1392    builtin_types: &HashMap<String, NodeIndex>,
1393    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1394    triggers: &HashMap<PgTriggerEntity<'a>, NodeIndex>,
1395) -> eyre::Result<()> {
1396    for (item, &index) in externs {
1397        let mut found_schema_declaration = false;
1398        for extern_attr in &item.extern_attrs {
1399            match extern_attr {
1400                crate::ExternArgs::Requires(requirements) => {
1401                    for requires in requirements {
1402                        if let Some(target) = find_positioning_ref_target(
1403                            requires,
1404                            types,
1405                            enums,
1406                            externs,
1407                            schemas,
1408                            extension_sqls,
1409                            triggers,
1410                        ) {
1411                            graph.add_edge(*target, index, SqlGraphRequires::By);
1412                        } else {
1413                            return Err(eyre!("Could not find `requires` target: {:?}", requires));
1414                        }
1415                    }
1416                }
1417                crate::ExternArgs::Support(support_fn) => {
1418                    if let Some(target) = find_positioning_ref_target(
1419                        support_fn,
1420                        types,
1421                        enums,
1422                        externs,
1423                        schemas,
1424                        extension_sqls,
1425                        triggers,
1426                    ) {
1427                        graph.add_edge(*target, index, SqlGraphRequires::By);
1428                    }
1429                }
1430                crate::ExternArgs::Schema(declared_schema_name) => {
1431                    for (schema, schema_index) in schemas {
1432                        if schema.name == declared_schema_name {
1433                            graph.add_edge(*schema_index, index, SqlGraphRequires::By);
1434                            found_schema_declaration = true;
1435                        }
1436                    }
1437                    if !found_schema_declaration {
1438                        return Err(eyre!(
1439                            "Got manual `schema = \"{declared_schema_name}\"` setting, but that schema did not exist."
1440                        ));
1441                    }
1442                }
1443                _ => (),
1444            }
1445        }
1446
1447        if !found_schema_declaration {
1448            make_schema_connection(
1449                graph,
1450                "Extern",
1451                index,
1452                &item.rust_identifier(),
1453                item.module_path,
1454                schemas,
1455            );
1456        }
1457
1458        // The hash function must be defined after the {typename}_eq function.
1459        for (hash_item, &hash_index) in hashes {
1460            if item.module_path == hash_item.module_path
1461                && item.name == hash_item.name.to_lowercase() + "_eq"
1462            {
1463                graph.add_edge(index, hash_index, SqlGraphRequires::By);
1464            }
1465        }
1466
1467        for arg in &item.fn_args {
1468            if !arg.used_ty.emits_argument_sql() || !arg.used_ty.needs_type_resolution() {
1469                continue;
1470            }
1471            let slot = format!("argument `{}`", arg.pattern);
1472            let (type_ident, type_origin) = arg
1473                .used_ty
1474                .resolution()
1475                .expect("SQL-visible extern arguments should carry resolution metadata");
1476            connect_resolved_type(
1477                graph,
1478                index,
1479                SqlGraphRequires::ByArg,
1480                type_ident,
1481                type_origin,
1482                types,
1483                enums,
1484                builtin_types,
1485                extension_sqls,
1486                "Function",
1487                item.full_path,
1488                &slot,
1489                arg.used_ty.full_path,
1490            )?;
1491        }
1492
1493        match &item.fn_return {
1494            PgExternReturnEntity::None | PgExternReturnEntity::Trigger => (),
1495            PgExternReturnEntity::Type { ty, .. } | PgExternReturnEntity::SetOf { ty, .. } => {
1496                if let Some((type_ident, type_origin)) = ty.resolution() {
1497                    connect_resolved_type(
1498                        graph,
1499                        index,
1500                        SqlGraphRequires::ByReturn,
1501                        type_ident,
1502                        type_origin,
1503                        types,
1504                        enums,
1505                        builtin_types,
1506                        extension_sqls,
1507                        "Function",
1508                        item.full_path,
1509                        "return type",
1510                        ty.full_path,
1511                    )?;
1512                }
1513            }
1514            PgExternReturnEntity::Iterated { tys: iterated_returns, .. } => {
1515                for PgExternReturnEntityIteratedItem { ty, .. } in iterated_returns {
1516                    if let Some((type_ident, type_origin)) = ty.resolution() {
1517                        connect_resolved_type(
1518                            graph,
1519                            index,
1520                            SqlGraphRequires::ByReturn,
1521                            type_ident,
1522                            type_origin,
1523                            types,
1524                            enums,
1525                            builtin_types,
1526                            extension_sqls,
1527                            "Function",
1528                            item.full_path,
1529                            "table return column",
1530                            ty.full_path,
1531                        )?;
1532                    }
1533                }
1534            }
1535        }
1536    }
1537    Ok(())
1538}
1539
1540fn initialize_ords<'a>(
1541    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1542    root: NodeIndex,
1543    bootstrap: Option<NodeIndex>,
1544    finalize: Option<NodeIndex>,
1545    ords: Vec<PostgresOrdEntity<'a>>,
1546) -> eyre::Result<HashMap<PostgresOrdEntity<'a>, NodeIndex>> {
1547    let mut mapped_ords = HashMap::default();
1548    for item in ords {
1549        let entity = item.clone().into();
1550        let index = graph.add_node(entity);
1551        mapped_ords.insert(item.clone(), index);
1552        build_base_edges(graph, index, root, bootstrap, finalize);
1553    }
1554    Ok(mapped_ords)
1555}
1556
1557fn connect_ords<'a>(
1558    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1559    ords: &HashMap<PostgresOrdEntity<'a>, NodeIndex>,
1560    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1561    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1562    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1563    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1564) {
1565    for (item, &index) in ords {
1566        make_schema_connection(
1567            graph,
1568            "Ord",
1569            index,
1570            &item.rust_identifier(),
1571            item.module_path,
1572            schemas,
1573        );
1574
1575        make_type_or_enum_connection(graph, index, item.type_ident, types, enums);
1576
1577        // Make PostgresOrdEntities (which will be translated into `CREATE OPERATOR CLASS` statements) depend
1578        // on the operators which they will reference. For example, a pgrx-defined Postgres type `parakeet`
1579        // which has `#[derive(PostgresOrd)]` will emit a `parakeet_btree_ops` operator class, which references
1580        // a definition of a < operator (among others) on the `parakeet` type. This code should ensure that the
1581        // < operator (along with all the others) is emitted before the `OPERATOR CLASS` itself.
1582
1583        for (extern_item, &extern_index) in externs {
1584            let fn_matches = |fn_name| {
1585                item.module_path == extern_item.module_path && extern_item.name == fn_name
1586            };
1587            let cmp_fn_matches = fn_matches(item.cmp_fn_name());
1588            let lt_fn_matches = fn_matches(item.lt_fn_name());
1589            let lte_fn_matches = fn_matches(item.le_fn_name());
1590            let eq_fn_matches = fn_matches(item.eq_fn_name());
1591            let gt_fn_matches = fn_matches(item.gt_fn_name());
1592            let gte_fn_matches = fn_matches(item.ge_fn_name());
1593            if cmp_fn_matches
1594                || lt_fn_matches
1595                || lte_fn_matches
1596                || eq_fn_matches
1597                || gt_fn_matches
1598                || gte_fn_matches
1599            {
1600                graph.add_edge(extern_index, index, SqlGraphRequires::By);
1601            }
1602        }
1603    }
1604}
1605
1606fn initialize_hashes<'a>(
1607    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1608    root: NodeIndex,
1609    bootstrap: Option<NodeIndex>,
1610    finalize: Option<NodeIndex>,
1611    hashes: Vec<PostgresHashEntity<'a>>,
1612) -> eyre::Result<HashMap<PostgresHashEntity<'a>, NodeIndex>> {
1613    let mut mapped_hashes = HashMap::default();
1614    for item in hashes {
1615        let entity: SqlGraphEntity = item.clone().into();
1616        let index = graph.add_node(entity);
1617        mapped_hashes.insert(item, index);
1618        build_base_edges(graph, index, root, bootstrap, finalize);
1619    }
1620    Ok(mapped_hashes)
1621}
1622
1623fn connect_hashes<'a>(
1624    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1625    hashes: &HashMap<PostgresHashEntity<'a>, NodeIndex>,
1626    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1627    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1628    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1629    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1630) {
1631    for (item, &index) in hashes {
1632        make_schema_connection(
1633            graph,
1634            "Hash",
1635            index,
1636            &item.rust_identifier(),
1637            item.module_path,
1638            schemas,
1639        );
1640
1641        make_type_or_enum_connection(graph, index, item.type_ident, types, enums);
1642
1643        if let Some((_, extern_index)) = externs.iter().find(|(extern_item, _)| {
1644            item.module_path == extern_item.module_path && extern_item.name == item.fn_name()
1645        }) {
1646            graph.add_edge(*extern_index, index, SqlGraphRequires::By);
1647        }
1648    }
1649}
1650
1651fn initialize_aggregates<'a>(
1652    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1653    root: NodeIndex,
1654    bootstrap: Option<NodeIndex>,
1655    finalize: Option<NodeIndex>,
1656    aggregates: Vec<PgAggregateEntity<'a>>,
1657    mapped_builtin_types: &mut HashMap<String, NodeIndex>,
1658    mapped_enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1659    mapped_types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1660    mapped_extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1661) -> eyre::Result<HashMap<PgAggregateEntity<'a>, NodeIndex>> {
1662    let mut mapped_aggregates = HashMap::default();
1663    for item in aggregates {
1664        let entity: SqlGraphEntity = item.clone().into();
1665        let index = graph.add_node(entity);
1666
1667        for arg in &item.args {
1668            if !arg.used_ty.needs_type_resolution() {
1669                continue;
1670            }
1671            let slot = aggregate_slot(arg.name, "argument");
1672            let (type_ident, type_origin) = arg
1673                .used_ty
1674                .resolution()
1675                .expect("aggregate arguments should carry resolution metadata");
1676            initialize_resolved_type(
1677                graph,
1678                mapped_builtin_types,
1679                type_ident,
1680                type_origin,
1681                mapped_types,
1682                mapped_enums,
1683                mapped_extension_sqls,
1684                "Aggregate",
1685                item.full_path,
1686                &slot,
1687                arg.used_ty.full_path,
1688            )?;
1689        }
1690
1691        for arg in item.direct_args.as_ref().unwrap_or(&vec![]) {
1692            if !arg.used_ty.needs_type_resolution() {
1693                continue;
1694            }
1695            let slot = aggregate_slot(arg.name, "direct argument");
1696            let (type_ident, type_origin) = arg
1697                .used_ty
1698                .resolution()
1699                .expect("aggregate direct arguments should carry resolution metadata");
1700            initialize_resolved_type(
1701                graph,
1702                mapped_builtin_types,
1703                type_ident,
1704                type_origin,
1705                mapped_types,
1706                mapped_enums,
1707                mapped_extension_sqls,
1708                "Aggregate",
1709                item.full_path,
1710                &slot,
1711                arg.used_ty.full_path,
1712            )?;
1713        }
1714
1715        if let Some((type_ident, type_origin)) = item.stype.used_ty.resolution() {
1716            initialize_resolved_type(
1717                graph,
1718                mapped_builtin_types,
1719                type_ident,
1720                type_origin,
1721                mapped_types,
1722                mapped_enums,
1723                mapped_extension_sqls,
1724                "Aggregate",
1725                item.full_path,
1726                "STYPE",
1727                item.stype.used_ty.full_path,
1728            )?;
1729        }
1730
1731        if let Some(arg) = &item.mstype
1732            && let Some((type_ident, type_origin)) = arg.resolution()
1733        {
1734            initialize_resolved_type(
1735                graph,
1736                mapped_builtin_types,
1737                type_ident,
1738                type_origin,
1739                mapped_types,
1740                mapped_enums,
1741                mapped_extension_sqls,
1742                "Aggregate",
1743                item.full_path,
1744                "MSTYPE",
1745                arg.full_path,
1746            )?;
1747        }
1748
1749        mapped_aggregates.insert(item, index);
1750        build_base_edges(graph, index, root, bootstrap, finalize);
1751    }
1752    Ok(mapped_aggregates)
1753}
1754
1755fn connect_aggregate<'a>(
1756    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1757    item: &PgAggregateEntity<'a>,
1758    index: NodeIndex,
1759    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1760    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1761    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1762    builtin_types: &HashMap<String, NodeIndex>,
1763    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1764    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1765) -> eyre::Result<()> {
1766    make_schema_connection(
1767        graph,
1768        "Aggregate",
1769        index,
1770        &item.rust_identifier(),
1771        item.module_path,
1772        schemas,
1773    );
1774
1775    for arg in &item.args {
1776        if !arg.used_ty.needs_type_resolution() {
1777            continue;
1778        }
1779        let slot = aggregate_slot(arg.name, "argument");
1780        let (type_ident, type_origin) =
1781            arg.used_ty.resolution().expect("aggregate arguments should carry resolution metadata");
1782        connect_resolved_type(
1783            graph,
1784            index,
1785            SqlGraphRequires::ByArg,
1786            type_ident,
1787            type_origin,
1788            types,
1789            enums,
1790            builtin_types,
1791            extension_sqls,
1792            "Aggregate",
1793            item.full_path,
1794            &slot,
1795            arg.used_ty.full_path,
1796        )?;
1797    }
1798
1799    for arg in item.direct_args.as_ref().unwrap_or(&vec![]) {
1800        if !arg.used_ty.needs_type_resolution() {
1801            continue;
1802        }
1803        let slot = aggregate_slot(arg.name, "direct argument");
1804        let (type_ident, type_origin) = arg
1805            .used_ty
1806            .resolution()
1807            .expect("aggregate direct arguments should carry resolution metadata");
1808        connect_resolved_type(
1809            graph,
1810            index,
1811            SqlGraphRequires::ByArg,
1812            type_ident,
1813            type_origin,
1814            types,
1815            enums,
1816            builtin_types,
1817            extension_sqls,
1818            "Aggregate",
1819            item.full_path,
1820            &slot,
1821            arg.used_ty.full_path,
1822        )?;
1823    }
1824
1825    if let Some(arg) = &item.mstype
1826        && let Some((type_ident, type_origin)) = arg.resolution()
1827    {
1828        connect_resolved_type(
1829            graph,
1830            index,
1831            SqlGraphRequires::ByArg,
1832            type_ident,
1833            type_origin,
1834            types,
1835            enums,
1836            builtin_types,
1837            extension_sqls,
1838            "Aggregate",
1839            item.full_path,
1840            "MSTYPE",
1841            arg.full_path,
1842        )?;
1843    }
1844
1845    if let Some((type_ident, type_origin)) = item.stype.used_ty.resolution() {
1846        connect_resolved_type(
1847            graph,
1848            index,
1849            SqlGraphRequires::ByArg,
1850            type_ident,
1851            type_origin,
1852            types,
1853            enums,
1854            builtin_types,
1855            extension_sqls,
1856            "Aggregate",
1857            item.full_path,
1858            "STYPE",
1859            item.stype.used_ty.full_path,
1860        )?;
1861    }
1862
1863    make_extern_connection(
1864        graph,
1865        "Aggregate",
1866        index,
1867        &item.rust_identifier(),
1868        &(item.module_path.to_string() + "::" + item.sfunc),
1869        externs,
1870    )?;
1871
1872    if let Some(value) = item.finalfunc {
1873        make_extern_connection(
1874            graph,
1875            "Aggregate",
1876            index,
1877            &item.rust_identifier(),
1878            &(item.module_path.to_string() + "::" + value),
1879            externs,
1880        )?;
1881    }
1882    if let Some(value) = item.combinefunc {
1883        make_extern_connection(
1884            graph,
1885            "Aggregate",
1886            index,
1887            &item.rust_identifier(),
1888            &(item.module_path.to_string() + "::" + value),
1889            externs,
1890        )?;
1891    }
1892    if let Some(value) = item.serialfunc {
1893        make_extern_connection(
1894            graph,
1895            "Aggregate",
1896            index,
1897            &item.rust_identifier(),
1898            &(item.module_path.to_string() + "::" + value),
1899            externs,
1900        )?;
1901    }
1902    if let Some(value) = item.deserialfunc {
1903        make_extern_connection(
1904            graph,
1905            "Aggregate",
1906            index,
1907            &item.rust_identifier(),
1908            &(item.module_path.to_string() + "::" + value),
1909            externs,
1910        )?;
1911    }
1912    if let Some(value) = item.msfunc {
1913        make_extern_connection(
1914            graph,
1915            "Aggregate",
1916            index,
1917            &item.rust_identifier(),
1918            &(item.module_path.to_string() + "::" + value),
1919            externs,
1920        )?;
1921    }
1922    if let Some(value) = item.minvfunc {
1923        make_extern_connection(
1924            graph,
1925            "Aggregate",
1926            index,
1927            &item.rust_identifier(),
1928            &(item.module_path.to_string() + "::" + value),
1929            externs,
1930        )?;
1931    }
1932    if let Some(value) = item.mfinalfunc {
1933        make_extern_connection(
1934            graph,
1935            "Aggregate",
1936            index,
1937            &item.rust_identifier(),
1938            &(item.module_path.to_string() + "::" + value),
1939            externs,
1940        )?;
1941    }
1942    if let Some(value) = item.sortop {
1943        make_extern_connection(
1944            graph,
1945            "Aggregate",
1946            index,
1947            &item.rust_identifier(),
1948            &(item.module_path.to_string() + "::" + value),
1949            externs,
1950        )?;
1951    }
1952    Ok(())
1953}
1954
1955fn connect_aggregates<'a>(
1956    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1957    aggregates: &HashMap<PgAggregateEntity<'a>, NodeIndex>,
1958    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
1959    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
1960    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
1961    builtin_types: &HashMap<String, NodeIndex>,
1962    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
1963    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
1964) -> eyre::Result<()> {
1965    for (item, &index) in aggregates {
1966        connect_aggregate(
1967            graph,
1968            item,
1969            index,
1970            schemas,
1971            types,
1972            enums,
1973            builtin_types,
1974            externs,
1975            extension_sqls,
1976        )?
1977    }
1978    Ok(())
1979}
1980
1981fn initialize_triggers<'a>(
1982    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
1983    root: NodeIndex,
1984    bootstrap: Option<NodeIndex>,
1985    finalize: Option<NodeIndex>,
1986    triggers: Vec<PgTriggerEntity<'a>>,
1987) -> eyre::Result<HashMap<PgTriggerEntity<'a>, NodeIndex>> {
1988    let mut mapped_triggers = HashMap::default();
1989    for item in triggers {
1990        let entity: SqlGraphEntity = item.clone().into();
1991        let index = graph.add_node(entity);
1992
1993        mapped_triggers.insert(item, index);
1994        build_base_edges(graph, index, root, bootstrap, finalize);
1995    }
1996    Ok(mapped_triggers)
1997}
1998
1999fn connect_triggers<'a>(
2000    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2001    triggers: &HashMap<PgTriggerEntity<'a>, NodeIndex>,
2002    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
2003) {
2004    for (item, &index) in triggers {
2005        make_schema_connection(
2006            graph,
2007            "Trigger",
2008            index,
2009            &item.rust_identifier(),
2010            item.module_path,
2011            schemas,
2012        );
2013    }
2014}
2015
2016fn make_schema_connection<'a>(
2017    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2018    _kind: &str,
2019    index: NodeIndex,
2020    _rust_identifier: &str,
2021    module_path: &str,
2022    schemas: &HashMap<SchemaEntity<'a>, NodeIndex>,
2023) -> bool {
2024    let mut found = false;
2025    for (schema_item, &schema_index) in schemas {
2026        if module_path == schema_item.module_path {
2027            graph.add_edge(schema_index, index, SqlGraphRequires::By);
2028            found = true;
2029            break;
2030        }
2031    }
2032    found
2033}
2034
2035fn make_extern_connection<'a>(
2036    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2037    _kind: &str,
2038    index: NodeIndex,
2039    _rust_identifier: &str,
2040    full_path: &str,
2041    externs: &HashMap<PgExternEntity<'a>, NodeIndex>,
2042) -> eyre::Result<()> {
2043    match externs.iter().find(|(extern_item, _)| full_path == extern_item.full_path) {
2044        Some((_, extern_index)) => {
2045            graph.add_edge(*extern_index, index, SqlGraphRequires::By);
2046            Ok(())
2047        }
2048        None => Err(eyre!("Did not find connection `{full_path}` in {:#?}", {
2049            let mut paths = externs.keys().map(|v| v.full_path).collect::<Vec<_>>();
2050            paths.sort();
2051            paths
2052        })),
2053    }
2054}
2055
2056fn resolve_function_path(module_path: &str, path: &str) -> String {
2057    if path.contains("::") { path.to_string() } else { format!("{module_path}::{path}") }
2058}
2059
2060fn aggregate_slot(name: Option<&str>, kind: &str) -> String {
2061    name.map(|name| format!("{kind} `{name}`")).unwrap_or_else(|| kind.to_string())
2062}
2063
2064fn find_type_or_enum<'a>(
2065    type_ident: &str,
2066    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2067    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2068) -> Option<NodeIndex> {
2069    types
2070        .iter()
2071        .map(type_keyed)
2072        .chain(enums.iter().map(type_keyed))
2073        .find(|(ty, _)| ty.matches_type_ident(type_ident))
2074        .map(|(_, index)| *index)
2075}
2076
2077fn find_declared_type_or_enum<'a>(
2078    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
2079    type_ident: &str,
2080) -> Option<NodeIndex> {
2081    extension_sqls.iter().find_map(|(item, index)| {
2082        item.creates
2083            .iter()
2084            .any(|declared| declared.matches_type_ident(type_ident))
2085            .then_some(*index)
2086    })
2087}
2088
2089fn find_graph_type_target<'a>(
2090    type_ident: &str,
2091    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2092    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2093    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
2094) -> Option<NodeIndex> {
2095    find_type_or_enum(type_ident, types, enums)
2096        .or_else(|| find_declared_type_or_enum(extension_sqls, type_ident))
2097}
2098
2099fn ensure_unique_type_targets<'a>(
2100    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2101    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2102    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
2103) -> eyre::Result<()> {
2104    let mut seen = BTreeMap::<String, Vec<String>>::new();
2105
2106    for item in types.keys() {
2107        seen.entry(item.type_ident.to_string())
2108            .or_default()
2109            .push(format!("type `{}`", item.full_path));
2110    }
2111
2112    for item in enums.keys() {
2113        seen.entry(item.type_ident.to_string())
2114            .or_default()
2115            .push(format!("enum `{}`", item.full_path));
2116    }
2117
2118    for item in extension_sqls.keys() {
2119        for declared in &item.creates {
2120            if let Some(type_ident) = declared.type_ident() {
2121                seen.entry(type_ident.to_string())
2122                    .or_default()
2123                    .push(format!("extension_sql `{}` ({declared})", item.name));
2124            }
2125        }
2126    }
2127
2128    for locations in seen.values_mut() {
2129        locations.sort();
2130    }
2131
2132    if let Some((type_ident, locations)) =
2133        seen.into_iter().find(|(_, locations)| locations.len() > 1)
2134    {
2135        return Err(eyre!(
2136            "type ident `{type_ident}` matched multiple SQL entities: {}",
2137            locations.join(", ")
2138        ));
2139    }
2140
2141    Ok(())
2142}
2143
2144fn unresolved_type_ident(
2145    owner_kind: &str,
2146    owner_name: &str,
2147    slot: &str,
2148    ty_name: &str,
2149    type_ident: &str,
2150) -> eyre::Report {
2151    eyre!(
2152        "{owner_kind} `{owner_name}` uses `{ty_name}` as {slot}, but type ident `{type_ident}` did not resolve. use `pgrx::pgrx_resolved_type!(T)` together with a matching `#[derive(PostgresType)]`, `#[derive(PostgresEnum)]`, or `extension_sql!(..., creates = [Type(T)]/[Enum(T)])`. for a manual mapping to an existing SQL type, set `TYPE_ORIGIN = TypeOrigin::External`."
2153    )
2154}
2155
2156fn initialize_resolved_type<'a>(
2157    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2158    builtin_types: &mut HashMap<String, NodeIndex>,
2159    type_ident: &str,
2160    type_origin: TypeOrigin,
2161    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2162    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2163    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
2164    owner_kind: &str,
2165    owner_name: &str,
2166    slot: &str,
2167    ty_name: &str,
2168) -> eyre::Result<()> {
2169    if find_graph_type_target(type_ident, types, enums, extension_sqls).is_some() {
2170        return Ok(());
2171    }
2172
2173    if matches!(type_origin, TypeOrigin::External) {
2174        builtin_types
2175            .entry(type_ident.to_string())
2176            .or_insert_with(|| graph.add_node(SqlGraphEntity::BuiltinType(type_ident.to_string())));
2177        return Ok(());
2178    }
2179
2180    Err(unresolved_type_ident(owner_kind, owner_name, slot, ty_name, type_ident))
2181}
2182
2183fn connect_resolved_type<'a>(
2184    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2185    index: NodeIndex,
2186    requires: SqlGraphRequires,
2187    type_ident: &str,
2188    type_origin: TypeOrigin,
2189    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2190    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2191    builtin_types: &HashMap<String, NodeIndex>,
2192    extension_sqls: &HashMap<ExtensionSqlEntity<'a>, NodeIndex>,
2193    owner_kind: &str,
2194    owner_name: &str,
2195    slot: &str,
2196    ty_name: &str,
2197) -> eyre::Result<()> {
2198    if let Some(ty_index) = find_graph_type_target(type_ident, types, enums, extension_sqls) {
2199        graph.add_edge(ty_index, index, requires);
2200        return Ok(());
2201    }
2202
2203    if let Some(builtin_index) = builtin_types.get(type_ident) {
2204        graph.add_edge(*builtin_index, index, requires);
2205        return Ok(());
2206    }
2207
2208    if matches!(type_origin, TypeOrigin::External) {
2209        return Err(eyre!(
2210            "missing external-type placeholder for type ident `{type_ident}` while connecting {owner_kind} `{owner_name}` {slot}"
2211        ));
2212    }
2213
2214    Err(unresolved_type_ident(owner_kind, owner_name, slot, ty_name, type_ident))
2215}
2216
2217fn make_type_or_enum_connection<'a>(
2218    graph: &mut StableGraph<SqlGraphEntity<'a>, SqlGraphRequires>,
2219    index: NodeIndex,
2220    type_ident: &str,
2221    types: &HashMap<PostgresTypeEntity<'a>, NodeIndex>,
2222    enums: &HashMap<PostgresEnumEntity<'a>, NodeIndex>,
2223) -> bool {
2224    find_type_or_enum(type_ident, types, enums)
2225        .map(|ty_index| graph.add_edge(ty_index, index, SqlGraphRequires::By))
2226        .is_some()
2227}
2228
2229#[cfg(test)]
2230mod tests {
2231    use super::*;
2232    use crate::UsedTypeEntity;
2233    use crate::aggregate::entity::{AggregateTypeEntity, PgAggregateEntity};
2234    use crate::extension_sql::entity::{
2235        ExtensionSqlEntity, SqlDeclaredEntity, SqlDeclaredTypeEntityData,
2236    };
2237    use crate::extern_args::ExternArgs;
2238    use crate::metadata::{FunctionMetadataTypeEntity, Returns, SqlArrayMapping, SqlMapping};
2239    use crate::pg_extern::entity::{
2240        PgExternArgumentEntity, PgExternEntity, PgExternReturnEntity, PgOperatorEntity,
2241    };
2242    use crate::pg_trigger::entity::PgTriggerEntity;
2243    use crate::postgres_enum::entity::PostgresEnumEntity;
2244    use crate::postgres_hash::entity::PostgresHashEntity;
2245    use crate::postgres_ord::entity::PostgresOrdEntity;
2246    use crate::postgres_type::entity::PostgresTypeEntity;
2247    use crate::schema::entity::SchemaEntity;
2248    use crate::to_sql::entity::ToSqlConfigEntity;
2249
2250    fn control_file() -> ControlFile {
2251        ControlFile {
2252            comment: "test".into(),
2253            default_version: "1.0".into(),
2254            module_pathname: None,
2255            relocatable: false,
2256            superuser: true,
2257            schema: None,
2258            trusted: false,
2259        }
2260    }
2261
2262    fn to_sql_config() -> ToSqlConfigEntity<'static> {
2263        ToSqlConfigEntity { enabled: true, content: None }
2264    }
2265
2266    fn used_type(
2267        full_path: &'static str,
2268        type_ident: &'static str,
2269        sql: &'static str,
2270        type_origin: TypeOrigin,
2271    ) -> UsedTypeEntity<'static> {
2272        UsedTypeEntity {
2273            ty_source: full_path,
2274            full_path,
2275            composite_type: None,
2276            variadic: false,
2277            default: None,
2278            optional: false,
2279            metadata: FunctionMetadataTypeEntity::resolved(
2280                type_ident,
2281                type_origin,
2282                Ok(SqlMapping::literal(sql)),
2283                Ok(Returns::One(SqlMapping::literal(sql))),
2284            ),
2285        }
2286    }
2287
2288    fn external_type(
2289        full_path: &'static str,
2290        type_ident: &'static str,
2291        sql: &'static str,
2292    ) -> UsedTypeEntity<'static> {
2293        used_type(full_path, type_ident, sql, TypeOrigin::External)
2294    }
2295
2296    fn extension_owned_type(
2297        full_path: &'static str,
2298        type_ident: &'static str,
2299        sql: &'static str,
2300    ) -> UsedTypeEntity<'static> {
2301        used_type(full_path, type_ident, sql, TypeOrigin::ThisExtension)
2302    }
2303
2304    fn function_entity(
2305        name: &'static str,
2306        fn_args: Vec<PgExternArgumentEntity<'static>>,
2307        fn_return: PgExternReturnEntity<'static>,
2308    ) -> PgExternEntity<'static> {
2309        PgExternEntity {
2310            name,
2311            unaliased_name: name,
2312            module_path: "tests",
2313            full_path: Box::leak(format!("tests::{name}").into_boxed_str()),
2314            fn_args,
2315            fn_return,
2316            schema: None,
2317            file: "test.rs",
2318            line: 1,
2319            extern_attrs: vec![],
2320            search_path: None,
2321            operator: None,
2322            cast: None,
2323            to_sql_config: to_sql_config(),
2324        }
2325    }
2326
2327    fn aggregate_entity(
2328        name: &'static str,
2329        args: Vec<AggregateTypeEntity<'static>>,
2330        stype: UsedTypeEntity<'static>,
2331        mstype: Option<UsedTypeEntity<'static>>,
2332    ) -> PgAggregateEntity<'static> {
2333        PgAggregateEntity {
2334            full_path: Box::leak(format!("tests::{name}").into_boxed_str()),
2335            module_path: "tests",
2336            file: "test.rs",
2337            line: 1,
2338            name,
2339            ordered_set: false,
2340            args,
2341            direct_args: None,
2342            stype: AggregateTypeEntity { used_ty: stype, name: None },
2343            sfunc: "state_fn",
2344            finalfunc: None,
2345            finalfunc_modify: None,
2346            combinefunc: None,
2347            serialfunc: None,
2348            deserialfunc: None,
2349            initcond: None,
2350            msfunc: None,
2351            minvfunc: None,
2352            mstype,
2353            mfinalfunc: None,
2354            mfinalfunc_modify: None,
2355            minitcond: None,
2356            sortop: None,
2357            parallel: None,
2358            hypothetical: false,
2359            to_sql_config: to_sql_config(),
2360        }
2361    }
2362
2363    fn declared_type_sql(
2364        module_path: &'static str,
2365        full_path: &'static str,
2366        declaration_name: &'static str,
2367        name: &'static str,
2368        type_ident: &'static str,
2369        sql: &'static str,
2370    ) -> ExtensionSqlEntity<'static> {
2371        ExtensionSqlEntity {
2372            module_path,
2373            full_path,
2374            sql: "CREATE TYPE custom_type;",
2375            file: "test.rs",
2376            line: 1,
2377            name: declaration_name,
2378            bootstrap: false,
2379            finalize: false,
2380            requires: vec![],
2381            creates: vec![SqlDeclaredEntity::Type(SqlDeclaredTypeEntityData {
2382                sql: sql.into(),
2383                name: name.into(),
2384                type_ident: type_ident.into(),
2385            })],
2386        }
2387    }
2388
2389    fn schema_entity(module_path: &'static str, name: &'static str) -> SchemaEntity<'static> {
2390        SchemaEntity { module_path, name, file: "test.rs", line: 1 }
2391    }
2392
2393    fn type_entity(
2394        name: &'static str,
2395        full_path: &'static str,
2396        type_ident: &'static str,
2397    ) -> PostgresTypeEntity<'static> {
2398        PostgresTypeEntity {
2399            name,
2400            file: "test.rs",
2401            line: 1,
2402            full_path,
2403            module_path: "tests",
2404            type_ident,
2405            in_fn_path: "in_fn",
2406            out_fn_path: "out_fn",
2407            receive_fn_path: None,
2408            send_fn_path: None,
2409            to_sql_config: to_sql_config(),
2410            alignment: None,
2411        }
2412    }
2413
2414    fn state_function() -> PgExternEntity<'static> {
2415        function_entity("state_fn", vec![], PgExternReturnEntity::None)
2416    }
2417
2418    #[test]
2419    fn external_function_type_resolution_succeeds() {
2420        let manual_text =
2421            used_type("tests::ManualText", "tests::ManualText", "TEXT", TypeOrigin::External);
2422        let function = function_entity(
2423            "manual_text_echo",
2424            vec![PgExternArgumentEntity { pattern: "value", used_ty: manual_text.clone() }],
2425            PgExternReturnEntity::Type { ty: manual_text.clone() },
2426        );
2427
2428        let sql = PgrxSql::build(
2429            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2430                .into_iter(),
2431            "test".into(),
2432            false,
2433        )
2434        .unwrap();
2435
2436        assert!(sql.builtin_types.contains_key("tests::ManualText"));
2437    }
2438
2439    fn skipped_type(full_path: &'static str, type_ident: &'static str) -> UsedTypeEntity<'static> {
2440        UsedTypeEntity {
2441            ty_source: full_path,
2442            full_path,
2443            composite_type: None,
2444            variadic: false,
2445            default: None,
2446            optional: false,
2447            metadata: FunctionMetadataTypeEntity::resolved(
2448                type_ident,
2449                TypeOrigin::ThisExtension,
2450                Ok(SqlMapping::Skip),
2451                Ok(Returns::One(SqlMapping::Skip)),
2452            ),
2453        }
2454    }
2455
2456    fn explicit_composite_type(name: &'static str) -> UsedTypeEntity<'static> {
2457        UsedTypeEntity {
2458            ty_source: "pgrx::heap_tuple::PgHeapTuple<'static, AllocatedByRust>",
2459            full_path: "pgrx::heap_tuple::PgHeapTuple<'static, AllocatedByRust>",
2460            composite_type: Some(name),
2461            variadic: false,
2462            default: None,
2463            optional: false,
2464            metadata: FunctionMetadataTypeEntity::sql_only(
2465                Ok(SqlMapping::Composite),
2466                Ok(Returns::One(SqlMapping::Composite)),
2467            ),
2468        }
2469    }
2470
2471    fn explicit_composite_array_type(name: &'static str) -> UsedTypeEntity<'static> {
2472        UsedTypeEntity {
2473            ty_source: "pgrx::heap_tuple::PgHeapTuple<'static, AllocatedByRust>",
2474            full_path: "pgrx::heap_tuple::PgHeapTuple<'static, AllocatedByRust>",
2475            composite_type: Some(name),
2476            variadic: false,
2477            default: None,
2478            optional: false,
2479            metadata: FunctionMetadataTypeEntity::sql_only(
2480                Ok(SqlMapping::Array(SqlArrayMapping::Composite)),
2481                Ok(Returns::One(SqlMapping::Array(SqlArrayMapping::Composite))),
2482            ),
2483        }
2484    }
2485
2486    #[test]
2487    fn extension_sql_declared_type_orders_before_function_and_aggregate() {
2488        let custom_type = extension_owned_type("tests::HexInt", "tests::HexInt", "hexint");
2489        let declared_type = declared_type_sql(
2490            "tests",
2491            "tests::concrete_type",
2492            "concrete_type",
2493            "tests::HexInt",
2494            "tests::HexInt",
2495            "hexint",
2496        );
2497        let function = function_entity(
2498            "takes_hexint",
2499            vec![PgExternArgumentEntity { pattern: "value", used_ty: custom_type.clone() }],
2500            PgExternReturnEntity::None,
2501        );
2502        let aggregate = aggregate_entity(
2503            "hexint_accum",
2504            vec![AggregateTypeEntity { used_ty: custom_type.clone(), name: Some("value") }],
2505            custom_type.clone(),
2506            Some(custom_type.clone()),
2507        );
2508        let state_fn = state_function();
2509
2510        let sql = PgrxSql::build(
2511            vec![
2512                SqlGraphEntity::ExtensionRoot(control_file()),
2513                SqlGraphEntity::CustomSql(declared_type.clone()),
2514                SqlGraphEntity::Function(state_fn),
2515                SqlGraphEntity::Function(function.clone()),
2516                SqlGraphEntity::Aggregate(aggregate.clone()),
2517            ]
2518            .into_iter(),
2519            "test".into(),
2520            false,
2521        )
2522        .unwrap();
2523
2524        let declared_index = sql.extension_sqls[&declared_type];
2525        let function_index = sql.externs[&function];
2526        let aggregate_index = sql.aggregates[&aggregate];
2527
2528        assert!(!sql.builtin_types.contains_key("tests::HexInt"));
2529        assert!(sql.graph.find_edge(declared_index, function_index).is_some());
2530        assert!(sql.graph.find_edge(declared_index, aggregate_index).is_some());
2531    }
2532
2533    #[test]
2534    fn declared_type_cycle_prefers_explicit_requirements_with_shell_type() {
2535        let custom_type = extension_owned_type("tests::HexInt", "tests::HexInt", "hexint");
2536        let text_type = external_type("alloc::string::String", "alloc::string::String", "text");
2537
2538        let shell_type = ExtensionSqlEntity {
2539            module_path: "tests",
2540            full_path: "tests::shell_type",
2541            sql: "CREATE TYPE hexint;",
2542            file: "test.rs",
2543            line: 1,
2544            name: "shell_type",
2545            bootstrap: true,
2546            finalize: false,
2547            requires: vec![],
2548            creates: vec![],
2549        };
2550
2551        let mut hexint_in = function_entity(
2552            "hexint_in",
2553            vec![],
2554            PgExternReturnEntity::Type { ty: custom_type.clone() },
2555        );
2556        hexint_in.extern_attrs =
2557            vec![ExternArgs::Requires(vec![PositioningRef::Name("shell_type".into())])];
2558
2559        let mut hexint_out = function_entity(
2560            "hexint_out",
2561            vec![PgExternArgumentEntity { pattern: "value", used_ty: custom_type.clone() }],
2562            PgExternReturnEntity::Type { ty: text_type },
2563        );
2564        hexint_out.extern_attrs =
2565            vec![ExternArgs::Requires(vec![PositioningRef::Name("shell_type".into())])];
2566
2567        let mut declared_type = declared_type_sql(
2568            "tests",
2569            "tests::concrete_type",
2570            "concrete_type",
2571            "tests::HexInt",
2572            "tests::HexInt",
2573            "hexint",
2574        );
2575        declared_type.sql = "CREATE TYPE hexint (\n    INPUT = hexint_in,\n    OUTPUT = hexint_out,\n    LIKE = int8\n);";
2576        declared_type.requires = vec![
2577            PositioningRef::Name("shell_type".into()),
2578            PositioningRef::FullPath("tests::hexint_in".into()),
2579            PositioningRef::FullPath("tests::hexint_out".into()),
2580        ];
2581
2582        let sql = PgrxSql::build(
2583            vec![
2584                SqlGraphEntity::ExtensionRoot(control_file()),
2585                SqlGraphEntity::CustomSql(shell_type),
2586                SqlGraphEntity::CustomSql(declared_type),
2587                SqlGraphEntity::Function(hexint_in),
2588                SqlGraphEntity::Function(hexint_out),
2589            ]
2590            .into_iter(),
2591            "test".into(),
2592            false,
2593        )
2594        .unwrap()
2595        .to_sql()
2596        .unwrap();
2597
2598        let shell = sql.find("CREATE TYPE hexint;").unwrap();
2599        let input = sql.find("-- tests::hexint_in").unwrap();
2600        let output = sql.find("-- tests::hexint_out").unwrap();
2601        let concrete = sql.find("CREATE TYPE hexint (\n").unwrap();
2602
2603        assert!(shell < input);
2604        assert!(shell < output);
2605        assert!(input < concrete);
2606        assert!(output < concrete);
2607    }
2608
2609    #[test]
2610    fn extension_sql_declared_type_in_custom_schema_prefixes_aggregate_state_type() {
2611        let custom_type = extension_owned_type("tests::HexInt", "tests::HexInt", "hexint");
2612        let declared_type = declared_type_sql(
2613            "tests::custom_schema",
2614            "tests::custom_schema::hexint_sql",
2615            "hexint_sql",
2616            "tests::HexInt",
2617            "tests::HexInt",
2618            "hexint",
2619        );
2620        let aggregate =
2621            aggregate_entity("hexint_accum", vec![], custom_type.clone(), Some(custom_type));
2622        let state_fn = state_function();
2623        let schema = schema_entity("tests::custom_schema", "custom_schema");
2624
2625        let sql = PgrxSql::build(
2626            vec![
2627                SqlGraphEntity::ExtensionRoot(control_file()),
2628                SqlGraphEntity::Schema(schema),
2629                SqlGraphEntity::CustomSql(declared_type),
2630                SqlGraphEntity::Function(state_fn),
2631                SqlGraphEntity::Aggregate(aggregate),
2632            ]
2633            .into_iter(),
2634            "test".into(),
2635            false,
2636        )
2637        .unwrap()
2638        .to_sql()
2639        .unwrap();
2640
2641        assert!(sql.contains("STYPE = custom_schema.hexint"));
2642        assert!(sql.contains("MSTYPE = custom_schema.hexint"));
2643    }
2644
2645    #[test]
2646    fn skipped_function_argument_does_not_require_schema_resolution() {
2647        let function = function_entity(
2648            "skipped_arg",
2649            vec![PgExternArgumentEntity {
2650                pattern: "virtual_arg",
2651                used_ty: skipped_type("tests::VirtualArg", "tests::VirtualArg"),
2652            }],
2653            PgExternReturnEntity::None,
2654        );
2655
2656        let sql = PgrxSql::build(
2657            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2658                .into_iter(),
2659            "test".into(),
2660            false,
2661        )
2662        .unwrap()
2663        .to_sql()
2664        .unwrap();
2665
2666        assert!(sql.contains("skipped_arg"));
2667        assert!(!sql.contains("virtual_arg"));
2668        assert!(!sql.contains("tests::VirtualArg"));
2669    }
2670
2671    #[test]
2672    fn explicit_composite_type_does_not_require_schema_resolution() {
2673        let dog = explicit_composite_type("Dog");
2674        assert!(!dog.needs_type_resolution());
2675
2676        let function = function_entity("make_dog", vec![], PgExternReturnEntity::Type { ty: dog });
2677
2678        let sql = PgrxSql::build(
2679            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2680                .into_iter(),
2681            "test".into(),
2682            false,
2683        )
2684        .unwrap()
2685        .to_sql()
2686        .unwrap();
2687
2688        assert!(sql.contains("RETURNS Dog"));
2689    }
2690
2691    #[test]
2692    fn explicit_composite_array_type_does_not_require_schema_resolution() {
2693        let dog_pack = explicit_composite_array_type("Dog");
2694        assert!(!dog_pack.needs_type_resolution());
2695
2696        let function =
2697            function_entity("make_dog_pack", vec![], PgExternReturnEntity::Type { ty: dog_pack });
2698
2699        let sql = PgrxSql::build(
2700            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2701                .into_iter(),
2702            "test".into(),
2703            false,
2704        )
2705        .unwrap()
2706        .to_sql()
2707        .unwrap();
2708
2709        assert!(sql.contains("RETURNS Dog[]"));
2710    }
2711
2712    #[test]
2713    fn explicit_composite_array_aggregate_state_does_not_require_schema_resolution() {
2714        let stype = explicit_composite_array_type("Dog");
2715        assert!(!stype.needs_type_resolution());
2716        let mstype = explicit_composite_array_type("Dog");
2717        assert!(!mstype.needs_type_resolution());
2718
2719        let aggregate = aggregate_entity("pack_dogs", vec![], stype, Some(mstype));
2720
2721        let sql = PgrxSql::build(
2722            vec![
2723                SqlGraphEntity::ExtensionRoot(control_file()),
2724                SqlGraphEntity::Function(state_function()),
2725                SqlGraphEntity::Aggregate(aggregate),
2726            ]
2727            .into_iter(),
2728            "test".into(),
2729            false,
2730        )
2731        .unwrap()
2732        .to_sql()
2733        .unwrap();
2734
2735        assert!(sql.contains("STYPE = Dog[]"));
2736        assert!(sql.contains("MSTYPE = Dog[]"));
2737    }
2738
2739    #[test]
2740    fn duplicate_type_ident_errors() {
2741        let left = type_entity("LeftType", "tests::LeftType", "tests::SharedType");
2742        let right = type_entity("RightType", "tests::RightType", "tests::SharedType");
2743
2744        let error = PgrxSql::build(
2745            vec![
2746                SqlGraphEntity::ExtensionRoot(control_file()),
2747                SqlGraphEntity::Type(left),
2748                SqlGraphEntity::Type(right),
2749            ]
2750            .into_iter(),
2751            "test".into(),
2752            false,
2753        )
2754        .expect_err("duplicate type idents should fail");
2755
2756        assert!(error.to_string().contains("tests::SharedType"));
2757        assert!(error.to_string().contains("tests::LeftType"));
2758        assert!(error.to_string().contains("tests::RightType"));
2759    }
2760
2761    #[test]
2762    fn unresolved_function_argument_type_ident_errors() {
2763        let bad_type = extension_owned_type("tests::BadArg", "tests::BadArg", "TEXT");
2764        let function = function_entity(
2765            "bad_arg",
2766            vec![PgExternArgumentEntity { pattern: "value", used_ty: bad_type }],
2767            PgExternReturnEntity::None,
2768        );
2769
2770        let error = PgrxSql::build(
2771            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2772                .into_iter(),
2773            "test".into(),
2774            false,
2775        )
2776        .expect_err("function argument should fail");
2777
2778        assert!(error.to_string().contains("Function `tests::bad_arg`"));
2779        assert!(error.to_string().contains("argument `value`"));
2780        assert!(error.to_string().contains("tests::BadArg"));
2781    }
2782
2783    #[test]
2784    fn unresolved_function_return_type_ident_errors() {
2785        let bad_type = extension_owned_type("tests::BadReturn", "tests::BadReturn", "TEXT");
2786        let function =
2787            function_entity("bad_return", vec![], PgExternReturnEntity::Type { ty: bad_type });
2788
2789        let error = PgrxSql::build(
2790            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(function)]
2791                .into_iter(),
2792            "test".into(),
2793            false,
2794        )
2795        .expect_err("function return should fail");
2796
2797        assert!(error.to_string().contains("Function `tests::bad_return`"));
2798        assert!(error.to_string().contains("return type"));
2799        assert!(error.to_string().contains("tests::BadReturn"));
2800    }
2801
2802    #[test]
2803    fn unresolved_aggregate_argument_type_ident_errors() {
2804        let aggregate = aggregate_entity(
2805            "bad_aggregate_arg",
2806            vec![AggregateTypeEntity {
2807                used_ty: extension_owned_type("tests::BadArg", "tests::BadArg", "TEXT"),
2808                name: Some("value"),
2809            }],
2810            external_type("tests::State", "tests::State", "TEXT"),
2811            None,
2812        );
2813
2814        let error = PgrxSql::build(
2815            vec![
2816                SqlGraphEntity::ExtensionRoot(control_file()),
2817                SqlGraphEntity::Function(state_function()),
2818                SqlGraphEntity::Aggregate(aggregate),
2819            ]
2820            .into_iter(),
2821            "test".into(),
2822            false,
2823        )
2824        .expect_err("aggregate argument should fail");
2825
2826        assert!(error.to_string().contains("Aggregate `tests::bad_aggregate_arg`"));
2827        assert!(error.to_string().contains("argument `value`"));
2828        assert!(error.to_string().contains("tests::BadArg"));
2829    }
2830
2831    #[test]
2832    fn unresolved_aggregate_stype_type_ident_errors() {
2833        let aggregate = aggregate_entity(
2834            "bad_aggregate_stype",
2835            vec![],
2836            extension_owned_type("tests::BadState", "tests::BadState", "TEXT"),
2837            None,
2838        );
2839
2840        let error = PgrxSql::build(
2841            vec![
2842                SqlGraphEntity::ExtensionRoot(control_file()),
2843                SqlGraphEntity::Function(state_function()),
2844                SqlGraphEntity::Aggregate(aggregate),
2845            ]
2846            .into_iter(),
2847            "test".into(),
2848            false,
2849        )
2850        .expect_err("aggregate stype should fail");
2851
2852        assert!(error.to_string().contains("Aggregate `tests::bad_aggregate_stype`"));
2853        assert!(error.to_string().contains("STYPE"));
2854        assert!(error.to_string().contains("tests::BadState"));
2855    }
2856
2857    #[test]
2858    fn unresolved_aggregate_mstype_type_ident_errors() {
2859        let aggregate = aggregate_entity(
2860            "bad_aggregate_mstype",
2861            vec![],
2862            external_type("tests::State", "tests::State", "TEXT"),
2863            Some(extension_owned_type("tests::BadMovingState", "tests::BadMovingState", "TEXT")),
2864        );
2865
2866        let error = PgrxSql::build(
2867            vec![
2868                SqlGraphEntity::ExtensionRoot(control_file()),
2869                SqlGraphEntity::Function(state_function()),
2870                SqlGraphEntity::Aggregate(aggregate),
2871            ]
2872            .into_iter(),
2873            "test".into(),
2874            false,
2875        )
2876        .expect_err("aggregate mstype should fail");
2877
2878        assert!(error.to_string().contains("Aggregate `tests::bad_aggregate_mstype`"));
2879        assert!(error.to_string().contains("MSTYPE"));
2880        assert!(error.to_string().contains("tests::BadMovingState"));
2881    }
2882
2883    #[test]
2884    fn to_sql_for_items_emits_only_targets_and_deps_with_lib_substitution() {
2885        let hexint = extension_owned_type("tests::HexInt", "tests::HexInt", "hexint");
2886        let declared = declared_type_sql(
2887            "tests",
2888            "tests::concrete_type",
2889            "concrete_type",
2890            "tests::HexInt",
2891            "tests::HexInt",
2892            "hexint",
2893        );
2894        let target =
2895            function_entity("emit_me", vec![], PgExternReturnEntity::Type { ty: hexint.clone() });
2896        let unused = function_entity(
2897            "leave_me_out",
2898            vec![],
2899            PgExternReturnEntity::Type {
2900                ty: external_type("alloc::string::String", "alloc::string::String", "text"),
2901            },
2902        );
2903
2904        let pgrx_sql = PgrxSql::build(
2905            vec![
2906                SqlGraphEntity::ExtensionRoot(control_file()),
2907                SqlGraphEntity::CustomSql(declared),
2908                SqlGraphEntity::Function(target),
2909                SqlGraphEntity::Function(unused),
2910            ]
2911            .into_iter(),
2912            "myext".into(),
2913            false,
2914        )
2915        .unwrap();
2916
2917        let sliced = pgrx_sql
2918            .to_sql_for_items(&["emit_me".into()], "myext", None)
2919            .expect("slice emission should succeed");
2920
2921        assert!(sliced.contains("emit_me"), "target function missing:\n{sliced}");
2922        assert!(sliced.contains("CREATE TYPE custom_type;"), "transitive dep missing:\n{sliced}");
2923        assert!(!sliced.contains("leave_me_out"), "unrelated function leaked:\n{sliced}");
2924        assert!(
2925            sliced.contains("'$libdir/myext'"),
2926            "MODULE_PATHNAME should be substituted:\n{sliced}"
2927        );
2928        assert!(!sliced.contains("'MODULE_PATHNAME'"), "raw placeholder remained:\n{sliced}");
2929    }
2930
2931    #[test]
2932    fn resolve_item_rejects_ambiguous_name_without_path() {
2933        let dup_a = function_entity("dup_fn", vec![], PgExternReturnEntity::None);
2934        let mut dup_b = function_entity("dup_fn", vec![], PgExternReturnEntity::None);
2935        dup_b.module_path = "tests::other";
2936        dup_b.full_path = "tests::other::dup_fn";
2937
2938        let pgrx_sql = PgrxSql::build(
2939            vec![
2940                SqlGraphEntity::ExtensionRoot(control_file()),
2941                SqlGraphEntity::Function(dup_a),
2942                SqlGraphEntity::Function(dup_b),
2943            ]
2944            .into_iter(),
2945            "test".into(),
2946            false,
2947        )
2948        .unwrap();
2949
2950        let err = pgrx_sql.resolve_item("dup_fn").expect_err("ambiguous name should fail");
2951        let msg = err.to_string();
2952        assert!(msg.contains("ambiguous"), "expected ambiguity error, got: {msg}");
2953        assert!(msg.contains("tests::dup_fn"), "got: {msg}");
2954        assert!(msg.contains("tests::other::dup_fn"), "got: {msg}");
2955
2956        let unique =
2957            pgrx_sql.resolve_item("tests::other::dup_fn").expect("qualified path should resolve");
2958        assert_eq!(pgrx_sql.graph[unique].rust_identifier(), "tests::other::dup_fn");
2959    }
2960
2961    fn slice_with_warnings(
2962        sql: &PgrxSql,
2963        items: &[String],
2964        lib_name: &str,
2965        ext: Option<&str>,
2966    ) -> (String, Vec<String>) {
2967        let mut warnings: Vec<String> = Vec::new();
2968        let out = sql
2969            .emit_slice_with_warnings(items, lib_name, ext, |msg| warnings.push(msg))
2970            .expect("slice emission should succeed");
2971        (out, warnings)
2972    }
2973
2974    fn slice_by_nodes(
2975        sql: &PgrxSql,
2976        targets: &[NodeIndex],
2977        lib_name: &str,
2978        ext: Option<&str>,
2979    ) -> (String, Vec<String>) {
2980        let mut warnings: Vec<String> = Vec::new();
2981        let out = sql
2982            .emit_slice_from_nodes(targets, lib_name, ext, |msg| warnings.push(msg))
2983            .expect("slice emission should succeed");
2984        (out, warnings)
2985    }
2986
2987    fn trigger_entity(function_name: &'static str) -> PgTriggerEntity<'static> {
2988        PgTriggerEntity {
2989            function_name,
2990            to_sql_config: to_sql_config(),
2991            file: "test.rs",
2992            line: 1,
2993            module_path: "tests",
2994            full_path: Box::leak(format!("tests::{function_name}").into_boxed_str()),
2995        }
2996    }
2997
2998    fn ord_entity(name: &'static str) -> PostgresOrdEntity<'static> {
2999        // full_path lives under `ord_for::` to avoid colliding with the
3000        // underlying type's full_path (which is `tests::{name}`). The `name`
3001        // field is what appears in CREATE OPERATOR FAMILY / CLASS.
3002        PostgresOrdEntity {
3003            name,
3004            file: "test.rs",
3005            line: 1,
3006            full_path: Box::leak(format!("tests::ord_for::{name}").into_boxed_str()),
3007            module_path: "tests::ord_for",
3008            type_ident: Box::leak(format!("tests::{name}").into_boxed_str()),
3009            to_sql_config: to_sql_config(),
3010        }
3011    }
3012
3013    fn hash_entity(name: &'static str) -> PostgresHashEntity<'static> {
3014        // Same disambiguation rationale as `ord_entity`.
3015        PostgresHashEntity {
3016            name,
3017            file: "test.rs",
3018            line: 1,
3019            full_path: Box::leak(format!("tests::hash_for::{name}").into_boxed_str()),
3020            module_path: "tests::hash_for",
3021            type_ident: Box::leak(format!("tests::{name}").into_boxed_str()),
3022            to_sql_config: to_sql_config(),
3023        }
3024    }
3025
3026    fn enum_entity(name: &'static str) -> PostgresEnumEntity<'static> {
3027        PostgresEnumEntity {
3028            name,
3029            file: "test.rs",
3030            line: 1,
3031            full_path: Box::leak(format!("tests::{name}").into_boxed_str()),
3032            module_path: "tests",
3033            type_ident: Box::leak(format!("tests::{name}").into_boxed_str()),
3034            variants: vec!["red", "green", "blue"],
3035            to_sql_config: to_sql_config(),
3036        }
3037    }
3038
3039    #[test]
3040    fn alter_extension_attaches_bare_function() {
3041        let fun = function_entity("state_fn", vec![], PgExternReturnEntity::None);
3042        let sql = PgrxSql::build(
3043            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(fun)]
3044                .into_iter(),
3045            "myext".into(),
3046            false,
3047        )
3048        .unwrap();
3049
3050        let (out, warnings) =
3051            slice_with_warnings(&sql, &["state_fn".into()], "myext", Some("myext"));
3052        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
3053        assert!(out.starts_with("BEGIN;"), "missing BEGIN:\n{out}");
3054        assert!(out.trim_end().ends_with("COMMIT;"), "missing COMMIT:\n{out}");
3055        assert!(
3056            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "state_fn"();"#),
3057            "missing ADD FUNCTION:\n{out}"
3058        );
3059    }
3060
3061    #[test]
3062    fn alter_extension_includes_argument_types() {
3063        let arg_ty = external_type("alloc::string::String", "alloc::string::String", "text");
3064        let fun = function_entity(
3065            "takes_text",
3066            vec![PgExternArgumentEntity { pattern: "value", used_ty: arg_ty }],
3067            PgExternReturnEntity::None,
3068        );
3069        let sql = PgrxSql::build(
3070            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(fun)]
3071                .into_iter(),
3072            "myext".into(),
3073            false,
3074        )
3075        .unwrap();
3076
3077        let (out, _) = slice_with_warnings(&sql, &["takes_text".into()], "myext", Some("myext"));
3078        assert!(
3079            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "takes_text"(text);"#),
3080            "missing argtype in ADD FUNCTION:\n{out}"
3081        );
3082    }
3083
3084    #[test]
3085    fn alter_extension_attaches_operator_in_addition_to_function() {
3086        let arg_ty = external_type("alloc::string::String", "alloc::string::String", "text");
3087        let mut fun = function_entity(
3088            "eq_ignoring_case",
3089            vec![
3090                PgExternArgumentEntity { pattern: "lhs", used_ty: arg_ty.clone() },
3091                PgExternArgumentEntity { pattern: "rhs", used_ty: arg_ty },
3092            ],
3093            PgExternReturnEntity::Type { ty: external_type("bool", "bool", "bool") },
3094        );
3095        fun.operator = Some(PgOperatorEntity {
3096            opname: Some("==="),
3097            commutator: None,
3098            negator: None,
3099            restrict: None,
3100            join: None,
3101            hashes: false,
3102            merges: false,
3103        });
3104
3105        let sql = PgrxSql::build(
3106            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(fun)]
3107                .into_iter(),
3108            "myext".into(),
3109            false,
3110        )
3111        .unwrap();
3112
3113        let (out, _) =
3114            slice_with_warnings(&sql, &["eq_ignoring_case".into()], "myext", Some("myext"));
3115        assert!(
3116            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "eq_ignoring_case"(text, text);"#),
3117            "missing ADD FUNCTION:\n{out}"
3118        );
3119        assert!(
3120            out.contains(r#"ALTER EXTENSION "myext" ADD OPERATOR ===(text, text);"#),
3121            "missing ADD OPERATOR:\n{out}"
3122        );
3123    }
3124
3125    #[test]
3126    fn alter_extension_attaches_type_and_its_io_functions() {
3127        let ty = type_entity("MyType", "tests::MyType", "tests::MyType");
3128        let in_fn = function_entity(
3129            "in_fn",
3130            vec![PgExternArgumentEntity {
3131                pattern: "input",
3132                used_ty: external_type("&core::ffi::CStr", "&core::ffi::CStr", "cstring"),
3133            }],
3134            PgExternReturnEntity::Type {
3135                ty: used_type(
3136                    "tests::MyType",
3137                    "tests::MyType",
3138                    "MyType",
3139                    TypeOrigin::ThisExtension,
3140                ),
3141            },
3142        );
3143        let out_fn = function_entity(
3144            "out_fn",
3145            vec![PgExternArgumentEntity {
3146                pattern: "input",
3147                used_ty: used_type(
3148                    "tests::MyType",
3149                    "tests::MyType",
3150                    "MyType",
3151                    TypeOrigin::ThisExtension,
3152                ),
3153            }],
3154            PgExternReturnEntity::Type {
3155                ty: external_type("alloc::ffi::CString", "alloc::ffi::CString", "cstring"),
3156            },
3157        );
3158
3159        let sql = PgrxSql::build(
3160            vec![
3161                SqlGraphEntity::ExtensionRoot(control_file()),
3162                SqlGraphEntity::Type(ty),
3163                SqlGraphEntity::Function(in_fn),
3164                SqlGraphEntity::Function(out_fn),
3165            ]
3166            .into_iter(),
3167            "myext".into(),
3168            false,
3169        )
3170        .unwrap();
3171
3172        let (out, _) = slice_with_warnings(&sql, &["tests::MyType".into()], "myext", Some("myext"));
3173        assert!(
3174            out.contains(r#"ALTER EXTENSION "myext" ADD TYPE MyType;"#),
3175            "missing ADD TYPE:\n{out}"
3176        );
3177        assert!(
3178            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "in_fn"(cstring);"#),
3179            "missing ADD FUNCTION for in_fn:\n{out}"
3180        );
3181        assert!(
3182            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "out_fn"(MyType);"#),
3183            "missing ADD FUNCTION for out_fn:\n{out}"
3184        );
3185    }
3186
3187    #[test]
3188    fn alter_extension_attaches_enum() {
3189        let en = enum_entity("Color");
3190        let sql = PgrxSql::build(
3191            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Enum(en)]
3192                .into_iter(),
3193            "myext".into(),
3194            false,
3195        )
3196        .unwrap();
3197
3198        let (out, _) = slice_with_warnings(&sql, &["Color".into()], "myext", Some("myext"));
3199        assert!(
3200            out.contains(r#"ALTER EXTENSION "myext" ADD TYPE Color;"#),
3201            "missing ADD TYPE:\n{out}"
3202        );
3203    }
3204
3205    #[test]
3206    fn alter_extension_attaches_aggregate_with_args() {
3207        let stype = external_type("tests::State", "tests::State", "TEXT");
3208        let arg_ty = external_type("i32", "i32", "integer");
3209        let agg = aggregate_entity(
3210            "sum_my",
3211            vec![AggregateTypeEntity { used_ty: arg_ty, name: Some("value") }],
3212            stype,
3213            None,
3214        );
3215
3216        let sql = PgrxSql::build(
3217            vec![
3218                SqlGraphEntity::ExtensionRoot(control_file()),
3219                SqlGraphEntity::Function(state_function()),
3220                SqlGraphEntity::Aggregate(agg),
3221            ]
3222            .into_iter(),
3223            "myext".into(),
3224            false,
3225        )
3226        .unwrap();
3227
3228        let (out, _) = slice_with_warnings(&sql, &["sum_my".into()], "myext", Some("myext"));
3229        assert!(
3230            out.contains(r#"ALTER EXTENSION "myext" ADD AGGREGATE "sum_my"(integer);"#),
3231            "missing ADD AGGREGATE:\n{out}"
3232        );
3233    }
3234
3235    #[test]
3236    fn alter_extension_attaches_trigger() {
3237        let trig = trigger_entity("my_trig");
3238        let sql = PgrxSql::build(
3239            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Trigger(trig)]
3240                .into_iter(),
3241            "myext".into(),
3242            false,
3243        )
3244        .unwrap();
3245
3246        let (out, _) = slice_with_warnings(&sql, &["my_trig".into()], "myext", Some("myext"));
3247        assert!(
3248            out.contains(r#"ALTER EXTENSION "myext" ADD FUNCTION "my_trig"();"#),
3249            "missing ADD FUNCTION (trigger):\n{out}"
3250        );
3251    }
3252
3253    #[test]
3254    fn alter_extension_attaches_ord_emits_family_and_class() {
3255        // Build the underlying type + comparison functions so the Ord node
3256        // has something to connect to. We don't assert on those; we only
3257        // care that the Ord node's ALTER EXTENSION clauses come out right.
3258        let mut ty = type_entity("Sortable", "tests::Sortable", "tests::Sortable");
3259        ty.in_fn_path = "sortable_in";
3260        ty.out_fn_path = "sortable_out";
3261        let text = external_type("alloc::string::String", "alloc::string::String", "text");
3262        let cstring = external_type("&core::ffi::CStr", "&core::ffi::CStr", "cstring");
3263        let in_fn = function_entity(
3264            "sortable_in",
3265            vec![PgExternArgumentEntity { pattern: "input", used_ty: cstring }],
3266            PgExternReturnEntity::Type {
3267                ty: used_type(
3268                    "tests::Sortable",
3269                    "tests::Sortable",
3270                    "Sortable",
3271                    TypeOrigin::ThisExtension,
3272                ),
3273            },
3274        );
3275        let out_fn = function_entity(
3276            "sortable_out",
3277            vec![PgExternArgumentEntity {
3278                pattern: "input",
3279                used_ty: used_type(
3280                    "tests::Sortable",
3281                    "tests::Sortable",
3282                    "Sortable",
3283                    TypeOrigin::ThisExtension,
3284                ),
3285            }],
3286            PgExternReturnEntity::Type { ty: text },
3287        );
3288        let ord = ord_entity("Sortable");
3289
3290        let sql = PgrxSql::build(
3291            vec![
3292                SqlGraphEntity::ExtensionRoot(control_file()),
3293                SqlGraphEntity::Type(ty),
3294                SqlGraphEntity::Function(in_fn),
3295                SqlGraphEntity::Function(out_fn),
3296                SqlGraphEntity::Ord(ord.clone()),
3297            ]
3298            .into_iter(),
3299            "myext".into(),
3300            false,
3301        )
3302        .unwrap();
3303
3304        let ord_idx = sql.ords[&ord];
3305        let (out, _) = slice_by_nodes(&sql, &[ord_idx], "myext", Some("myext"));
3306        assert!(
3307            out.contains(
3308                r#"ALTER EXTENSION "myext" ADD OPERATOR FAMILY Sortable_btree_ops USING btree;"#
3309            ),
3310            "missing ADD OPERATOR FAMILY:\n{out}"
3311        );
3312        assert!(
3313            out.contains(
3314                r#"ALTER EXTENSION "myext" ADD OPERATOR CLASS Sortable_btree_ops USING btree;"#
3315            ),
3316            "missing ADD OPERATOR CLASS:\n{out}"
3317        );
3318    }
3319
3320    #[test]
3321    fn alter_extension_attaches_hash_emits_family_and_class() {
3322        let mut ty = type_entity("Hashable", "tests::Hashable", "tests::Hashable");
3323        ty.in_fn_path = "hashable_in";
3324        ty.out_fn_path = "hashable_out";
3325        let text = external_type("alloc::string::String", "alloc::string::String", "text");
3326        let cstring = external_type("&core::ffi::CStr", "&core::ffi::CStr", "cstring");
3327        let in_fn = function_entity(
3328            "hashable_in",
3329            vec![PgExternArgumentEntity { pattern: "input", used_ty: cstring }],
3330            PgExternReturnEntity::Type {
3331                ty: used_type(
3332                    "tests::Hashable",
3333                    "tests::Hashable",
3334                    "Hashable",
3335                    TypeOrigin::ThisExtension,
3336                ),
3337            },
3338        );
3339        let out_fn = function_entity(
3340            "hashable_out",
3341            vec![PgExternArgumentEntity {
3342                pattern: "input",
3343                used_ty: used_type(
3344                    "tests::Hashable",
3345                    "tests::Hashable",
3346                    "Hashable",
3347                    TypeOrigin::ThisExtension,
3348                ),
3349            }],
3350            PgExternReturnEntity::Type { ty: text },
3351        );
3352        let hash = hash_entity("Hashable");
3353
3354        let sql = PgrxSql::build(
3355            vec![
3356                SqlGraphEntity::ExtensionRoot(control_file()),
3357                SqlGraphEntity::Type(ty),
3358                SqlGraphEntity::Function(in_fn),
3359                SqlGraphEntity::Function(out_fn),
3360                SqlGraphEntity::Hash(hash.clone()),
3361            ]
3362            .into_iter(),
3363            "myext".into(),
3364            false,
3365        )
3366        .unwrap();
3367
3368        let hash_idx = sql.hashes[&hash];
3369        let (out, _) = slice_by_nodes(&sql, &[hash_idx], "myext", Some("myext"));
3370        assert!(
3371            out.contains(
3372                r#"ALTER EXTENSION "myext" ADD OPERATOR FAMILY Hashable_hash_ops USING hash;"#
3373            ),
3374            "missing ADD OPERATOR FAMILY:\n{out}"
3375        );
3376        assert!(
3377            out.contains(
3378                r#"ALTER EXTENSION "myext" ADD OPERATOR CLASS Hashable_hash_ops USING hash;"#
3379            ),
3380            "missing ADD OPERATOR CLASS:\n{out}"
3381        );
3382    }
3383
3384    #[test]
3385    fn alter_extension_attaches_schema_but_skips_public() {
3386        let schema = schema_entity("tests::my_schema", "my_schema");
3387        let fun_arg = external_type("i32", "i32", "integer");
3388        let mut fun = function_entity(
3389            "my_fn",
3390            vec![PgExternArgumentEntity { pattern: "x", used_ty: fun_arg }],
3391            PgExternReturnEntity::None,
3392        );
3393        fun.module_path = "tests::my_schema";
3394        fun.full_path = "tests::my_schema::my_fn";
3395
3396        let sql = PgrxSql::build(
3397            vec![
3398                SqlGraphEntity::ExtensionRoot(control_file()),
3399                SqlGraphEntity::Schema(schema),
3400                SqlGraphEntity::Function(fun),
3401            ]
3402            .into_iter(),
3403            "myext".into(),
3404            false,
3405        )
3406        .unwrap();
3407
3408        let (out, _) =
3409            slice_with_warnings(&sql, &["tests::my_schema::my_fn".into()], "myext", Some("myext"));
3410        assert!(
3411            out.contains(r#"ALTER EXTENSION "myext" ADD SCHEMA my_schema;"#),
3412            "missing ADD SCHEMA:\n{out}"
3413        );
3414        assert!(
3415            !out.contains("ADD SCHEMA public"),
3416            "should not emit ADD SCHEMA for public:\n{out}"
3417        );
3418    }
3419
3420    #[test]
3421    fn alter_extension_custom_sql_with_creates_emits_add_type() {
3422        let hexint = extension_owned_type("tests::HexInt", "tests::HexInt", "hexint");
3423        let declared = declared_type_sql(
3424            "tests",
3425            "tests::concrete_type",
3426            "concrete_type",
3427            "tests::HexInt",
3428            "tests::HexInt",
3429            "hexint",
3430        );
3431        let target =
3432            function_entity("uses_hexint", vec![], PgExternReturnEntity::Type { ty: hexint });
3433
3434        let sql = PgrxSql::build(
3435            vec![
3436                SqlGraphEntity::ExtensionRoot(control_file()),
3437                SqlGraphEntity::CustomSql(declared),
3438                SqlGraphEntity::Function(target),
3439            ]
3440            .into_iter(),
3441            "myext".into(),
3442            false,
3443        )
3444        .unwrap();
3445
3446        let (out, warnings) =
3447            slice_with_warnings(&sql, &["uses_hexint".into()], "myext", Some("myext"));
3448        assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
3449        assert!(
3450            out.contains(r#"ALTER EXTENSION "myext" ADD TYPE hexint;"#),
3451            "missing ADD TYPE for declared type:\n{out}"
3452        );
3453    }
3454
3455    #[test]
3456    fn alter_extension_custom_sql_without_creates_warns() {
3457        let free_form = ExtensionSqlEntity {
3458            module_path: "tests",
3459            full_path: "tests::free_form_sql",
3460            sql: "CREATE TABLE some_table(id INT);",
3461            file: "somefile.rs",
3462            line: 42,
3463            name: "free_form_sql",
3464            bootstrap: false,
3465            finalize: false,
3466            requires: vec![],
3467            creates: vec![],
3468        };
3469
3470        // Emit a function that transitively pulls the free-form block in
3471        // through a `requires`. Simplest path: slice the free-form block
3472        // directly by name.
3473        let sql = PgrxSql::build(
3474            vec![
3475                SqlGraphEntity::ExtensionRoot(control_file()),
3476                SqlGraphEntity::CustomSql(free_form),
3477            ]
3478            .into_iter(),
3479            "myext".into(),
3480            false,
3481        )
3482        .unwrap();
3483
3484        let (out, warnings) =
3485            slice_with_warnings(&sql, &["free_form_sql".into()], "myext", Some("myext"));
3486        assert!(
3487            !out.contains(r#"ALTER EXTENSION "myext" ADD"#),
3488            "free-form block should not emit ADD:\n{out}"
3489        );
3490        assert_eq!(warnings.len(), 1, "expected one warning, got: {warnings:?}");
3491        assert!(warnings[0].contains("somefile.rs:42"), "warning missing file:line: {warnings:?}");
3492        assert!(
3493            warnings[0].contains("free-form") || warnings[0].contains("creates"),
3494            "warning missing reason: {warnings:?}"
3495        );
3496    }
3497
3498    #[test]
3499    fn no_alter_extension_mode_matches_pre_feature_output() {
3500        let fun = function_entity("state_fn", vec![], PgExternReturnEntity::None);
3501        let sql = PgrxSql::build(
3502            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(fun)]
3503                .into_iter(),
3504            "myext".into(),
3505            false,
3506        )
3507        .unwrap();
3508
3509        let (out, _) = slice_with_warnings(&sql, &["state_fn".into()], "myext", None);
3510        assert!(!out.contains("ALTER EXTENSION"), "unexpected ALTER EXTENSION:\n{out}");
3511        assert!(!out.contains("BEGIN;"), "unexpected BEGIN:\n{out}");
3512        assert!(!out.contains("COMMIT;"), "unexpected COMMIT:\n{out}");
3513    }
3514
3515    #[test]
3516    fn alter_extension_substitutes_module_pathname() {
3517        let fun = function_entity("state_fn", vec![], PgExternReturnEntity::None);
3518        let sql = PgrxSql::build(
3519            vec![SqlGraphEntity::ExtensionRoot(control_file()), SqlGraphEntity::Function(fun)]
3520                .into_iter(),
3521            "myext".into(),
3522            false,
3523        )
3524        .unwrap();
3525
3526        let (out, _) = slice_with_warnings(&sql, &["state_fn".into()], "myext", Some("myext"));
3527        assert!(out.contains("'$libdir/myext'"), "missing libdir substitution:\n{out}");
3528        assert!(!out.contains("'MODULE_PATHNAME'"), "raw placeholder leaked:\n{out}");
3529    }
3530}