1use mz_expr_derive::sqlfunc;
30use mz_repr::adt::jsonb::{Jsonb, JsonbRef};
31use mz_repr::{Datum, Row, RowPacker};
32
33use crate::EvalError;
34
35#[sqlfunc]
44fn parse_catalog_audit_log_details<'a>(a: JsonbRef<'a>) -> Result<Jsonb, EvalError> {
45 const FLATTENED_FIELDS: &[(&str, &str, &str, Option<&str>)] = &[
49 ("IdFullNameV1", "", "name", None),
50 ("CreateSourceSinkV1", "", "name", None),
51 ("CreateSourceSinkV2", "", "name", None),
52 ("CreateSourceSinkV3", "", "name", None),
53 ("CreateSourceSinkV4", "", "name", None),
54 ("CreateIndexV1", "", "name", None),
55 ("CreateMaterializedViewV1", "", "name", None),
56 ("AlterSourceSinkV1", "", "name", None),
57 ("AlterSetClusterV1", "", "name", None),
58 ("UpdateItemV1", "", "name", None),
59 (
62 "AlterApplyReplacementV1",
63 "",
64 "target",
65 Some("IdFullNameV1"),
66 ),
67 ];
68
69 const RENAMES: &[(&str, &str, &str, &str)] = &[
72 ("CreateSourceSinkV2", "", "external_type", "type"),
73 ("CreateSourceSinkV3", "", "external_type", "type"),
74 ("CreateSourceSinkV4", "", "external_type", "type"),
75 (
76 "RefreshDecisionWithReasonV1",
77 "",
78 "rehydration_time_estimate",
79 "hydration_time_estimate",
80 ),
81 (
82 "RefreshDecisionWithReasonV2",
83 "",
84 "rehydration_time_estimate",
85 "hydration_time_estimate",
86 ),
87 ];
88
89 const DROP_NULL: &[(&str, &str, &str)] = &[
93 ("CreateClusterReplicaV1", "", "replica_id"),
94 ("DropClusterReplicaV1", "", "replica_id"),
95 ("CreateClusterReplicaV2", "", "scheduling_policies"),
96 ("CreateClusterReplicaV3", "", "scheduling_policies"),
97 ("CreateClusterReplicaV4", "", "scheduling_policies"),
98 ("DropClusterReplicaV2", "", "scheduling_policies"),
99 ("DropClusterReplicaV3", "", "scheduling_policies"),
100 ("CreateMaterializedViewV1", "", "replacement_target_id"),
101 ];
102
103 const REASON_MAP: &[(&str, &str)] = &[
106 ("Manual", "manual"),
107 ("Schedule", "schedule"),
108 ("System", "system"),
109 ("Reconfiguration", "reconfiguration"),
110 ("HydrationBurst", "hydration-burst"),
111 ("Retired", "retired"),
112 ];
113
114 #[allow(dead_code)]
120 #[derive(Copy, Clone)]
121 enum WrapKind {
122 Single,
123 Double,
124 }
125 const ENUM_COLLAPSE: &[(&str, &str, &str, WrapKind, &[(&str, &str)])] = &[
126 (
127 "RefreshDecisionWithReasonV1",
128 "",
129 "decision",
130 WrapKind::Single,
131 &[("On", "on"), ("Off", "off")],
132 ),
133 (
134 "RefreshDecisionWithReasonV2",
135 "",
136 "decision",
137 WrapKind::Single,
138 &[("On", "on"), ("Off", "off")],
139 ),
140 (
141 "CreateClusterReplicaV2",
142 "",
143 "reason",
144 WrapKind::Double,
145 REASON_MAP,
146 ),
147 (
148 "CreateClusterReplicaV3",
149 "",
150 "reason",
151 WrapKind::Double,
152 REASON_MAP,
153 ),
154 (
155 "CreateClusterReplicaV4",
156 "",
157 "reason",
158 WrapKind::Double,
159 REASON_MAP,
160 ),
161 (
162 "DropClusterReplicaV2",
163 "",
164 "reason",
165 WrapKind::Double,
166 REASON_MAP,
167 ),
168 (
169 "DropClusterReplicaV3",
170 "",
171 "reason",
172 WrapKind::Double,
173 REASON_MAP,
174 ),
175 (
176 "AlterClusterReconfigurationV1",
177 "",
178 "transition",
179 WrapKind::Double,
180 &[
181 ("Started", "started"),
182 ("Finalized", "finalized"),
183 ("TimedOut", "timed-out"),
184 ("Cancelled", "cancelled"),
185 ("ResourceExhausted", "resource-exhausted"),
186 ],
187 ),
188 (
189 "ClusterHydrationBurstV1",
190 "",
191 "transition",
192 WrapKind::Double,
193 &[("Started", "started"), ("Finished", "finished")],
194 ),
195 (
196 "ClusterHydrationBurstV1",
197 "",
198 "finish_cause",
199 WrapKind::Double,
200 &[
201 ("LingerElapsed", "linger-elapsed"),
202 ("NoLongerWarranted", "no-longer-warranted"),
203 ],
204 ),
205 ];
206
207 const DESCEND: &[(&str, &str, &str, &str)] = &[
213 ("AlterApplyReplacementV1", "", "replacement", "IdFullNameV1"),
214 (
215 "CreateClusterReplicaV2",
216 "",
217 "scheduling_policies",
218 "SchedulingDecisionsWithReasonsV1",
219 ),
220 (
221 "CreateClusterReplicaV3",
222 "",
223 "scheduling_policies",
224 "SchedulingDecisionsWithReasonsV2",
225 ),
226 (
227 "CreateClusterReplicaV4",
228 "",
229 "scheduling_policies",
230 "SchedulingDecisionsWithReasonsV2",
231 ),
232 (
233 "DropClusterReplicaV2",
234 "",
235 "scheduling_policies",
236 "SchedulingDecisionsWithReasonsV1",
237 ),
238 (
239 "DropClusterReplicaV3",
240 "",
241 "scheduling_policies",
242 "SchedulingDecisionsWithReasonsV2",
243 ),
244 (
245 "SchedulingDecisionsWithReasonsV1",
246 "",
247 "on_refresh",
248 "RefreshDecisionWithReasonV1",
249 ),
250 (
251 "SchedulingDecisionsWithReasonsV2",
252 "",
253 "on_refresh",
254 "RefreshDecisionWithReasonV2",
255 ),
256 ];
257
258 fn lookup_flatten(variant: &str, path: &str, field: &str) -> Option<Option<&'static str>> {
259 FLATTENED_FIELDS
260 .iter()
261 .find(|(v, p, f, _)| *v == variant && *p == path && *f == field)
262 .map(|(_, _, _, sv)| *sv)
263 }
264
265 fn lookup_rename(variant: &str, path: &str, field: &str) -> Option<&'static str> {
266 RENAMES
267 .iter()
268 .find(|(v, p, k, _)| *v == variant && *p == path && *k == field)
269 .map(|(_, _, _, out)| *out)
270 }
271
272 fn is_drop_null(variant: &str, path: &str, field: &str) -> bool {
273 DROP_NULL
274 .iter()
275 .any(|(v, p, f)| *v == variant && *p == path && *f == field)
276 }
277
278 fn lookup_enum_collapse(
279 variant: &str,
280 path: &str,
281 field: &str,
282 ) -> Option<(WrapKind, &'static [(&'static str, &'static str)])> {
283 ENUM_COLLAPSE
284 .iter()
285 .find(|(v, p, f, _, _)| *v == variant && *p == path && *f == field)
286 .map(|(_, _, _, wrap, map)| (*wrap, *map))
287 }
288
289 fn lookup_descend(variant: &str, path: &str, field: &str) -> Option<&'static str> {
290 DESCEND
291 .iter()
292 .find(|(v, p, f, _)| *v == variant && *p == path && *f == field)
293 .map(|(_, _, _, sv)| *sv)
294 }
295
296 fn collapse_enum(
300 datum: Datum,
301 wrap: WrapKind,
302 map: &[(&'static str, &'static str)],
303 context: &str,
304 ) -> Result<&'static str, String> {
305 let inner = match wrap {
306 WrapKind::Single => datum,
307 WrapKind::Double => {
308 let Datum::Map(outer) = datum else {
309 return Err(format!(
310 "{context}: double-wrap enum expected outer object, got {datum:?}"
311 ));
312 };
313 let mut iter = outer.iter();
314 let (Some((_, v)), None) = (iter.next(), iter.next()) else {
315 return Err(format!(
316 "{context}: double-wrap enum expected single-key outer object"
317 ));
318 };
319 v
320 }
321 };
322 let Datum::Map(dict) = inner else {
323 return Err(format!(
324 "{context}: enum collapse expected object, got {inner:?}"
325 ));
326 };
327 let mut iter = dict.iter();
328 let (Some((variant_key, payload)), None) = (iter.next(), iter.next()) else {
329 return Err(format!(
330 "{context}: enum collapse expected single-key object"
331 ));
332 };
333 if let Datum::Map(payload_dict) = payload {
336 if payload_dict.iter().next().is_some() {
337 return Err(format!(
338 "{context}: enum variant {variant_key} carries non-empty payload"
339 ));
340 }
341 } else {
342 return Err(format!(
343 "{context}: enum variant {variant_key} payload is not an object"
344 ));
345 }
346 map.iter()
347 .find(|(k, _)| *k == variant_key)
348 .map(|(_, kebab)| *kebab)
349 .ok_or_else(|| format!("{context}: unknown enum variant {variant_key}"))
350 }
351
352 fn extend_path(path: &str, field: &str) -> String {
353 if path.is_empty() {
354 field.to_string()
355 } else {
356 format!("{path}.{field}")
357 }
358 }
359
360 fn rewrite(datum: Datum, variant: &str, path: &str, out: &mut RowPacker) -> Result<(), String> {
362 match datum {
363 Datum::Map(dict) => {
364 let mut iter = dict.iter();
366 if let (Some((k, v)), None) = (iter.next(), iter.next()) {
367 if k == "inner" {
368 return rewrite(v, variant, path, out);
369 }
370 }
371 rewrite_map(dict, variant, path, out)
372 }
373 Datum::List(list) => {
374 let mut result: Result<(), String> = Ok(());
375 out.push_list_with(|out| {
376 for v in list.iter() {
377 if let Err(e) = rewrite(v, variant, path, out) {
378 result = Err(e);
379 break;
380 }
381 }
382 });
383 result
384 }
385 other => {
386 out.push(other);
387 Ok(())
388 }
389 }
390 }
391
392 fn rewrite_map(
393 dict: mz_repr::DatumMap,
394 variant: &str,
395 path: &str,
396 out: &mut RowPacker,
397 ) -> Result<(), String> {
398 let mut entries = collect_entries(dict, variant, path)?;
399 entries.sort_by(|a, b| a.0.cmp(&b.0));
401 out.push_dict_with(|out| {
402 for (k, temp) in &entries {
403 out.push(Datum::String(k));
404 out.push(temp.unpack_first());
405 }
406 });
407 Ok(())
408 }
409
410 fn collect_entries(
414 dict: mz_repr::DatumMap,
415 variant: &str,
416 path: &str,
417 ) -> Result<Vec<(String, Row)>, String> {
418 let mut entries: Vec<(String, Row)> = Vec::new();
419
420 for (k, v) in dict.iter() {
421 if is_drop_null(variant, path, k) && matches!(v, Datum::JsonNull) {
422 continue;
423 }
424
425 if let Some(sub_variant_opt) = lookup_flatten(variant, path, k) {
427 let sub_variant = sub_variant_opt.unwrap_or("");
428 let Datum::Map(sub) = v else {
429 return Err(format!(
430 "expected flatten target {variant}.{k} to be an object"
431 ));
432 };
433 entries.extend(collect_entries(sub, sub_variant, "")?);
434 continue;
435 }
436
437 let out_key = lookup_rename(variant, path, k).unwrap_or(k).to_string();
438
439 if let Some((wrap, map)) = lookup_enum_collapse(variant, path, k) {
443 if matches!(v, Datum::JsonNull) {
444 let mut temp = Row::default();
445 temp.packer().push(Datum::JsonNull);
446 entries.push((out_key, temp));
447 continue;
448 }
449 let kebab = collapse_enum(v, wrap, map, &format!("{variant}.{k}"))?;
450 let mut temp = Row::default();
451 temp.packer().push(Datum::String(kebab));
452 entries.push((out_key, temp));
453 continue;
454 }
455
456 if let Some(sub_variant) = lookup_descend(variant, path, k) {
459 let mut temp = Row::default();
460 rewrite(v, sub_variant, "", &mut temp.packer())?;
461 entries.push((out_key, temp));
462 continue;
463 }
464
465 let child_path = extend_path(path, k);
467 let mut temp = Row::default();
468 rewrite(v, variant, &child_path, &mut temp.packer())?;
469 entries.push((out_key, temp));
470 }
471
472 Ok(entries)
473 }
474
475 let parse = || -> Result<Jsonb, String> {
476 let Datum::Map(dict) = a.into_datum() else {
477 return Err("expected object".into());
478 };
479 let mut iter = dict.iter();
480 let (variant, inner) = iter
481 .next()
482 .ok_or_else(|| "empty details enum".to_string())?;
483 if iter.next().is_some() {
484 return Err("details enum had multiple keys".into());
485 }
486 let mut row = Row::default();
487 if variant == "ResetAllV1" {
490 row.packer().push(Datum::JsonNull);
491 return Ok(Jsonb::from_row(row));
492 }
493 let Datum::Map(_) = inner else {
494 return Err(format!("expected inner object for variant {variant}"));
495 };
496 rewrite(inner, variant, "", &mut row.packer())?;
497 Ok(Jsonb::from_row(row))
498 };
499
500 parse().map_err(|e| EvalError::InvalidCatalogJson(e.into()))
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506
507 fn check(input: &str, expected: &str) {
510 let input: Jsonb = input.parse().expect("valid input JSONB");
511 let actual = parse_catalog_audit_log_details(input.as_ref())
512 .expect("helper succeeded")
513 .to_string();
514 let actual_value: serde_json::Value =
515 serde_json::from_str(&actual).expect("valid output JSON");
516 let expected_value: serde_json::Value =
517 serde_json::from_str(expected).expect("valid expected JSON");
518 assert_eq!(actual_value, expected_value);
519 }
520
521 fn check_err(input: &str, expected_substr: &str) {
524 let input: Jsonb = input.parse().expect("valid input JSONB");
525 let err = parse_catalog_audit_log_details(input.as_ref()).expect_err("helper should error");
526 let msg = format!("{err:?}");
527 assert!(
528 msg.contains(expected_substr),
529 "error did not contain {expected_substr:?}: {msg}",
530 );
531 }
532
533 #[mz_ore::test]
536 fn variant_strip() {
537 check(
538 r#"{"IdNameV1": {"id": "u1", "name": "foo"}}"#,
539 r#"{"id": "u1", "name": "foo"}"#,
540 );
541 }
542
543 #[mz_ore::test]
546 fn flatten_name() {
547 check(
548 r#"{"IdFullNameV1": {
549 "id": "u1",
550 "name": {"database": "materialize", "schema": "public", "item": "t"}
551 }}"#,
552 r#"{"id": "u1", "database": "materialize", "schema": "public", "item": "t"}"#,
553 );
554 }
555
556 #[mz_ore::test]
561 fn flatten_recursive_and_nested_full_name() {
562 check(
563 r#"{"AlterApplyReplacementV1": {
564 "target": {
565 "id": "u1",
566 "name": {"database": "materialize", "schema": "public", "item": "mv"}
567 },
568 "replacement": {
569 "id": "u2",
570 "name": {"database": "materialize", "schema": "public", "item": "rp"}
571 }
572 }}"#,
573 r#"{
574 "id": "u1",
575 "database": "materialize",
576 "schema": "public",
577 "item": "mv",
578 "replacement": {
579 "id": "u2",
580 "database": "materialize",
581 "schema": "public",
582 "item": "rp"
583 }
584 }"#,
585 );
586 }
587
588 #[mz_ore::test]
592 fn string_wrapper_unwrap() {
593 check(
594 r#"{"AlterDefaultPrivilegeV1": {
595 "role_id": "u1",
596 "database_id": {"inner": "u2"},
597 "schema_id": {"inner": "u3"},
598 "grantee_id": "p",
599 "privileges": "r"
600 }}"#,
601 r#"{
602 "role_id": "u1",
603 "database_id": "u2",
604 "schema_id": "u3",
605 "grantee_id": "p",
606 "privileges": "r"
607 }"#,
608 );
609 }
610
611 #[mz_ore::test]
613 fn null_option() {
614 check(
615 r#"{"AlterDefaultPrivilegeV1": {
616 "role_id": "u1",
617 "database_id": null,
618 "schema_id": null,
619 "grantee_id": "p",
620 "privileges": "r"
621 }}"#,
622 r#"{
623 "role_id": "u1",
624 "database_id": null,
625 "schema_id": null,
626 "grantee_id": "p",
627 "privileges": "r"
628 }"#,
629 );
630 }
631
632 #[mz_ore::test]
638 fn nested_full_name_stays_nested() {
639 check(
640 r#"{"RenameItemV1": {
641 "id": "u1",
642 "old_name": {"database": "d", "schema": "s", "item": "a"},
643 "new_name": {"database": "d", "schema": "s", "item": "b"}
644 }}"#,
645 r#"{
646 "id": "u1",
647 "old_name": {"database": "d", "schema": "s", "item": "a"},
648 "new_name": {"database": "d", "schema": "s", "item": "b"}
649 }"#,
650 );
651 }
652
653 #[mz_ore::test]
656 fn rename_external_type() {
657 check(
658 r#"{"CreateSourceSinkV2": {
659 "id": "u1",
660 "name": {"database": "d", "schema": "s", "item": "src"},
661 "size": {"inner": "small"},
662 "external_type": "kafka"
663 }}"#,
664 r#"{
665 "id": "u1",
666 "database": "d",
667 "schema": "s",
668 "item": "src",
669 "size": "small",
670 "type": "kafka"
671 }"#,
672 );
673 }
674
675 #[mz_ore::test]
681 fn rename_hydration_time_estimate_under_scheduling() {
682 check(
683 r#"{"CreateClusterReplicaV3": {
684 "cluster_id": "u1",
685 "cluster_name": "c",
686 "replica_id": {"inner": "r1"},
687 "replica_name": "n",
688 "logical_size": "small",
689 "disk": false,
690 "billed_as": null,
691 "internal": false,
692 "reason": {"reason": {"Manual": {}}},
693 "scheduling_policies": {
694 "on_refresh": {
695 "decision": {"On": {}},
696 "objects_needing_refresh": [],
697 "objects_needing_compaction": [],
698 "rehydration_time_estimate": "00:00:07"
699 }
700 }
701 }}"#,
702 r#"{
703 "cluster_id": "u1",
704 "cluster_name": "c",
705 "replica_id": "r1",
706 "replica_name": "n",
707 "logical_size": "small",
708 "disk": false,
709 "billed_as": null,
710 "internal": false,
711 "reason": "manual",
712 "scheduling_policies": {
713 "on_refresh": {
714 "decision": "on",
715 "objects_needing_refresh": [],
716 "objects_needing_compaction": [],
717 "hydration_time_estimate": "00:00:07"
718 }
719 }
720 }"#,
721 );
722 }
723
724 #[mz_ore::test]
728 fn drop_null_replica_id_v1() {
729 check(
730 r#"{"CreateClusterReplicaV1": {
731 "cluster_id": "u1",
732 "cluster_name": "c",
733 "replica_id": null,
734 "replica_name": "n",
735 "logical_size": "small",
736 "disk": false,
737 "billed_as": null,
738 "internal": false
739 }}"#,
740 r#"{
741 "cluster_id": "u1",
742 "cluster_name": "c",
743 "replica_name": "n",
744 "logical_size": "small",
745 "disk": false,
746 "billed_as": null,
747 "internal": false
748 }"#,
749 );
750 }
751
752 #[mz_ore::test]
757 fn drop_null_replica_id_v2_kept() {
758 check(
759 r#"{"CreateClusterReplicaV2": {
760 "cluster_id": "u1",
761 "cluster_name": "c",
762 "replica_id": null,
763 "replica_name": "n",
764 "logical_size": "small",
765 "disk": false,
766 "billed_as": null,
767 "internal": false,
768 "reason": {"reason": {"System": {}}},
769 "scheduling_policies": null
770 }}"#,
771 r#"{
772 "cluster_id": "u1",
773 "cluster_name": "c",
774 "replica_id": null,
775 "replica_name": "n",
776 "logical_size": "small",
777 "disk": false,
778 "billed_as": null,
779 "internal": false,
780 "reason": "system"
781 }"#,
782 );
783 }
784
785 #[mz_ore::test]
788 fn enum_single_wrap_decision() {
789 check(
790 r#"{"CreateClusterReplicaV2": {
791 "cluster_id": "u1",
792 "cluster_name": "c",
793 "replica_id": {"inner": "r1"},
794 "replica_name": "n",
795 "logical_size": "small",
796 "disk": false,
797 "billed_as": null,
798 "internal": false,
799 "reason": {"reason": {"Schedule": {}}},
800 "scheduling_policies": {
801 "on_refresh": {
802 "decision": {"Off": {}},
803 "objects_needing_refresh": [],
804 "rehydration_time_estimate": "00:00:00"
805 }
806 }
807 }}"#,
808 r#"{
809 "cluster_id": "u1",
810 "cluster_name": "c",
811 "replica_id": "r1",
812 "replica_name": "n",
813 "logical_size": "small",
814 "disk": false,
815 "billed_as": null,
816 "internal": false,
817 "reason": "schedule",
818 "scheduling_policies": {
819 "on_refresh": {
820 "decision": "off",
821 "objects_needing_refresh": [],
822 "hydration_time_estimate": "00:00:00"
823 }
824 }
825 }"#,
826 );
827 }
828
829 #[mz_ore::test]
833 fn enum_double_wrap_reason_kebab() {
834 check(
835 r#"{"DropClusterReplicaV2": {
836 "cluster_id": "u1",
837 "cluster_name": "c",
838 "replica_id": "r1",
839 "replica_name": "n",
840 "reason": {"reason": {"HydrationBurst": {}}},
841 "scheduling_policies": null
842 }}"#,
843 r#"{
844 "cluster_id": "u1",
845 "cluster_name": "c",
846 "replica_id": "r1",
847 "replica_name": "n",
848 "reason": "hydration-burst"
849 }"#,
850 );
851 }
852
853 #[mz_ore::test]
857 #[cfg_attr(miri, ignore)] fn enum_double_wrap_transition_timed_out() {
859 check(
860 r#"{"AlterClusterReconfigurationV1": {
861 "cluster_id": "u1",
862 "cluster_name": "c",
863 "transition": {"transition": {"TimedOut": {}}},
864 "target_size": "small",
865 "target_replication_factor": 1,
866 "target_availability_zones": [],
867 "target_logging": {"log_logging": false, "interval": null},
868 "deadline": null
869 }}"#,
870 r#"{
871 "cluster_id": "u1",
872 "cluster_name": "c",
873 "transition": "timed-out",
874 "target_size": "small",
875 "target_replication_factor": 1,
876 "target_availability_zones": [],
877 "target_logging": {"log_logging": false, "interval": null},
878 "deadline": null
879 }"#,
880 );
881 }
882
883 #[mz_ore::test]
890 fn enum_double_wrap_hydration_burst_finished() {
891 check(
892 r#"{"ClusterHydrationBurstV1": {
893 "cluster_id": "u1",
894 "cluster_name": "c",
895 "transition": {"transition": {"Finished": {}}},
896 "finish_cause": {"cause": {"LingerElapsed": {}}},
897 "burst_size": "small"
898 }}"#,
899 r#"{
900 "cluster_id": "u1",
901 "cluster_name": "c",
902 "transition": "finished",
903 "finish_cause": "linger-elapsed",
904 "burst_size": "small"
905 }"#,
906 );
907 check(
908 r#"{"ClusterHydrationBurstV1": {
909 "cluster_id": "u1",
910 "cluster_name": "c",
911 "transition": {"transition": {"Started": {}}},
912 "finish_cause": null,
913 "burst_size": "small"
914 }}"#,
915 r#"{
916 "cluster_id": "u1",
917 "cluster_name": "c",
918 "transition": "started",
919 "finish_cause": null,
920 "burst_size": "small"
921 }"#,
922 );
923 }
924
925 #[mz_ore::test]
929 fn reset_all_v1_is_null() {
930 check(r#"{"ResetAllV1": {}}"#, r#"null"#);
931 }
932
933 #[mz_ore::test]
937 fn enum_unknown_variant_errors() {
938 check_err(
939 r#"{"DropClusterReplicaV2": {
940 "cluster_id": "u1",
941 "cluster_name": "c",
942 "replica_id": "r1",
943 "replica_name": "n",
944 "reason": {"reason": {"NoSuchVariant": {}}},
945 "scheduling_policies": null
946 }}"#,
947 "unknown enum variant NoSuchVariant",
948 );
949 }
950
951 #[mz_ore::test]
955 fn enum_non_empty_payload_errors() {
956 check_err(
957 r#"{"DropClusterReplicaV2": {
958 "cluster_id": "u1",
959 "cluster_name": "c",
960 "replica_id": "r1",
961 "replica_name": "n",
962 "reason": {"reason": {"Manual": {"unexpected": "x"}}},
963 "scheduling_policies": null
964 }}"#,
965 "non-empty payload",
966 );
967 }
968
969 #[mz_ore::test]
971 #[cfg_attr(miri, ignore)] fn error_cases() {
973 check_err(r#"{}"#, "empty details enum");
974 check_err(
975 r#"{"A": {"x": 1}, "B": {"y": 2}}"#,
976 "details enum had multiple keys",
977 );
978 check_err(r#"["IdNameV1", {"id": "u1"}]"#, "expected object");
979 check_err(r#"{"IdNameV1": "not an object"}"#, "expected inner object");
980 }
981}