1use 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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1087pub 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 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 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 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 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 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 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 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}