Files
jspg/src/queryer/compiler.rs
2026-03-18 04:39:48 -04:00

743 lines
24 KiB
Rust

use crate::database::Database;
use std::sync::Arc;
pub struct Compiler<'a> {
pub db: &'a Database,
pub filter_keys: &'a [String],
pub is_stem_query: bool,
pub alias_counter: usize,
}
#[derive(Clone, Debug)]
pub struct Node<'a> {
pub schema: std::sync::Arc<crate::database::schema::Schema>,
pub parent_alias: String,
pub parent_type_aliases: Option<std::sync::Arc<std::collections::HashMap<String, String>>>,
pub parent_type: Option<&'a crate::database::r#type::Type>,
pub property_name: Option<String>,
pub depth: usize,
pub stem_path: String,
}
impl<'a> Compiler<'a> {
/// Compiles a JSON schema into a nested PostgreSQL query returning JSONB
pub fn compile(
&self,
schema_id: &str,
stem_path: Option<&str>,
filter_keys: &[String],
) -> Result<String, String> {
let schema = self
.db
.schemas
.get(schema_id)
.ok_or_else(|| format!("Schema not found: {}", schema_id))?;
let target_schema = if let Some(path) = stem_path.filter(|p| !p.is_empty() && *p != "/") {
if let Some(stems_map) = self.db.stems.get(schema_id) {
if let Some(stem) = stems_map.get(path) {
stem.schema.clone()
} else {
return Err(format!(
"Stem entity type '{}' not found in schema '{}'",
path, schema_id
));
}
} else {
return Err(format!(
"Stem entity type '{}' not found in schema '{}'",
path, schema_id
));
}
} else {
std::sync::Arc::new(schema.clone())
};
let is_stem_query = stem_path.is_some();
let mut compiler = Compiler {
db: &self.db,
filter_keys,
is_stem_query,
alias_counter: 0,
};
let node = Node {
schema: target_schema,
parent_alias: "t1".to_string(),
parent_type_aliases: None,
parent_type: None,
property_name: None,
depth: 0,
stem_path: String::new(),
};
let (sql, _) = compiler.compile_node(node)?;
Ok(sql)
}
/// Recursively walks the schema AST emitting native PostgreSQL jsonb mapping
/// Returns a tuple of (SQL_String, Field_Type)
fn compile_node(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// Determine the base schema type (could be an array, object, or literal)
match &node.schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(t)) if t == "array" => {
self.compile_array(node)
}
_ => self.compile_reference(node),
}
}
fn compile_array(&mut self, node: Node<'a>) -> Result<(String, String), String> {
if let Some(items) = &node.schema.obj.items {
let next_path = if node.stem_path.is_empty() {
String::from("#")
} else {
format!("{}.#", node.stem_path)
};
if let Some(ref_id) = &items.obj.r#ref {
if let Some(type_def) = self.db.types.get(ref_id) {
let mut entity_noke = node.clone();
entity_noke.stem_path = next_path;
entity_noke.schema = std::sync::Arc::clone(items);
return self.compile_entity(type_def, entity_noke, true);
}
}
let mut next_node = node.clone();
next_node.depth += 1;
next_node.stem_path = next_path;
next_node.schema = std::sync::Arc::clone(items);
let (item_sql, _) = self.compile_node(next_node)?;
return Ok((
format!("(SELECT jsonb_agg({}) FROM TODO)", item_sql),
"array".to_string(),
));
}
Ok((
"SELECT jsonb_agg(TODO) FROM TODO".to_string(),
"array".to_string(),
))
}
fn compile_reference(&mut self, node: Node<'a>) -> Result<(String, String), String> {
// Determine if this schema represents a Database Entity
let mut resolved_type = None;
if let Some(family_target) = node.schema.obj.family.as_ref() {
resolved_type = self.db.types.get(family_target);
} else if let Some(lookup_key) = node
.schema
.obj
.id
.as_ref()
.or(node.schema.obj.r#ref.as_ref())
{
let base_type_name = lookup_key.split('.').next_back().unwrap_or("").to_string();
resolved_type = self.db.types.get(&base_type_name);
}
if let Some(type_def) = resolved_type {
return self.compile_entity(type_def, node.clone(), false);
}
// Handle Direct Refs
if let Some(ref_id) = &node.schema.obj.r#ref {
// If it's just an ad-hoc struct ref, we should resolve it
if let Some(target_schema) = self.db.schemas.get(ref_id) {
let mut ref_node = node.clone();
ref_node.schema = std::sync::Arc::new(target_schema.clone());
return self.compile_node(ref_node);
}
return Err(format!("Unresolved $ref: {}", ref_id));
}
// Handle $family Polymorphism fallbacks for relations
if let Some(family_target) = &node.schema.obj.family {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target)
.to_string();
if let Some(type_def) = self.db.types.get(&base_type_name) {
if type_def.variations.len() == 1 {
let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.r#ref = Some(family_target.clone());
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(bypass_schema);
return self.compile_node(bypass_node);
}
let mut sorted_variations: Vec<String> = type_def.variations.iter().cloned().collect();
sorted_variations.sort();
let mut family_schemas = Vec::new();
for variation in &sorted_variations {
let mut ref_schema = crate::database::schema::Schema::default();
ref_schema.obj.r#ref = Some(variation.clone());
family_schemas.push(std::sync::Arc::new(ref_schema));
}
return self.compile_one_of(&family_schemas, node);
}
}
// Handle oneOf Polymorphism fallbacks for relations
if let Some(one_of) = &node.schema.obj.one_of {
return self.compile_one_of(one_of, node.clone());
}
// Just an inline object definition?
if let Some(props) = &node.schema.obj.properties {
return self.compile_object(props, node.clone());
}
// Literal fallback
Ok((
format!(
"{}.{}",
node.parent_alias,
node.property_name.as_deref().unwrap_or("unknown_prop")
),
"string".to_string(),
))
}
fn compile_entity(
&mut self,
r#type: &'a crate::database::r#type::Type,
node: Node<'a>,
is_array: bool,
) -> Result<(String, String), String> {
let (table_aliases, from_clauses) = self.compile_from_clause(r#type);
// 2. Map properties and build jsonb_build_object args
let mut select_args = self.compile_select_clause(r#type, &table_aliases, node.clone())?;
// 2.5 Inject polymorphism directly into the query object
let mut poly_args = self.compile_polymorphism_select(r#type, &table_aliases, node.clone())?;
select_args.append(&mut poly_args);
let jsonb_obj_sql = if select_args.is_empty() {
"jsonb_build_object()".to_string()
} else {
format!("jsonb_build_object({})", select_args.join(", "))
};
// 3. Build WHERE clauses
let where_clauses = self.compile_where_clause(r#type, &table_aliases, node)?;
let selection = if is_array {
format!("COALESCE(jsonb_agg({}), '[]'::jsonb)", jsonb_obj_sql)
} else {
jsonb_obj_sql
};
let full_sql = format!(
"(SELECT {} FROM {} WHERE {})",
selection,
from_clauses.join(" "),
where_clauses.join(" AND ")
);
Ok((
full_sql,
if is_array {
"array".to_string()
} else {
"object".to_string()
},
))
}
fn compile_polymorphism_select(
&mut self,
r#type: &'a crate::database::r#type::Type,
table_aliases: &std::collections::HashMap<String, String>,
node: Node<'a>,
) -> Result<Vec<String>, String> {
let mut select_args = Vec::new();
if let Some(family_target) = node.schema.obj.family.as_ref() {
let base_type_name = family_target
.split('.')
.next_back()
.unwrap_or(family_target)
.to_string();
if let Some(fam_type_def) = self.db.types.get(&base_type_name) {
if fam_type_def.variations.len() == 1 {
let mut bypass_schema = crate::database::schema::Schema::default();
bypass_schema.obj.r#ref = Some(family_target.clone());
let mut bypass_node = node.clone();
bypass_node.schema = std::sync::Arc::new(bypass_schema);
let mut bypassed_args =
self.compile_select_clause(r#type, table_aliases, bypass_node)?;
select_args.append(&mut bypassed_args);
} else {
let mut family_schemas = Vec::new();
let mut sorted_fam_variations: Vec<String> =
fam_type_def.variations.iter().cloned().collect();
sorted_fam_variations.sort();
for variation in &sorted_fam_variations {
let mut ref_schema = crate::database::schema::Schema::default();
ref_schema.obj.r#ref = Some(variation.clone());
family_schemas.push(std::sync::Arc::new(ref_schema));
}
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
select_args.push(format!("'id', {}.id", base_alias));
let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases);
let (case_sql, _) = self.compile_one_of(&family_schemas, case_node)?;
select_args.push(format!("'type', {}", case_sql));
}
}
} else if let Some(one_of) = &node.schema.obj.one_of {
let base_alias = table_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
select_args.push(format!("'id', {}.id", base_alias));
let mut case_node = node.clone();
case_node.parent_alias = base_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
case_node.parent_type_aliases = Some(arc_aliases);
let (case_sql, _) = self.compile_one_of(one_of, case_node)?;
select_args.push(format!("'type', {}", case_sql));
}
Ok(select_args)
}
fn compile_object(
&mut self,
props: &std::collections::BTreeMap<String, std::sync::Arc<crate::database::schema::Schema>>,
node: Node<'a>,
) -> Result<(String, String), String> {
let mut build_args = Vec::new();
for (k, v) in props {
let next_path = if node.stem_path.is_empty() {
k.clone()
} else {
format!("{}.{}", node.stem_path, k)
};
let mut child_node = node.clone();
child_node.property_name = Some(k.clone());
child_node.depth += 1;
child_node.stem_path = next_path;
child_node.schema = std::sync::Arc::clone(v);
let (child_sql, val_type) = self.compile_node(child_node)?;
if val_type == "abort" {
continue;
}
build_args.push(format!("'{}', {}", k, child_sql));
}
let combined = format!("jsonb_build_object({})", build_args.join(", "));
Ok((combined, "object".to_string()))
}
fn compile_one_of(
&mut self,
schemas: &[Arc<crate::database::schema::Schema>],
node: Node<'a>,
) -> Result<(String, String), String> {
let mut case_statements = Vec::new();
let type_col = if let Some(prop) = &node.property_name {
format!("{}_type", prop)
} else {
"type".to_string()
};
for option_schema in schemas {
if let Some(ref_id) = &option_schema.obj.r#ref {
// Find the physical type this ref maps to
let base_type_name = ref_id.split('.').next_back().unwrap_or("").to_string();
// Generate the nested SQL for this specific target type
let mut child_node = node.clone();
child_node.schema = std::sync::Arc::clone(option_schema);
let (val_sql, _) = self.compile_node(child_node)?;
case_statements.push(format!(
"WHEN {}.{} = '{}' THEN ({})",
node.parent_alias, type_col, base_type_name, val_sql
));
}
}
if case_statements.is_empty() {
return Ok(("NULL".to_string(), "string".to_string()));
}
case_statements.sort();
let sql = format!("CASE {} ELSE NULL END", case_statements.join(" "));
Ok((sql, "object".to_string()))
}
fn compile_from_clause(
&mut self,
r#type: &crate::database::r#type::Type,
) -> (std::collections::HashMap<String, String>, Vec<String>) {
let mut table_aliases = std::collections::HashMap::new();
let mut from_clauses = Vec::new();
for (i, table_name) in r#type.hierarchy.iter().enumerate() {
self.alias_counter += 1;
let alias = format!("{}_{}", table_name, self.alias_counter);
table_aliases.insert(table_name.clone(), alias.clone());
if i == 0 {
from_clauses.push(format!("agreego.{} {}", table_name, alias));
} else {
let prev_alias = format!("{}_{}", r#type.hierarchy[i - 1], self.alias_counter - 1);
from_clauses.push(format!(
"JOIN agreego.{} {} ON {}.id = {}.id",
table_name, alias, alias, prev_alias
));
}
}
(table_aliases, from_clauses)
}
fn compile_select_clause(
&mut self,
r#type: &'a crate::database::r#type::Type,
table_aliases: &std::collections::HashMap<String, String>,
node: Node<'a>,
) -> Result<Vec<String>, String> {
let mut select_args = Vec::new();
let grouped_fields = r#type.grouped_fields.as_ref().and_then(|v| v.as_object());
let merged_props = self.get_merged_properties(node.schema.as_ref());
let mut sorted_keys: Vec<&String> = merged_props.keys().collect();
sorted_keys.sort();
for prop_key in sorted_keys {
let prop_schema = &merged_props[prop_key];
let is_object_or_array = match &prop_schema.obj.type_ {
Some(crate::database::schema::SchemaTypeOrArray::Single(s)) => {
s == "object" || s == "array"
}
Some(crate::database::schema::SchemaTypeOrArray::Multiple(v)) => {
v.contains(&"object".to_string()) || v.contains(&"array".to_string())
}
_ => false,
};
let is_primitive = prop_schema.obj.r#ref.is_none()
&& !is_object_or_array
&& prop_schema.obj.family.is_none()
&& prop_schema.obj.one_of.is_none();
if is_primitive {
if let Some(ft) = r#type.field_types.as_ref().and_then(|v| v.as_object()) {
if !ft.contains_key(prop_key) {
continue; // Skip frontend virtual properties missing from physical table fields
}
}
}
let mut owner_alias = table_aliases
.get("entity")
.cloned()
.unwrap_or_else(|| format!("{}_t_err", node.parent_alias));
if let Some(gf) = grouped_fields {
for (t_name, fields_val) in gf {
if let Some(fields_arr) = fields_val.as_array() {
if fields_arr.iter().any(|v| v.as_str() == Some(prop_key)) {
owner_alias = table_aliases
.get(t_name)
.cloned()
.unwrap_or_else(|| node.parent_alias.to_string());
break;
}
}
}
}
let mut child_node = node.clone();
child_node.parent_alias = owner_alias.clone();
let arc_aliases = std::sync::Arc::new(table_aliases.clone());
child_node.parent_type_aliases = Some(arc_aliases);
child_node.parent_type = Some(r#type);
child_node.property_name = Some(prop_key.clone());
child_node.depth += 1;
let next_path = if node.stem_path.is_empty() {
prop_key.clone()
} else {
format!("{}.{}", node.stem_path, prop_key)
};
child_node.stem_path = next_path;
child_node.schema = std::sync::Arc::clone(prop_schema);
let (val_sql, val_type) = self.compile_node(child_node)?;
if val_type != "abort" {
select_args.push(format!("'{}', {}", prop_key, val_sql));
}
}
Ok(select_args)
}
fn compile_where_clause(
&self,
r#type: &'a crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
node: Node<'a>,
) -> Result<Vec<String>, String> {
let base_alias = type_aliases
.get(&r#type.name)
.cloned()
.unwrap_or_else(|| "err".to_string());
let entity_alias = type_aliases
.get("entity")
.cloned()
.unwrap_or_else(|| base_alias.clone());
let mut where_clauses = Vec::new();
where_clauses.push(format!("NOT {}.archived", entity_alias));
self.compile_filter_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses);
self.compile_relation_conditions(r#type, type_aliases, &node, &base_alias, &mut where_clauses)?;
Ok(where_clauses)
}
fn resolve_filter_alias(
r#type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
base_alias: &str,
field_name: &str,
) -> String {
if let Some(gf) = r#type.grouped_fields.as_ref().and_then(|v| v.as_object()) {
for (t_name, fields_val) in gf {
if let Some(fields_arr) = fields_val.as_array() {
if fields_arr.iter().any(|v| v.as_str() == Some(field_name)) {
return type_aliases.get(t_name).cloned().unwrap_or_else(|| base_alias.to_string());
}
}
}
}
base_alias.to_string()
}
fn determine_sql_cast_and_op(
r#type: &crate::database::r#type::Type,
node: &Node,
field_name: &str,
) -> (&'static str, bool) {
let mut is_ilike = false;
let mut cast = "";
if let Some(field_types) = r#type.field_types.as_ref().and_then(|v| v.as_object()) {
if let Some(pg_type_val) = field_types.get(field_name) {
if let Some(pg_type) = pg_type_val.as_str() {
if pg_type == "uuid" {
cast = "::uuid";
} else if pg_type == "boolean" || pg_type == "bool" {
cast = "::boolean";
} else if pg_type.contains("timestamp") || pg_type == "timestamptz" || pg_type == "date" {
cast = "::timestamptz";
} else if pg_type == "numeric"
|| pg_type.contains("int")
|| pg_type == "real"
|| pg_type == "double precision"
{
cast = "::numeric";
} else if pg_type == "text" || pg_type.contains("char") {
let mut is_enum = false;
if let Some(props) = &node.schema.obj.properties {
if let Some(ps) = props.get(field_name) {
is_enum = ps.obj.enum_.is_some();
}
}
if !is_enum {
is_ilike = true;
}
}
}
}
}
(cast, is_ilike)
}
fn compile_filter_conditions(
&self,
r#type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
node: &Node,
base_alias: &str,
where_clauses: &mut Vec<String>,
) {
for (i, filter_key) in self.filter_keys.iter().enumerate() {
let mut parts = filter_key.split(':');
let full_field_path = parts.next().unwrap_or(filter_key);
let op = parts.next().unwrap_or("$eq");
let field_name = if node.stem_path.is_empty() {
if full_field_path.contains('.') || full_field_path.contains('#') {
continue;
}
full_field_path
} else {
let prefix = format!("{}.", node.stem_path);
if full_field_path.starts_with(&prefix) {
let remainder = &full_field_path[prefix.len()..];
if remainder.contains('.') || remainder.contains('#') {
continue;
}
remainder
} else {
continue;
}
};
let filter_alias = Self::resolve_filter_alias(r#type, type_aliases, base_alias, field_name);
let (cast, is_ilike) = Self::determine_sql_cast_and_op(r#type, node, field_name);
let param_index = i + 1;
let p_val = format!("${}#>>'{{}}'", param_index);
if op == "$in" || op == "$nin" {
let sql_op = if op == "$in" { "IN" } else { "NOT IN" };
let subquery = format!(
"(SELECT value{} FROM jsonb_array_elements_text(({})::jsonb))",
cast, p_val
);
where_clauses.push(format!(
"{}.{} {} {}",
filter_alias, field_name, sql_op, subquery
));
} else {
let sql_op = match op {
"$eq" => if is_ilike { "ILIKE" } else { "=" },
"$ne" => if is_ilike { "NOT ILIKE" } else { "!=" },
"$gt" => ">",
"$gte" => ">=",
"$lt" => "<",
"$lte" => "<=",
_ => if is_ilike { "ILIKE" } else { "=" },
};
let param_sql = if is_ilike && (op == "$eq" || op == "$ne") {
p_val
} else {
format!("({}){}", p_val, cast)
};
where_clauses.push(format!(
"{}.{} {} {}",
filter_alias, field_name, sql_op, param_sql
));
}
}
}
fn compile_relation_conditions(
&self,
r#type: &crate::database::r#type::Type,
type_aliases: &std::collections::HashMap<String, String>,
node: &Node,
base_alias: &str,
where_clauses: &mut Vec<String>,
) -> Result<(), String> {
if let Some(prop_ref) = &node.property_name {
let prop = prop_ref.as_str();
let mut parent_relation_alias = node.parent_alias.clone();
let mut child_relation_alias = base_alias.to_string();
if let Some(parent_type) = node.parent_type {
let merged_props = self.get_merged_properties(node.schema.as_ref());
let relative_keys: Vec<String> = merged_props.keys().cloned().collect();
let (relation, is_parent_source) = self
.db
.get_relation(&parent_type.name, &r#type.name, prop, Some(&relative_keys))
.ok_or_else(|| {
format!(
"Could not dynamically resolve database relation mapping for {} -> {} on property {}",
parent_type.name, r#type.name, prop
)
})?;
let source_col = &relation.source_columns[0];
let dest_col = &relation.destination_columns[0];
if let Some(pta) = &node.parent_type_aliases {
let p_search_type = if is_parent_source {
&relation.source_type
} else {
&relation.destination_type
};
if let Some(a) = pta.get(p_search_type) {
parent_relation_alias = a.clone();
}
}
let c_search_type = if is_parent_source {
&relation.destination_type
} else {
&relation.source_type
};
if let Some(a) = type_aliases.get(c_search_type) {
child_relation_alias = a.clone();
}
let sql_string = if is_parent_source {
format!(
"{}.{} = {}.{}",
parent_relation_alias, source_col, child_relation_alias, dest_col
)
} else {
format!(
"{}.{} = {}.{}",
child_relation_alias, source_col, parent_relation_alias, dest_col
)
};
where_clauses.push(sql_string);
}
}
Ok(())
}
fn get_merged_properties(
&self,
schema: &crate::database::schema::Schema,
) -> std::collections::BTreeMap<String, Arc<crate::database::schema::Schema>> {
let mut props = std::collections::BTreeMap::new();
if let Some(ref_id) = &schema.obj.r#ref {
if let Some(parent_schema) = self.db.schemas.get(ref_id) {
props.extend(self.get_merged_properties(parent_schema));
}
}
if let Some(local_props) = &schema.obj.properties {
for (k, v) in local_props {
props.insert(k.clone(), v.clone());
}
}
props
}
}