use crate::error::Result;
use crate::plan::{
CallParams, CallPlan, CoercibleFilter, CoercibleLogicTree, CoercibleOrderTerm,
CoercibleSelectField, MutatePlan, ReadPlan, ReadPlanTree,
};
use postrust_sql::{escape_ident, from_qi, OrderExpr, SelectBuilder, SqlFragment, SqlParam};
pub struct QueryBuilder;
impl QueryBuilder {
pub fn build_read(tree: &ReadPlanTree) -> Result<SqlFragment> {
Self::build_read_plan(&tree.root)
}
fn build_read_plan(plan: &ReadPlan) -> Result<SqlFragment> {
let mut builder = SelectBuilder::new();
let qi = &plan.from;
if let Some(alias) = &plan.from_alias {
builder = builder.from_table_as(
&postrust_sql::identifier::QualifiedIdentifier::new(&qi.schema, &qi.name),
alias,
);
} else {
builder = builder.from_table(&postrust_sql::identifier::QualifiedIdentifier::new(
&qi.schema, &qi.name,
));
}
for field in &plan.select {
let col_frag = Self::build_select_field(field)?;
builder = builder.column_raw(col_frag);
}
for clause in &plan.where_clauses {
let expr = Self::build_logic_tree(clause)?;
builder = builder.where_raw(expr);
}
if plan.select.iter().any(|f| f.aggregate.is_some()) {
for field in plan.select.iter().filter(|f| f.aggregate.is_none()) {
builder = builder.group_by(&field.field.name);
}
}
for term in plan.order.iter().filter(|t| t.relation.is_none()) {
let order = Self::build_order_term(term);
builder = builder.order_by(order);
}
if let Some(limit) = plan.range.limit {
builder = builder.limit(limit);
}
if plan.range.offset > 0 {
builder = builder.offset(plan.range.offset);
}
Ok(builder.build())
}
fn build_select_field(field: &CoercibleSelectField) -> Result<SqlFragment> {
let mut frag = SqlFragment::new();
if field.field.name == "*" && field.aggregate.is_none() && field.cast.is_none() {
frag.push("*");
return Ok(frag);
}
if let Some(agg) = &field.aggregate {
frag.push(agg.to_sql());
frag.push("(");
}
if field.aggregate.is_some() && field.field.name.is_empty() {
frag.push("*)");
if let Some(cast) = &field.aggregate_cast {
frag.push("::");
frag.push(cast);
}
frag.push(" AS ");
frag.push(&escape_ident(field.alias.as_deref().unwrap_or("count")));
return Ok(frag);
}
let render_as_json = field.aggregate.is_none()
&& field.cast.is_none()
&& field.field.transform.is_none()
&& field.field.json_path.is_empty()
&& !decodable_type(&field.field.ir_type);
if render_as_json {
let mut inner = SqlFragment::new();
push_field_ref(&mut inner, &field.field);
frag.push("to_jsonb(");
frag.push(inner.sql());
frag.push(") AS ");
frag.push(&escape_ident(
field.alias.as_deref().unwrap_or(&field.field.name),
));
return Ok(frag);
}
let needs_parens = field.cast.is_some() && !field.field.json_path.is_empty();
if needs_parens {
frag.push("(");
}
match &field.field.transform {
Some(function) => {
frag.push(function);
frag.push("(");
push_field_ref(&mut frag, &field.field);
frag.push(")");
}
None => push_field_ref(&mut frag, &field.field),
}
if needs_parens {
frag.push(")");
}
if let Some(cast) = &field.cast {
frag.push("::");
frag.push(cast);
}
if let Some(agg) = &field.aggregate {
frag.push(")");
if let Some(cast) = &field.aggregate_cast {
frag.push("::");
frag.push(cast);
}
frag.push(" AS ");
frag.push(&escape_ident(
field
.alias
.as_deref()
.unwrap_or(&agg.to_sql().to_lowercase()),
));
return Ok(frag);
}
let implicit_alias = if field.alias.is_some() {
None
} else if field.field.transform.is_some() {
Some(field.field.name.clone())
} else if !field.field.json_path.is_empty() {
Some(json_path_alias(&field.field.name, &field.field.json_path))
} else if field.field.computed.is_some() {
Some(field.field.name.clone())
} else {
None
};
if let Some(alias) = field.alias.as_deref().or(implicit_alias.as_deref()) {
frag.push(" AS ");
frag.push(&escape_ident(alias));
}
Ok(frag)
}
fn build_logic_tree(tree: &CoercibleLogicTree) -> Result<SqlFragment> {
Self::build_logic_tree_in(None, tree)
}
fn build_logic_tree_in(
qualifier: Option<&str>,
tree: &CoercibleLogicTree,
) -> Result<SqlFragment> {
match tree {
CoercibleLogicTree::Expr {
negated,
op,
children,
} => {
let sep = match op {
crate::api_request::LogicOperator::And => " AND ",
crate::api_request::LogicOperator::Or => " OR ",
};
let child_frags: Result<Vec<_>> = children
.iter()
.map(|child| Self::build_logic_tree_in(qualifier, child))
.collect();
let mut combined = SqlFragment::join(sep, child_frags?).parens();
if *negated {
let mut neg = SqlFragment::raw("NOT ");
neg.append(combined);
combined = neg;
}
Ok(combined)
}
CoercibleLogicTree::Stmt(filter) => Self::build_filter_in(qualifier, filter),
CoercibleLogicTree::NullEmbed {
negated,
field_name,
} => {
let mut frag = SqlFragment::new();
frag.push(&escape_ident(field_name));
if *negated {
frag.push(" IS NOT NULL");
} else {
frag.push(" IS NULL");
}
Ok(frag)
}
}
}
pub fn column_sql(field: &crate::plan::CoercibleField) -> String {
Self::qualified_column_sql(None, field)
}
pub fn qualified_column_sql(
qualifier: Option<&str>,
field: &crate::plan::CoercibleField,
) -> String {
let mut frag = SqlFragment::new();
push_qualified_field_ref(&mut frag, qualifier, field);
frag.sql().to_string()
}
pub fn filter_sql(filter: &crate::api_request::Filter, pg_type: &str) -> Result<SqlFragment> {
Self::build_filter(&CoercibleFilter::from_filter(filter, pg_type))
}
pub fn logic_sql<F>(
tree: &crate::api_request::LogicTree,
type_resolver: F,
) -> Result<SqlFragment>
where
F: Fn(&str) -> String + Copy,
{
Self::build_logic_tree(&CoercibleLogicTree::from_logic_tree(tree, type_resolver))
}
fn build_filter(filter: &CoercibleFilter) -> Result<SqlFragment> {
Self::build_filter_in(None, filter)
}
fn build_filter_in(qualifier: Option<&str>, filter: &CoercibleFilter) -> Result<SqlFragment> {
let mut frag = SqlFragment::new();
if filter.op_expr.negated {
frag.push("NOT (");
}
let to_tsvector = match &filter.op_expr.operation {
crate::api_request::Operation::Fts { language, .. }
if !filter.field.ir_type.starts_with("tsvector") =>
{
Some(language.clone())
}
_ => None,
};
if let Some(language) = &to_tsvector {
frag.push("to_tsvector(");
if let Some(language) = language {
frag.push_typed_param(language.clone(), "regconfig");
frag.push(", ");
}
}
push_qualified_field_ref(&mut frag, qualifier, &filter.field);
if to_tsvector.is_some() {
frag.push(")");
}
let cast = if filter.field.json_path.is_empty() {
castable_type(&filter.field.ir_type)
} else {
json_path_result_cast(&filter.field.json_path)
};
let cast = match (&filter.op_expr.operation, filter.field.ir_type.as_str()) {
(
crate::api_request::Operation::Quant {
op:
crate::api_request::QuantOperator::Match
| crate::api_request::QuantOperator::IMatch,
quantifier: None,
..
},
"ltree",
) => Some("lquery"),
_ => cast,
};
let parser = filter.field.transform.as_deref();
let push_value = |frag: &mut SqlFragment, value: String| {
if let Some(parser) = parser {
frag.push(parser);
frag.push("(");
frag.push_param(value);
frag.push(")");
return;
}
match cast {
Some(pg_type) => {
frag.push_typed_param(value, pg_type);
}
None => {
frag.push_param(value);
}
}
};
match &filter.op_expr.operation {
crate::api_request::Operation::Simple { op, value } => {
frag.push(" ");
frag.push(op.to_sql());
frag.push(" ");
push_value(&mut frag, value.clone());
}
crate::api_request::Operation::Quant {
op,
quantifier,
value,
} => {
let value = &match op {
crate::api_request::QuantOperator::Like
| crate::api_request::QuantOperator::ILike => value.replace('*', "%"),
_ => value.clone(),
};
frag.push(" ");
frag.push(op.to_sql());
frag.push(" ");
if let Some(q) = quantifier {
match q {
crate::api_request::OpQuantifier::Any => frag.push("ANY("),
crate::api_request::OpQuantifier::All => frag.push("ALL("),
};
match cast.filter(|t| !t.starts_with('_')) {
Some(pg_type) => {
frag.push_typed_param(value.clone(), &format!("{}[]", pg_type));
}
None => {
frag.push_param(value.clone());
}
}
frag.push(")");
} else {
push_value(&mut frag, value.clone());
}
}
crate::api_request::Operation::In(values) if values.is_empty() => {
frag.push(" = ANY('{}')");
}
crate::api_request::Operation::In(values) => {
frag.push(" IN (");
for (i, v) in values.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
push_value(&mut frag, v.clone());
}
frag.push(")");
}
crate::api_request::Operation::Is(is_val) => {
frag.push(" IS ");
frag.push(is_val.to_sql());
}
crate::api_request::Operation::IsDistinctFrom(value) => {
frag.push(" IS DISTINCT FROM ");
push_value(&mut frag, value.clone());
}
crate::api_request::Operation::Fts {
op,
language,
value,
} => {
frag.push(" @@ ");
frag.push(op.to_function());
frag.push("(");
if let Some(lang) = language {
frag.push_typed_param(lang.clone(), "regconfig");
frag.push(", ");
}
frag.push_param(value.clone());
frag.push(")");
}
}
if filter.op_expr.negated {
frag.push(")");
}
Ok(frag)
}
fn build_order_term(term: &CoercibleOrderTerm) -> OrderExpr {
let mut order = if term.field.json_path.is_empty() && term.field.computed.is_none() {
OrderExpr::new(&term.field.name)
} else {
let mut frag = SqlFragment::new();
push_field_ref(&mut frag, &term.field);
debug_assert!(
frag.params().is_empty(),
"an ORDER BY term bound a parameter, which this discards: {}",
frag.sql()
);
OrderExpr::raw(frag.sql())
};
if let Some(dir) = &term.direction {
order = match dir {
crate::api_request::OrderDirection::Asc => order.asc(),
crate::api_request::OrderDirection::Desc => order.desc(),
};
}
if let Some(nulls) = &term.nulls {
order = match nulls {
crate::api_request::OrderNulls::First => order.nulls_first(),
crate::api_request::OrderNulls::Last => order.nulls_last(),
};
}
order
}
pub fn build_mutate(plan: &MutatePlan) -> Result<SqlFragment> {
match plan {
MutatePlan::Insert {
target,
columns,
body,
on_conflict,
where_clauses,
returning,
reports_inserted,
apply_defaults,
..
} => {
let qi = postrust_sql::identifier::QualifiedIdentifier::new(
&target.schema,
&target.name,
);
let mut frag = SqlFragment::new();
frag.push("INSERT INTO ");
frag.push(&from_qi(&qi));
let object = body
.as_ref()
.map(|body| String::from_utf8_lossy(body).trim_start().starts_with('{'))
.unwrap_or(true);
match (body, columns.is_empty()) {
(Some(body), true) if !object => {
frag.push(" SELECT FROM (SELECT ");
frag.push_param(String::from_utf8_lossy(body).into_owned());
frag.push(
"::json AS json_data) pgrst_payload, LATERAL \
(SELECT FROM json_array_elements(pgrst_payload.json_data)) pgrst_body",
);
}
(_, true) => {
frag.push(" DEFAULT VALUES");
}
(Some(body), false) => {
frag.push(" (");
push_column_list(&mut frag, columns);
frag.push(") SELECT ");
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push("pgrst_body.");
frag.push(&escape_ident(&column.name));
}
frag.push(" ");
push_json_body(&mut frag, columns, body, false, *apply_defaults);
if !where_clauses.is_empty() {
frag.push(" WHERE ");
for (i, clause) in where_clauses.iter().enumerate() {
if i > 0 {
frag.push(" AND ");
}
frag.append(Self::build_logic_tree_in(Some("pgrst_body"), clause)?);
}
}
}
(None, false) => {
frag.push(" DEFAULT VALUES");
}
}
if let Some((resolution, conflict_cols)) = on_conflict {
frag.push(" ON CONFLICT (");
for (i, column) in conflict_cols.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push(&escape_ident(column));
}
frag.push(") DO ");
match resolution {
crate::api_request::PreferResolution::IgnoreDuplicates => {
frag.push("NOTHING");
}
crate::api_request::PreferResolution::MergeDuplicates
if columns.is_empty() =>
{
frag.push("NOTHING");
}
crate::api_request::PreferResolution::MergeDuplicates => {
frag.push("UPDATE SET ");
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push(&escape_ident(&column.name));
frag.push(" = EXCLUDED.");
frag.push(&escape_ident(&column.name));
}
}
}
}
push_returning(&mut frag, &target.name, returning);
if on_conflict.is_some() && *reports_inserted && !returning.is_empty() {
frag.push(", (xmax = 0) AS ");
frag.push(&escape_ident(INSERTED_COLUMN));
}
Ok(frag)
}
MutatePlan::Update {
target,
columns,
body,
where_clauses,
returning,
apply_defaults,
..
} => {
let qi = postrust_sql::identifier::QualifiedIdentifier::new(
&target.schema,
&target.name,
);
if columns.is_empty() || body.is_none() {
let mut frag = SqlFragment::new();
frag.push("SELECT * FROM ");
frag.push(&from_qi(&qi));
frag.push(" WHERE false");
return Ok(frag);
}
let body = body.as_ref().expect("checked above");
let mut frag = SqlFragment::new();
frag.push("UPDATE ");
frag.push(&from_qi(&qi));
frag.push(" SET ");
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push(&escape_ident(&column.name));
frag.push(" = pgrst_body.");
frag.push(&escape_ident(&column.name));
}
frag.push(" ");
push_json_body(&mut frag, columns, body, true, *apply_defaults);
if !where_clauses.is_empty() {
frag.push(" WHERE ");
for (i, clause) in where_clauses.iter().enumerate() {
if i > 0 {
frag.push(" AND ");
}
frag.append(Self::build_logic_tree_in(Some(&target.name), clause)?);
}
}
push_returning(&mut frag, &target.name, returning);
Ok(frag)
}
MutatePlan::Delete {
target,
where_clauses,
returning,
} => {
let qi = postrust_sql::identifier::QualifiedIdentifier::new(
&target.schema,
&target.name,
);
let mut frag = SqlFragment::new();
frag.push("DELETE FROM ");
frag.push(&from_qi(&qi));
if !where_clauses.is_empty() {
frag.push(" WHERE ");
for (i, clause) in where_clauses.iter().enumerate() {
if i > 0 {
frag.push(" AND ");
}
frag.append(Self::build_logic_tree(clause)?);
}
}
push_returning(&mut frag, &target.name, returning);
Ok(frag)
}
}
}
pub fn build_call(plan: &CallPlan, read: Option<&ReadPlanTree>) -> Result<SqlFragment> {
let qi = postrust_sql::identifier::QualifiedIdentifier::new(
&plan.function.schema,
&plan.function.name,
);
let render_as_json = read.is_none()
&& plan.media_type.is_none()
&& !plan.returns_composite
&& plan
.return_type
.as_deref()
.map(|t| !decodable_type(t) || t == "record")
.unwrap_or(false);
let mut frag = SqlFragment::new();
frag.push("SELECT ");
if render_as_json {
frag.push("to_jsonb(pgrst_call.pgrst_scalar) AS ");
frag.push(&escape_ident(&plan.function.name));
frag.push(" FROM (SELECT ");
frag.push(&from_qi(&qi));
frag.push("(");
}
match read.filter(|tree| !tree.root.select.is_empty()) {
Some(tree) => {
for (i, field) in tree.root.select.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
let column = Self::build_select_field(field)?;
frag.append(column);
}
}
None if !render_as_json => push_declared_columns(&mut frag, plan),
None => {}
}
if !render_as_json {
frag.push(" FROM ");
frag.push(&from_qi(&qi));
frag.push("(");
}
let declared_type = |name: &str| {
plan.param_types
.iter()
.find(|(n, _)| n == name)
.map(|(_, t)| t.clone())
};
match &plan.params {
CallParams::Named(params) => {
for (i, (name, value)) in params.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
if plan.variadic_params.iter().any(|v| v == name) {
frag.push("VARIADIC ");
frag.push(&escape_ident(name));
frag.push(" := ");
} else {
frag.push(&escape_ident(name));
frag.push(" => ");
}
match declared_type(name) {
Some(pg_type) => {
frag.push_typed_param(SqlParam::Text(value.clone()), &pg_type)
}
None => frag.push_param(SqlParam::Text(value.clone())),
};
}
}
CallParams::Positional(values) => {
for (i, value) in values.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
match plan.param_types.get(i) {
Some((_, pg_type)) => {
frag.push_typed_param(SqlParam::Text(value.clone()), pg_type)
}
None => frag.push_param(SqlParam::Text(value.clone())),
};
}
}
CallParams::SingleObject(body) => {
let body_str = String::from_utf8_lossy(body);
frag.push_param(SqlParam::Text(body_str.to_string()));
}
CallParams::SingleUnnamed { body, pg_type } => {
match body {
Some(body) => frag.push_typed_param(
SqlParam::Text(String::from_utf8_lossy(body).into_owned()),
pg_type,
),
None => frag.push(&format!("NULL::{}", pg_type)),
};
}
CallParams::None => {}
}
frag.push(")");
if render_as_json {
frag.push(" AS pgrst_scalar) pgrst_call");
}
if let Some(alias) = read.map(|tree| tree.root.from.name.as_str()) {
if !render_as_json && !alias.is_empty() {
frag.push(" AS ");
frag.push(&escape_ident(alias));
}
}
if let Some(tree) = read {
for (i, clause) in tree.root.where_clauses.iter().enumerate() {
frag.push(if i == 0 { " WHERE " } else { " AND " });
let expr = Self::build_logic_tree(clause)?;
frag.append(expr);
}
for (i, term) in tree.root.order.iter().enumerate() {
frag.push(if i == 0 { " ORDER BY " } else { ", " });
let order = Self::build_order_term(term).into_fragment();
frag.append(order);
}
if let Some(limit) = tree.root.range.limit {
frag.push(&format!(" LIMIT {}", limit));
}
if tree.root.range.offset > 0 {
frag.push(&format!(" OFFSET {}", tree.root.range.offset));
}
}
Ok(frag)
}
}
pub const INSERTED_COLUMN: &str = "pgrst_inserted";
fn push_column_list(frag: &mut SqlFragment, columns: &[crate::plan::CoercibleField]) {
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push(&escape_ident(&column.name));
}
}
fn push_json_body(
frag: &mut SqlFragment,
columns: &[crate::plan::CoercibleField],
body: &bytes::Bytes,
single: bool,
apply_defaults: bool,
) {
let body = String::from_utf8_lossy(body).into_owned();
let object = body.trim_start().starts_with('{');
let defaults: Vec<&crate::plan::CoercibleField> = match apply_defaults {
true => columns.iter().filter(|c| c.default.is_some()).collect(),
false => Vec::new(),
};
let merged = !defaults.is_empty();
frag.push("FROM (SELECT ");
frag.push_param(body);
frag.push(match merged {
true => "::jsonb AS json_data) pgrst_payload, ",
false => "::json AS json_data) pgrst_payload, ",
});
if merged {
let mut pairs = String::new();
for (i, column) in defaults.iter().enumerate() {
if i > 0 {
pairs.push_str(", ");
}
pairs.push_str(&format!(
"{}, {}",
postrust_sql::quote_literal(&column.name),
column.default.as_deref().unwrap_or("NULL")
));
}
frag.push(match object {
true => "LATERAL (SELECT jsonb_build_object(",
false => "LATERAL (SELECT jsonb_agg(jsonb_build_object(",
});
frag.push(&pairs);
frag.push(match object {
true => ") || pgrst_payload.json_data AS val) pgrst_defaults, ",
false => {
") || pgrst_element) AS val \
FROM jsonb_array_elements(pgrst_payload.json_data) pgrst_element) \
pgrst_defaults, "
}
});
}
frag.push("LATERAL (SELECT ");
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
match &column.transform {
Some(parser) => {
frag.push(parser);
frag.push("(");
frag.push(&escape_ident(&column.name));
frag.push(") AS ");
frag.push(&escape_ident(&column.name));
}
None => {
frag.push(&escape_ident(&column.name));
}
}
}
frag.push(match (merged, object) {
(false, true) => " FROM json_to_record(pgrst_payload.json_data) AS _(",
(false, false) => " FROM json_to_recordset(pgrst_payload.json_data) AS _(",
(true, true) => " FROM jsonb_to_record(pgrst_defaults.val) AS _(",
(true, false) => " FROM jsonb_to_recordset(pgrst_defaults.val) AS _(",
});
for (i, column) in columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
frag.push(&escape_ident(&column.name));
frag.push(" ");
frag.push(castable_type(&column.ir_type).unwrap_or("text"));
}
frag.push(match single {
true => ") LIMIT 1) pgrst_body",
false => ")) pgrst_body",
});
}
pub const AFFECTED_COLUMN: &str = "pgrst_affected";
pub const PARENT_ROW_COLUMN: &str = "pgrst_parent_row";
fn push_returning(frag: &mut SqlFragment, relation: &str, returning: &[String]) {
if returning.is_empty() {
frag.push(" RETURNING 1 AS ");
frag.push(&escape_ident(AFFECTED_COLUMN));
return;
}
frag.push(" RETURNING ");
for (i, column) in returning.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
if column == PARENT_ROW_COLUMN {
frag.push(&escape_ident(relation));
frag.push(" AS ");
frag.push(&escape_ident(PARENT_ROW_COLUMN));
continue;
}
frag.push(&escape_ident(relation));
frag.push(".");
frag.push(&escape_ident(column));
}
}
fn push_field_ref(frag: &mut SqlFragment, field: &crate::plan::CoercibleField) {
push_qualified_field_ref(frag, None, field)
}
fn push_qualified_field_ref(
frag: &mut SqlFragment,
qualifier: Option<&str>,
field: &crate::plan::CoercibleField,
) {
let column = match &field.computed {
Some(computed) => format!(
"{}.{}({}::{}.{})",
escape_ident(&computed.function.schema),
escape_ident(&computed.function.name),
escape_ident(&computed.relation),
escape_ident(&computed.row_type.schema),
escape_ident(&computed.row_type.name),
),
None => match qualifier {
Some(alias) => format!("{}.{}", escape_ident(alias), escape_ident(&field.name)),
None => escape_ident(&field.name),
},
};
if field.to_json {
frag.push("to_jsonb(");
frag.push(&column);
frag.push(")");
} else {
frag.push(&column);
}
push_json_path(frag, &field.json_path);
}
fn push_declared_columns(frag: &mut SqlFragment, plan: &crate::plan::CallPlan) {
if plan.output_columns.is_empty() {
frag.push("*");
return;
}
for (i, (name, pg_type)) in plan.output_columns.iter().enumerate() {
if i > 0 {
frag.push(", ");
}
match decodable_type(pg_type) {
true => {
frag.push(&escape_ident(name));
}
false => {
frag.push("to_jsonb(");
frag.push(&escape_ident(name));
frag.push(") AS ");
frag.push(&escape_ident(name));
}
}
}
}
fn json_path_result_cast(path: &crate::api_request::JsonPath) -> Option<&'static str> {
use crate::api_request::JsonOperation;
match path.last()? {
JsonOperation::Arrow(_) => Some("jsonb"),
JsonOperation::DoubleArrow(_) => None,
}
}
fn push_json_path(frag: &mut SqlFragment, path: &crate::api_request::JsonPath) {
use crate::api_request::{JsonOperand, JsonOperation};
for operation in path {
let (arrow, operand) = match operation {
JsonOperation::Arrow(operand) => ("->", operand),
JsonOperation::DoubleArrow(operand) => ("->>", operand),
};
frag.push(" ");
frag.push(arrow);
frag.push(" ");
match operand {
JsonOperand::Key(key) => {
frag.push(&format!("'{}'", key.replace('\'', "''")));
}
JsonOperand::Idx(index) => {
frag.push(&index.to_string());
}
}
}
}
fn json_path_alias(column: &str, path: &crate::api_request::JsonPath) -> String {
use crate::api_request::{JsonOperand, JsonOperation};
path.iter()
.rev()
.find_map(|operation| match operation {
JsonOperation::Arrow(JsonOperand::Key(key))
| JsonOperation::DoubleArrow(JsonOperand::Key(key)) => Some(key.clone()),
_ => None,
})
.unwrap_or_else(|| column.to_string())
}
fn decodable_type(data_type: &str) -> bool {
matches!(
data_type,
"" | "smallint"
| "integer"
| "bigint"
| "numeric"
| "decimal"
| "real"
| "double precision"
| "boolean"
| "text"
| "character varying"
| "character"
| "name"
| "date"
| "time"
| "time without time zone"
| "time with time zone"
| "timestamp"
| "timestamp without time zone"
| "timestamp with time zone"
| "uuid"
| "json"
| "jsonb"
)
}
fn castable_type(pg_type: &str) -> Option<&str> {
if pg_type.is_empty() {
return None;
}
let unbounded = match pg_type {
"character" | "bpchar" => Some("varchar"),
"bit" => Some("varbit"),
"character[]" | "bpchar[]" | "_bpchar" => Some("_varchar"),
"bit[]" | "_bit" => Some("_varbit"),
_ => None,
};
if unbounded.is_some() {
return unbounded;
}
let is_bare_name = pg_type
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_');
if is_bare_name {
Some(pg_type)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn castable_type_accepts_bare_type_names() {
assert_eq!(castable_type("int4"), Some("int4"));
assert_eq!(castable_type("timestamptz"), Some("timestamptz"));
assert_eq!(castable_type("_text"), Some("_text"));
}
#[test]
fn castable_type_gives_the_unbounded_spelling_of_a_measured_type() {
assert_eq!(castable_type("character"), Some("varchar"));
assert_eq!(castable_type("bit"), Some("varbit"));
assert_eq!(castable_type("_bpchar"), Some("_varchar"));
}
#[test]
fn castable_type_rejects_unsafe_names() {
assert_eq!(castable_type(""), None);
assert_eq!(castable_type("character varying"), None);
assert_eq!(castable_type("USER-DEFINED"), None);
assert_eq!(castable_type("int4; DROP TABLE users"), None);
}
fn filter_sql_for(query: &str, pg_type: &str) -> (String, Vec<String>) {
let params = crate::api_request::parse_query_params(query, false).unwrap();
let frag = QueryBuilder::filter_sql(¶ms.filters_root[0], pg_type).unwrap();
let bound = frag
.params()
.iter()
.map(|p| format!("{:?}", p))
.collect::<Vec<_>>();
(frag.sql().to_string(), bound)
}
#[test]
fn quantified_comparison_binds_an_array() {
let (sql, params) = filter_sql_for("id=eq(any).{1,2,3}", "int4");
assert!(sql.contains("= ANY("), "got {sql}");
assert!(sql.contains("int4[]"), "got {sql}");
assert_eq!(params.len(), 1);
assert!(params[0].contains("{1,2,3}"), "got {:?}", params[0]);
}
#[test]
fn quantified_all_renders_all() {
let (sql, _) = filter_sql_for("id=lt(all).{4,5}", "int4");
assert!(sql.contains("< ALL("), "got {sql}");
}
#[test]
fn quantified_like_maps_the_wildcard_inside_the_array() {
let (_, params) = filter_sql_for("name=like(any).{foo*,*bar}", "text");
assert!(params[0].contains("{foo%,%bar}"), "got {:?}", params[0]);
}
#[test]
fn array_column_is_not_double_arrayed() {
let (sql, _) = filter_sql_for("tags=eq(any).{a,b}", "_text");
assert!(sql.contains("= ANY("), "got {sql}");
assert!(!sql.contains("_text[]"), "got {sql}");
}
}