use super::types::*;
use crate::api_request::{ApiRequest, JoinType, QualifiedIdentifier, Range, SelectItem};
use crate::error::{Error, Result};
use crate::schema_cache::{Relationship, SchemaCache, Table};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ReadPlan {
pub select: Vec<CoercibleSelectField>,
pub from: QualifiedIdentifier,
pub from_alias: Option<String>,
pub where_clauses: Vec<CoercibleLogicTree>,
pub order: Vec<CoercibleOrderTerm>,
pub range: Range,
pub rel_name: String,
pub rel_to_parent: Option<Relationship>,
pub rel_join_conds: Vec<JoinCondition>,
pub rel_join_type: Option<JoinType>,
pub rel_select: Vec<RelSelectField>,
pub depth: u32,
}
impl ReadPlan {
pub fn from_request(
request: &ApiRequest,
table: &Table,
schema_cache: &SchemaCache,
) -> Result<Self> {
let qi = table.qualified_identifier();
let mut select = build_select_fields(&request.query_params.select, table)?;
for field in select.iter_mut() {
if field.field.transform.is_some() || field.aggregate.is_some() {
continue;
}
let Some(column) = table.get_column(&field.field.name) else {
continue;
};
if let Some(function) =
schema_cache.representation(column.representation_type(), "json")
{
field.field.transform = Some(function.to_string());
}
}
if request.preferences.timezone.is_some() {
for field in select.iter_mut() {
if field.cast.is_none()
&& field.aggregate.is_none()
&& field.field.json_path.is_empty()
&& matches!(
field.field.ir_type.as_str(),
"timestamptz"
| "timetz"
| "timestamp with time zone"
| "time with time zone"
)
{
field.field.transform = Some("to_jsonb".to_string());
}
}
}
let mut where_clauses = build_where_clauses(request, table)?;
for clause in where_clauses.iter_mut() {
attach_representation_tree(clause, table, schema_cache);
}
let order = build_order_terms(request, table)?;
let rel_select = build_relation_selects(&request.query_params.select, table, schema_cache)?;
Ok(Self {
select,
from: qi,
from_alias: None,
where_clauses,
order,
range: resolve_top_level_range(request),
rel_name: table.name.clone(),
rel_to_parent: None,
rel_join_conds: vec![],
rel_join_type: None,
rel_select,
depth: 0,
})
}
pub fn for_mutation(
request: &ApiRequest,
table: &Table,
schema_cache: &SchemaCache,
) -> Result<Self> {
let mut plan = Self::from_request(request, table, schema_cache)?;
plan.from = QualifiedIdentifier::unqualified(crate::query::MUTATION_RESULT);
plan.from_alias = None;
plan.where_clauses.clear();
for field in plan.select.iter_mut() {
if let Some(computed) = field.field.computed.as_mut() {
computed.relation = crate::query::MUTATION_RESULT.to_string();
}
}
Ok(plan)
}
pub fn has_where(&self) -> bool {
!self.where_clauses.is_empty()
}
pub fn has_order(&self) -> bool {
!self.order.is_empty()
}
pub fn has_pagination(&self) -> bool {
self.range.limit.is_some() || self.range.offset > 0
}
}
fn build_select_fields(items: &[SelectItem], table: &Table) -> Result<Vec<CoercibleSelectField>> {
if items.is_empty() {
return Ok(table
.columns
.iter()
.map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type))
.collect());
}
let mut fields = Vec::new();
for item in items {
match item {
SelectItem::Field { field, .. } if field.name == "*" => {
fields.extend(
table
.columns
.iter()
.map(|(name, col)| CoercibleSelectField::simple(name, &col.data_type)),
);
}
SelectItem::Field {
field,
aggregate,
aggregate_cast,
cast,
alias,
} => {
let legacy_count = aggregate.is_none()
&& field.name == "count"
&& field.json_path.is_empty()
&& table.get_column("count").is_none();
let computed = match table.get_column(&field.name) {
Some(_) => None,
None => table
.get_computed_column(&field.name)
.filter(|c| !c.takes_arguments),
};
let pg_type = if field.name.is_empty() || legacy_count {
String::new()
} else if let Some(computed) = computed {
computed.data_type.clone()
} else {
table
.get_column(&field.name)
.ok_or_else(|| Error::ColumnNotFound(field.name.clone()))?
.data_type
.clone()
};
let (field, aggregate) = if legacy_count {
(
&crate::api_request::Field::simple(""),
&Some(crate::api_request::AggregateFunction::Count),
)
} else {
(field, aggregate)
};
let mut resolved = CoercibleField::from_field(field, &pg_type);
resolved.computed = computed.map(|computed| crate::plan::ComputedRef {
function: computed.function.clone(),
relation: table.name.clone(),
row_type: table.qualified_identifier(),
});
fields.push(CoercibleSelectField {
field: resolved,
aggregate: aggregate.clone(),
aggregate_cast: aggregate_cast.clone(),
cast: cast.clone(),
alias: alias.clone(),
});
}
SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
}
}
Ok(fields)
}
pub fn resolve_top_level_range(request: &ApiRequest) -> Range {
let mut range = request.top_level_range.clone();
if let Some(from_params) = request.query_params.ranges.get("") {
if from_params.limit.is_some() {
range.limit = from_params.limit;
}
if from_params.offset_explicit {
range.offset = from_params.offset;
}
}
if let Some(max_rows) = request.max_rows {
range.limit = Some(match range.limit {
Some(requested) => requested.min(max_rows),
None => max_rows,
});
}
range
}
fn build_where_clauses(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleLogicTree>> {
let type_resolver = |name: &str| -> String {
table
.get_column(name)
.map(|c| c.nominal_type.clone())
.unwrap_or_else(|| "text".to_string())
};
let embedded: std::collections::HashSet<&str> = request
.query_params
.select
.iter()
.filter_map(|item| match item {
SelectItem::Relation {
relation, alias, ..
} => Some(alias.as_deref().unwrap_or(relation)),
SelectItem::SpreadRelation { relation, .. } => Some(relation.as_str()),
SelectItem::Field { .. } => None,
})
.filter(|name| table.get_column(name).is_none())
.collect();
let mut clauses = Vec::new();
for filter in &request.query_params.filters_root {
if embedded.contains(filter.field.name.as_str()) {
continue;
}
let pg_type = type_resolver(&filter.field.name);
clauses.push(CoercibleLogicTree::Stmt(CoercibleFilter::from_filter(
filter, &pg_type,
)));
}
let embeds = crate::api_request::embedded_names(&request.query_params.select);
for (path, tree) in &request.query_params.logic {
if path.is_empty() && !tree.names_only(&embeds) {
clauses.push(CoercibleLogicTree::from_logic_tree(tree, type_resolver));
}
}
for clause in clauses.iter_mut() {
attach_computed_tree(clause, table);
}
Ok(clauses)
}
fn attach_representation(field: &mut CoercibleField, table: &Table, schema_cache: &SchemaCache) {
let Some(column) = table.get_column(&field.name) else {
return;
};
if let Some(function) = schema_cache.representation("text", column.representation_type()) {
field.transform = Some(function.to_string());
}
}
fn attach_representation_tree(
tree: &mut CoercibleLogicTree,
table: &Table,
schema_cache: &SchemaCache,
) {
match tree {
CoercibleLogicTree::Expr { children, .. } => {
for child in children {
attach_representation_tree(child, table, schema_cache);
}
}
CoercibleLogicTree::Stmt(filter) => {
attach_representation(&mut filter.field, table, schema_cache)
}
CoercibleLogicTree::NullEmbed { .. } => {}
}
}
fn attach_computed(field: &mut CoercibleField, table: &Table) {
if table.get_column(&field.name).is_some() {
return;
}
if let Some(computed) = table
.get_computed_column(&field.name)
.filter(|c| !c.takes_arguments)
{
field.ir_type = computed.data_type.clone();
field.base_type = computed.data_type.clone();
field.computed = Some(crate::plan::ComputedRef {
function: computed.function.clone(),
relation: table.name.clone(),
row_type: table.qualified_identifier(),
});
}
}
fn attach_computed_tree(tree: &mut CoercibleLogicTree, table: &Table) {
match tree {
CoercibleLogicTree::Expr { children, .. } => {
for child in children {
attach_computed_tree(child, table);
}
}
CoercibleLogicTree::Stmt(filter) => attach_computed(&mut filter.field, table),
CoercibleLogicTree::NullEmbed { .. } => {}
}
}
fn build_order_terms(request: &ApiRequest, table: &Table) -> Result<Vec<CoercibleOrderTerm>> {
let mut terms = Vec::new();
for (path, order_terms) in &request.query_params.order {
if path.is_empty() {
for term in order_terms {
let field_name = match term {
crate::api_request::OrderTerm::Field { field, .. } => &field.name,
crate::api_request::OrderTerm::Relation { field, .. } => &field.name,
};
let pg_type = table
.get_column(field_name)
.map(|c| c.data_type.as_str())
.unwrap_or("text");
let mut resolved = CoercibleOrderTerm::from_order_term(term, pg_type);
attach_computed(&mut resolved.field, table);
terms.push(resolved);
}
}
}
Ok(terms)
}
fn build_relation_selects(
items: &[SelectItem],
table: &Table,
schema_cache: &SchemaCache,
) -> Result<Vec<RelSelectField>> {
let mut rel_selects = Vec::new();
for item in items {
match item {
SelectItem::Relation {
relation,
alias,
hint,
join_type,
select: _,
} => {
let _rel = schema_cache
.find_relationship(
&table.qualified_identifier(),
relation,
hint.as_deref(),
&table.schema,
)?
.ok_or_else(|| {
schema_cache.relationship_not_found(
&table.qualified_identifier(),
relation,
hint.as_deref(),
&table.schema,
)
})?;
rel_selects.push(RelSelectField {
name: relation.clone(),
agg_alias: alias
.clone()
.unwrap_or_else(|| format!("pgrst_{}", relation)),
join_type: join_type.clone().unwrap_or_default(),
is_spread: false,
});
}
SelectItem::SpreadRelation {
relation,
hint,
join_type,
select: _,
} => {
let _rel = schema_cache
.find_relationship(
&table.qualified_identifier(),
relation,
hint.as_deref(),
&table.schema,
)?
.ok_or_else(|| {
schema_cache.relationship_not_found(
&table.qualified_identifier(),
relation,
hint.as_deref(),
&table.schema,
)
})?;
rel_selects.push(RelSelectField {
name: relation.clone(),
agg_alias: format!("pgrst_spread_{}", relation),
join_type: join_type.clone().unwrap_or_default(),
is_spread: true,
});
}
_ => {}
}
}
Ok(rel_selects)
}
#[derive(Clone, Debug)]
pub struct ReadPlanTree {
pub root: ReadPlan,
pub children: Vec<ReadPlanTree>,
}
impl ReadPlanTree {
pub fn empty() -> Self {
Self {
root: ReadPlan {
select: vec![],
from: QualifiedIdentifier::unqualified(""),
from_alias: None,
where_clauses: vec![],
order: vec![],
range: Range::default(),
rel_name: String::new(),
rel_to_parent: None,
rel_join_conds: vec![],
rel_join_type: None,
rel_select: vec![],
depth: 0,
},
children: vec![],
}
}
pub fn leaf(plan: ReadPlan) -> Self {
Self {
root: plan,
children: vec![],
}
}
pub fn add_child(&mut self, child: ReadPlanTree) {
self.children.push(child);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_plan_tree_empty() {
let tree = ReadPlanTree::empty();
assert!(tree.root.select.is_empty());
assert!(tree.children.is_empty());
}
}