jspg cleanup

This commit is contained in:
2026-07-07 12:42:05 -04:00
parent abf1d12e43
commit edd0dd4763
31 changed files with 992 additions and 962 deletions

View File

@ -165,20 +165,8 @@ impl Schema {
} else if db.enums.contains_key(custom) {
Some(vec![format!("{}.condition", custom)])
} else {
// A named type gets a reference to its dynamically built .filter — either a
// Relational cross-boundary (table-backed) or a named value type, whose filter
// is likewise synthesized (see Database::compile_filters). A named type with no
// compilable structure gets no filter — omit it rather than dangle a reference.
let base = custom.split('.').next_back().unwrap_or(custom);
let has_value_filter = db
.schemas
.get(custom)
.map_or(false, |s| Database::is_value_filter_candidate(custom, s));
if db.types.contains_key(base) || has_value_filter {
Some(vec![format!("{}.filter", custom)])
} else {
None
}
// Assume anything else is a Relational cross-boundary that already has its own .filter dynamically built
Some(vec![format!("{}.filter", custom)])
}
}
}

View File

@ -116,17 +116,99 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
return None;
}
// The merger's existence read emits one SELECT per probe (id / lookup key)
// joined by UNION. Match each arm independently against its own tables and
// WHERE clause; a mock matching any arm is returned once, mirroring UNION's
// dedup semantics.
let union_regex = Regex::new(r"(?i)\s+UNION\s+(?:ALL\s+)?").ok()?;
let arms: Vec<&str> = union_regex.split(sql).collect();
// 1. Extract table name
let table_regex = Regex::new(r#"(?i)\s+FROM\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#).ok()?;
let table = if let Some(caps) = table_regex.captures(sql) {
caps.get(1)?.as_str()
} else {
return None;
};
// 2. Extract WHERE conditions string
let mut where_clause = String::new();
if let Some(where_idx) = sql_upper.find(" WHERE ") {
let mut where_end = sql_upper.find(" ORDER BY ").unwrap_or(sql_upper.len());
if let Some(limit_idx) = sql_upper.find(" LIMIT ") {
if limit_idx < where_end {
where_end = limit_idx;
}
}
where_clause = sql[where_idx + 7..where_end].to_string();
}
// 3. Find matching mocks
let mut matches = Vec::new();
let or_regex = Regex::new(r"(?i)\s+OR\s+").ok()?;
let and_regex = Regex::new(r"(?i)\s+AND\s+").ok()?;
for mock in mocks {
if let Some(mock_obj) = mock.as_object() {
if arms.iter().any(|arm| arm_matches_mock(arm, mock_obj)) {
if let Some(t) = mock_obj.get("type") {
if t.as_str() != Some(table) {
continue;
}
}
if where_clause.is_empty() {
matches.push(mock.clone());
continue;
}
let or_parts = or_regex.split(&where_clause);
let mut any_branch_matched = false;
for or_part in or_parts {
let branch_str = or_part.replace('(', "").replace(')', "");
let mut branch_matches = true;
for part in and_regex.split(&branch_str) {
if let Some(eq_idx) = part.find('=') {
let left = part[..eq_idx]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let right = part[eq_idx + 1..].trim().trim_matches('\'');
let mock_val_str = match mock_obj.get(left) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(),
Some(Value::Bool(b)) => b.to_string(),
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != right {
branch_matches = false;
break;
}
} else if part.to_uppercase().contains(" IS NULL") {
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let mock_val_str = match mock_obj.get(left) {
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != "null" {
branch_matches = false;
break;
}
}
}
if branch_matches {
any_branch_matched = true;
break;
}
}
if any_branch_matched {
matches.push(mock.clone());
}
}
@ -134,103 +216,3 @@ fn parse_and_match_mocks(sql: &str, mocks: &[Value]) -> Option<Vec<Value>> {
Some(matches)
}
#[cfg(test)]
fn arm_matches_mock(arm: &str, mock_obj: &serde_json::Map<String, Value>) -> bool {
let arm_upper = arm.to_uppercase();
// 1. The mock's type must name one of the arm's tables. Hierarchy reads
// SELECT FROM the root (entity) and JOIN the subtype tables, so every
// FROM/JOIN table counts, not just the first.
let table_regex = match Regex::new(r#"(?i)\b(?:FROM|JOIN)\s+(?:[a-zA-Z_]\w*\.)?"?([a-zA-Z_]\w*)"?"#) {
Ok(r) => r,
Err(_) => return false,
};
let tables: Vec<&str> = table_regex.captures_iter(arm).filter_map(|c| c.get(1)).map(|m| m.as_str()).collect();
if tables.is_empty() {
return false;
}
if let Some(t) = mock_obj.get("type") {
if !tables.iter().any(|table| t.as_str() == Some(table)) {
return false;
}
}
// 2. Extract this arm's WHERE conditions
let mut where_clause = String::new();
if let Some(where_idx) = arm_upper.find(" WHERE ") {
let mut where_end = arm_upper.find(" ORDER BY ").unwrap_or(arm_upper.len());
if let Some(limit_idx) = arm_upper.find(" LIMIT ") {
if limit_idx < where_end {
where_end = limit_idx;
}
}
where_clause = arm[where_idx + 7..where_end].to_string();
}
if where_clause.is_empty() {
return true;
}
// 3. Match the mock against the conditions
let or_regex = match Regex::new(r"(?i)\s+OR\s+") {
Ok(r) => r,
Err(_) => return false,
};
let and_regex = match Regex::new(r"(?i)\s+AND\s+") {
Ok(r) => r,
Err(_) => return false,
};
for or_part in or_regex.split(&where_clause) {
let branch_str = or_part.replace('(', "").replace(')', "");
let mut branch_matches = true;
for part in and_regex.split(&branch_str) {
if let Some(eq_idx) = part.find('=') {
let left = part[..eq_idx]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let right = part[eq_idx + 1..].trim().trim_matches('\'');
let mock_val_str = match mock_obj.get(left) {
Some(Value::String(s)) => s.clone(),
Some(Value::Number(n)) => n.to_string(),
Some(Value::Bool(b)) => b.to_string(),
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != right {
branch_matches = false;
break;
}
} else if part.to_uppercase().contains(" IS NULL") {
let left = part[..part.to_uppercase().find(" IS NULL").unwrap()]
.trim()
.split('.')
.last()
.unwrap_or("")
.trim_matches('"');
let mock_val_str = match mock_obj.get(left) {
Some(Value::Null) => "null".to_string(),
_ => "".to_string(),
};
if mock_val_str != "null" {
branch_matches = false;
break;
}
}
}
if branch_matches {
return true;
}
}
false
}

View File

@ -262,23 +262,15 @@ impl Database {
}
}
/// Synthesizes Composed Filter References for all table-backed boundaries — and for
/// named non-table value types (schema-only objects, e.g. an operating-hours config),
/// so a property reference resolves to ONE named filter instead of inlining anonymous
/// per-path copies (which duplicate identical leaf type names for downstream codegen).
/// Synthesizes Composed Filter References for all table-backed boundaries.
fn compile_filters(&mut self, errors: &mut Vec<crate::drop::Error>) -> Vec<(String, String)> {
let mut filter_schemas = Vec::new();
let mut seen_value_ids = std::collections::HashSet::new();
for (type_name, type_def) in &self.types {
for (id, schema_arc) in &type_def.schemas {
// Run synthesis on structured table-backed boundaries and named value types.
// Exclude subschemas!
// Only run synthesis on actual structured, table-backed boundaries. Exclude subschemas!
let base_name = id.split('.').last().unwrap_or(id);
let is_table_backed = base_name == type_def.name;
let is_value_type = !is_table_backed
&& Self::is_value_filter_candidate(id, schema_arc)
&& seen_value_ids.insert(id.clone());
if (is_table_backed || is_value_type) && !id.contains('/') {
if is_table_backed && !id.contains('/') {
if let Some(filter_schema) = schema_arc.compile_filter(self, id, errors) {
filter_schemas.push((
type_name.clone(),
@ -301,20 +293,6 @@ impl Database {
filter_ids
}
/// A named non-table value type that earns its own synthesized filter: a bare-named
/// (dotless) schema-only object with compiled properties. The base `filter`/`condition`
/// schemas are infrastructure, not value types.
pub fn is_value_filter_candidate(id: &str, schema: &Arc<Schema>) -> bool {
!id.contains('.')
&& id != "filter"
&& id != "condition"
&& schema
.obj
.compiled_properties
.get()
.map_or(false, |props| !props.is_empty())
}
/// Synthesizes strong Enum Conditions mirroring the string.condition capabilities.
fn compile_conditions(&mut self) -> Vec<(String, String)> {
let mut enum_conditions = Vec::new();

View File

@ -744,10 +744,6 @@ impl Merger {
return Ok(None);
}
// An OR across the hierarchy join is un-indexable — it forces a full scan
// of the subtype table on every child merge. UNION lets each arm use its
// own index (pk for the id arm, the lk_ unique index for the lookup arm)
// and still dedups, so TOO_MANY_LOOKUP_ROWS semantics are preserved.
let final_sql = if where_parts.len() == 1 {
format!("{} WHERE {}", fetch_sql_template, where_parts[0])
} else {

View File

@ -7660,15 +7660,3 @@ fn test_merger_0_15() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 15).unwrap();
}
#[test]
fn test_merger_0_16() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 16).unwrap();
}
#[test]
fn test_merger_0_17() {
let path = format!("{}/fixtures/merger.json", env!("CARGO_MANIFEST_DIR"));
crate::tests::runner::run_test_case(&path, 0, 17).unwrap();
}

View File

@ -179,14 +179,35 @@ impl SqlFormatter {
}
fn format_query(&mut self, query: &Query) {
match &*query.body {
self.format_set_expr(&query.body);
}
fn format_set_expr(&mut self, set_expr: &SetExpr) {
match set_expr {
SetExpr::Select(select) => self.format_select(select),
SetExpr::Query(inner_query) => {
self.push_str("(");
self.format_query(inner_query);
self.push_str(")");
}
_ => self.push_str(&query.to_string()),
SetExpr::SetOperation { op, left, right, set_quantifier } => {
self.format_set_expr(left);
let op_str = match op {
sqlparser::ast::SetOperator::Union => "UNION",
sqlparser::ast::SetOperator::Intersect => "INTERSECT",
sqlparser::ast::SetOperator::Except => "EXCEPT",
_ => "UNION",
};
let quant_str = match set_quantifier {
sqlparser::ast::SetQuantifier::All => " ALL",
sqlparser::ast::SetQuantifier::Distinct => " DISTINCT",
sqlparser::ast::SetQuantifier::None => "",
_ => "",
};
self.push_line(&format!("{}{} ", op_str, quant_str));
self.format_set_expr(right);
}
_ => self.push_str(&set_expr.to_string()),
}
}
@ -236,20 +257,22 @@ impl SqlFormatter {
}
fn format_join(&mut self, join: &Join) {
let op = match &join.join_operator {
let op_str = match &join.join_operator {
JoinOperator::Inner(_) => "JOIN",
JoinOperator::LeftOuter(_) => "LEFT JOIN",
JoinOperator::Left(_) => "LEFT JOIN",
JoinOperator::Right(_) => "RIGHT JOIN",
_ => "JOIN",
};
self.push_str(&format!("{} {} ON ", op, join.relation));
self.push_str(&format!("{} {}", op_str, join.relation));
match &join.join_operator {
JoinOperator::Inner(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::LeftOuter(JoinConstraint::On(expr)) => self.format_expr(expr),
JoinOperator::Join(JoinConstraint::On(expr)) => self.format_expr(expr),
_ => {
println!("FALLBACK JOIN OP: {:?}", join.join_operator);
JoinOperator::Inner(JoinConstraint::On(expr))
| JoinOperator::Left(JoinConstraint::On(expr))
| JoinOperator::Right(JoinConstraint::On(expr)) => {
self.push_str(" ON ");
self.format_expr(expr);
}
_ => {}
}
}