Skip to content

Commit aba9ed5

Browse files
committed
refactor(sql): split database DDL parser into submodule
database.rs (1117 lines) is split into focused files under parse/database/ — one file per DDL operation family (create, alter, drop_db, clone, mirror, backup_restore, move_tenant, use_db, quota_spec, show_extras, with_options) plus a dispatch.rs and mod.rs. No behaviour changes; all existing parser logic is preserved verbatim.
1 parent 3f69614 commit aba9ed5

14 files changed

Lines changed: 1411 additions & 1117 deletions

File tree

nodedb-sql/src/ddl_ast/parse/database.rs

Lines changed: 0 additions & 1117 deletions
This file was deleted.
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
//! `ALTER DATABASE <name> { RENAME TO <new> | SET QUOTA (...) | SET DEFAULT |
4+
//! SET AUDIT_DML = <mode> | SET IDLE_TIMEOUT = <secs> |
5+
//! MATERIALIZE | PROMOTE }`.
6+
7+
use nodedb_types::AuditDmlMode;
8+
9+
use crate::ddl_ast::statement::{AlterDatabaseOperation, NodedbStatement};
10+
use crate::error::SqlError;
11+
12+
use super::quota_spec::parse_quota_spec;
13+
14+
/// Maximum accepted value for `ALTER DATABASE ... SET IDLE_TIMEOUT = <secs>`,
15+
/// in seconds. Caps the parser so an administrative typo such as
16+
/// `SET IDLE_TIMEOUT = 999999999999` is rejected at parse time rather than
17+
/// silently accepted as an effectively-infinite timeout. One year is far
18+
/// beyond any reasonable interactive-session lifetime; use `0` to disable
19+
/// the timeout entirely.
20+
pub const MAX_IDLE_TIMEOUT_SECS: u64 = 365 * 24 * 60 * 60;
21+
22+
pub(super) fn parse_alter_database(
23+
parts: &[&str],
24+
original: &str,
25+
) -> Result<NodedbStatement, SqlError> {
26+
let name = parts
27+
.get(2)
28+
.copied()
29+
.ok_or_else(|| SqlError::Parse {
30+
detail: "ALTER DATABASE requires a name".into(),
31+
})?
32+
.trim_matches('"')
33+
.to_string();
34+
35+
let verb = parts
36+
.get(3)
37+
.copied()
38+
.ok_or_else(|| SqlError::Parse {
39+
detail: "ALTER DATABASE requires an operation keyword".into(),
40+
})?
41+
.to_uppercase();
42+
43+
let operation = match verb.as_str() {
44+
"RENAME" => parse_rename(parts)?,
45+
"SET" => parse_set(parts, original)?,
46+
"MATERIALIZE" => AlterDatabaseOperation::Materialize,
47+
"PROMOTE" => AlterDatabaseOperation::Promote,
48+
other => {
49+
return Err(SqlError::Parse {
50+
detail: format!("ALTER DATABASE: unknown operation '{other}'"),
51+
});
52+
}
53+
};
54+
55+
Ok(NodedbStatement::AlterDatabase { name, operation })
56+
}
57+
58+
fn parse_rename(parts: &[&str]) -> Result<AlterDatabaseOperation, SqlError> {
59+
let to_kw = parts.get(4).map(|w| w.to_uppercase()).unwrap_or_default();
60+
if to_kw != "TO" {
61+
return Err(SqlError::Parse {
62+
detail: format!("ALTER DATABASE RENAME requires keyword 'TO', got '{to_kw}'"),
63+
});
64+
}
65+
let new_name = parts
66+
.get(5)
67+
.copied()
68+
.ok_or_else(|| SqlError::Parse {
69+
detail: "ALTER DATABASE RENAME TO requires a new name".into(),
70+
})?
71+
.trim_matches('"')
72+
.to_string();
73+
Ok(AlterDatabaseOperation::Rename { new_name })
74+
}
75+
76+
fn parse_set(parts: &[&str], original: &str) -> Result<AlterDatabaseOperation, SqlError> {
77+
let target = parts.get(4).map(|w| w.to_uppercase()).unwrap_or_default();
78+
match target.as_str() {
79+
"QUOTA" => {
80+
let spec = parse_quota_spec(original, "ALTER DATABASE SET QUOTA")?;
81+
Ok(AlterDatabaseOperation::SetQuota(spec))
82+
}
83+
"DEFAULT" => Ok(AlterDatabaseOperation::SetDefault),
84+
"AUDIT_DML" => parse_set_audit_dml(parts),
85+
"IDLE_TIMEOUT" => parse_set_idle_timeout(parts),
86+
other => Err(SqlError::Parse {
87+
detail: format!("ALTER DATABASE SET: unknown target '{other}'"),
88+
}),
89+
}
90+
}
91+
92+
fn parse_set_audit_dml(parts: &[&str]) -> Result<AlterDatabaseOperation, SqlError> {
93+
let eq = parts.get(5).copied().unwrap_or("");
94+
if eq != "=" {
95+
return Err(SqlError::Parse {
96+
detail: format!("ALTER DATABASE SET AUDIT_DML requires '=', got '{eq}'"),
97+
});
98+
}
99+
let raw = parts.get(6).copied().ok_or_else(|| SqlError::Parse {
100+
detail: "ALTER DATABASE SET AUDIT_DML requires a value (NONE, WRITES, ALL)".into(),
101+
})?;
102+
let mode = raw
103+
.trim_matches('\'')
104+
.trim_matches('"')
105+
.parse::<AuditDmlMode>()
106+
.map_err(|e| SqlError::Parse {
107+
detail: format!("ALTER DATABASE SET AUDIT_DML: {e}"),
108+
})?;
109+
Ok(AlterDatabaseOperation::SetAuditDml(mode))
110+
}
111+
112+
fn parse_set_idle_timeout(parts: &[&str]) -> Result<AlterDatabaseOperation, SqlError> {
113+
let eq = parts.get(5).copied().unwrap_or("");
114+
if eq != "=" {
115+
return Err(SqlError::Parse {
116+
detail: format!("ALTER DATABASE SET IDLE_TIMEOUT requires '=', got '{eq}'"),
117+
});
118+
}
119+
let raw = parts.get(6).copied().ok_or_else(|| SqlError::Parse {
120+
detail: "ALTER DATABASE SET IDLE_TIMEOUT requires a non-negative integer (seconds)".into(),
121+
})?;
122+
let secs = raw
123+
.trim_matches('\'')
124+
.trim_matches('"')
125+
.parse::<u64>()
126+
.map_err(|_| SqlError::Parse {
127+
detail: format!(
128+
"ALTER DATABASE SET IDLE_TIMEOUT: invalid value '{raw}', expected non-negative integer"
129+
),
130+
})?;
131+
if secs > MAX_IDLE_TIMEOUT_SECS {
132+
return Err(SqlError::Parse {
133+
detail: format!(
134+
"ALTER DATABASE SET IDLE_TIMEOUT: {secs}s exceeds maximum {MAX_IDLE_TIMEOUT_SECS}s ({} days). \
135+
Use 0 to disable the timeout entirely.",
136+
MAX_IDLE_TIMEOUT_SECS / 86_400
137+
),
138+
});
139+
}
140+
Ok(AlterDatabaseOperation::SetIdleTimeout(secs))
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::super::dispatch::try_parse;
146+
use super::*;
147+
148+
fn ok(sql: &str) -> NodedbStatement {
149+
let upper = sql.to_uppercase();
150+
let parts: Vec<&str> = sql.split_whitespace().collect();
151+
try_parse(&upper, &parts, sql)
152+
.expect("expected Some")
153+
.expect("expected Ok")
154+
}
155+
156+
#[test]
157+
fn parse_alter_database_rename() {
158+
let stmt = ok("ALTER DATABASE mydb RENAME TO newdb");
159+
assert_eq!(
160+
stmt,
161+
NodedbStatement::AlterDatabase {
162+
name: "mydb".into(),
163+
operation: AlterDatabaseOperation::Rename {
164+
new_name: "newdb".into()
165+
},
166+
}
167+
);
168+
}
169+
170+
#[test]
171+
fn parse_alter_database_set_quota() {
172+
let stmt = ok("ALTER DATABASE mydb SET QUOTA (max_memory_bytes = 1073741824)");
173+
match stmt {
174+
NodedbStatement::AlterDatabase {
175+
name,
176+
operation: AlterDatabaseOperation::SetQuota(spec),
177+
} => {
178+
assert_eq!(name, "mydb");
179+
assert_eq!(spec.max_memory_bytes, Some(1_073_741_824));
180+
}
181+
other => panic!("expected AlterDatabase SetQuota, got {other:?}"),
182+
}
183+
}
184+
185+
#[test]
186+
fn parse_alter_database_set_quota_cache_weight_zero_rejected() {
187+
let sql = "ALTER DATABASE mydb SET QUOTA (cache_weight = 0)";
188+
let upper = sql.to_uppercase();
189+
let parts: Vec<&str> = sql.split_whitespace().collect();
190+
let err = try_parse(&upper, &parts, sql).unwrap().unwrap_err();
191+
match err {
192+
SqlError::Parse { detail } => {
193+
assert!(detail.contains("cache_weight"), "unexpected: {detail}");
194+
assert!(detail.contains("≥ 1"), "unexpected: {detail}");
195+
}
196+
other => panic!("unexpected error: {other:?}"),
197+
}
198+
}
199+
200+
#[test]
201+
fn parse_alter_database_set_quota_maintenance_pct_over_100_rejected() {
202+
let sql = "ALTER DATABASE mydb SET QUOTA (maintenance_cpu_pct = 150)";
203+
let upper = sql.to_uppercase();
204+
let parts: Vec<&str> = sql.split_whitespace().collect();
205+
let err = try_parse(&upper, &parts, sql).unwrap().unwrap_err();
206+
match err {
207+
SqlError::Parse { detail } => {
208+
assert!(
209+
detail.contains("maintenance_cpu_pct"),
210+
"unexpected: {detail}"
211+
);
212+
}
213+
other => panic!("unexpected error: {other:?}"),
214+
}
215+
}
216+
217+
#[test]
218+
fn parse_alter_set_audit_dml_writes() {
219+
let stmt = ok("ALTER DATABASE mydb SET AUDIT_DML = WRITES");
220+
assert_eq!(
221+
stmt,
222+
NodedbStatement::AlterDatabase {
223+
name: "mydb".into(),
224+
operation: AlterDatabaseOperation::SetAuditDml(AuditDmlMode::Writes),
225+
}
226+
);
227+
}
228+
229+
#[test]
230+
fn parse_alter_set_audit_dml_all() {
231+
let stmt = ok("ALTER DATABASE mydb SET AUDIT_DML = ALL");
232+
assert_eq!(
233+
stmt,
234+
NodedbStatement::AlterDatabase {
235+
name: "mydb".into(),
236+
operation: AlterDatabaseOperation::SetAuditDml(AuditDmlMode::All),
237+
}
238+
);
239+
}
240+
241+
#[test]
242+
fn parse_alter_set_audit_dml_none() {
243+
let stmt = ok("ALTER DATABASE mydb SET AUDIT_DML = NONE");
244+
assert_eq!(
245+
stmt,
246+
NodedbStatement::AlterDatabase {
247+
name: "mydb".into(),
248+
operation: AlterDatabaseOperation::SetAuditDml(AuditDmlMode::None),
249+
}
250+
);
251+
}
252+
253+
#[test]
254+
fn parse_alter_set_audit_dml_invalid_value_rejected() {
255+
let sql = "ALTER DATABASE mydb SET AUDIT_DML = INVALID";
256+
let upper = sql.to_uppercase();
257+
let parts: Vec<&str> = sql.split_whitespace().collect();
258+
let err = try_parse(&upper, &parts, sql).unwrap().unwrap_err();
259+
match err {
260+
SqlError::Parse { detail } => {
261+
assert!(
262+
detail.contains("AUDIT_DML"),
263+
"expected AUDIT_DML in error: {detail}"
264+
);
265+
}
266+
other => panic!("unexpected error: {other:?}"),
267+
}
268+
}
269+
270+
#[test]
271+
fn parse_alter_set_idle_timeout_explicit_value() {
272+
let stmt = ok("ALTER DATABASE foo SET IDLE_TIMEOUT = 1800");
273+
assert_eq!(
274+
stmt,
275+
NodedbStatement::AlterDatabase {
276+
name: "foo".into(),
277+
operation: AlterDatabaseOperation::SetIdleTimeout(1800),
278+
}
279+
);
280+
}
281+
282+
#[test]
283+
fn parse_alter_set_idle_timeout_zero_disables() {
284+
// 0 means "disabled" per the design.
285+
let stmt = ok("ALTER DATABASE foo SET IDLE_TIMEOUT = 0");
286+
match stmt {
287+
NodedbStatement::AlterDatabase {
288+
operation: AlterDatabaseOperation::SetIdleTimeout(secs),
289+
..
290+
} => assert_eq!(secs, 0),
291+
other => panic!("expected SetIdleTimeout(0), got {other:?}"),
292+
}
293+
}
294+
295+
#[test]
296+
fn parse_alter_set_idle_timeout_non_numeric_rejected() {
297+
let sql = "ALTER DATABASE foo SET IDLE_TIMEOUT = abc";
298+
let upper = sql.to_uppercase();
299+
let parts: Vec<&str> = sql.split_whitespace().collect();
300+
let result = try_parse(&upper, &parts, sql);
301+
assert!(
302+
result.as_ref().map(|r| r.is_err()).unwrap_or(true),
303+
"non-numeric value must be rejected"
304+
);
305+
}
306+
307+
#[test]
308+
fn parse_alter_set_idle_timeout_at_cap_accepted() {
309+
let sql = format!("ALTER DATABASE foo SET IDLE_TIMEOUT = {MAX_IDLE_TIMEOUT_SECS}");
310+
let stmt = ok(&sql);
311+
match stmt {
312+
NodedbStatement::AlterDatabase {
313+
operation: AlterDatabaseOperation::SetIdleTimeout(secs),
314+
..
315+
} => assert_eq!(secs, MAX_IDLE_TIMEOUT_SECS),
316+
other => panic!("expected SetIdleTimeout at cap, got {other:?}"),
317+
}
318+
}
319+
320+
#[test]
321+
fn parse_alter_set_idle_timeout_over_cap_rejected() {
322+
let sql = format!(
323+
"ALTER DATABASE foo SET IDLE_TIMEOUT = {}",
324+
MAX_IDLE_TIMEOUT_SECS + 1
325+
);
326+
let upper = sql.to_uppercase();
327+
let parts: Vec<&str> = sql.split_whitespace().collect();
328+
let err = try_parse(&upper, &parts, &sql).unwrap().unwrap_err();
329+
match err {
330+
SqlError::Parse { detail } => {
331+
assert!(detail.contains("exceeds maximum"), "{detail}");
332+
assert!(detail.contains("IDLE_TIMEOUT"), "{detail}");
333+
}
334+
other => panic!("unexpected error: {other:?}"),
335+
}
336+
}
337+
338+
#[test]
339+
fn parse_alter_set_idle_timeout_u64_max_rejected() {
340+
// The original silent-fallback bug: u64::MAX was accepted as a valid
341+
// timeout. The cap rejects it cleanly.
342+
let sql = format!("ALTER DATABASE foo SET IDLE_TIMEOUT = {}", u64::MAX);
343+
let upper = sql.to_uppercase();
344+
let parts: Vec<&str> = sql.split_whitespace().collect();
345+
let err = try_parse(&upper, &parts, &sql).unwrap().unwrap_err();
346+
match err {
347+
SqlError::Parse { detail } => {
348+
assert!(detail.contains("exceeds maximum"), "{detail}");
349+
}
350+
other => panic!("unexpected error: {other:?}"),
351+
}
352+
}
353+
354+
#[test]
355+
fn parse_alter_set_idle_timeout_missing_value_rejected() {
356+
let sql = "ALTER DATABASE foo SET IDLE_TIMEOUT =";
357+
let upper = sql.to_uppercase();
358+
let parts: Vec<&str> = sql.split_whitespace().collect();
359+
let result = try_parse(&upper, &parts, sql);
360+
assert!(
361+
result.as_ref().map(|r| r.is_err()).unwrap_or(true),
362+
"missing value must be rejected"
363+
);
364+
}
365+
}

0 commit comments

Comments
 (0)