icydb_diagnostic_code/
query_field.rs1use crate::ErrorCode;
7
8pub const MAX_PUBLIC_QUERY_FIELD_BYTES: usize = 256;
10
11#[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 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum QueryFieldSchemaMismatch {
54 UnknownRole,
55 DisallowedCodeRole,
56 EmptyField,
57 FieldTooLong,
58}
59
60pub 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}