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