#![allow(dead_code)]
use crate::{
db::{
DbSession, PersistedRow, QueryError,
access::AccessPlan,
executor::{
EntityAuthority, SharedPreparedExecutionPlan,
pipeline::execute_initial_grouped_rows_for_canister,
},
query::builder,
query::plan::{AccessPlannedQuery, LogicalPlan},
session::sql::{
SqlProjectionContract,
projection::{
SqlProjectionPayload, execute_sql_projection_rows_for_canister,
grouped_sql_statement_result_from_page,
},
},
sql::lowering::{
LoweredSqlQuery, PreparedSqlParameterContract, PreparedSqlParameterTypeFamily,
PreparedSqlStatement, lower_sql_command_from_prepared_statement,
},
sql::parser::{
SqlAggregateCall, SqlDeleteStatement, SqlExplainTarget, SqlExpr, SqlProjection,
SqlSelectStatement, SqlStatement,
},
},
model::{entity::EntityModel, field::FieldKind},
traits::{CanisterKind, EntityValue},
value::Value,
};
#[derive(Clone, Debug)]
pub(in crate::db) struct PreparedSqlQuery {
source_sql: String,
statement: PreparedSqlStatement,
parameter_contracts: Vec<PreparedSqlParameterContract>,
execution_template: Option<PreparedSqlExecutionTemplate>,
}
#[derive(Clone, Debug)]
enum PreparedSqlExecutionTemplate {
SymbolicScalar(PreparedSqlSymbolicScalarTemplate),
SymbolicGrouped(PreparedSqlSymbolicGroupedTemplate),
Legacy(PreparedSqlLegacyExecutionTemplate),
}
#[derive(Clone, Debug)]
struct PreparedSqlLegacyExecutionTemplate {
authority: EntityAuthority,
projection: SqlProjectionContract,
plan: AccessPlannedQuery,
}
#[derive(Clone, Debug)]
struct PreparedSqlSymbolicScalarTemplate {
authority: EntityAuthority,
projection: SqlProjectionContract,
plan: AccessPlannedQuery,
predicate: Option<PreparedSqlScalarPredicateTemplate>,
access: Option<PreparedSqlScalarAccessPathTemplate>,
}
#[derive(Clone, Debug)]
struct PreparedSqlSymbolicGroupedTemplate {
authority: EntityAuthority,
projection: SqlProjectionContract,
plan: AccessPlannedQuery,
predicate: Option<PreparedSqlScalarPredicateTemplate>,
access: Option<PreparedSqlScalarAccessPathTemplate>,
having_expr: Option<PreparedSqlGroupedExprTemplate>,
}
#[derive(Clone, Debug)]
struct PreparedSqlScalarCompareSlotTemplate {
field: String,
op: crate::db::predicate::CompareOp,
coercion: crate::db::predicate::CoercionId,
slot_index: usize,
}
#[derive(Clone, Debug)]
enum PreparedSqlScalarPredicateTemplate {
Compare(PreparedSqlScalarCompareSlotTemplate),
And(Vec<Self>),
Or(Vec<Self>),
Not(Box<Self>),
}
#[derive(Clone, Debug)]
enum PreparedSqlScalarAccessValueTemplate {
Static(Value),
SlotLiteral(usize),
}
#[derive(Clone, Debug)]
enum PreparedSqlScalarAccessPathTemplate {
ByKey {
key: PreparedSqlScalarAccessValueTemplate,
},
KeyRange {
start: PreparedSqlScalarAccessValueTemplate,
end: PreparedSqlScalarAccessValueTemplate,
},
IndexPrefix {
index: crate::model::index::IndexModel,
values: Vec<PreparedSqlScalarAccessValueTemplate>,
},
}
#[derive(Clone, Debug)]
enum PreparedSqlGroupedExprTemplate {
Static(crate::db::query::plan::expr::Expr),
SlotLiteral(usize),
Unary {
op: crate::db::query::plan::expr::UnaryOp,
expr: Box<Self>,
},
Binary {
op: crate::db::query::plan::expr::BinaryOp,
left: Box<Self>,
right: Box<Self>,
},
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum PreparedSqlExecutionTemplateKind {
SymbolicScalar,
SymbolicGrouped,
Legacy,
}
impl PreparedSqlQuery {
#[must_use]
pub(in crate::db) fn source_sql(&self) -> &str {
&self.source_sql
}
#[must_use]
pub(in crate::db) const fn parameter_contracts(&self) -> &[PreparedSqlParameterContract] {
self.parameter_contracts.as_slice()
}
#[must_use]
pub(in crate::db) const fn parameter_count(&self) -> usize {
self.parameter_contracts.len()
}
#[cfg(test)]
#[must_use]
pub(in crate::db) const fn template_kind_for_test(
&self,
) -> Option<PreparedSqlExecutionTemplateKind> {
match self.execution_template.as_ref() {
Some(PreparedSqlExecutionTemplate::SymbolicScalar(_)) => {
Some(PreparedSqlExecutionTemplateKind::SymbolicScalar)
}
Some(PreparedSqlExecutionTemplate::SymbolicGrouped(_)) => {
Some(PreparedSqlExecutionTemplateKind::SymbolicGrouped)
}
Some(PreparedSqlExecutionTemplate::Legacy(_)) => {
Some(PreparedSqlExecutionTemplateKind::Legacy)
}
None => None,
}
}
}
impl<C: CanisterKind> DbSession<C> {
pub(in crate::db) fn prepare_sql_query<E>(
&self,
sql: &str,
) -> Result<PreparedSqlQuery, QueryError>
where
E: PersistedRow<Canister = C> + EntityValue,
{
let statement = crate::db::session::sql::parse_sql_statement_with_attribution(sql)
.map(|(statement, _)| statement)?;
Self::ensure_sql_query_statement_supported(&statement)?;
let authority = EntityAuthority::for_type::<E>();
let prepared = Self::prepare_sql_statement_for_authority(&statement, authority)?;
let parameter_contracts = prepared
.parameter_contracts(authority.model())
.map_err(QueryError::from_sql_lowering_error)?;
let execution_template =
self.build_prepared_sql_execution_template(&prepared, ¶meter_contracts, authority)?;
Ok(PreparedSqlQuery {
source_sql: sql.to_string(),
statement: prepared,
parameter_contracts,
execution_template,
})
}
pub(in crate::db) fn execute_prepared_sql_query<E>(
&self,
prepared: &PreparedSqlQuery,
bindings: &[Value],
) -> Result<crate::db::session::sql::SqlStatementResult, QueryError>
where
E: PersistedRow<Canister = C> + EntityValue,
{
validate_parameter_bindings(prepared.parameter_contracts(), bindings)?;
if let Some(template) = prepared.execution_template.as_ref()
&& bindings.iter().all(|value| !matches!(value, Value::Null))
{
let bound_plan = match template {
PreparedSqlExecutionTemplate::SymbolicScalar(template) => {
bind_symbolic_scalar_template_plan(
template.plan.clone(),
template.predicate.as_ref(),
template.access.as_ref(),
bindings,
template.authority,
)?
}
PreparedSqlExecutionTemplate::SymbolicGrouped(template) => {
bind_symbolic_grouped_template_plan(
template.plan.clone(),
template.predicate.as_ref(),
template.access.as_ref(),
template.having_expr.as_ref(),
bindings,
template.authority,
)?
}
PreparedSqlExecutionTemplate::Legacy(template) => bind_prepared_template_plan(
template.plan.clone(),
prepared.parameter_contracts(),
bindings,
template.authority,
)?,
};
let prepared_plan =
SharedPreparedExecutionPlan::from_plan(bound_plan.authority(), bound_plan.plan);
let projection = match template {
PreparedSqlExecutionTemplate::SymbolicScalar(template) => &template.projection,
PreparedSqlExecutionTemplate::SymbolicGrouped(template) => &template.projection,
PreparedSqlExecutionTemplate::Legacy(template) => &template.projection,
};
return self.execute_prepared_template_plan(prepared_plan, projection);
}
let bound_statement = prepared.statement.bind_literals(bindings)?;
let authority = EntityAuthority::for_type::<E>();
self.execute_bound_prepared_sql_query_without_caches(&bound_statement, authority)
}
fn build_prepared_sql_execution_template(
&self,
prepared: &PreparedSqlStatement,
parameter_contracts: &[PreparedSqlParameterContract],
authority: EntityAuthority,
) -> Result<Option<PreparedSqlExecutionTemplate>, QueryError> {
if parameter_contracts.is_empty() {
return Ok(None);
}
if let SqlStatement::Select(select) = prepared.statement()
&& select
.predicate
.as_ref()
.is_some_and(sql_expr_uses_general_filter_expr_parameters)
{
return Ok(None);
}
if let Some(template) = self.build_symbolic_scalar_prepared_sql_execution_template(
prepared,
parameter_contracts,
authority,
)? {
return Ok(Some(template));
}
if let Some(template) = self.build_symbolic_grouped_prepared_sql_execution_template(
prepared,
parameter_contracts,
authority,
)? {
return Ok(Some(template));
}
let Some(template_bindings) = parameter_contracts
.iter()
.map(|contract| contract.template_binding().cloned())
.collect::<Option<Vec<_>>>()
else {
return Ok(None);
};
if prepared_statement_contains_template_literal_collision(
prepared.statement(),
template_bindings.as_slice(),
) {
return Ok(None);
}
let bound_statement = prepared.bind_literals(template_bindings.as_slice())?;
let normalized_prepared =
Self::prepare_sql_statement_for_authority(&bound_statement, authority)?;
let lowered =
lower_sql_command_from_prepared_statement(normalized_prepared, authority.model())
.map_err(QueryError::from_sql_lowering_error)?;
let Some(LoweredSqlQuery::Select(select)) = lowered.into_query() else {
return Ok(None);
};
let structural = Self::structural_query_from_lowered_select(select, authority)?;
let (_, plan) =
self.build_structural_plan_with_visible_indexes_for_authority(structural, authority)?;
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan.clone());
let projection = Self::sql_select_projection_contract_from_shared_prepared_plan(
authority,
&prepared_plan,
);
Ok(Some(PreparedSqlExecutionTemplate::Legacy(
PreparedSqlLegacyExecutionTemplate {
authority,
projection,
plan,
},
)))
}
#[expect(clippy::too_many_lines)]
fn build_symbolic_scalar_prepared_sql_execution_template(
&self,
prepared: &PreparedSqlStatement,
parameter_contracts: &[PreparedSqlParameterContract],
authority: EntityAuthority,
) -> Result<Option<PreparedSqlExecutionTemplate>, QueryError> {
if parameter_contracts.is_empty() {
return Ok(None);
}
let SqlStatement::Select(select) = prepared.statement() else {
return Ok(None);
};
let Some(sql_predicate) = select.predicate.as_ref() else {
return Ok(None);
};
let Some(exemplar_bindings) = parameter_contracts
.iter()
.map(prepared_sql_contract_exemplar_binding)
.collect::<Option<Vec<_>>>()
else {
return Ok(None);
};
let mut exemplar_bindings = exemplar_bindings;
apply_sql_range_exemplar_override(
select.predicate.as_ref(),
parameter_contracts,
exemplar_bindings.as_mut_slice(),
authority.model(),
);
let bound_statement = prepared.bind_literals(exemplar_bindings.as_slice())?;
let normalized_prepared =
Self::prepare_sql_statement_for_authority(&bound_statement, authority)?;
let lowered =
lower_sql_command_from_prepared_statement(normalized_prepared, authority.model())
.map_err(QueryError::from_sql_lowering_error)?;
let Some(LoweredSqlQuery::Select(select)) = lowered.into_query() else {
return Ok(None);
};
let structural = Self::structural_query_from_lowered_select(select, authority)?;
let (_, plan) =
self.build_structural_plan_with_visible_indexes_for_authority(structural, authority)?;
let LogicalPlan::Scalar(scalar) = &plan.logical else {
return Ok(None);
};
let predicate_template = match scalar.predicate.as_ref() {
Some(predicate) => {
let Some(predicate_template) =
build_symbolic_scalar_predicate_template(sql_predicate, predicate)
else {
return Ok(None);
};
Some(predicate_template)
}
None => None,
};
let symbolic_value_candidates = parameter_contracts
.iter()
.zip(exemplar_bindings.iter())
.filter(|(contract, _)| contract.type_family() != PreparedSqlParameterTypeFamily::Bool)
.map(|(_, binding)| binding.clone())
.collect::<Vec<_>>();
if predicate_template.is_none()
&& scalar.predicate.as_ref().is_some_and(|predicate| {
predicate_contains_any_runtime_values(
predicate,
symbolic_value_candidates.as_slice(),
)
})
{
return Ok(None);
}
let access_template = build_symbolic_scalar_access_path_template(
&plan.access,
parameter_contracts,
exemplar_bindings.as_slice(),
);
if access_template.is_none()
&& !symbolic_value_candidates.is_empty()
&& plan
.access
.contains_any_runtime_values(symbolic_value_candidates.as_slice())
{
return Ok(None);
}
if access_template.is_some()
&& prepared_statement_contains_template_literal_collision(
prepared.statement(),
exemplar_bindings.as_slice(),
)
{
return Ok(None);
}
if predicate_template.is_none() && access_template.is_none() {
return Ok(None);
}
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan.clone());
let projection = Self::sql_select_projection_contract_from_shared_prepared_plan(
authority,
&prepared_plan,
);
Ok(Some(PreparedSqlExecutionTemplate::SymbolicScalar(
PreparedSqlSymbolicScalarTemplate {
authority,
projection,
plan,
predicate: predicate_template,
access: access_template,
},
)))
}
#[expect(clippy::too_many_lines)]
fn build_symbolic_grouped_prepared_sql_execution_template(
&self,
prepared: &PreparedSqlStatement,
parameter_contracts: &[PreparedSqlParameterContract],
authority: EntityAuthority,
) -> Result<Option<PreparedSqlExecutionTemplate>, QueryError> {
if parameter_contracts.is_empty() {
return Ok(None);
}
let SqlStatement::Select(statement) = prepared.statement() else {
return Ok(None);
};
let Some(exemplar_bindings) = parameter_contracts
.iter()
.map(prepared_sql_contract_exemplar_binding)
.collect::<Option<Vec<_>>>()
else {
return Ok(None);
};
let mut exemplar_bindings = exemplar_bindings;
apply_sql_range_exemplar_override(
statement.predicate.as_ref(),
parameter_contracts,
exemplar_bindings.as_mut_slice(),
authority.model(),
);
if prepared_statement_contains_template_literal_collision(
prepared.statement(),
exemplar_bindings.as_slice(),
) {
return Ok(None);
}
let bound_statement = prepared.bind_literals(exemplar_bindings.as_slice())?;
let normalized_prepared =
Self::prepare_sql_statement_for_authority(&bound_statement, authority)?;
let lowered =
lower_sql_command_from_prepared_statement(normalized_prepared, authority.model())
.map_err(QueryError::from_sql_lowering_error)?;
let Some(LoweredSqlQuery::Select(select)) = lowered.into_query() else {
return Ok(None);
};
let structural = Self::structural_query_from_lowered_select(select, authority)?;
let (_, plan) =
self.build_structural_plan_with_visible_indexes_for_authority(structural, authority)?;
let LogicalPlan::Grouped(grouped) = &plan.logical else {
return Ok(None);
};
let symbolic_access_candidates = parameter_contracts
.iter()
.zip(exemplar_bindings.iter())
.filter(|(contract, _)| contract.type_family() != PreparedSqlParameterTypeFamily::Bool)
.map(|(_, binding)| binding.clone())
.collect::<Vec<_>>();
let access_template = build_symbolic_scalar_access_path_template(
&plan.access,
parameter_contracts,
exemplar_bindings.as_slice(),
);
let predicate_template = match (
statement.predicate.as_ref(),
grouped.scalar.predicate.as_ref(),
) {
(Some(sql_predicate), Some(predicate)) => {
let Some(predicate_template) =
build_symbolic_scalar_predicate_template(sql_predicate, predicate)
else {
return Ok(None);
};
Some(predicate_template)
}
(Some(_), None) if access_template.is_some() => None,
(None, None) => None,
_ => return Ok(None),
};
if access_template.is_some()
&& statement
.predicate
.as_ref()
.is_some_and(sql_expr_is_compound_boolean)
&& !(predicate_template.is_none()
&& sql_simple_range_slots(
statement.predicate.as_ref(),
authority.model(),
parameter_contracts,
)
.is_some())
{
return Ok(None);
}
if access_template.is_none()
&& !symbolic_access_candidates.is_empty()
&& plan
.access
.contains_any_runtime_values(symbolic_access_candidates.as_slice())
{
return Ok(None);
}
let having_template = match grouped.having_expr.as_ref() {
Some(having_expr) => {
let Some(having_template) = build_symbolic_grouped_expr_template(
having_expr,
parameter_contracts,
exemplar_bindings.as_slice(),
) else {
return Ok(None);
};
Some(having_template)
}
None => None,
};
if access_template.is_some()
&& prepared_statement_contains_template_literal_collision(
prepared.statement(),
exemplar_bindings.as_slice(),
)
{
return Ok(None);
}
if predicate_template.is_none() && access_template.is_none() && having_template.is_none() {
return Ok(None);
}
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan.clone());
let projection = Self::sql_select_projection_contract_from_shared_prepared_plan(
authority,
&prepared_plan,
);
Ok(Some(PreparedSqlExecutionTemplate::SymbolicGrouped(
PreparedSqlSymbolicGroupedTemplate {
authority,
projection,
plan,
predicate: predicate_template,
access: access_template,
having_expr: having_template,
},
)))
}
fn execute_prepared_template_plan(
&self,
prepared_plan: SharedPreparedExecutionPlan,
projection: &SqlProjectionContract,
) -> Result<crate::db::session::sql::SqlStatementResult, QueryError> {
if prepared_plan.logical_plan().grouped_plan().is_some() {
let authority = prepared_plan.authority();
let plan = prepared_plan.logical_plan().clone();
let (columns, fixed_scales) = projection.clone().into_parts();
let page =
execute_initial_grouped_rows_for_canister(&self.db, self.debug, authority, plan)
.map_err(QueryError::execute)?;
let statement_result =
grouped_sql_statement_result_from_page(columns, fixed_scales, page)?;
Ok(statement_result)
} else {
let (columns, fixed_scales) = projection.clone().into_parts();
let projected =
execute_sql_projection_rows_for_canister(&self.db, self.debug, prepared_plan)
.map_err(QueryError::execute)?;
let (rows, row_count) = projected.into_parts();
let payload = SqlProjectionPayload::new(columns, fixed_scales, rows, row_count);
Ok(payload.into_statement_result())
}
}
fn execute_bound_prepared_sql_query_without_caches(
&self,
bound_statement: &SqlStatement,
authority: EntityAuthority,
) -> Result<crate::db::session::sql::SqlStatementResult, QueryError> {
let normalized_prepared =
Self::prepare_sql_statement_for_authority(bound_statement, authority)?;
let lowered =
lower_sql_command_from_prepared_statement(normalized_prepared, authority.model())
.map_err(QueryError::from_sql_lowering_error)?;
let Some(LoweredSqlQuery::Select(select)) = lowered.into_query() else {
return Err(QueryError::invariant(
"prepared SQL query fallback must lower to lowered SQL SELECT",
));
};
let structural = Self::structural_query_from_lowered_select(select, authority)?;
let (_, plan) =
self.build_structural_plan_with_visible_indexes_for_authority(structural, authority)?;
let prepared_plan = SharedPreparedExecutionPlan::from_plan(authority, plan);
let projection = Self::sql_select_projection_contract_from_shared_prepared_plan(
authority,
&prepared_plan,
);
self.execute_prepared_template_plan(prepared_plan, &projection)
}
}
#[derive(Clone, Debug)]
struct BoundPreparedTemplatePlan {
authority: EntityAuthority,
plan: AccessPlannedQuery,
}
impl BoundPreparedTemplatePlan {
#[must_use]
const fn new(authority: EntityAuthority, plan: AccessPlannedQuery) -> Self {
Self { authority, plan }
}
#[must_use]
const fn authority(&self) -> EntityAuthority {
self.authority
}
}
fn prepared_statement_contains_template_literal_collision(
statement: &SqlStatement,
template_bindings: &[Value],
) -> bool {
match statement {
SqlStatement::Select(select) => {
sql_select_contains_template_literal_collision(select, template_bindings)
}
SqlStatement::Delete(delete) => {
sql_delete_contains_template_literal_collision(delete, template_bindings)
}
SqlStatement::Explain(explain) => match &explain.statement {
SqlExplainTarget::Select(select) => {
sql_select_contains_template_literal_collision(select, template_bindings)
}
SqlExplainTarget::Delete(delete) => {
sql_delete_contains_template_literal_collision(delete, template_bindings)
}
},
SqlStatement::Insert(_)
| SqlStatement::Update(_)
| SqlStatement::Describe(_)
| SqlStatement::ShowIndexes(_)
| SqlStatement::ShowColumns(_)
| SqlStatement::ShowEntities(_) => false,
}
}
fn sql_select_contains_template_literal_collision(
select: &SqlSelectStatement,
template_bindings: &[Value],
) -> bool {
sql_projection_contains_template_literal_collision(&select.projection, template_bindings)
|| select.predicate.as_ref().is_some_and(|expr| {
sql_expr_contains_template_literal_collision(expr, template_bindings)
})
|| select
.having
.iter()
.any(|expr| sql_expr_contains_template_literal_collision(expr, template_bindings))
|| select.order_by.iter().any(|term| {
sql_expr_contains_template_literal_collision(&term.field, template_bindings)
})
}
fn sql_delete_contains_template_literal_collision(
delete: &SqlDeleteStatement,
template_bindings: &[Value],
) -> bool {
delete
.predicate
.as_ref()
.is_some_and(|expr| sql_expr_contains_template_literal_collision(expr, template_bindings))
}
const fn sql_expr_is_compound_boolean(expr: &SqlExpr) -> bool {
matches!(
expr,
SqlExpr::Binary {
op: crate::db::sql::parser::SqlExprBinaryOp::And
| crate::db::sql::parser::SqlExprBinaryOp::Or,
..
} | SqlExpr::Unary { .. }
)
}
fn sql_expr_uses_general_filter_expr_parameters(expr: &SqlExpr) -> bool {
match expr {
SqlExpr::Field(_) | SqlExpr::Literal(_) | SqlExpr::Param { .. } | SqlExpr::Aggregate(_) => {
false
}
SqlExpr::Membership { expr, .. } => sql_expr_contains_param(expr.as_ref()),
SqlExpr::NullTest { expr, .. } => !sql_null_test_target_is_template_predicate_shape(expr),
SqlExpr::Unary { expr, .. } => sql_expr_uses_general_filter_expr_parameters(expr),
SqlExpr::Binary { op, left, right } => match op {
crate::db::sql::parser::SqlExprBinaryOp::And
| crate::db::sql::parser::SqlExprBinaryOp::Or => {
sql_expr_uses_general_filter_expr_parameters(left)
|| sql_expr_uses_general_filter_expr_parameters(right)
}
crate::db::sql::parser::SqlExprBinaryOp::Eq
| crate::db::sql::parser::SqlExprBinaryOp::Ne
| crate::db::sql::parser::SqlExprBinaryOp::Lt
| crate::db::sql::parser::SqlExprBinaryOp::Lte
| crate::db::sql::parser::SqlExprBinaryOp::Gt
| crate::db::sql::parser::SqlExprBinaryOp::Gte => {
!(sql_compare_target_is_template_predicate_shape(left)
&& sql_compare_target_is_template_predicate_shape(right))
&& (sql_expr_contains_param(left) || sql_expr_contains_param(right))
}
crate::db::sql::parser::SqlExprBinaryOp::Add
| crate::db::sql::parser::SqlExprBinaryOp::Sub
| crate::db::sql::parser::SqlExprBinaryOp::Mul
| crate::db::sql::parser::SqlExprBinaryOp::Div => {
sql_expr_contains_param(left) || sql_expr_contains_param(right)
}
},
SqlExpr::FunctionCall { function, args } => {
if sql_text_predicate_function_is_template_shape(*function, args.as_slice()) {
return false;
}
args.iter().any(sql_expr_contains_param)
}
SqlExpr::Case { arms, else_expr } => {
arms.iter().any(|arm| {
sql_expr_contains_param(&arm.condition) || sql_expr_contains_param(&arm.result)
}) || else_expr
.as_ref()
.is_some_and(|expr| sql_expr_contains_param(expr))
}
}
}
fn sql_compare_target_is_template_predicate_shape(expr: &SqlExpr) -> bool {
match expr {
SqlExpr::Field(_) | SqlExpr::Literal(_) | SqlExpr::Param { .. } => true,
SqlExpr::FunctionCall { function, args }
if matches!(
function,
crate::db::sql::parser::SqlScalarFunction::Lower
| crate::db::sql::parser::SqlScalarFunction::Upper
) && matches!(args.as_slice(), [SqlExpr::Field(_)]) =>
{
true
}
_ => false,
}
}
const fn sql_null_test_target_is_template_predicate_shape(expr: &SqlExpr) -> bool {
matches!(expr, SqlExpr::Field(_))
}
fn sql_text_predicate_function_is_template_shape(
function: crate::db::sql::parser::SqlScalarFunction,
args: &[SqlExpr],
) -> bool {
matches!(
function,
crate::db::sql::parser::SqlScalarFunction::StartsWith
| crate::db::sql::parser::SqlScalarFunction::EndsWith
| crate::db::sql::parser::SqlScalarFunction::Contains
) && matches!(
args,
[left, right]
if sql_compare_target_is_template_predicate_shape(left)
&& sql_compare_target_is_template_predicate_shape(right)
)
}
fn sql_expr_contains_param(expr: &SqlExpr) -> bool {
match expr {
SqlExpr::Field(_) | SqlExpr::Literal(_) => false,
SqlExpr::Param { .. } => true,
SqlExpr::Aggregate(aggregate) => {
aggregate
.input
.as_ref()
.is_some_and(|expr| sql_expr_contains_param(expr))
|| aggregate
.filter_expr
.as_ref()
.is_some_and(|expr| sql_expr_contains_param(expr))
}
SqlExpr::Membership { expr, .. } => sql_expr_contains_param(expr.as_ref()),
SqlExpr::NullTest { expr, .. } | SqlExpr::Unary { expr, .. } => {
sql_expr_contains_param(expr)
}
SqlExpr::FunctionCall { args, .. } => args.iter().any(sql_expr_contains_param),
SqlExpr::Binary { left, right, .. } => {
sql_expr_contains_param(left) || sql_expr_contains_param(right)
}
SqlExpr::Case { arms, else_expr } => {
arms.iter().any(|arm| {
sql_expr_contains_param(&arm.condition) || sql_expr_contains_param(&arm.result)
}) || else_expr
.as_ref()
.is_some_and(|expr| sql_expr_contains_param(expr))
}
}
}
fn sql_projection_contains_template_literal_collision(
projection: &SqlProjection,
template_bindings: &[Value],
) -> bool {
match projection {
SqlProjection::All => false,
SqlProjection::Items(items) => items.iter().any(|item| {
sql_expr_contains_template_literal_collision(
&SqlExpr::from_select_item(item),
template_bindings,
)
}),
}
}
fn sql_aggregate_contains_template_literal_collision(
aggregate: &SqlAggregateCall,
template_bindings: &[Value],
) -> bool {
aggregate
.input
.as_ref()
.is_some_and(|expr| sql_expr_contains_template_literal_collision(expr, template_bindings))
|| aggregate.filter_expr.as_ref().is_some_and(|expr| {
sql_expr_contains_template_literal_collision(expr, template_bindings)
})
}
fn sql_expr_contains_template_literal_collision(
expr: &SqlExpr,
template_bindings: &[Value],
) -> bool {
match expr {
SqlExpr::Field(_) | SqlExpr::Param { .. } => false,
SqlExpr::Aggregate(aggregate) => {
sql_aggregate_contains_template_literal_collision(aggregate, template_bindings)
}
SqlExpr::Literal(value) => template_bindings.contains(value),
SqlExpr::Membership { expr, values, .. } => {
sql_expr_contains_template_literal_collision(expr, template_bindings)
|| values.iter().any(|value| template_bindings.contains(value))
}
SqlExpr::NullTest { expr, .. } | SqlExpr::Unary { expr, .. } => {
sql_expr_contains_template_literal_collision(expr, template_bindings)
}
SqlExpr::FunctionCall { args, .. } => args
.iter()
.any(|arg| sql_expr_contains_template_literal_collision(arg, template_bindings)),
SqlExpr::Binary { left, right, .. } => {
sql_expr_contains_template_literal_collision(left, template_bindings)
|| sql_expr_contains_template_literal_collision(right, template_bindings)
}
SqlExpr::Case { arms, else_expr } => {
arms.iter().any(|arm| {
sql_expr_contains_template_literal_collision(&arm.condition, template_bindings)
|| sql_expr_contains_template_literal_collision(&arm.result, template_bindings)
}) || else_expr.as_ref().is_some_and(|expr| {
sql_expr_contains_template_literal_collision(expr, template_bindings)
})
}
}
}
fn bind_prepared_template_plan(
mut plan: AccessPlannedQuery,
contracts: &[PreparedSqlParameterContract],
bindings: &[Value],
authority: EntityAuthority,
) -> Result<BoundPreparedTemplatePlan, QueryError> {
let replacements = contracts
.iter()
.filter_map(|contract| {
contract.template_binding().map(|template_binding| {
(
template_binding.clone(),
bindings
.get(contract.index())
.expect("validated binding vector must cover every contract index")
.clone(),
)
})
})
.collect::<Vec<_>>();
match &mut plan.logical {
LogicalPlan::Scalar(scalar) => {
scalar.predicate = scalar.predicate.take().map(|predicate| {
bind_prepared_template_predicate(predicate, replacements.as_slice())
});
}
LogicalPlan::Grouped(grouped) => {
grouped.scalar.predicate = grouped.scalar.predicate.take().map(|predicate| {
bind_prepared_template_predicate(predicate, replacements.as_slice())
});
grouped.having_expr = grouped
.having_expr
.take()
.map(|expr| bind_prepared_template_expr(expr, replacements.as_slice()));
}
}
plan.access = plan.access.bind_runtime_values(replacements.as_slice());
plan.finalize_planner_route_profile_for_model(authority.model());
plan.finalize_static_planning_shape_for_model(authority.model())
.map_err(QueryError::execute)?;
Ok(BoundPreparedTemplatePlan::new(authority, plan))
}
fn bind_symbolic_scalar_template_plan(
mut plan: AccessPlannedQuery,
predicate_template: Option<&PreparedSqlScalarPredicateTemplate>,
access_template: Option<&PreparedSqlScalarAccessPathTemplate>,
bindings: &[Value],
authority: EntityAuthority,
) -> Result<BoundPreparedTemplatePlan, QueryError> {
let LogicalPlan::Scalar(scalar) = &mut plan.logical else {
return Err(QueryError::unsupported_query(
"symbolic scalar prepared template expected scalar logical plan",
));
};
if let Some(predicate_template) = predicate_template {
let predicate =
instantiate_symbolic_scalar_predicate_template(predicate_template, bindings)?;
scalar.predicate = Some(predicate);
}
if let Some(access_template) = access_template {
plan.access = instantiate_symbolic_scalar_access_path_template(access_template, bindings)?;
}
plan.finalize_planner_route_profile_for_model(authority.model());
plan.finalize_static_planning_shape_for_model(authority.model())
.map_err(QueryError::execute)?;
Ok(BoundPreparedTemplatePlan::new(authority, plan))
}
fn bind_symbolic_grouped_template_plan(
mut plan: AccessPlannedQuery,
predicate_template: Option<&PreparedSqlScalarPredicateTemplate>,
access_template: Option<&PreparedSqlScalarAccessPathTemplate>,
having_template: Option<&PreparedSqlGroupedExprTemplate>,
bindings: &[Value],
authority: EntityAuthority,
) -> Result<BoundPreparedTemplatePlan, QueryError> {
let LogicalPlan::Grouped(grouped) = &mut plan.logical else {
return Err(QueryError::unsupported_query(
"symbolic grouped prepared template expected grouped logical plan",
));
};
if let Some(predicate_template) = predicate_template {
grouped.scalar.predicate = Some(instantiate_symbolic_scalar_predicate_template(
predicate_template,
bindings,
)?);
}
if let Some(access_template) = access_template {
plan.access = instantiate_symbolic_scalar_access_path_template(access_template, bindings)?;
}
if let Some(having_template) = having_template {
let having_expr = instantiate_symbolic_grouped_expr_template(having_template, bindings)?;
grouped.having_expr = Some(having_expr);
}
plan.finalize_planner_route_profile_for_model(authority.model());
plan.finalize_static_planning_shape_for_model(authority.model())
.map_err(QueryError::execute)?;
Ok(BoundPreparedTemplatePlan::new(authority, plan))
}
fn prepared_sql_contract_exemplar_binding(
contract: &PreparedSqlParameterContract,
) -> Option<Value> {
match contract.type_family() {
PreparedSqlParameterTypeFamily::Bool => Some(Value::Bool(false)),
PreparedSqlParameterTypeFamily::Numeric | PreparedSqlParameterTypeFamily::Text => {
contract.template_binding().cloned()
}
}
}
fn apply_sql_range_exemplar_override(
predicate: Option<&SqlExpr>,
contracts: &[PreparedSqlParameterContract],
exemplar_bindings: &mut [Value],
model: &'static EntityModel,
) {
let Some((field_kind, lower_slot, upper_slot)) =
sql_simple_range_slots(predicate, model, contracts)
else {
return;
};
let Some((lower_value, upper_value)) =
ordered_exemplar_range_values_for_field_kind(field_kind, lower_slot, upper_slot)
else {
return;
};
let Some(lower_binding) = exemplar_bindings.get_mut(lower_slot) else {
return;
};
*lower_binding = lower_value;
let Some(upper_binding) = exemplar_bindings.get_mut(upper_slot) else {
return;
};
*upper_binding = upper_value;
}
fn sql_simple_range_slots(
predicate: Option<&SqlExpr>,
model: &'static EntityModel,
contracts: &[PreparedSqlParameterContract],
) -> Option<(FieldKind, usize, usize)> {
let SqlExpr::Binary {
op: crate::db::sql::parser::SqlExprBinaryOp::And,
left,
right,
} = predicate?
else {
return None;
};
let first = sql_range_compare_descriptor(left)?;
let second = sql_range_compare_descriptor(right)?;
if first.field != second.field {
return None;
}
let (lower_slot, upper_slot) = match (first.bound, second.bound) {
(SqlRangeBoundKind::Lower, SqlRangeBoundKind::Upper) => {
(first.slot_index, second.slot_index)
}
(SqlRangeBoundKind::Upper, SqlRangeBoundKind::Lower) => {
(second.slot_index, first.slot_index)
}
_ => return None,
};
if contracts.get(lower_slot)?.type_family() != contracts.get(upper_slot)?.type_family() {
return None;
}
let field_kind = model
.fields()
.iter()
.find(|candidate| candidate.name() == first.field)
.map(crate::model::field::FieldModel::kind)?;
Some((field_kind, lower_slot, upper_slot))
}
enum SqlRangeBoundKind {
Lower,
Upper,
}
struct SqlRangeCompareDescriptor<'a> {
field: &'a str,
slot_index: usize,
bound: SqlRangeBoundKind,
}
fn sql_range_compare_descriptor(expr: &SqlExpr) -> Option<SqlRangeCompareDescriptor<'_>> {
let SqlExpr::Binary { op, left, right } = expr else {
return None;
};
let (SqlExpr::Field(field), SqlExpr::Param { index }) = (&**left, &**right) else {
return None;
};
let bound = match op {
crate::db::sql::parser::SqlExprBinaryOp::Gt
| crate::db::sql::parser::SqlExprBinaryOp::Gte => SqlRangeBoundKind::Lower,
crate::db::sql::parser::SqlExprBinaryOp::Lt
| crate::db::sql::parser::SqlExprBinaryOp::Lte => SqlRangeBoundKind::Upper,
_ => return None,
};
Some(SqlRangeCompareDescriptor {
field,
slot_index: *index,
bound,
})
}
fn ordered_exemplar_range_values_for_field_kind(
field_kind: FieldKind,
lower_slot: usize,
upper_slot: usize,
) -> Option<(Value, Value)> {
let lower_offset = u64::try_from(lower_slot).ok()?;
let upper_offset = u64::try_from(upper_slot).ok()?;
match field_kind {
FieldKind::Int => {
let lower = i64::try_from(lower_offset).ok()?;
let upper = i64::try_from(upper_offset).ok()?.saturating_add(1);
Some((Value::Int(lower), Value::Int(upper)))
}
FieldKind::Int128 => Some((
Value::Int128((i128::from(lower_offset)).into()),
Value::Int128((i128::from(upper_offset).saturating_add(1)).into()),
)),
FieldKind::IntBig => Some((
Value::IntBig(crate::types::Int::from(i32::try_from(lower_offset).ok()?)),
Value::IntBig(crate::types::Int::from(
i32::try_from(upper_offset.saturating_add(1)).ok()?,
)),
)),
FieldKind::Uint => Some((
Value::Uint(lower_offset),
Value::Uint(upper_offset.saturating_add(1)),
)),
FieldKind::Uint128 => Some((
Value::Uint128((u128::from(lower_offset)).into()),
Value::Uint128((u128::from(upper_offset).saturating_add(1)).into()),
)),
FieldKind::UintBig => Some((
Value::UintBig(lower_offset.into()),
Value::UintBig(upper_offset.saturating_add(1).into()),
)),
FieldKind::Decimal { scale } => Some((
Value::Decimal(crate::types::Decimal::from_i128_with_scale(
i128::from(lower_offset),
scale,
)),
Value::Decimal(crate::types::Decimal::from_i128_with_scale(
i128::from(upper_offset).saturating_add(1),
scale,
)),
)),
FieldKind::Duration => Some((
Value::Duration(crate::types::Duration::from_millis(lower_offset)),
Value::Duration(crate::types::Duration::from_millis(
upper_offset.saturating_add(1),
)),
)),
FieldKind::Timestamp => Some((
Value::Timestamp(crate::types::Timestamp::from_millis(
i64::try_from(lower_offset).ok()?,
)),
Value::Timestamp(crate::types::Timestamp::from_millis(
i64::try_from(upper_offset).ok()?.saturating_add(1),
)),
)),
FieldKind::Text => Some((
Value::Text(format!("__icydb_prepared_range_lower_{lower_slot}__")),
Value::Text(format!("__icydb_prepared_range_upper_{upper_slot}__")),
)),
_ => None,
}
}
fn build_symbolic_scalar_access_path_template(
access: &AccessPlan<Value>,
parameter_contracts: &[PreparedSqlParameterContract],
exemplar_bindings: &[Value],
) -> Option<PreparedSqlScalarAccessPathTemplate> {
if let Some(key) = access.as_by_key_path() {
return Some(PreparedSqlScalarAccessPathTemplate::ByKey {
key: build_symbolic_scalar_access_value_template(
key,
parameter_contracts,
exemplar_bindings,
),
});
}
if let Some((start, end)) = access.as_primary_key_range_path() {
let start = build_symbolic_scalar_access_value_template(
start,
parameter_contracts,
exemplar_bindings,
);
let end = build_symbolic_scalar_access_value_template(
end,
parameter_contracts,
exemplar_bindings,
);
if matches!(start, PreparedSqlScalarAccessValueTemplate::Static(_))
&& matches!(end, PreparedSqlScalarAccessValueTemplate::Static(_))
{
return None;
}
return Some(PreparedSqlScalarAccessPathTemplate::KeyRange { start, end });
}
let (index, values) = access.as_index_prefix_path()?;
let mut had_slot = false;
let mut templates = Vec::with_capacity(values.len());
for value in values {
let template = build_symbolic_scalar_access_value_template(
value,
parameter_contracts,
exemplar_bindings,
);
had_slot |= matches!(
template,
PreparedSqlScalarAccessValueTemplate::SlotLiteral(_)
);
templates.push(template);
}
if !had_slot {
return None;
}
Some(PreparedSqlScalarAccessPathTemplate::IndexPrefix {
index: *index,
values: templates,
})
}
fn build_symbolic_scalar_access_value_template(
value: &Value,
parameter_contracts: &[PreparedSqlParameterContract],
exemplar_bindings: &[Value],
) -> PreparedSqlScalarAccessValueTemplate {
prepared_sql_slot_index_for_exemplar_value(value, parameter_contracts, exemplar_bindings)
.map_or_else(
|| PreparedSqlScalarAccessValueTemplate::Static(value.clone()),
PreparedSqlScalarAccessValueTemplate::SlotLiteral,
)
}
fn prepared_sql_slot_index_for_exemplar_value(
value: &Value,
parameter_contracts: &[PreparedSqlParameterContract],
exemplar_bindings: &[Value],
) -> Option<usize> {
let mut matching_slot = None;
for (contract, exemplar) in parameter_contracts.iter().zip(exemplar_bindings.iter()) {
if contract.type_family() == PreparedSqlParameterTypeFamily::Bool || exemplar != value {
continue;
}
if matching_slot.is_some() {
return None;
}
matching_slot = Some(contract.index());
}
matching_slot
}
fn instantiate_symbolic_scalar_access_path_template(
template: &PreparedSqlScalarAccessPathTemplate,
bindings: &[Value],
) -> Result<AccessPlan<Value>, QueryError> {
match template {
PreparedSqlScalarAccessPathTemplate::ByKey { key } => Ok(AccessPlan::by_key(
instantiate_symbolic_scalar_access_value_template(key, bindings)?,
)),
PreparedSqlScalarAccessPathTemplate::KeyRange { start, end } => Ok(AccessPlan::key_range(
instantiate_symbolic_scalar_access_value_template(start, bindings)?,
instantiate_symbolic_scalar_access_value_template(end, bindings)?,
)),
PreparedSqlScalarAccessPathTemplate::IndexPrefix { index, values } => {
let mut bound_values = Vec::with_capacity(values.len());
for value in values {
bound_values.push(instantiate_symbolic_scalar_access_value_template(
value, bindings,
)?);
}
Ok(AccessPlan::index_prefix(*index, bound_values))
}
}
}
fn instantiate_symbolic_scalar_access_value_template(
template: &PreparedSqlScalarAccessValueTemplate,
bindings: &[Value],
) -> Result<Value, QueryError> {
match template {
PreparedSqlScalarAccessValueTemplate::Static(value) => Ok(value.clone()),
PreparedSqlScalarAccessValueTemplate::SlotLiteral(slot_index) => {
bindings.get(*slot_index).cloned().ok_or_else(|| {
QueryError::unsupported_query(format!(
"missing prepared SQL binding at index={slot_index}",
))
})
}
}
}
fn predicate_contains_any_runtime_values(
predicate: &crate::db::predicate::Predicate,
candidates: &[Value],
) -> bool {
match predicate {
crate::db::predicate::Predicate::True
| crate::db::predicate::Predicate::False
| crate::db::predicate::Predicate::CompareFields(_)
| crate::db::predicate::Predicate::IsNull { .. }
| crate::db::predicate::Predicate::IsNotNull { .. }
| crate::db::predicate::Predicate::IsMissing { .. }
| crate::db::predicate::Predicate::IsEmpty { .. }
| crate::db::predicate::Predicate::IsNotEmpty { .. } => false,
crate::db::predicate::Predicate::Compare(compare) => candidates.contains(compare.value()),
crate::db::predicate::Predicate::And(children)
| crate::db::predicate::Predicate::Or(children) => children
.iter()
.any(|child| predicate_contains_any_runtime_values(child, candidates)),
crate::db::predicate::Predicate::Not(child) => {
predicate_contains_any_runtime_values(child, candidates)
}
crate::db::predicate::Predicate::TextContains { value, .. }
| crate::db::predicate::Predicate::TextContainsCi { value, .. } => {
candidates.contains(value)
}
}
}
fn build_symbolic_scalar_predicate_template(
sql_expr: &SqlExpr,
predicate: &crate::db::predicate::Predicate,
) -> Option<PreparedSqlScalarPredicateTemplate> {
match (sql_expr, predicate) {
(
SqlExpr::Binary {
op: crate::db::sql::parser::SqlExprBinaryOp::And,
left,
right,
},
crate::db::predicate::Predicate::And(children),
) if children.len() == 2 => build_symbolic_scalar_binary_children_template(
left,
right,
&children[0],
&children[1],
PreparedSqlScalarPredicateTemplate::And,
),
(
SqlExpr::Binary {
op: crate::db::sql::parser::SqlExprBinaryOp::Or,
left,
right,
},
crate::db::predicate::Predicate::Or(children),
) if children.len() == 2 => build_symbolic_scalar_binary_children_template(
left,
right,
&children[0],
&children[1],
PreparedSqlScalarPredicateTemplate::Or,
),
(SqlExpr::Unary { expr, .. }, crate::db::predicate::Predicate::Not(child)) => {
Some(PreparedSqlScalarPredicateTemplate::Not(Box::new(
build_symbolic_scalar_predicate_template(expr, child)?,
)))
}
(
SqlExpr::Binary {
op:
crate::db::sql::parser::SqlExprBinaryOp::Eq
| crate::db::sql::parser::SqlExprBinaryOp::Ne
| crate::db::sql::parser::SqlExprBinaryOp::Lt
| crate::db::sql::parser::SqlExprBinaryOp::Lte
| crate::db::sql::parser::SqlExprBinaryOp::Gt
| crate::db::sql::parser::SqlExprBinaryOp::Gte,
left,
right,
},
crate::db::predicate::Predicate::Compare(compare),
) => match (&**left, &**right) {
(SqlExpr::Field(_), SqlExpr::Param { index }) => Some(
PreparedSqlScalarPredicateTemplate::Compare(PreparedSqlScalarCompareSlotTemplate {
field: compare.field.clone(),
op: compare.op,
coercion: compare.coercion.id,
slot_index: *index,
}),
),
_ => None,
},
_ => None,
}
}
fn build_symbolic_scalar_binary_children_template(
left_sql: &SqlExpr,
right_sql: &SqlExpr,
first_child: &crate::db::predicate::Predicate,
second_child: &crate::db::predicate::Predicate,
ctor: fn(Vec<PreparedSqlScalarPredicateTemplate>) -> PreparedSqlScalarPredicateTemplate,
) -> Option<PreparedSqlScalarPredicateTemplate> {
if let (Some(left), Some(right)) = (
build_symbolic_scalar_predicate_template(left_sql, first_child),
build_symbolic_scalar_predicate_template(right_sql, second_child),
) {
return Some(ctor(vec![left, right]));
}
let (Some(left), Some(right)) = (
build_symbolic_scalar_predicate_template(left_sql, second_child),
build_symbolic_scalar_predicate_template(right_sql, first_child),
) else {
return None;
};
Some(ctor(vec![left, right]))
}
fn instantiate_symbolic_scalar_predicate_template(
template: &PreparedSqlScalarPredicateTemplate,
bindings: &[Value],
) -> Result<crate::db::predicate::Predicate, QueryError> {
match template {
PreparedSqlScalarPredicateTemplate::Compare(compare) => {
let binding = bindings
.get(compare.slot_index)
.ok_or_else(|| {
QueryError::unsupported_query(format!(
"missing prepared SQL binding at index={}",
compare.slot_index,
))
})?
.clone();
Ok(crate::db::predicate::Predicate::Compare(
crate::db::predicate::ComparePredicate::with_coercion(
compare.field.clone(),
compare.op,
binding,
compare.coercion,
),
))
}
PreparedSqlScalarPredicateTemplate::And(children) => {
Ok(crate::db::predicate::Predicate::And(
children
.iter()
.map(|child| instantiate_symbolic_scalar_predicate_template(child, bindings))
.collect::<Result<Vec<_>, _>>()?,
))
}
PreparedSqlScalarPredicateTemplate::Or(children) => {
Ok(crate::db::predicate::Predicate::Or(
children
.iter()
.map(|child| instantiate_symbolic_scalar_predicate_template(child, bindings))
.collect::<Result<Vec<_>, _>>()?,
))
}
PreparedSqlScalarPredicateTemplate::Not(child) => {
Ok(crate::db::predicate::Predicate::Not(Box::new(
instantiate_symbolic_scalar_predicate_template(child, bindings)?,
)))
}
}
}
fn build_symbolic_grouped_expr_template(
expr: &crate::db::query::plan::expr::Expr,
parameter_contracts: &[PreparedSqlParameterContract],
exemplar_bindings: &[Value],
) -> Option<PreparedSqlGroupedExprTemplate> {
match expr {
crate::db::query::plan::expr::Expr::Literal(value) => {
let slot_index = parameter_contracts.iter().find_map(|contract| {
exemplar_bindings
.get(contract.index())
.filter(|binding| *binding == value)
.map(|_| contract.index())
});
Some(slot_index.map_or_else(
|| PreparedSqlGroupedExprTemplate::Static(expr.clone()),
PreparedSqlGroupedExprTemplate::SlotLiteral,
))
}
crate::db::query::plan::expr::Expr::Unary { op, expr } => {
let child =
build_symbolic_grouped_expr_template(expr, parameter_contracts, exemplar_bindings)?;
if matches!(child, PreparedSqlGroupedExprTemplate::Static(_)) {
Some(PreparedSqlGroupedExprTemplate::Static(
crate::db::query::plan::expr::Expr::Unary {
op: *op,
expr: Box::new(
instantiate_symbolic_grouped_expr_template(&child, exemplar_bindings)
.ok()?,
),
},
))
} else {
Some(PreparedSqlGroupedExprTemplate::Unary {
op: *op,
expr: Box::new(child),
})
}
}
crate::db::query::plan::expr::Expr::Binary { op, left, right } => {
let left_template =
build_symbolic_grouped_expr_template(left, parameter_contracts, exemplar_bindings)?;
let right_template = build_symbolic_grouped_expr_template(
right,
parameter_contracts,
exemplar_bindings,
)?;
if matches!(left_template, PreparedSqlGroupedExprTemplate::Static(_))
&& matches!(right_template, PreparedSqlGroupedExprTemplate::Static(_))
{
Some(PreparedSqlGroupedExprTemplate::Static(expr.clone()))
} else {
Some(PreparedSqlGroupedExprTemplate::Binary {
op: *op,
left: Box::new(left_template),
right: Box::new(right_template),
})
}
}
_ => Some(PreparedSqlGroupedExprTemplate::Static(expr.clone())),
}
}
fn instantiate_symbolic_grouped_expr_template(
template: &PreparedSqlGroupedExprTemplate,
bindings: &[Value],
) -> Result<crate::db::query::plan::expr::Expr, QueryError> {
match template {
PreparedSqlGroupedExprTemplate::Static(expr) => Ok(expr.clone()),
PreparedSqlGroupedExprTemplate::SlotLiteral(index) => {
Ok(crate::db::query::plan::expr::Expr::Literal(
bindings
.get(*index)
.ok_or_else(|| {
QueryError::unsupported_query(format!(
"missing prepared SQL binding at index={index}",
))
})?
.clone(),
))
}
PreparedSqlGroupedExprTemplate::Unary { op, expr } => {
Ok(crate::db::query::plan::expr::Expr::Unary {
op: *op,
expr: Box::new(instantiate_symbolic_grouped_expr_template(expr, bindings)?),
})
}
PreparedSqlGroupedExprTemplate::Binary { op, left, right } => {
Ok(crate::db::query::plan::expr::Expr::Binary {
op: *op,
left: Box::new(instantiate_symbolic_grouped_expr_template(left, bindings)?),
right: Box::new(instantiate_symbolic_grouped_expr_template(right, bindings)?),
})
}
}
}
fn bind_prepared_template_predicate(
predicate: crate::db::predicate::Predicate,
replacements: &[(Value, Value)],
) -> crate::db::predicate::Predicate {
match predicate {
crate::db::predicate::Predicate::True => crate::db::predicate::Predicate::True,
crate::db::predicate::Predicate::False => crate::db::predicate::Predicate::False,
crate::db::predicate::Predicate::And(children) => crate::db::predicate::Predicate::And(
children
.into_iter()
.map(|child| bind_prepared_template_predicate(child, replacements))
.collect(),
),
crate::db::predicate::Predicate::Or(children) => crate::db::predicate::Predicate::Or(
children
.into_iter()
.map(|child| bind_prepared_template_predicate(child, replacements))
.collect(),
),
crate::db::predicate::Predicate::Not(child) => crate::db::predicate::Predicate::Not(
Box::new(bind_prepared_template_predicate(*child, replacements)),
),
crate::db::predicate::Predicate::Compare(compare) => {
crate::db::predicate::Predicate::Compare(
crate::db::predicate::ComparePredicate::with_coercion(
compare.field,
compare.op,
bind_prepared_template_value(compare.value, replacements),
compare.coercion.id,
),
)
}
crate::db::predicate::Predicate::CompareFields(compare) => {
crate::db::predicate::Predicate::CompareFields(compare)
}
crate::db::predicate::Predicate::IsNull { field } => {
crate::db::predicate::Predicate::IsNull { field }
}
crate::db::predicate::Predicate::IsNotNull { field } => {
crate::db::predicate::Predicate::IsNotNull { field }
}
crate::db::predicate::Predicate::IsMissing { field } => {
crate::db::predicate::Predicate::IsMissing { field }
}
crate::db::predicate::Predicate::IsEmpty { field } => {
crate::db::predicate::Predicate::IsEmpty { field }
}
crate::db::predicate::Predicate::IsNotEmpty { field } => {
crate::db::predicate::Predicate::IsNotEmpty { field }
}
crate::db::predicate::Predicate::TextContains { field, value } => {
crate::db::predicate::Predicate::TextContains {
field,
value: bind_prepared_template_value(value, replacements),
}
}
crate::db::predicate::Predicate::TextContainsCi { field, value } => {
crate::db::predicate::Predicate::TextContainsCi {
field,
value: bind_prepared_template_value(value, replacements),
}
}
}
}
fn bind_prepared_template_expr(
expr: crate::db::query::plan::expr::Expr,
replacements: &[(Value, Value)],
) -> crate::db::query::plan::expr::Expr {
match expr {
crate::db::query::plan::expr::Expr::Field(field) => {
crate::db::query::plan::expr::Expr::Field(field)
}
crate::db::query::plan::expr::Expr::Literal(value) => {
crate::db::query::plan::expr::Expr::Literal(bind_prepared_template_value(
value,
replacements,
))
}
crate::db::query::plan::expr::Expr::FunctionCall { function, args } => {
crate::db::query::plan::expr::Expr::FunctionCall {
function,
args: args
.into_iter()
.map(|arg| bind_prepared_template_expr(arg, replacements))
.collect(),
}
}
crate::db::query::plan::expr::Expr::Unary { op, expr } => {
crate::db::query::plan::expr::Expr::Unary {
op,
expr: Box::new(bind_prepared_template_expr(*expr, replacements)),
}
}
crate::db::query::plan::expr::Expr::Binary { op, left, right } => {
crate::db::query::plan::expr::Expr::Binary {
op,
left: Box::new(bind_prepared_template_expr(*left, replacements)),
right: Box::new(bind_prepared_template_expr(*right, replacements)),
}
}
crate::db::query::plan::expr::Expr::Case {
when_then_arms,
else_expr,
} => crate::db::query::plan::expr::Expr::Case {
when_then_arms: when_then_arms
.into_iter()
.map(|arm| {
crate::db::query::plan::expr::CaseWhenArm::new(
bind_prepared_template_expr(arm.condition().clone(), replacements),
bind_prepared_template_expr(arm.result().clone(), replacements),
)
})
.collect(),
else_expr: Box::new(bind_prepared_template_expr(*else_expr, replacements)),
},
crate::db::query::plan::expr::Expr::Aggregate(aggregate) => {
crate::db::query::plan::expr::Expr::Aggregate(bind_prepared_template_aggregate(
aggregate,
replacements,
))
}
#[cfg(test)]
crate::db::query::plan::expr::Expr::Alias { expr, name } => {
crate::db::query::plan::expr::Expr::Alias {
expr: Box::new(bind_prepared_template_expr(*expr, replacements)),
name,
}
}
}
}
fn bind_prepared_template_aggregate(
aggregate: crate::db::query::builder::AggregateExpr,
replacements: &[(Value, Value)],
) -> crate::db::query::builder::AggregateExpr {
let kind = aggregate.kind();
let input_expr = aggregate
.input_expr()
.cloned()
.map(|expr| bind_prepared_template_expr(expr, replacements));
let filter_expr = aggregate
.filter_expr()
.cloned()
.map(|expr| bind_prepared_template_expr(expr, replacements));
let mut rebound = input_expr.map_or_else(
|| match kind {
crate::db::query::plan::AggregateKind::Count => builder::aggregate::count(),
crate::db::query::plan::AggregateKind::Exists => builder::aggregate::exists(),
crate::db::query::plan::AggregateKind::First => builder::aggregate::first(),
crate::db::query::plan::AggregateKind::Last => builder::aggregate::last(),
crate::db::query::plan::AggregateKind::Min => builder::aggregate::min(),
crate::db::query::plan::AggregateKind::Max => builder::aggregate::max(),
crate::db::query::plan::AggregateKind::Sum
| crate::db::query::plan::AggregateKind::Avg => {
unreachable!("SUM/AVG aggregate templates must preserve one input expression")
}
},
|expr| crate::db::query::builder::AggregateExpr::from_expression_input(kind, expr),
);
if let Some(filter_expr) = filter_expr {
rebound = rebound.with_filter_expr(filter_expr);
}
if aggregate.is_distinct() {
rebound = rebound.distinct();
}
rebound
}
fn bind_prepared_template_value(value: Value, replacements: &[(Value, Value)]) -> Value {
replacements
.iter()
.find(|(template, _)| *template == value)
.map_or(value, |(_, bound)| bound.clone())
}
fn validate_parameter_bindings(
contracts: &[PreparedSqlParameterContract],
bindings: &[Value],
) -> Result<(), QueryError> {
if bindings.len() != contracts.len() {
return Err(QueryError::unsupported_query(format!(
"prepared SQL expected {} bindings, found {}",
contracts.len(),
bindings.len(),
)));
}
for contract in contracts {
let binding = bindings.get(contract.index()).ok_or_else(|| {
QueryError::unsupported_query(format!(
"missing prepared SQL binding at index={}",
contract.index(),
))
})?;
if !binding_matches_contract(binding, contract) {
return Err(QueryError::unsupported_query(format!(
"prepared SQL binding at index={} does not match the required {:?} contract",
contract.index(),
contract.type_family(),
)));
}
}
Ok(())
}
const fn binding_matches_contract(value: &Value, contract: &PreparedSqlParameterContract) -> bool {
if matches!(value, Value::Null) {
return contract.null_allowed();
}
match contract.type_family() {
PreparedSqlParameterTypeFamily::Numeric => matches!(
value,
Value::Int(_)
| Value::Int128(_)
| Value::IntBig(_)
| Value::Uint(_)
| Value::Uint128(_)
| Value::UintBig(_)
| Value::Float32(_)
| Value::Float64(_)
| Value::Decimal(_)
| Value::Duration(_)
| Value::Timestamp(_)
),
PreparedSqlParameterTypeFamily::Text => {
matches!(value, Value::Text(_) | Value::Enum(_))
}
PreparedSqlParameterTypeFamily::Bool => matches!(value, Value::Bool(_)),
}
}