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::ddl_ast::parse::clause::{ClauseCursor, Tok, tokenize};
11use crate::error::SqlError;
12
13/// Parse the `QUOTA (field = value, ...)` clause of a statement into a
14/// [`QuotaSpec`].
15///
16/// The clause is the `QUOTA` keyword followed by a parenthesized list, and
17/// it must end the statement. A `(` inside a name or a literal before it is
18/// never read as the list. Unknown keys and `=>` used instead of `=` are
19/// refused.
20pub 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
46/// Parse the inside of a quota list, `field = value, ...`, into a
47/// [`QuotaSpec`].
48pub(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        // Reject `=>` (fat arrow used in vector kwargs) — this is `=` only.
60        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}