use toasty_core::{
schema::{app, mapping},
stmt,
};
use crate::engine::lower::LowerStatement;
impl LowerStatement<'_, '_> {
pub(super) fn build_via_include_subquery(
&mut self,
field_index: usize,
via: &app::Via,
nested: &[stmt::Projection],
) -> stmt::Expr {
if !nested.is_empty() {
todo!("nested `.include()` through a multi-step `via` relation");
}
let schema = self.schema();
let model = self.model_unwrap();
let single = match &model.fields[field_index].ty {
app::FieldTy::Via(via) => via.is_one(),
_ => unreachable!("build_via_include_subquery called on non-via field"),
};
let nullable = model.fields[field_index].nullable();
let join = ViaJoin::resolve(schema, model.id, via);
let (link_field, parent_key_field) = join.link();
let link_col = model_level_column_expr(schema, link_field, join.slot(1));
let filter = stmt::Expr::eq(link_col.clone(), stmt::Expr::ref_field(1, parent_key_field));
let target_record = schema.mapping_for(join.target()).default_returning.clone();
let returning = stmt::Expr::record_from_vec(vec![link_col, target_record]);
let mut select = stmt::Select::new(join.build_source(schema), filter);
select.returning = stmt::Returning::Project(returning);
select.distinct = true;
let mut query = stmt::Query::builder(select).build();
query.single = single;
let sub_expr = self.lower_sub_stmt(stmt::Statement::Query(query));
if !single {
return stmt::Expr::map(sub_expr, stmt::Expr::project(stmt::Expr::arg(0), [1usize]));
}
if nullable {
super::map_nullable_single(sub_expr, stmt::Expr::project(stmt::Expr::arg(0), [1usize]))
} else {
stmt::Expr::project(sub_expr, [1usize])
}
}
}
struct ViaJoin {
models: Vec<app::ModelId>,
edges: Vec<Edge>,
}
impl ViaJoin {
fn resolve(schema: &toasty_core::Schema, root: app::ModelId, via: &app::Via) -> ViaJoin {
let steps = flatten_via_steps(schema, root, via.path.projection.as_slice());
assert!(
!steps.is_empty(),
"via path must have at least one step (validated at schema build time)"
);
let mut models = Vec::with_capacity(steps.len() + 1);
let mut edges = Vec::with_capacity(steps.len());
models.push(root);
for &field_id in &steps {
debug_assert_eq!(field_id.model, *models.last().unwrap());
models.push(schema.app.field(field_id).relation_target_id().unwrap());
edges.push(Edge::resolve(schema, field_id));
}
ViaJoin { models, edges }
}
fn target(&self) -> app::ModelId {
*self.models.last().unwrap()
}
fn slot(&self, pos: usize) -> usize {
self.models.len() - 1 - pos
}
fn link(&self) -> (app::FieldId, app::FieldId) {
let root_edge = &self.edges[0];
(root_edge.target_side, root_edge.root_side)
}
fn build_source(&self, schema: &toasty_core::Schema) -> stmt::Source {
let mut tables = Vec::with_capacity(self.edges.len());
let mut joins = Vec::with_capacity(self.edges.len().saturating_sub(1));
tables.push(stmt::TableRef::Table(schema.table_id_for(self.target())));
for pos in (1..self.edges.len()).rev() {
tables.push(stmt::TableRef::Table(schema.table_id_for(self.models[pos])));
let edge = &self.edges[pos];
joins.push(stmt::Join {
table: stmt::SourceTableId(self.slot(pos)),
constraint: stmt::JoinOp::Inner(stmt::Expr::eq(
raw_column(schema, self.slot(pos), edge.root_side),
raw_column(schema, self.slot(pos + 1), edge.target_side),
)),
});
}
stmt::Source::Table(stmt::SourceTable {
tables,
from: vec![stmt::TableWithJoins {
relation: stmt::TableFactor::Table(stmt::SourceTableId(0)),
joins,
}],
})
}
}
struct Edge {
root_side: app::FieldId,
target_side: app::FieldId,
}
impl Edge {
fn resolve(schema: &toasty_core::Schema, field_id: app::FieldId) -> Edge {
let field = schema.app.field(field_id);
let (belongs_to, owner_is_target_side) = match &field.ty {
app::FieldTy::Has(_) => {
let pair = field
.pair()
.expect("via paths are unfolded into direct steps before edge resolution");
(schema.app.field(pair).ty.as_belongs_to_unwrap(), true)
}
app::FieldTy::BelongsTo(belongs_to) => (belongs_to, false),
_ => unreachable!("via step is not a relation field"),
};
let [fk] = &belongs_to.foreign_key.fields[..] else {
todo!("composite foreign keys in via include path");
};
if owner_is_target_side {
Edge {
root_side: fk.target,
target_side: fk.source,
}
} else {
Edge {
root_side: fk.source,
target_side: fk.target,
}
}
}
}
fn flatten_via_steps(
schema: &toasty_core::Schema,
source_model_id: app::ModelId,
initial_steps: &[usize],
) -> Vec<app::FieldId> {
let mut result = Vec::with_capacity(initial_steps.len());
let mut current_model = source_model_id;
let mut queue: Vec<usize> = initial_steps.to_vec();
queue.reverse();
while let Some(idx) = queue.pop() {
let field = &schema.app.model(current_model).as_root_unwrap().fields[idx];
let field_id = app::FieldId {
model: current_model,
index: idx,
};
let nested_via = match &field.ty {
app::FieldTy::Via(via) => Some(via),
_ => None,
};
if let Some(via) = nested_via {
for step in via.path.projection.as_slice().iter().rev() {
queue.push(*step);
}
continue;
}
current_model = field
.relation_target_id()
.expect("via path step is a relation");
result.push(field_id);
}
result
}
fn fk_primitive(schema: &toasty_core::Schema, field_id: app::FieldId) -> &mapping::FieldPrimitive {
schema.mapping_for(field_id.model).fields[field_id.index]
.as_primitive()
.expect("FK field maps to a single column")
}
fn raw_column(schema: &toasty_core::Schema, slot: usize, field_id: app::FieldId) -> stmt::Expr {
stmt::Expr::column(stmt::ExprReference::column(
slot,
fk_primitive(schema, field_id).column.index,
))
}
fn model_level_column_expr(
schema: &toasty_core::Schema,
field_id: app::FieldId,
slot: usize,
) -> stmt::Expr {
let mut expr = fk_primitive(schema, field_id).column_expr.clone();
stmt::visit_mut::for_each_expr_mut(&mut expr, |e| {
if let stmt::Expr::Reference(stmt::ExprReference::Column(col)) = e {
col.table = slot;
}
});
expr
}