Skip to main content

uqa_sql/ast/
role_specification.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL role keywords remain distinct from identically spelled quoted names.
8
9use serde::{Deserialize, Deserializer, Serialize};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12#[serde(tag = "kind", content = "name", rename_all = "snake_case")]
13pub enum RoleSpecification {
14    Named(String),
15    CurrentUser,
16    SessionUser,
17}
18
19impl From<String> for RoleSpecification {
20    fn from(name: String) -> Self {
21        Self::Named(name)
22    }
23}
24
25impl From<&str> for RoleSpecification {
26    fn from(name: &str) -> Self {
27        Self::Named(name.into())
28    }
29}
30
31impl std::fmt::Display for RoleSpecification {
32    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        formatter.write_str(match self {
34            Self::Named(name) => name,
35            Self::CurrentUser => "CURRENT_USER",
36            Self::SessionUser => "SESSION_USER",
37        })
38    }
39}
40
41impl<'de> Deserialize<'de> for RoleSpecification {
42    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
43        #[derive(Deserialize)]
44        #[serde(
45            tag = "kind",
46            content = "name",
47            rename_all = "snake_case",
48            deny_unknown_fields
49        )]
50        enum Tagged {
51            Named(String),
52            CurrentUser,
53            SessionUser,
54        }
55        #[derive(Deserialize)]
56        #[serde(untagged)]
57        enum Stored {
58            Tagged(Tagged),
59            Legacy(String),
60        }
61        Ok(match Stored::deserialize(deserializer)? {
62            Stored::Tagged(Tagged::Named(name)) => Self::Named(name),
63            Stored::Tagged(Tagged::CurrentUser) => Self::CurrentUser,
64            Stored::Tagged(Tagged::SessionUser) => Self::SessionUser,
65            // Preserve the meaning of old stored statements; newly compiled names always carry an explicit Named tag.
66            Stored::Legacy(name) => match name.as_str() {
67                "CURRENT_USER" => Self::CurrentUser,
68                "SESSION_USER" => Self::SessionUser,
69                _ => Self::Named(name),
70            },
71        })
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn explicit_names_and_keywords_have_distinct_durable_encodings() {
81        for specification in [
82            RoleSpecification::Named("CURRENT_USER".into()),
83            RoleSpecification::Named("SESSION_USER".into()),
84            RoleSpecification::CurrentUser,
85            RoleSpecification::SessionUser,
86        ] {
87            let stored = serde_json::to_string(&specification).unwrap();
88            assert!(stored.starts_with('{'));
89            assert_eq!(
90                serde_json::from_str::<RoleSpecification>(&stored).unwrap(),
91                specification
92            );
93        }
94    }
95
96    #[test]
97    fn legacy_role_specifications_preserve_their_previous_keyword_meaning() {
98        for (legacy, expected) in [
99            ("CURRENT_USER", RoleSpecification::CurrentUser),
100            ("SESSION_USER", RoleSpecification::SessionUser),
101            ("reader", RoleSpecification::Named("reader".into())),
102        ] {
103            let json = serde_json::to_string(legacy).unwrap();
104            assert_eq!(
105                serde_json::from_str::<RoleSpecification>(&json).unwrap(),
106                expected
107            );
108        }
109    }
110}