Skip to main content

icydb_diagnostic_code/
query_field.rs

1//! Module: query_field
2//! Responsibility: compact public query-field roles and their bounded schema.
3//! Does not own: rejected-field discovery, planner propagation, or rendering.
4//! Boundary: validates only the public error-code/role/field contract.
5
6use crate::ErrorCode;
7
8/// Maximum UTF-8 byte length of one public rejected query-field reference.
9pub const MAX_PUBLIC_QUERY_FIELD_BYTES: usize = 256;
10
11/// Compact semantic role of one rejected query-field reference.
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
13pub enum QueryFieldRole {
14    Predicate,
15    Projection,
16    GroupBy,
17    Having,
18    OrderBy,
19    AggregateTarget,
20}
21
22impl QueryFieldRole {
23    /// Return the stable public wire identity.
24    #[must_use]
25    pub const fn raw(self) -> u8 {
26        match self {
27            Self::Predicate => 1,
28            Self::Projection => 2,
29            Self::GroupBy => 3,
30            Self::Having => 4,
31            Self::OrderBy => 5,
32            Self::AggregateTarget => 6,
33        }
34    }
35
36    /// Recover one known role from its public wire identity.
37    #[must_use]
38    pub const fn known(raw: u8) -> Option<Self> {
39        match raw {
40            1 => Some(Self::Predicate),
41            2 => Some(Self::Projection),
42            3 => Some(Self::GroupBy),
43            4 => Some(Self::Having),
44            5 => Some(Self::OrderBy),
45            6 => Some(Self::AggregateTarget),
46            _ => None,
47        }
48    }
49}
50
51/// Reason one optional public query-field context failed schema validation.
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum QueryFieldSchemaMismatch {
54    UnknownRole,
55    DisallowedCodeRole,
56    EmptyField,
57    FieldTooLong,
58}
59
60/// Validate one raw public query-field context without inferring its producer.
61pub fn validate_query_field_schema(
62    code: ErrorCode,
63    raw_role: u8,
64    field: &str,
65) -> Result<QueryFieldRole, QueryFieldSchemaMismatch> {
66    let Some(role) = QueryFieldRole::known(raw_role) else {
67        return Err(QueryFieldSchemaMismatch::UnknownRole);
68    };
69    if code != ErrorCode::QUERY_PLAN {
70        return Err(QueryFieldSchemaMismatch::DisallowedCodeRole);
71    }
72    if field.is_empty() {
73        return Err(QueryFieldSchemaMismatch::EmptyField);
74    }
75    if field.len() > MAX_PUBLIC_QUERY_FIELD_BYTES {
76        return Err(QueryFieldSchemaMismatch::FieldTooLong);
77    }
78
79    Ok(role)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn role_registry_is_exact_and_closed() {
88        let roles = [
89            QueryFieldRole::Predicate,
90            QueryFieldRole::Projection,
91            QueryFieldRole::GroupBy,
92            QueryFieldRole::Having,
93            QueryFieldRole::OrderBy,
94            QueryFieldRole::AggregateTarget,
95        ];
96
97        for (index, role) in roles.into_iter().enumerate() {
98            let raw = u8::try_from(index + 1).expect("six roles fit u8");
99            assert_eq!(role.raw(), raw);
100            assert_eq!(QueryFieldRole::known(raw), Some(role));
101        }
102        assert_eq!(QueryFieldRole::known(0), None);
103        assert_eq!(QueryFieldRole::known(7), None);
104        assert_eq!(QueryFieldRole::known(u8::MAX), None);
105    }
106
107    #[test]
108    fn schema_accepts_only_query_plan_and_bounded_nonempty_fields() {
109        for role in [
110            QueryFieldRole::Predicate,
111            QueryFieldRole::Projection,
112            QueryFieldRole::GroupBy,
113            QueryFieldRole::Having,
114            QueryFieldRole::OrderBy,
115            QueryFieldRole::AggregateTarget,
116        ] {
117            assert_eq!(
118                validate_query_field_schema(ErrorCode::QUERY_PLAN, role.raw(), "missing"),
119                Ok(role)
120            );
121        }
122        assert_eq!(
123            validate_query_field_schema(ErrorCode::QUERY_VALIDATE, 1, "missing"),
124            Err(QueryFieldSchemaMismatch::DisallowedCodeRole)
125        );
126        assert_eq!(
127            validate_query_field_schema(ErrorCode::QUERY_PLAN, 0, "missing"),
128            Err(QueryFieldSchemaMismatch::UnknownRole)
129        );
130        assert_eq!(
131            validate_query_field_schema(ErrorCode::QUERY_PLAN, 1, ""),
132            Err(QueryFieldSchemaMismatch::EmptyField)
133        );
134    }
135
136    #[test]
137    fn schema_uses_utf8_bytes_without_truncation() {
138        let exact_ascii = "a".repeat(MAX_PUBLIC_QUERY_FIELD_BYTES);
139        let over_ascii = "a".repeat(MAX_PUBLIC_QUERY_FIELD_BYTES + 1);
140        let exact_multibyte = "é".repeat(MAX_PUBLIC_QUERY_FIELD_BYTES / 2);
141        let over_multibyte = format!("{exact_multibyte}a");
142
143        for field in [&exact_ascii, &exact_multibyte] {
144            assert_eq!(
145                validate_query_field_schema(ErrorCode::QUERY_PLAN, 5, field),
146                Ok(QueryFieldRole::OrderBy)
147            );
148        }
149        for field in [&over_ascii, &over_multibyte] {
150            assert_eq!(
151                validate_query_field_schema(ErrorCode::QUERY_PLAN, 5, field),
152                Err(QueryFieldSchemaMismatch::FieldTooLong)
153            );
154        }
155    }
156}