Skip to main content

uqa_storage/catalog/
schema.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use uqa_core::catalog_schema::SchemaRow as LegacySchemaRow;
9pub use uqa_core::catalog_schema::{
10    BoundSchemaRow, SchemaAclEntry, SchemaPrivileges, SchemaTupleIdentity,
11};
12
13/// A catalog read distinguishes old names from already captured role identities.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum SchemaRow {
16    Bound(BoundSchemaRow),
17    Legacy(LegacySchemaRow),
18}
19
20impl SchemaRow {
21    pub fn name(&self) -> &str {
22        match self {
23            Self::Bound(row) => &row.name,
24            Self::Legacy(row) => &row.name,
25        }
26    }
27
28    pub fn legacy(name: impl Into<String>) -> Self {
29        Self::Legacy(LegacySchemaRow::legacy(name))
30    }
31
32    pub fn bootstrap(name: impl Into<String>) -> Self {
33        Self::Bound(BoundSchemaRow::bootstrap(name))
34    }
35}
36
37impl From<BoundSchemaRow> for SchemaRow {
38    fn from(row: BoundSchemaRow) -> Self {
39        Self::Bound(row)
40    }
41}
42
43impl Serialize for SchemaRow {
44    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
45        #[derive(Serialize)]
46        struct Bound<'a> {
47            schema_security_format: u32,
48            #[serde(flatten)]
49            row: &'a BoundSchemaRow,
50        }
51        match self {
52            Self::Legacy(row) => row.serialize(serializer),
53            Self::Bound(row) => {
54                if row.tuple.is_some_and(|tuple| !tuple.is_valid()) {
55                    return Err(serde::ser::Error::custom(
56                        "invalid schema catalog tuple identity",
57                    ));
58                }
59                Bound {
60                    schema_security_format: if row.tuple.is_some() { 2 } else { 1 },
61                    row,
62                }
63                .serialize(serializer)
64            }
65        }
66    }
67}
68
69impl<'de> Deserialize<'de> for SchemaRow {
70    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
71        use serde::de::Error;
72        let value = serde_json::Value::deserialize(deserializer)?;
73        if let Some(version) = value.get("schema_security_format") {
74            let version = version.as_u64();
75            if !matches!(version, Some(1 | 2)) {
76                return Err(D::Error::custom("unsupported schema security format"));
77            }
78            if version == Some(1) && value.get("tuple").is_some() {
79                return Err(D::Error::custom(
80                    "legacy schema security contains a tuple identity",
81                ));
82            }
83            let row: BoundSchemaRow = serde_json::from_value(value).map_err(D::Error::custom)?;
84            if version == Some(2) && !row.tuple.is_some_and(SchemaTupleIdentity::is_valid) {
85                return Err(D::Error::custom("invalid schema catalog tuple identity"));
86            }
87            Ok(Self::Bound(row))
88        } else {
89            if value.get("tuple").is_some() {
90                return Err(D::Error::custom(
91                    "schema tuple identity has no format marker",
92                ));
93            }
94            serde_json::from_value(value)
95                .map(Self::Legacy)
96                .map_err(D::Error::custom)
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests;