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