use crate::error::SqlError;
pub(super) fn extract_column_pairs(body: &str) -> Result<Vec<(String, String)>, SqlError> {
let paren_start = match body.find('(') {
Some(p) => p,
None => return Ok(Vec::new()),
};
let mut depth = 0usize;
let mut paren_end = None;
for (i, b) in body.bytes().enumerate().skip(paren_start) {
match b {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
paren_end = Some(i);
break;
}
}
_ => {}
}
}
let paren_end = match paren_end {
Some(p) => p,
None => return Ok(Vec::new()),
};
let inner = &body[paren_start + 1..paren_end];
let upper_inner = inner.to_uppercase();
if is_with_clause_inner(&upper_inner) {
return Ok(Vec::new());
}
split_column_pairs(inner)
}
fn is_with_clause_inner(upper_inner: &str) -> bool {
let first_tok = upper_inner.split_whitespace().next().unwrap_or("");
matches!(
first_tok.trim_end_matches(['=', '\'']),
"ENGINE"
| "PROFILE"
| "VECTOR_FIELD"
| "PARTITION_BY"
| "DIM"
| "METRIC"
| "PAYLOAD_INDEXES"
| "APPEND_ONLY"
| "HASH_CHAIN"
| "BITEMPORAL"
)
}
fn split_column_pairs(inner: &str) -> Result<Vec<(String, String)>, SqlError> {
let mut pairs: Vec<(String, String)> = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, c) in inner.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
}
',' if depth == 0 => {
let token = inner[start..i].trim();
if !token.is_empty()
&& let Some(pair) = parse_col_token(token)?
{
pairs.push(pair);
}
start = i + 1;
}
_ => {}
}
}
let last = inner[start..].trim();
if !last.is_empty()
&& let Some(pair) = parse_col_token(last)?
{
pairs.push(pair);
}
Ok(pairs)
}
fn parse_col_token(token: &str) -> Result<Option<(String, String)>, SqlError> {
use crate::reserved::check_identifier;
let mut toks = token.split_whitespace();
let raw_name = match toks.next() {
None => return Ok(None),
Some(s) => s,
};
let upper_name = raw_name.to_uppercase();
match upper_name.as_str() {
"PRIMARY" => {
return Err(SqlError::UnsupportedConstraint {
feature: "PRIMARY KEY".to_string(),
hint: "use the inline form on the column instead: \
`<colname> <TYPE> PRIMARY KEY`"
.to_string(),
});
}
"UNIQUE" => {
return Err(SqlError::UnsupportedConstraint {
feature: "UNIQUE constraint".to_string(),
hint: "use a UNIQUE secondary index: \
CREATE INDEX ... ON collection (field) UNIQUE"
.to_string(),
});
}
"CHECK" => {
return Err(SqlError::UnsupportedConstraint {
feature: "CHECK constraint".to_string(),
hint: "CHECK constraints are unsupported; enforce in application code \
or use a typed function in INSERT"
.to_string(),
});
}
"FOREIGN" => {
return Err(SqlError::UnsupportedConstraint {
feature: "FOREIGN KEY constraint".to_string(),
hint: "FOREIGN KEY enforcement is unsupported; \
enforce in application code"
.to_string(),
});
}
"REFERENCES" => {
return Err(SqlError::UnsupportedConstraint {
feature: "REFERENCES constraint".to_string(),
hint: "FOREIGN KEY enforcement is unsupported; \
enforce in application code"
.to_string(),
});
}
"CONSTRAINT" => {
let mut rest = toks.clone();
let _constraint_name = rest.next(); let kind_tok = rest.next().map(|t| t.to_uppercase()).unwrap_or_default();
let (feature, hint) = match kind_tok.as_str() {
"PRIMARY" => (
"CONSTRAINT ... PRIMARY KEY".to_string(),
"use the inline form on the column instead: \
`<colname> <TYPE> PRIMARY KEY`"
.to_string(),
),
"UNIQUE" => (
"CONSTRAINT ... UNIQUE".to_string(),
"use a UNIQUE secondary index: \
CREATE INDEX ... ON collection (field) UNIQUE"
.to_string(),
),
"CHECK" => (
"CONSTRAINT ... CHECK".to_string(),
"CHECK constraints are unsupported; enforce in application code \
or use a typed function in INSERT"
.to_string(),
),
"FOREIGN" => (
"CONSTRAINT ... FOREIGN KEY".to_string(),
"FOREIGN KEY enforcement is unsupported; \
enforce in application code"
.to_string(),
),
_ => (
format!("CONSTRAINT {}", kind_tok),
"named constraints are unsupported; \
use NodeDB-native enforcement (indexes, typeguards)"
.to_string(),
),
};
return Err(SqlError::UnsupportedConstraint { feature, hint });
}
_ => {}
}
let name = check_identifier(raw_name)?;
let mut type_parts: Vec<&str> = Vec::new();
let mut in_paren = false;
let mut hit_generated = false;
for t in toks {
let upper_t = t.to_uppercase();
let stripped = upper_t.trim_end_matches(['(', ')', ',']);
if !in_paren && stripped == "GENERATED" {
hit_generated = true;
break;
}
if !in_paren {
match stripped {
"UNIQUE" => {
return Err(SqlError::UnsupportedConstraint {
feature: "UNIQUE constraint".to_string(),
hint: "use a UNIQUE secondary index: \
CREATE INDEX ... ON collection (field) UNIQUE"
.to_string(),
});
}
"CHECK" => {
return Err(SqlError::UnsupportedConstraint {
feature: "CHECK constraint".to_string(),
hint: "CHECK constraints are unsupported; enforce in application code \
or use a typed function in INSERT"
.to_string(),
});
}
"FOREIGN" => {
return Err(SqlError::UnsupportedConstraint {
feature: "FOREIGN KEY constraint".to_string(),
hint: "FOREIGN KEY enforcement is unsupported; \
enforce in application code"
.to_string(),
});
}
"REFERENCES" => {
return Err(SqlError::UnsupportedConstraint {
feature: "REFERENCES constraint".to_string(),
hint: "FOREIGN KEY enforcement is unsupported; \
enforce in application code"
.to_string(),
});
}
"CONSTRAINT" => {
return Err(SqlError::UnsupportedConstraint {
feature: "CONSTRAINT clause".to_string(),
hint: "named constraints are unsupported; \
use NodeDB-native enforcement (indexes, typeguards)"
.to_string(),
});
}
_ => {}
}
}
if t.contains('(') {
in_paren = true;
}
if t.contains(')') {
in_paren = false;
}
type_parts.push(t);
}
if type_parts.is_empty() {
return Ok(None);
}
let mut type_str = type_parts.join(" ");
if hit_generated {
let upper_token = token.to_uppercase();
if let Some(gen_pos) = upper_token.find("GENERATED") {
type_str.push(' ');
type_str.push_str(token[gen_pos..].trim());
}
}
Ok(Some((name, type_str)))
}