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