use nodedb_types::value::Value;
use crate::error::LiteError;
pub(crate) fn parse_strict_create_sql(
sql: &str,
) -> Result<(String, nodedb_types::columnar::StrictSchema), LiteError> {
use nodedb_types::columnar::{ColumnDef, ColumnType, StrictSchema};
let upper = sql.to_uppercase();
let after_create = sql
.get(
upper
.find("COLLECTION")
.ok_or(LiteError::Query("expected COLLECTION keyword".into()))?
+ 10..,
)
.ok_or(LiteError::Query("unexpected end of SQL".into()))?
.trim();
let name_end = after_create
.find(|c: char| c == '(' || c.is_whitespace())
.unwrap_or(after_create.len());
let name = after_create[..name_end].trim().to_lowercase();
if name.is_empty() {
return Err(LiteError::Query("missing collection name".into()));
}
let paren_start = sql.find('(').ok_or(LiteError::Query(
"expected column definitions in parentheses".into(),
))?;
let sql_bytes = sql.as_bytes();
let mut depth = 0;
let mut paren_end = None;
for (i, &b) in sql_bytes.iter().enumerate().skip(paren_start) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
paren_end = Some(i);
break;
}
}
_ => {}
}
}
let paren_end = paren_end.ok_or(LiteError::Query("unmatched parenthesis".into()))?;
let col_defs_str = &sql[paren_start + 1..paren_end];
let col_parts = split_top_level_commas(col_defs_str);
let mut columns = Vec::new();
for part in &col_parts {
let col = parse_column_def(part.trim())?;
columns.push(col);
}
if columns.is_empty() {
return Err(LiteError::Query("at least one column required".into()));
}
if !columns.iter().any(|c| c.primary_key) {
columns.insert(
0,
ColumnDef::required("_rowid", ColumnType::Int64).with_primary_key(),
);
}
let schema = StrictSchema::new(columns).map_err(|e| LiteError::Query(e.to_string()))?;
Ok((name, schema))
}
pub(crate) fn split_top_level_commas(s: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0;
let mut start = 0;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => depth -= 1,
',' if depth == 0 => {
parts.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
if start < s.len() {
parts.push(&s[start..]);
}
parts
}
pub(crate) fn parse_column_def(s: &str) -> Result<nodedb_types::columnar::ColumnDef, LiteError> {
use nodedb_types::columnar::{ColumnDef, ColumnType};
let upper = s.to_uppercase();
let tokens: Vec<&str> = s.split_whitespace().collect();
if tokens.len() < 2 {
return Err(LiteError::Query(format!(
"column definition requires at least name and type, got: '{s}'"
)));
}
let name = tokens[0].to_lowercase();
let after_name = s.split_whitespace().skip(1).collect::<Vec<_>>().join(" ");
let type_end = find_keyword_start(&after_name);
let type_str = after_name[..type_end].trim();
let column_type: ColumnType =
type_str
.parse()
.map_err(|e: nodedb_types::columnar::ColumnTypeParseError| {
LiteError::Query(e.to_string())
})?;
let is_not_null = upper.contains("NOT NULL");
let is_pk = upper.contains("PRIMARY KEY");
let nullable = !is_not_null && !is_pk;
let default = if let Some(pos) = upper.find("DEFAULT ") {
let after_default = s[pos + 8..].trim();
let end = find_keyword_start(after_default);
let expr = after_default[..end].trim();
if expr.is_empty() {
None
} else {
Some(expr.to_string())
}
} else {
None
};
let base = if nullable {
ColumnDef::nullable(name, column_type)
} else {
ColumnDef::required(name, column_type)
};
let mut col = base.with_declared_width(type_str);
if is_pk {
col = col.with_primary_key();
}
if let Some(d) = default {
col = col.with_default(d);
}
Ok(col)
}
pub(crate) fn find_keyword_start(s: &str) -> usize {
let upper = s.to_uppercase();
let keywords = ["NOT", "NULL", "PRIMARY", "DEFAULT"];
let mut earliest = s.len();
for kw in &keywords {
if let Some(pos) = upper.find(kw) {
let at_boundary = pos == 0
|| upper
.as_bytes()
.get(pos - 1)
.is_some_and(|b| b.is_ascii_whitespace());
if at_boundary && pos < earliest {
earliest = pos;
}
}
}
earliest
}
pub(crate) fn describe_strict_collection(
name: &str,
schema: &nodedb_types::columnar::StrictSchema,
) -> nodedb_types::result::QueryResult {
use nodedb_types::columnar::SchemaOps;
let mut rows = Vec::with_capacity(schema.len() + 2);
rows.push(vec![
Value::String("__collection".into()),
Value::String(name.to_string()),
Value::String(String::new()),
Value::String(String::new()),
Value::String(String::new()),
]);
rows.push(vec![
Value::String("__storage".into()),
Value::String("document".into()),
Value::String("strict".into()),
Value::String(String::new()),
Value::String(format!("v{}", schema.version)),
]);
for col in &schema.columns {
rows.push(vec![
Value::String(col.name.clone()),
Value::String(col.column_type.to_string()),
Value::String(if col.nullable { "YES" } else { "NO" }.into()),
Value::String(if col.primary_key { "YES" } else { "NO" }.into()),
Value::String(col.default.clone().unwrap_or_default()),
]);
}
nodedb_types::result::QueryResult {
columns: vec![
"field".into(),
"type".into(),
"nullable".into(),
"primary_key".into(),
"default".into(),
],
rows,
rows_affected: 0,
command: None,
}
}