Skip to main content

nodedb_sql/ddl_ast/parse/database/
quota_spec.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Shared `(field = value, ...)` parser for database quota specifications.
4//!
5//! Re-exported through `database::parse_quota_spec` because the tenant DDL
6//! parser also uses it (`MOVE TENANT ... WITH QUOTA (...)`).
7
8use nodedb_types::{PriorityClass, QuotaSpec};
9
10use crate::error::SqlError;
11
12/// Parse a `(field = value, ...)` clause from a raw SQL string into a [`QuotaSpec`].
13///
14/// Finds the first `(` after the `QUOTA` keyword, reads key=value pairs until `)`,
15/// and rejects unknown keys or `=>` used instead of `=`.
16pub fn parse_quota_spec(sql: &str, context: &str) -> Result<QuotaSpec, SqlError> {
17    // Find the opening paren.
18    let paren_start = sql.find('(').ok_or_else(|| SqlError::Parse {
19        detail: format!("{context}: expected '(' before quota arguments"),
20    })?;
21    let after = &sql[paren_start + 1..];
22    let paren_end = after.find(')').ok_or_else(|| SqlError::Parse {
23        detail: format!("{context}: unterminated '(' in quota clause"),
24    })?;
25    let inner = &after[..paren_end];
26
27    let mut spec = QuotaSpec::default();
28
29    for pair in inner.split(',') {
30        let pair = pair.trim();
31        if pair.is_empty() {
32            continue;
33        }
34        // Reject `=>` (fat arrow used in vector kwargs) — this is `=` only.
35        if pair.contains("=>") {
36            return Err(SqlError::Parse {
37                detail: format!(
38                    "{context}: use '=' not '=>' for quota key-value pairs (near '{pair}')"
39                ),
40            });
41        }
42        let mut it = pair.splitn(2, '=');
43        let key = it.next().unwrap_or("").trim().to_lowercase();
44        let val = it
45            .next()
46            .ok_or_else(|| SqlError::Parse {
47                detail: format!("{context}: expected '=' in quota pair '{pair}'"),
48            })?
49            .trim()
50            .trim_matches('\'')
51            .trim_matches('"');
52
53        match key.as_str() {
54            "max_memory_bytes" => {
55                spec.max_memory_bytes = Some(val.parse::<u64>().map_err(|_| SqlError::Parse {
56                    detail: format!(
57                        "{context}: max_memory_bytes must be a non-negative integer, got '{val}'"
58                    ),
59                })?);
60            }
61            "max_storage_bytes" => {
62                spec.max_storage_bytes = Some(val.parse::<u64>().map_err(|_| SqlError::Parse {
63                    detail: format!(
64                        "{context}: max_storage_bytes must be a non-negative integer, got '{val}'"
65                    ),
66                })?);
67            }
68            "max_qps" => {
69                spec.max_qps = Some(val.parse::<u32>().map_err(|_| SqlError::Parse {
70                    detail: format!(
71                        "{context}: max_qps must be a non-negative integer, got '{val}'"
72                    ),
73                })?);
74            }
75            "max_connections" => {
76                spec.max_connections = Some(val.parse::<u32>().map_err(|_| SqlError::Parse {
77                    detail: format!(
78                        "{context}: max_connections must be a non-negative integer, got '{val}'"
79                    ),
80                })?);
81            }
82            "cache_weight" => {
83                let w = val.parse::<u32>().map_err(|_| SqlError::Parse {
84                    detail: format!(
85                        "{context}: cache_weight must be a positive integer, got '{val}'"
86                    ),
87                })?;
88                if w == 0 {
89                    return Err(SqlError::Parse {
90                        detail: format!(
91                            "{context}: cache_weight must be ≥ 1 (zero would mean \
92                             no doc-cache capacity at all)"
93                        ),
94                    });
95                }
96                spec.cache_weight = Some(w);
97            }
98            "priority_class" => {
99                let pc = val.parse::<PriorityClass>().map_err(|e| SqlError::Parse {
100                    detail: format!("{context}: invalid priority_class — {e}"),
101                })?;
102                spec.priority_class = Some(pc);
103            }
104            "maintenance_cpu_pct" => {
105                let pct = val.parse::<u8>().map_err(|_| SqlError::Parse {
106                    detail: format!("{context}: maintenance_cpu_pct must be 0–100, got '{val}'"),
107                })?;
108                if pct > 100 {
109                    return Err(SqlError::Parse {
110                        detail: format!("{context}: maintenance_cpu_pct must be ≤ 100, got {pct}"),
111                    });
112                }
113                spec.maintenance_cpu_pct = Some(pct);
114            }
115            other => {
116                return Err(SqlError::Parse {
117                    detail: format!(
118                        "{context}: unknown quota field '{other}'. \
119                         Valid fields: max_memory_bytes, max_storage_bytes, max_qps, \
120                         max_connections, cache_weight, priority_class, maintenance_cpu_pct"
121                    ),
122                });
123            }
124        }
125    }
126
127    Ok(spec)
128}