Skip to main content

mz_catalog_decode/
durable.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Decoders for values in their durable catalog JSON encoding, the serde shape
11//! of the `mz-catalog-protos` types.
12
13use mz_repr::Datum;
14use mz_repr::adt::jsonb::JsonbRef;
15use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem};
16use mz_repr::adt::numeric;
17use mz_repr::role_id::RoleId;
18
19/// Converts a JSONB `Datum` into a `u64`.
20fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
21    let Datum::Numeric(n) = d else {
22        return Err("expected numeric value".into());
23    };
24
25    let mut cx = numeric::cx_datum();
26    cx.try_into_u64(n.0)
27        .map_err(|_| format!("number out of u64 range: {n}"))
28}
29
30/// Decodes a JSONB object of shape `{"bitflags": <u64>}` into an `AclMode`.
31///
32/// Shared decoder for [`privileges`] (which embeds the object as the
33/// `acl_mode` field of each privilege) and [`acl_mode`] (which receives the
34/// object at the top level).
35fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
36    let Datum::Map(dict) = d else {
37        return Err(format!("unexpected acl_mode: {d}"));
38    };
39    let mut bits = None;
40    for (key, val) in dict.iter() {
41        match key {
42            "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
43            other => return Err(format!("unexpected acl_mode field: {other}")),
44        }
45    }
46    let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
47    AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
48}
49
50/// Converts a JSONB `Datum` into a `RoleId`.
51fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
52    match d {
53        Datum::String("Public") => Ok(RoleId::Public),
54        Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
55        Datum::Map(dict) => {
56            let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
57            let n = jsonb_datum_to_u64(val)?;
58            match key {
59                "User" => Ok(RoleId::User(n)),
60                "System" => Ok(RoleId::System(n)),
61                "Predefined" => Ok(RoleId::Predefined(n)),
62                other => Err(format!("unexpected role ID variant: {other}")),
63            }
64        }
65        _ => Err("expected string or object".into()),
66    }
67}
68
69/// Converts a catalog JSON-serialized ID value into the appropriate string format.
70///
71/// Supports all of Materialize's various ID types of the form `<prefix><u64>`.
72pub fn catalog_id(a: JsonbRef<'_>) -> Result<String, String> {
73    match a.into_datum() {
74        // Unit variant, e.g. "Public"
75        Datum::String(variant) => match variant {
76            "Explain" => Ok("e".to_string()),
77            "Public" => Ok("p".to_string()),
78            other => Err(format!("unexpected ID variant: {other}")),
79        },
80        // Newtype variant, e.g. {"User": 1}
81        Datum::Map(dict) => {
82            let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
83            let prefix = match key {
84                "IntrospectionSourceIndex" => "si",
85                "Predefined" => "g",
86                "System" => "s",
87                "Transient" => "t",
88                "User" => "u",
89                other => return Err(format!("unexpected ID variant: {other}")),
90            };
91            let n = jsonb_datum_to_u64(val)?;
92            Ok(format!("{prefix}{n}"))
93        }
94        _ => Err("expected string or object".into()),
95    }
96}
97
98/// Converts a catalog JSON-serialized privilege array into a list of `MzAclItem`s.
99pub fn privileges(a: JsonbRef<'_>) -> Result<Vec<MzAclItem>, String> {
100    let parse_one = |datum| match datum {
101        Datum::Map(dict) => {
102            let mut grantee = None;
103            let mut grantor = None;
104            let mut acl_mode = None;
105            for (key, val) in dict.iter() {
106                match key {
107                    "grantee" => {
108                        let id = jsonb_datum_to_role_id(val)?;
109                        grantee = Some(id);
110                    }
111                    "grantor" => {
112                        let id = jsonb_datum_to_role_id(val)?;
113                        grantor = Some(id);
114                    }
115                    "acl_mode" => {
116                        acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
117                    }
118                    other => return Err(format!("unexpected privilege field: {other}")),
119                }
120            }
121            Ok(MzAclItem {
122                grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
123                grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
124                acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
125            })
126        }
127        other => Err(format!("expected object in array, found: {other}")),
128    };
129
130    match a.into_datum() {
131        Datum::List(list) => {
132            let mut result = Vec::new();
133            for item in list.iter() {
134                result.push(parse_one(item)?);
135            }
136            Ok(result)
137        }
138        _ => Err("expected array".to_string()),
139    }
140}
141
142/// Converts a catalog JSON-serialized `AclMode` bitflags object
143/// (e.g. `{"bitflags": 514}`) into an `AclMode`.
144pub fn acl_mode(a: JsonbRef<'_>) -> Result<AclMode, String> {
145    jsonb_datum_to_acl_mode(a.into_datum())
146}