Skip to main content

uqa_sql/catalog/roles/
tuple.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! A definition mutation retains its originally selected catalog tuple, independently of role authority.
8
9use super::{
10    identity::{RoleBinding, RoleSubject},
11    RoleDefinition,
12};
13use crate::SQLError;
14use std::collections::BTreeMap;
15
16#[derive(Clone, Debug)]
17pub struct RoleTuple {
18    pub role: RoleBinding,
19    pub revision: u64,
20}
21
22impl RoleTuple {
23    pub fn bind(definition: &RoleDefinition) -> Result<Self, SQLError> {
24        if definition.revision == 0 {
25            return Err(SQLError::Internal(
26                "role definition has no tuple revision".into(),
27            ));
28        }
29        Ok(Self {
30            role: RoleBinding::from_definition(definition)?,
31            revision: definition.revision,
32        })
33    }
34
35    pub fn revalidate<'a>(
36        &self,
37        roles: &'a BTreeMap<String, RoleDefinition>,
38    ) -> Result<&'a RoleDefinition, SQLError> {
39        let current = self
40            .role
41            .role_definition(roles)
42            .ok_or_else(|| SQLError::Routine {
43                sqlstate: "XX000".into(),
44                message: "tuple concurrently deleted".into(),
45            })?;
46        if current.revision != self.revision {
47            return Err(SQLError::Routine {
48                sqlstate: "XX000".into(),
49                message: "tuple concurrently updated".into(),
50            });
51        }
52        Ok(current)
53    }
54}
55
56pub fn validate_revisions(roles: &BTreeMap<String, RoleDefinition>) -> Result<(), String> {
57    for (name, role) in roles {
58        if role.revision == 0 {
59            return Err(format!("persisted role `{name}` has no tuple revision"));
60        }
61    }
62    Ok(())
63}
64
65pub fn validate_revision_changes(
66    before: &BTreeMap<String, RoleDefinition>,
67    after: &BTreeMap<String, RoleDefinition>,
68) -> Result<(), SQLError> {
69    validate_revisions(before).map_err(SQLError::Internal)?;
70    validate_revisions(after).map_err(SQLError::Internal)?;
71    for (name, role) in after {
72        let previous = before
73            .get(name)
74            .filter(|previous| previous.identity() == role.identity())
75            .or_else(|| {
76                before
77                    .values()
78                    .find(|previous| previous.identity() == role.identity())
79            });
80        let valid = previous.map_or(role.revision == 1, |previous| {
81            previous == role || previous.revision.checked_add(1) == Some(role.revision)
82        });
83        if !valid {
84            return Err(SQLError::Internal(format!(
85                "role `{name}` did not advance its tuple revision"
86            )));
87        }
88    }
89    Ok(())
90}
91
92#[cfg(test)]
93mod tests;