#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiveTable {
pub name: String,
pub rootpage: u32,
pub column_names: Option<Vec<String>>,
pub affinities: Vec<Affinity>,
pub create_sql: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Affinity {
Integer,
Text,
Blob,
Real,
Numeric,
}
#[must_use]
pub fn column_affinity(declared_type: &str) -> Affinity {
let t = declared_type.to_ascii_uppercase();
if t.contains("INT") {
Affinity::Integer
} else if t.contains("CHAR") || t.contains("CLOB") || t.contains("TEXT") {
Affinity::Text
} else if t.is_empty() || t.contains("BLOB") {
Affinity::Blob
} else if t.contains("REAL") || t.contains("FLOA") || t.contains("DOUB") {
Affinity::Real
} else {
Affinity::Numeric
}
}
#[must_use]
pub fn column_defs(create_sql: &str) -> Option<Vec<(String, String)>> {
let inner = outermost_paren_body(create_sql)?;
let parts = split_top_level(inner);
let mut cols = Vec::new();
for part in parts {
let part = part.trim();
if part.is_empty() {
continue;
}
let mut tokens = ColumnTokens::new(part);
let Some(name) = tokens.next_identifier() else {
continue; };
if is_table_constraint_keyword(&name) {
continue;
}
let declared_type = tokens.rest_as_type();
cols.push((name, declared_type));
}
if cols.is_empty() {
None
} else {
Some(cols)
}
}
#[must_use]
pub fn column_names(create_sql: &str) -> Option<Vec<String>> {
Some(
column_defs(create_sql)?
.into_iter()
.map(|(n, _)| n)
.collect(),
)
}
fn outermost_paren_body(sql: &str) -> Option<&str> {
let bytes = sql.as_bytes();
let mut i = 0usize;
let mut start = None;
let mut depth = 0i32;
let mut quote: Option<u8> = None;
while i < bytes.len() {
let c = bytes[i];
if let Some(q) = quote {
if c == q {
quote = None;
}
i += 1;
continue;
}
match c {
b'\'' | b'"' | b'`' => quote = Some(c),
b'[' => quote = Some(b']'),
b'(' => {
if start.is_none() {
start = Some(i + 1);
depth = 1;
} else {
depth += 1;
}
}
b')' if start.is_some() => {
depth -= 1;
if depth == 0 {
return sql.get(start?..i);
}
}
_ => {}
}
i += 1;
}
None
}
fn split_top_level(body: &str) -> Vec<&str> {
let bytes = body.as_bytes();
let mut parts = Vec::new();
let mut start = 0usize;
let mut depth = 0i32;
let mut quote: Option<u8> = None;
let mut i = 0usize;
while i < bytes.len() {
let c = bytes[i];
if let Some(q) = quote {
if c == q {
quote = None;
}
i += 1;
continue;
}
match c {
b'\'' | b'"' | b'`' => quote = Some(c),
b'[' => quote = Some(b']'),
b'(' => depth += 1,
b')' => depth = depth.saturating_sub(1),
b',' if depth == 0 => {
if let Some(p) = body.get(start..i) {
parts.push(p);
}
start = i + 1;
}
_ => {}
}
i += 1;
}
if let Some(p) = body.get(start..) {
parts.push(p);
}
parts
}
fn is_table_constraint_keyword(word: &str) -> bool {
matches!(
word.to_ascii_uppercase().as_str(),
"CONSTRAINT" | "PRIMARY" | "UNIQUE" | "CHECK" | "FOREIGN" | "KEY"
)
}
struct ColumnTokens<'a> {
rest: &'a str,
}
impl<'a> ColumnTokens<'a> {
fn new(part: &'a str) -> Self {
Self { rest: part }
}
fn next_identifier(&mut self) -> Option<String> {
let s = self.rest.trim_start();
let bytes = s.as_bytes();
let first = *bytes.first()?;
let (name, consumed) = match first {
b'"' | b'`' | b'\'' => read_quoted(s, char::from(first)),
b'[' => read_quoted(s, ']'),
_ => read_bare(s),
}?;
self.rest = s.get(consumed..).unwrap_or("");
Some(name)
}
fn rest_as_type(&self) -> String {
self.rest.trim().to_string()
}
}
fn read_quoted(s: &str, close: char) -> Option<(String, usize)> {
let bytes = s.as_bytes();
let close_b = close as u8;
let mut name = String::new();
let mut i = 1usize; while i < bytes.len() {
let c = bytes[i];
if c == close_b {
if close_b != b']' && bytes.get(i + 1) == Some(&close_b) {
name.push(close);
i += 2;
continue;
}
return Some((name, i + 1));
}
name.push(char::from(c));
i += 1;
}
None }
fn read_bare(s: &str) -> Option<(String, usize)> {
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_whitespace() || c == b'(' {
break;
}
i += 1;
}
if i == 0 {
return None; }
Some((s.get(..i)?.to_string(), i))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn affinity_rules_match_spec_priority() {
assert_eq!(column_affinity("INTEGER"), Affinity::Integer);
assert_eq!(column_affinity("BIGINT"), Affinity::Integer);
assert_eq!(column_affinity("VARCHAR(255)"), Affinity::Text);
assert_eq!(column_affinity("CLOB"), Affinity::Text);
assert_eq!(column_affinity("TEXT"), Affinity::Text);
assert_eq!(column_affinity("BLOB"), Affinity::Blob);
assert_eq!(column_affinity(""), Affinity::Blob);
assert_eq!(column_affinity("REAL"), Affinity::Real);
assert_eq!(column_affinity("DOUBLE"), Affinity::Real);
assert_eq!(column_affinity("FLOAT"), Affinity::Real);
assert_eq!(column_affinity("NUMERIC"), Affinity::Numeric);
assert_eq!(column_affinity("DATETIME"), Affinity::Numeric);
assert_eq!(column_affinity("BOOLEAN"), Affinity::Numeric);
}
#[test]
fn plain_columns() {
let cols = column_names("CREATE TABLE t (id INTEGER, name TEXT, age INT)").unwrap();
assert_eq!(cols, vec!["id", "name", "age"]);
}
#[test]
fn quoted_bracketed_backtick_identifiers() {
let sql = r#"CREATE TABLE "My Tbl" ("first name" TEXT, [second] INTEGER, `third` BLOB)"#;
let cols = column_names(sql).unwrap();
assert_eq!(cols, vec!["first name", "second", "third"]);
}
#[test]
fn skips_table_level_constraints() {
let sql = "CREATE TABLE t (\
id INTEGER PRIMARY KEY, \
a TEXT, \
b REAL, \
PRIMARY KEY (id), \
UNIQUE (a), \
CONSTRAINT fk FOREIGN KEY (b) REFERENCES other(x), \
CHECK (a <> b))";
let cols = column_names(sql).unwrap();
assert_eq!(cols, vec!["id", "a", "b"]);
}
#[test]
fn check_constraint_with_commas_does_not_oversplit() {
let sql = "CREATE TABLE t (x INTEGER, y INTEGER, CHECK (x IN (1, 2, 3)))";
let cols = column_names(sql).unwrap();
assert_eq!(cols, vec!["x", "y"]);
}
#[test]
fn typed_columns_with_parenthesized_and_multiword_types() {
let sql = "CREATE TABLE t (a VARCHAR(20), b DOUBLE PRECISION, c DECIMAL(10,2))";
let defs = column_defs(sql).unwrap();
assert_eq!(defs[0], ("a".to_string(), "VARCHAR(20)".to_string()));
assert_eq!(defs[1], ("b".to_string(), "DOUBLE PRECISION".to_string()));
assert_eq!(defs[2], ("c".to_string(), "DECIMAL(10,2)".to_string()));
assert_eq!(column_affinity(&defs[0].1), Affinity::Text);
assert_eq!(column_affinity(&defs[1].1), Affinity::Real);
assert_eq!(column_affinity(&defs[2].1), Affinity::Numeric);
}
#[test]
fn no_parens_is_low_confidence_none() {
assert_eq!(column_names("CREATE TABLE t"), None);
assert_eq!(column_names("not even ddl"), None);
}
#[test]
fn unterminated_quote_yields_none() {
assert_eq!(column_names(r#"CREATE TABLE t ("oops)"#), None);
}
#[test]
fn empty_column_list_is_none() {
assert_eq!(column_names("CREATE TABLE t ()"), None);
}
#[test]
fn single_quoted_identifiers_are_unquoted() {
let sql = "CREATE TABLE users (\n\t'id' INT NOT NULL,\n\t'name' TEXT NULL)";
let cols = column_names(sql).unwrap();
assert_eq!(cols, vec!["id", "name"]);
}
}