use crate::protocol::SchemaColumn; // 读取 scripts/init.sql,只认 CREATE TABLE 与 CREATE INDEX。 // 系统库的 schema 由这份脚本声明,进程按它建系统表。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct InitColumn { pub name: String, pub ty: String, pub nullable: bool, pub auto_increment: bool, pub primary_key: bool, pub unique: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct InitTable { pub name: String, pub columns: Vec, pub indexes: Vec, } impl InitColumn { /// 转成数据字典里的列 pub fn to_schema(&self) -> SchemaColumn { SchemaColumn { name: self.name.clone(), ty: self.ty.clone(), nullable: self.nullable, default: None, auto_increment: self.auto_increment, primary_key: self.primary_key, unique: self.unique, check: None, } } } /// 解析整份脚本;不认识的语句直接报错,避免悄悄漏建表 pub fn parse(text: &str) -> Result, String> { let mut tables: Vec = Vec::new(); for statement in statements(text) { let words = statement.split_whitespace().collect::>(); let head = |index: usize| words.get(index).map(|word| word.to_ascii_uppercase()); match (head(0).as_deref(), head(1).as_deref()) { (Some("CREATE"), Some("TABLE")) => { let name = clean_name(words.get(2).ok_or("CREATE TABLE needs a name")?); let body = statement.split_once('(').ok_or("CREATE TABLE needs a column list")?.1; let inner = body.rsplit_once(')').map(|(inner, _)| inner).unwrap_or(body); tables.push(InitTable { name, columns: columns(inner)?, indexes: Vec::new() }); } (Some("CREATE"), Some("INDEX")) => { let table = clean_name(words.get(2).ok_or("CREATE INDEX needs a table")?); let column = statement .split_once('(') .and_then(|(_, rest)| rest.split_once(')')) .map(|(column, _)| clean_name(column)) .ok_or("CREATE INDEX needs a column")?; let entry = tables .iter_mut() .find(|entry| entry.name.eq_ignore_ascii_case(&table)) .ok_or_else(|| format!("CREATE INDEX on unknown table: {table}"))?; entry.indexes.push(column); } _ => return Err(format!("unsupported statement: {}", statement.trim())), } } Ok(tables) } /// 解析表定义里的列清单 fn columns(body: &str) -> Result, String> { let mut out = Vec::new(); for part in split_top_level(body) { let words = part.split_whitespace().collect::>(); if words.is_empty() { continue; } let keyword = words[0].to_ascii_uppercase(); if matches!(keyword.as_str(), "PRIMARY" | "UNIQUE" | "KEY" | "CONSTRAINT" | "INDEX" | "CHECK" | "FOREIGN") { return Err(format!("table-level constraint is not supported: {}", part.trim())); } let name = clean_name(words[0]); let ty = words .get(1) .ok_or_else(|| format!("column {name} needs a type"))? .to_string(); let upper = part.to_ascii_uppercase(); out.push(InitColumn { name, ty, nullable: !upper.contains("NOT NULL"), auto_increment: upper.contains("AUTO_INCREMENT"), primary_key: upper.contains("PRIMARY KEY"), unique: upper.contains("UNIQUE"), }); } Ok(out) } /// 按顶层的逗号切开(类型里的括号不算) fn split_top_level(body: &str) -> Vec { let mut parts = Vec::new(); let mut current = String::new(); let mut depth = 0; for ch in body.chars() { match ch { '(' => { depth += 1; current.push(ch); } ')' => { depth -= 1; current.push(ch); } ',' if depth == 0 => { parts.push(std::mem::take(&mut current)); } _ => current.push(ch), } } if !current.trim().is_empty() { parts.push(current); } parts } /// 去掉反引号与空白 fn clean_name(raw: &str) -> String { raw.trim().trim_matches('`').trim_matches('"').trim().to_string() } /// 去掉注释后按分号切开 fn statements(text: &str) -> Vec { let mut cleaned = String::new(); let mut chars = text.chars().peekable(); while let Some(ch) = chars.next() { if ch == '-' && chars.peek() == Some(&'-') { for next in chars.by_ref() { if next == '\n' { cleaned.push('\n'); break; } } } else if ch == '/' && chars.peek() == Some(&'*') { chars.next(); while let Some(next) = chars.next() { if next == '*' && chars.peek() == Some(&'/') { chars.next(); break; } } } else { cleaned.push(ch); } } cleaned .split(';') .map(|statement| statement.trim().to_string()) .filter(|statement| !statement.is_empty()) .collect() } #[cfg(test)] mod tests { use super::*; /// 仓库里的系统库脚本必须始终能解析 const SYSTEM_SCHEMA: &str = include_str!("../../../scripts/init.sql"); /// 仓库自带脚本必须能解析出账号表 #[test] fn parses_the_repository_init_script() { let tables = parse(SYSTEM_SCHEMA).expect("scripts/init.sql must parse"); let users = tables .iter() .find(|table| table.name == "__chusql_users") .expect("init.sql must declare the account table"); let names: Vec<&str> = users.columns.iter().map(|column| column.name.as_str()).collect(); assert_eq!(names, ["id", "user", "password_hash", "registered_at", "last_login_at", "revision"]); assert_eq!(users.columns[0].ty, "int"); assert!(users.columns[0].primary_key && users.columns[0].auto_increment && !users.columns[0].nullable); assert_eq!(users.columns[1].ty, "varchar(64)"); assert!(users.columns[1].unique); assert_eq!(users.columns[4].ty, "timestamp"); assert!(users.columns[4].nullable); assert_eq!(users.indexes, ["user"]); } /// 未知语句与表级约束要报错 #[test] fn rejects_unknown_statements_and_table_level_constraints() { assert!(parse("DROP TABLE users;").is_err()); assert!(parse("CREATE TABLE t (id int, PRIMARY KEY (id));").is_err()); assert!(parse("CREATE INDEX missing (user);").is_err()); } /// 注释与空语句要被跳过 #[test] fn ignores_comments_and_blank_statements() { let tables = parse("-- note\nCREATE TABLE t (a int NOT NULL);\n\n/* done */\n").unwrap(); assert_eq!(tables.len(), 1); assert_eq!(tables[0].columns[0].name, "a"); assert!(!tables[0].columns[0].nullable); } }