nodedb_sql/ddl_ast/parse/database/
quota_spec.rs1use nodedb_types::{PriorityClass, QuotaSpec};
9
10use crate::ddl_ast::parse::clause::{ClauseCursor, Tok, tokenize};
11use crate::error::SqlError;
12
13pub fn parse_quota_spec(sql: &str, context: &str) -> Result<QuotaSpec, SqlError> {
21 let toks = tokenize(sql);
22 let at = toks
23 .windows(2)
24 .position(|w| {
25 matches!(&w[0].tok, Tok::Word(kw) if kw.eq_ignore_ascii_case("QUOTA"))
26 && w[1].tok == Tok::Punct('(')
27 })
28 .ok_or_else(|| SqlError::Parse {
29 detail: format!("{context}: expected '(' before quota arguments"),
30 })?;
31 let mut c = ClauseCursor::new(sql, &toks[at + 1..], "QUOTA");
32 let inner = c.raw_paren("QUOTA").map_err(|_| SqlError::Parse {
33 detail: format!("{context}: unterminated '(' in quota clause"),
34 })?;
35 if !c.at_end() {
36 return Err(SqlError::Parse {
37 detail: format!(
38 "{context}: unexpected {} after the quota list",
39 c.describe_next()
40 ),
41 });
42 }
43 parse_quota_pairs(inner, context)
44}
45
46pub(in crate::ddl_ast::parse) fn parse_quota_pairs(
49 inner: &str,
50 context: &str,
51) -> Result<QuotaSpec, SqlError> {
52 let mut spec = QuotaSpec::default();
53
54 for pair in inner.split(',') {
55 let pair = pair.trim();
56 if pair.is_empty() {
57 continue;
58 }
59 if pair.contains("=>") {
61 return Err(SqlError::Parse {
62 detail: format!(
63 "{context}: use '=' not '=>' for quota key-value pairs (near '{pair}')"
64 ),
65 });
66 }
67 let mut it = pair.splitn(2, '=');
68 let key = it.next().unwrap_or("").trim().to_lowercase();
69 let val = it
70 .next()
71 .ok_or_else(|| SqlError::Parse {
72 detail: format!("{context}: expected '=' in quota pair '{pair}'"),
73 })?
74 .trim()
75 .trim_matches('\'')
76 .trim_matches('"');
77
78 match key.as_str() {
79 "max_memory_bytes" => {
80 spec.max_memory_bytes = Some(val.parse::<u64>().map_err(|_| SqlError::Parse {
81 detail: format!(
82 "{context}: max_memory_bytes must be a non-negative integer, got '{val}'"
83 ),
84 })?);
85 }
86 "max_storage_bytes" => {
87 spec.max_storage_bytes = Some(val.parse::<u64>().map_err(|_| SqlError::Parse {
88 detail: format!(
89 "{context}: max_storage_bytes must be a non-negative integer, got '{val}'"
90 ),
91 })?);
92 }
93 "max_qps" => {
94 spec.max_qps = Some(val.parse::<u32>().map_err(|_| SqlError::Parse {
95 detail: format!(
96 "{context}: max_qps must be a non-negative integer, got '{val}'"
97 ),
98 })?);
99 }
100 "max_connections" => {
101 spec.max_connections = Some(val.parse::<u32>().map_err(|_| SqlError::Parse {
102 detail: format!(
103 "{context}: max_connections must be a non-negative integer, got '{val}'"
104 ),
105 })?);
106 }
107 "cache_weight" => {
108 let w = val.parse::<u32>().map_err(|_| SqlError::Parse {
109 detail: format!(
110 "{context}: cache_weight must be a positive integer, got '{val}'"
111 ),
112 })?;
113 if w == 0 {
114 return Err(SqlError::Parse {
115 detail: format!(
116 "{context}: cache_weight must be ≥ 1 (zero would mean \
117 no doc-cache capacity at all)"
118 ),
119 });
120 }
121 spec.cache_weight = Some(w);
122 }
123 "priority_class" => {
124 let pc = val.parse::<PriorityClass>().map_err(|e| SqlError::Parse {
125 detail: format!("{context}: invalid priority_class — {e}"),
126 })?;
127 spec.priority_class = Some(pc);
128 }
129 "maintenance_cpu_pct" => {
130 let pct = val.parse::<u8>().map_err(|_| SqlError::Parse {
131 detail: format!("{context}: maintenance_cpu_pct must be 0–100, got '{val}'"),
132 })?;
133 if pct > 100 {
134 return Err(SqlError::Parse {
135 detail: format!("{context}: maintenance_cpu_pct must be ≤ 100, got {pct}"),
136 });
137 }
138 spec.maintenance_cpu_pct = Some(pct);
139 }
140 other => {
141 return Err(SqlError::Parse {
142 detail: format!(
143 "{context}: unknown quota field '{other}'. \
144 Valid fields: max_memory_bytes, max_storage_bytes, max_qps, \
145 max_connections, cache_weight, priority_class, maintenance_cpu_pct"
146 ),
147 });
148 }
149 }
150 }
151
152 Ok(spec)
153}