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, and conversions from parsed SQL values
12//! into `mz_repr` types.
13
14use mz_repr::Datum;
15use mz_repr::adt::interval::Interval;
16use mz_repr::adt::jsonb::JsonbRef;
17use mz_repr::adt::mz_acl_item::{AclMode, MzAclItem};
18use mz_repr::adt::numeric;
19use mz_repr::role_id::RoleId;
20use mz_sql_parser::ast::{DateTimeField, IntervalValue};
21
22/// Converts a JSONB `Datum` into a `u64`.
23fn jsonb_datum_to_u64<'a>(d: Datum<'a>) -> Result<u64, String> {
24    let Datum::Numeric(n) = d else {
25        return Err("expected numeric value".into());
26    };
27
28    let mut cx = numeric::cx_datum();
29    cx.try_into_u64(n.0)
30        .map_err(|_| format!("number out of u64 range: {n}"))
31}
32
33/// Decodes a JSONB object of shape `{"bitflags": <u64>}` into an `AclMode`.
34///
35/// Shared decoder for [`privileges`] (which embeds the object as the
36/// `acl_mode` field of each privilege) and [`acl_mode`] (which receives the
37/// object at the top level).
38fn jsonb_datum_to_acl_mode(d: Datum) -> Result<AclMode, String> {
39    let Datum::Map(dict) = d else {
40        return Err(format!("unexpected acl_mode: {d}"));
41    };
42    let mut bits = None;
43    for (key, val) in dict.iter() {
44        match key {
45            "bitflags" => bits = Some(jsonb_datum_to_u64(val)?),
46            other => return Err(format!("unexpected acl_mode field: {other}")),
47        }
48    }
49    let bits = bits.ok_or_else(|| "missing acl_mode bitflags".to_string())?;
50    AclMode::from_bits(bits).ok_or_else(|| format!("invalid acl_mode bitflags: {bits}"))
51}
52
53/// Converts a JSONB `Datum` into a `RoleId`.
54fn jsonb_datum_to_role_id(d: Datum) -> Result<RoleId, String> {
55    match d {
56        Datum::String("Public") => Ok(RoleId::Public),
57        Datum::String(other) => Err(format!("unexpected role ID variant: {other}")),
58        Datum::Map(dict) => {
59            let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
60            let n = jsonb_datum_to_u64(val)?;
61            match key {
62                "User" => Ok(RoleId::User(n)),
63                "System" => Ok(RoleId::System(n)),
64                "Predefined" => Ok(RoleId::Predefined(n)),
65                other => Err(format!("unexpected role ID variant: {other}")),
66            }
67        }
68        _ => Err("expected string or object".into()),
69    }
70}
71
72/// Converts a catalog JSON-serialized ID value into the appropriate string format.
73///
74/// Supports all of Materialize's various ID types of the form `<prefix><u64>`.
75pub fn catalog_id(a: JsonbRef<'_>) -> Result<String, String> {
76    match a.into_datum() {
77        // Unit variant, e.g. "Public"
78        Datum::String(variant) => match variant {
79            "Explain" => Ok("e".to_string()),
80            "Public" => Ok("p".to_string()),
81            other => Err(format!("unexpected ID variant: {other}")),
82        },
83        // Newtype variant, e.g. {"User": 1}
84        Datum::Map(dict) => {
85            let (key, val) = dict.iter().next().ok_or_else(|| "empty".to_string())?;
86            let prefix = match key {
87                "IntrospectionSourceIndex" => "si",
88                "Predefined" => "g",
89                "System" => "s",
90                "Transient" => "t",
91                "User" => "u",
92                other => return Err(format!("unexpected ID variant: {other}")),
93            };
94            let n = jsonb_datum_to_u64(val)?;
95            Ok(format!("{prefix}{n}"))
96        }
97        _ => Err("expected string or object".into()),
98    }
99}
100
101/// Converts a catalog JSON-serialized privilege array into a list of `MzAclItem`s.
102pub fn privileges(a: JsonbRef<'_>) -> Result<Vec<MzAclItem>, String> {
103    let parse_one = |datum| match datum {
104        Datum::Map(dict) => {
105            let mut grantee = None;
106            let mut grantor = None;
107            let mut acl_mode = None;
108            for (key, val) in dict.iter() {
109                match key {
110                    "grantee" => {
111                        let id = jsonb_datum_to_role_id(val)?;
112                        grantee = Some(id);
113                    }
114                    "grantor" => {
115                        let id = jsonb_datum_to_role_id(val)?;
116                        grantor = Some(id);
117                    }
118                    "acl_mode" => {
119                        acl_mode = Some(jsonb_datum_to_acl_mode(val)?);
120                    }
121                    other => return Err(format!("unexpected privilege field: {other}")),
122                }
123            }
124            Ok(MzAclItem {
125                grantee: grantee.ok_or_else(|| format!("missing grantee: {dict:?}"))?,
126                grantor: grantor.ok_or_else(|| "missing grantor in privilege".to_string())?,
127                acl_mode: acl_mode.ok_or_else(|| "missing acl_mode in privilege".to_string())?,
128            })
129        }
130        other => Err(format!("expected object in array, found: {other}")),
131    };
132
133    match a.into_datum() {
134        Datum::List(list) => {
135            let mut result = Vec::new();
136            for item in list.iter() {
137                result.push(parse_one(item)?);
138            }
139            Ok(result)
140        }
141        _ => Err("expected array".to_string()),
142    }
143}
144
145/// Converts a catalog JSON-serialized `AclMode` bitflags object
146/// (e.g. `{"bitflags": 514}`) into an `AclMode`.
147pub fn acl_mode(a: JsonbRef<'_>) -> Result<AclMode, String> {
148    jsonb_datum_to_acl_mode(a.into_datum())
149}
150
151/// Converts the parts of an `INTERVAL '...'` literal into an `Interval`.
152///
153/// Mirrors `plan_interval` in `mz_sql`.
154pub(crate) fn interval_literal(literal: &IntervalValue) -> Result<Interval, String> {
155    Interval::from_literal(
156        &literal.value,
157        date_time_field(literal.precision_high),
158        date_time_field(literal.precision_low),
159        literal.fsec_max_precision,
160    )
161    .map_err(|e| e.to_string())
162}
163
164fn date_time_field(field: DateTimeField) -> mz_repr::adt::datetime::DateTimeField {
165    use mz_repr::adt::datetime::DateTimeField as Repr;
166    match field {
167        DateTimeField::Millennium => Repr::Millennium,
168        DateTimeField::Century => Repr::Century,
169        DateTimeField::Decade => Repr::Decade,
170        DateTimeField::Year => Repr::Year,
171        DateTimeField::Month => Repr::Month,
172        DateTimeField::Day => Repr::Day,
173        DateTimeField::Hour => Repr::Hour,
174        DateTimeField::Minute => Repr::Minute,
175        DateTimeField::Second => Repr::Second,
176        DateTimeField::Milliseconds => Repr::Milliseconds,
177        DateTimeField::Microseconds => Repr::Microseconds,
178    }
179}