use toasty_core::{
driver::Capability,
schema::{Schema, app},
stmt::{self, VisitMut},
};
pub(super) fn statement(schema: &Schema, capability: &Capability, stmt: &mut stmt::Statement) {
LegalizeDocumentPaths { schema, capability }.visit_mut(stmt);
}
pub(super) fn table_expr(schema: &Schema, capability: &Capability, expr: &mut stmt::Expr) {
LegalizeDocumentPaths { schema, capability }.visit_expr_mut(expr);
}
struct LegalizeDocumentPaths<'a> {
schema: &'a Schema,
#[cfg_attr(
not(any(feature = "jiff", feature = "rust_decimal", feature = "bigdecimal")),
allow(dead_code)
)]
capability: &'a Capability,
}
impl LegalizeDocumentPaths<'_> {
fn lower_project(&self, expr: &mut stmt::Expr) {
let stmt::Expr::Project(project) = expr else {
return;
};
let stmt::Expr::Cast(cast) = &mut *project.base else {
return;
};
let stmt::Type::Model(embed_id) = cast.ty else {
return;
};
if !matches!(&*cast.expr, stmt::Expr::Reference(_)) {
return;
}
let Some((path, ty)) =
build_json_path(self.schema, embed_id, project.projection.as_slice())
else {
return;
};
let base = Box::new(cast.expr.take());
*expr = stmt::Expr::from(stmt::FuncJsonExtract { base, path, ty });
}
fn leaf_compares_as_text(&self, ty: &stmt::Type) -> bool {
match ty {
#[cfg(feature = "jiff")]
stmt::Type::Timestamp => !self.capability.native_timestamp,
#[cfg(feature = "jiff")]
stmt::Type::Date => !self.capability.native_date,
#[cfg(feature = "jiff")]
stmt::Type::Time => !self.capability.native_time,
#[cfg(feature = "jiff")]
stmt::Type::DateTime => !self.capability.native_datetime,
#[cfg(feature = "rust_decimal")]
stmt::Type::Decimal => !self.capability.native_decimal,
#[cfg(feature = "bigdecimal")]
stmt::Type::BigDecimal => !self.capability.native_decimal,
_ => false,
}
}
fn textify_comparison(&self, extract_side: &mut stmt::Expr, operand: &mut stmt::Expr) {
let stmt::Expr::Func(stmt::ExprFunc::JsonExtract(func)) = extract_side else {
return;
};
if !self.leaf_compares_as_text(&func.ty) {
return;
}
let stmt::Expr::Value(value) = operand else {
return;
};
let Some(text) = document_text(value) else {
return;
};
*value = stmt::Value::String(text);
func.ty = stmt::Type::String;
}
}
impl VisitMut for LegalizeDocumentPaths<'_> {
fn visit_expr_mut(&mut self, expr: &mut stmt::Expr) {
if let stmt::Expr::Project(_) = expr {
self.lower_project(expr);
}
stmt::visit_mut::visit_expr_mut(self, expr);
match expr {
stmt::Expr::Cast(expr_cast) if expr_cast.ty.contains_model() => {
*expr = expr_cast.expr.take();
}
stmt::Expr::BinaryOp(binary) => {
self.textify_comparison(&mut binary.lhs, &mut binary.rhs);
self.textify_comparison(&mut binary.rhs, &mut binary.lhs);
}
stmt::Expr::InList(in_list) => {
let stmt::Expr::Func(stmt::ExprFunc::JsonExtract(func)) = &mut *in_list.expr else {
return;
};
if !self.leaf_compares_as_text(&func.ty) {
return;
}
match &mut *in_list.list {
stmt::Expr::List(list) => {
for item in &mut list.items {
if let stmt::Expr::Value(value) = item
&& let Some(text) = document_text(value)
{
*value = stmt::Value::String(text);
}
}
}
stmt::Expr::Value(stmt::Value::List(items)) => {
for value in items.iter_mut() {
if let Some(text) = document_text(value) {
*value = stmt::Value::String(text);
}
}
}
_ => return,
}
func.ty = stmt::Type::String;
}
_ => {}
}
}
}
fn build_json_path(
schema: &Schema,
embed_id: app::ModelId,
projection: &[usize],
) -> Option<(Vec<String>, stmt::Type)> {
let mut path = Vec::with_capacity(projection.len());
let mut leaf_ty = None;
for field in schema.app.project_fields(embed_id, projection) {
path.push(field.name.app.as_deref()?.to_owned());
leaf_ty = Some(field.expr_ty().clone());
}
(!path.is_empty() && path.len() == projection.len())
.then(|| (path, leaf_ty.expect("a non-empty path has a leaf type")))
}
fn document_text(value: &stmt::Value) -> Option<String> {
#[cfg(any(feature = "jiff", feature = "rust_decimal", feature = "bigdecimal"))]
{
value.document_storage_text().map(|text| text.to_string())
}
#[cfg(not(any(feature = "jiff", feature = "rust_decimal", feature = "bigdecimal")))]
{
let _ = value;
None
}
}