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