1use std::collections::{BTreeMap, BTreeSet};
21
22use crate::diff::change::{Change, FunctionChange, ProcedureChange};
23use crate::diff::changeset::ChangeSet;
24use crate::diff::destructiveness::Destructiveness;
25use crate::diff::owner_grants::{ColumnGrantMode, diff_owner_and_grants};
26use crate::diff::owner_op::{CatalogObjectRef, RoutineSignature};
27use crate::identifier::{Identifier, QualifiedName};
28use crate::ir::function::{ArgMode, Function, NormalizedArgTypes, ReturnType};
29use crate::ir::procedure::Procedure;
30
31pub fn diff_functions(
37 catalog: &[Function],
38 source: &[Function],
39 out: &mut ChangeSet,
40 managed_roles: &BTreeSet<Identifier>,
41) {
42 type Key = (QualifiedName, [u8; 32]);
45
46 let cat: BTreeMap<Key, &Function> = catalog
47 .iter()
48 .map(|f| ((f.qname.clone(), f.arg_types_normalized.canonical_hash), f))
49 .collect();
50 let src: BTreeMap<Key, &Function> = source
51 .iter()
52 .map(|f| ((f.qname.clone(), f.arg_types_normalized.canonical_hash), f))
53 .collect();
54
55 let all_keys: BTreeSet<Key> = cat.keys().chain(src.keys()).cloned().collect();
56
57 for key in all_keys {
58 match (cat.get(&key), src.get(&key)) {
59 (None, Some(s)) => out.push(
60 Change::Function(FunctionChange::Create((*s).clone())),
61 Destructiveness::Safe,
62 ),
63 (Some(c), None) => out.push(
64 Change::Function(FunctionChange::Drop {
65 qname: c.qname.clone(),
66 args: c.arg_types_normalized.clone(),
67 }),
68 Destructiveness::RequiresApprovalAndDataLossWarning {
69 reason: format!("drops function {}", c.qname),
70 },
71 ),
72 (Some(c), Some(s)) => {
73 diff_same_function(c, s, out);
74 diff_function_owner_grants(c, s, out, managed_roles);
75 }
76 (None, None) => unreachable!(),
77 }
78 }
79}
80
81fn diff_function_owner_grants(
83 catalog: &Function,
84 source: &Function,
85 out: &mut ChangeSet,
86 managed_roles: &BTreeSet<Identifier>,
87) {
88 let args_label = NormalizedArgTypes::from_args(&source.args)
90 .types
91 .iter()
92 .map(crate::ir::column_type::ColumnType::render_sql)
93 .collect::<Vec<_>>()
94 .join(", ");
95 let signature = format!("({args_label})");
96
97 diff_owner_and_grants(
98 &CatalogObjectRef::Function {
99 name: source.qname.clone(),
100 signature: RoutineSignature::new(signature),
101 },
102 catalog.owner.as_ref(),
103 source.owner.as_ref(),
104 &catalog.grants,
105 &source.grants,
106 managed_roles,
107 ColumnGrantMode::ObjectOnly,
108 out,
109 );
110}
111
112fn diff_same_function(catalog: &Function, source: &Function, out: &mut ChangeSet) {
114 let body_changed = catalog.body.canonical_hash() != source.body.canonical_hash();
115 let attrs_changed = catalog.return_type != source.return_type
116 || catalog.language != source.language
117 || catalog.volatility != source.volatility
118 || catalog.strict != source.strict
119 || catalog.security != source.security
120 || catalog.parallel != source.parallel
121 || catalog.leakproof != source.leakproof
122 || catalog.cost.map(f32::to_bits) != source.cost.map(f32::to_bits)
123 || catalog.rows.map(f32::to_bits) != source.rows.map(f32::to_bits)
124 || catalog.args != source.args;
125
126 if !body_changed && !attrs_changed {
127 if catalog.comment != source.comment {
129 out.push(
130 Change::Function(FunctionChange::SetComment {
131 qname: source.qname.clone(),
132 args: source.arg_types_normalized.clone(),
133 comment: source.comment.clone(),
134 }),
135 Destructiveness::Safe,
136 );
137 }
138 return;
139 }
140
141 if function_can_or_replace(catalog, source) {
142 let dest = if arg_default_removed(catalog, source) {
143 Destructiveness::RequiresApproval {
144 reason: format!(
145 "function {} removes an argument default (may break callers passing fewer args)",
146 source.qname
147 ),
148 }
149 } else {
150 Destructiveness::Safe
151 };
152 out.push(
153 Change::Function(FunctionChange::CreateOrReplace(source.clone())),
154 dest,
155 );
156 } else {
157 out.push(
158 Change::Function(FunctionChange::ReplaceViaDropCreate {
159 catalog: catalog.clone(),
160 source: source.clone(),
161 }),
162 Destructiveness::RequiresApprovalAndDataLossWarning {
163 reason: format!(
164 "function {} return-type or language change requires DROP+CREATE CASCADE",
165 source.qname
166 ),
167 },
168 );
169 }
170}
171
172pub(crate) fn function_can_or_replace(catalog: &Function, source: &Function) -> bool {
182 if catalog.language != source.language {
183 return false;
184 }
185 if !return_type_compatible(&catalog.return_type, &source.return_type) {
186 return false;
187 }
188 let cat_outs: Vec<_> = catalog
190 .args
191 .iter()
192 .filter(|a| matches!(a.mode, ArgMode::Out | ArgMode::InOut))
193 .map(|a| (a.name.clone(), a.ty.clone()))
194 .collect();
195 let src_outs: Vec<_> = source
196 .args
197 .iter()
198 .filter(|a| matches!(a.mode, ArgMode::Out | ArgMode::InOut))
199 .map(|a| (a.name.clone(), a.ty.clone()))
200 .collect();
201 if cat_outs != src_outs {
202 return false;
203 }
204 true
205}
206
207fn return_type_compatible(a: &ReturnType, b: &ReturnType) -> bool {
215 a == b
216}
217
218fn arg_default_removed(catalog: &Function, source: &Function) -> bool {
222 catalog
223 .args
224 .iter()
225 .zip(source.args.iter())
226 .any(|(c, s)| c.default.is_some() && s.default.is_none())
227}
228
229pub fn diff_procedures(
234 catalog: &[Procedure],
235 source: &[Procedure],
236 out: &mut ChangeSet,
237 managed_roles: &BTreeSet<Identifier>,
238) {
239 let cat: BTreeMap<QualifiedName, &Procedure> =
240 catalog.iter().map(|p| (p.qname.clone(), p)).collect();
241 let src: BTreeMap<QualifiedName, &Procedure> =
242 source.iter().map(|p| (p.qname.clone(), p)).collect();
243 let all: BTreeSet<QualifiedName> = cat.keys().chain(src.keys()).cloned().collect();
244
245 for key in all {
246 match (cat.get(&key), src.get(&key)) {
247 (None, Some(s)) => out.push(
248 Change::Procedure(ProcedureChange::Create((*s).clone())),
249 Destructiveness::Safe,
250 ),
251 (Some(c), None) => out.push(
252 Change::Procedure(ProcedureChange::Drop(c.qname.clone())),
253 Destructiveness::RequiresApprovalAndDataLossWarning {
254 reason: format!("drops procedure {}", c.qname),
255 },
256 ),
257 (Some(c), Some(s)) => {
258 diff_same_procedure(c, s, out);
259 diff_procedure_owner_grants(c, s, out, managed_roles);
260 }
261 (None, None) => unreachable!(),
262 }
263 }
264}
265
266fn diff_procedure_owner_grants(
268 catalog: &Procedure,
269 source: &Procedure,
270 out: &mut ChangeSet,
271 managed_roles: &BTreeSet<Identifier>,
272) {
273 let args_label = NormalizedArgTypes::from_args(&source.args)
276 .types
277 .iter()
278 .map(crate::ir::column_type::ColumnType::render_sql)
279 .collect::<Vec<_>>()
280 .join(", ");
281 let signature = format!("({args_label})");
282
283 diff_owner_and_grants(
284 &CatalogObjectRef::Procedure {
285 name: source.qname.clone(),
286 signature: RoutineSignature::new(signature),
287 },
288 catalog.owner.as_ref(),
289 source.owner.as_ref(),
290 &catalog.grants,
291 &source.grants,
292 managed_roles,
293 ColumnGrantMode::ObjectOnly,
294 out,
295 );
296}
297
298fn diff_same_procedure(catalog: &Procedure, source: &Procedure, out: &mut ChangeSet) {
300 let body_changed = catalog.body.canonical_hash() != source.body.canonical_hash();
301 let attrs_changed = catalog.language != source.language
302 || catalog.security != source.security
303 || catalog.args != source.args
304 || catalog.commits_in_body != source.commits_in_body;
305
306 if !body_changed && !attrs_changed {
307 if catalog.comment != source.comment {
308 out.push(
309 Change::Procedure(ProcedureChange::SetComment {
310 qname: source.qname.clone(),
311 comment: source.comment.clone(),
312 }),
313 Destructiveness::Safe,
314 );
315 }
316 return;
317 }
318
319 out.push(
321 Change::Procedure(ProcedureChange::CreateOrReplace(source.clone())),
322 Destructiveness::Safe,
323 );
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use std::collections::BTreeSet;
330
331 use crate::diff::change::{Change, FunctionChange, ProcedureChange};
332 use crate::diff::changeset::ChangeSet;
333 use crate::diff::destructiveness::Destructiveness;
334 use crate::identifier::{Identifier, QualifiedName};
335 use crate::ir::column_type::ColumnType;
336 use crate::ir::function::{
337 ArgMode, FunctionArg, FunctionLanguage, NormalizedArgTypes, ParallelSafety, ReturnType,
338 SecurityMode, Volatility,
339 };
340 use crate::ir::procedure::Procedure;
341 use crate::parse::normalize_body::NormalizedBody;
342
343 fn ident(s: &str) -> Identifier {
344 Identifier::from_unquoted(s).unwrap()
345 }
346
347 fn qn(schema: &str, name: &str) -> QualifiedName {
348 QualifiedName::new(ident(schema), ident(name))
349 }
350
351 fn make_func_no_args(qname: QualifiedName, body: &str) -> Function {
352 let args: Vec<FunctionArg> = vec![];
353 let arg_types_normalized = NormalizedArgTypes::from_args(&args);
354 Function {
355 qname,
356 args,
357 arg_types_normalized,
358 return_type: ReturnType::Scalar {
359 ty: ColumnType::Integer,
360 },
361 language: FunctionLanguage::Sql,
362 body: NormalizedBody::from_sql(body).unwrap(),
363 body_dependencies: vec![],
364 volatility: Volatility::Immutable,
365 strict: false,
366 security: SecurityMode::Invoker,
367 parallel: ParallelSafety::Safe,
368 leakproof: false,
369 cost: None,
370 rows: None,
371 comment: None,
372 owner: None,
373 grants: vec![],
374 }
375 }
376
377 fn make_func_with_arg(
378 qname: QualifiedName,
379 arg_ty: ColumnType,
380 ret_ty: ColumnType,
381 ) -> Function {
382 let args = vec![FunctionArg {
383 name: Some(ident("x")),
384 mode: ArgMode::In,
385 ty: arg_ty,
386 default: None,
387 }];
388 let arg_types_normalized = NormalizedArgTypes::from_args(&args);
389 Function {
390 qname,
391 args,
392 arg_types_normalized,
393 return_type: ReturnType::Scalar { ty: ret_ty },
394 language: FunctionLanguage::Sql,
395 body: NormalizedBody::from_sql("SELECT $1").unwrap(),
396 body_dependencies: vec![],
397 volatility: Volatility::Immutable,
398 strict: false,
399 security: SecurityMode::Invoker,
400 parallel: ParallelSafety::Safe,
401 leakproof: false,
402 cost: None,
403 rows: None,
404 comment: None,
405 owner: None,
406 grants: vec![],
407 }
408 }
409
410 fn make_procedure(qname: QualifiedName, body_text: &str) -> Procedure {
411 let body = NormalizedBody::from_raw_canonical(body_text.to_string());
414 Procedure {
415 qname,
416 args: vec![],
417 language: FunctionLanguage::PlPgSql,
418 body,
419 body_dependencies: vec![],
420 security: SecurityMode::Invoker,
421 commits_in_body: false,
422 comment: None,
423 owner: None,
424 grants: vec![],
425 }
426 }
427
428 #[test]
431 fn function_create_emits_create() {
432 let f = make_func_no_args(qn("app", "add_one"), "SELECT 1");
433 let mut cs = ChangeSet::new();
434 diff_functions(&[], std::slice::from_ref(&f), &mut cs, &BTreeSet::new());
435 assert_eq!(cs.len(), 1);
436 assert!(matches!(
437 &cs.entries[0].change,
438 Change::Function(FunctionChange::Create(ff)) if ff.qname == f.qname
439 ));
440 assert!(matches!(
441 cs.entries[0].destructiveness,
442 Destructiveness::Safe
443 ));
444 }
445
446 #[test]
447 fn function_drop_is_data_loss() {
448 let f = make_func_no_args(qn("app", "old_fn"), "SELECT 99");
449 let mut cs = ChangeSet::new();
450 diff_functions(std::slice::from_ref(&f), &[], &mut cs, &BTreeSet::new());
451 assert_eq!(cs.len(), 1);
452 assert!(matches!(
453 &cs.entries[0].change,
454 Change::Function(FunctionChange::Drop { qname, .. }) if *qname == f.qname
455 ));
456 assert!(matches!(
457 cs.entries[0].destructiveness,
458 Destructiveness::RequiresApprovalAndDataLossWarning { .. }
459 ));
460 }
461
462 #[test]
463 fn function_body_change_emits_create_or_replace() {
464 let f_cat = make_func_no_args(qn("app", "fn1"), "SELECT 1");
465 let f_src = make_func_no_args(qn("app", "fn1"), "SELECT 2");
466 let mut cs = ChangeSet::new();
467 diff_functions(
468 std::slice::from_ref(&f_cat),
469 std::slice::from_ref(&f_src),
470 &mut cs,
471 &BTreeSet::new(),
472 );
473 assert_eq!(cs.len(), 1);
474 assert!(matches!(
475 &cs.entries[0].change,
476 Change::Function(FunctionChange::CreateOrReplace(ff)) if ff.qname == f_src.qname
477 ));
478 assert!(matches!(
479 cs.entries[0].destructiveness,
480 Destructiveness::Safe
481 ));
482 }
483
484 #[test]
485 fn function_return_type_kind_change_emits_cascade() {
486 let mut f_cat = make_func_no_args(qn("app", "fn2"), "SELECT 1");
487 f_cat.return_type = ReturnType::Scalar {
488 ty: ColumnType::Integer,
489 };
490 let mut f_src = make_func_no_args(qn("app", "fn2"), "SELECT 1");
491 f_src.return_type = ReturnType::SetOf {
492 ty: ColumnType::Integer,
493 };
494 let mut cs = ChangeSet::new();
495 diff_functions(
496 std::slice::from_ref(&f_cat),
497 std::slice::from_ref(&f_src),
498 &mut cs,
499 &BTreeSet::new(),
500 );
501 assert_eq!(cs.len(), 1);
502 assert!(matches!(
503 &cs.entries[0].change,
504 Change::Function(FunctionChange::ReplaceViaDropCreate { source, .. }) if source.qname == f_src.qname
505 ));
506 assert!(matches!(
507 cs.entries[0].destructiveness,
508 Destructiveness::RequiresApprovalAndDataLossWarning { .. }
509 ));
510 }
511
512 #[test]
513 fn function_overloads_treated_independently() {
514 let f_int = make_func_with_arg(
516 qn("app", "process"),
517 ColumnType::Integer,
518 ColumnType::Integer,
519 );
520 let f_txt = make_func_with_arg(qn("app", "process"), ColumnType::Text, ColumnType::Text);
521
522 let src = [f_int.clone(), f_txt];
524 let mut cs = ChangeSet::new();
525 diff_functions(
526 std::slice::from_ref(&f_int),
527 &src,
528 &mut cs,
529 &BTreeSet::new(),
530 );
531
532 assert_eq!(cs.len(), 1);
534 assert!(matches!(
535 &cs.entries[0].change,
536 Change::Function(FunctionChange::Create(ff)) if ff.arg_types_normalized.types == [ColumnType::Text]
537 ));
538 }
539
540 #[test]
541 fn function_comment_only_change_is_safe() {
542 let f_cat = make_func_no_args(qn("app", "fn3"), "SELECT 1");
543 let mut f_src = make_func_no_args(qn("app", "fn3"), "SELECT 1");
544 f_src.comment = Some("new comment".to_string());
545 let mut cs = ChangeSet::new();
546 diff_functions(
547 std::slice::from_ref(&f_cat),
548 std::slice::from_ref(&f_src),
549 &mut cs,
550 &BTreeSet::new(),
551 );
552 assert_eq!(cs.len(), 1);
553 assert!(matches!(
554 &cs.entries[0].change,
555 Change::Function(FunctionChange::SetComment { .. })
556 ));
557 assert!(matches!(
558 cs.entries[0].destructiveness,
559 Destructiveness::Safe
560 ));
561 }
562
563 #[test]
564 fn function_arg_default_removed_requires_approval() {
565 use crate::ir::default_expr::NormalizedExpr;
566
567 let make_with_default = |has_default: bool| {
568 let default = if has_default {
569 Some(NormalizedExpr::from_text("42"))
570 } else {
571 None
572 };
573 let args = vec![FunctionArg {
574 name: Some(ident("x")),
575 mode: ArgMode::In,
576 ty: ColumnType::Integer,
577 default,
578 }];
579 let norm = NormalizedArgTypes::from_args(&args);
580 Function {
581 qname: qn("app", "fn_default"),
582 args,
583 arg_types_normalized: norm,
584 return_type: ReturnType::Scalar {
585 ty: ColumnType::Integer,
586 },
587 language: FunctionLanguage::Sql,
588 body: NormalizedBody::from_sql("SELECT $1").unwrap(),
589 body_dependencies: vec![],
590 volatility: Volatility::Immutable,
591 strict: false,
592 security: SecurityMode::Invoker,
593 parallel: ParallelSafety::Safe,
594 leakproof: false,
595 cost: None,
596 rows: None,
597 comment: None,
598 owner: None,
599 grants: vec![],
600 }
601 };
602
603 let f_cat = make_with_default(true);
604 let f_src = make_with_default(false);
605 let mut cs = ChangeSet::new();
606 diff_functions(
608 std::slice::from_ref(&f_cat),
609 std::slice::from_ref(&f_src),
610 &mut cs,
611 &BTreeSet::new(),
612 );
613 assert_eq!(cs.len(), 1);
614 assert!(matches!(
615 &cs.entries[0].change,
616 Change::Function(FunctionChange::CreateOrReplace(_))
617 ));
618 assert!(matches!(
619 cs.entries[0].destructiveness,
620 Destructiveness::RequiresApproval { .. }
621 ));
622 }
623
624 #[test]
625 fn function_identical_emits_nothing() {
626 let f = make_func_no_args(qn("app", "unchanged"), "SELECT 42");
627 let mut cs = ChangeSet::new();
628 diff_functions(
629 std::slice::from_ref(&f),
630 std::slice::from_ref(&f),
631 &mut cs,
632 &BTreeSet::new(),
633 );
634 assert!(cs.is_empty());
635 }
636
637 #[test]
640 fn procedure_create_emits_create() {
641 let p = make_procedure(qn("app", "do_work"), "BEGIN NULL; END");
642 let mut cs = ChangeSet::new();
643 diff_procedures(&[], std::slice::from_ref(&p), &mut cs, &BTreeSet::new());
644 assert_eq!(cs.len(), 1);
645 assert!(matches!(
646 &cs.entries[0].change,
647 Change::Procedure(ProcedureChange::Create(pp)) if pp.qname == p.qname
648 ));
649 assert!(matches!(
650 cs.entries[0].destructiveness,
651 Destructiveness::Safe
652 ));
653 }
654
655 #[test]
656 fn procedure_drop_is_data_loss() {
657 let p = make_procedure(qn("app", "old_proc"), "BEGIN NULL; END");
658 let mut cs = ChangeSet::new();
659 diff_procedures(std::slice::from_ref(&p), &[], &mut cs, &BTreeSet::new());
660 assert_eq!(cs.len(), 1);
661 assert!(matches!(
662 &cs.entries[0].change,
663 Change::Procedure(ProcedureChange::Drop(q)) if *q == p.qname
664 ));
665 assert!(matches!(
666 cs.entries[0].destructiveness,
667 Destructiveness::RequiresApprovalAndDataLossWarning { .. }
668 ));
669 }
670
671 #[test]
672 fn procedure_body_change_emits_create_or_replace() {
673 let p_cat = make_procedure(qn("app", "proc1"), "BEGIN NULL; END");
674 let p_src = make_procedure(qn("app", "proc1"), "BEGIN RAISE NOTICE 'hi'; END");
675 let mut cs = ChangeSet::new();
676 diff_procedures(
677 std::slice::from_ref(&p_cat),
678 std::slice::from_ref(&p_src),
679 &mut cs,
680 &BTreeSet::new(),
681 );
682 assert_eq!(cs.len(), 1);
683 assert!(matches!(
684 &cs.entries[0].change,
685 Change::Procedure(ProcedureChange::CreateOrReplace(pp)) if pp.qname == p_src.qname
686 ));
687 assert!(matches!(
688 cs.entries[0].destructiveness,
689 Destructiveness::Safe
690 ));
691 }
692
693 #[test]
694 fn procedure_identical_emits_nothing() {
695 let p = make_procedure(qn("app", "stable_proc"), "BEGIN NULL; END");
696 let mut cs = ChangeSet::new();
697 diff_procedures(
698 std::slice::from_ref(&p),
699 std::slice::from_ref(&p),
700 &mut cs,
701 &BTreeSet::new(),
702 );
703 assert!(cs.is_empty());
704 }
705
706 #[test]
709 fn function_grant_change_carries_signature() {
710 use crate::ir::grant::{Grant, GrantTarget, Privilege};
711
712 let f_cat = make_func_with_arg(qn("app", "foo"), ColumnType::Integer, ColumnType::Integer);
714 let mut f_src = f_cat.clone();
715
716 let managed_role = ident("app_user");
718 f_src.grants = vec![Grant {
719 grantee: GrantTarget::Role(managed_role.clone()),
720 privilege: Privilege::Execute,
721 with_grant_option: false,
722 columns: None,
723 }];
724
725 let mut managed = BTreeSet::new();
726 managed.insert(managed_role);
727
728 let mut cs = ChangeSet::new();
729 diff_functions(
730 std::slice::from_ref(&f_cat),
731 std::slice::from_ref(&f_src),
732 &mut cs,
733 &managed,
734 );
735
736 let grant_entry = cs
738 .entries
739 .iter()
740 .find(|e| matches!(&e.change, Change::GrantObjectPrivilege { .. }));
741 assert!(
742 grant_entry.is_some(),
743 "expected GrantObjectPrivilege change"
744 );
745
746 if let Change::GrantObjectPrivilege { object, .. } = &grant_entry.unwrap().change {
747 assert!(
748 matches!(
749 object,
750 CatalogObjectRef::Function { signature, .. }
751 if signature == &RoutineSignature::new("(integer)".to_string())
752 ),
753 "signature must carry the IN arg type on a Function variant"
754 );
755 }
756 }
757
758 #[test]
759 fn function_grant_change_no_args_has_empty_parens_signature() {
760 use crate::ir::grant::{Grant, GrantTarget, Privilege};
761
762 let f_cat = make_func_no_args(qn("app", "bar"), "SELECT 1");
763 let mut f_src = f_cat.clone();
764
765 let managed_role = ident("app_user");
766 f_src.grants = vec![Grant {
767 grantee: GrantTarget::Role(managed_role.clone()),
768 privilege: Privilege::Execute,
769 with_grant_option: false,
770 columns: None,
771 }];
772
773 let mut managed = BTreeSet::new();
774 managed.insert(managed_role);
775
776 let mut cs = ChangeSet::new();
777 diff_functions(
778 std::slice::from_ref(&f_cat),
779 std::slice::from_ref(&f_src),
780 &mut cs,
781 &managed,
782 );
783
784 let grant_entry = cs
785 .entries
786 .iter()
787 .find(|e| matches!(&e.change, Change::GrantObjectPrivilege { .. }));
788 assert!(
789 grant_entry.is_some(),
790 "expected GrantObjectPrivilege change"
791 );
792
793 if let Change::GrantObjectPrivilege { object, .. } = &grant_entry.unwrap().change {
794 assert!(
795 matches!(
796 object,
797 CatalogObjectRef::Function { signature, .. }
798 if signature == &RoutineSignature::new("()".to_string())
799 ),
800 "no-arg function signature must be ()"
801 );
802 }
803 }
804
805 #[test]
806 fn procedure_grant_change_carries_signature() {
807 use crate::ir::function::{ArgMode, FunctionArg};
808 use crate::ir::grant::{Grant, GrantTarget, Privilege};
809
810 let mut p_cat = make_procedure(qn("app", "do_work"), "BEGIN NULL; END");
812 p_cat.args = vec![FunctionArg {
813 name: Some(ident("n")),
814 mode: ArgMode::In,
815 ty: ColumnType::Integer,
816 default: None,
817 }];
818 let mut p_src = p_cat.clone();
819
820 let managed_role = ident("app_user");
821 p_src.grants = vec![Grant {
822 grantee: GrantTarget::Role(managed_role.clone()),
823 privilege: Privilege::Execute,
824 with_grant_option: false,
825 columns: None,
826 }];
827
828 let mut managed = BTreeSet::new();
829 managed.insert(managed_role);
830
831 let mut cs = ChangeSet::new();
832 diff_procedures(
833 std::slice::from_ref(&p_cat),
834 std::slice::from_ref(&p_src),
835 &mut cs,
836 &managed,
837 );
838
839 let grant_entry = cs
840 .entries
841 .iter()
842 .find(|e| matches!(&e.change, Change::GrantObjectPrivilege { .. }));
843 assert!(
844 grant_entry.is_some(),
845 "expected GrantObjectPrivilege change"
846 );
847
848 if let Change::GrantObjectPrivilege { object, .. } = &grant_entry.unwrap().change {
849 assert!(
850 matches!(
851 object,
852 CatalogObjectRef::Procedure { signature, .. }
853 if signature == &RoutineSignature::new("(integer)".to_string())
854 ),
855 "procedure signature must carry the IN arg type on a Procedure variant"
856 );
857 }
858 }
859}