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