diff --git a/phono-models/src/expression.rs b/phono-models/src/expression.rs index b7e9603..7fdcd90 100644 --- a/phono-models/src/expression.rs +++ b/phono-models/src/expression.rs @@ -2,147 +2,8 @@ use std::fmt::Display; use phono_backends::escape_identifier; use serde::{Deserialize, Serialize}; -use sqlx::{Postgres, QueryBuilder}; -use crate::datum::Datum; - -/// Representation of a partial, parameterized SQL query. Allows callers to -/// build queries iteratively and dynamically, handling parameter numbering -/// (`$1`, `$2`, `$3`, ...) automatically. -/// -/// This is similar to [`sqlx::QueryBuilder`], except that [`QueryFragment`] -/// objects are composable and may be concatenated to each other. -#[derive(Clone, Debug, PartialEq)] -pub struct QueryFragment { - /// SQL string, split wherever there is a query parameter. For example, - /// `select * from foo where id = $1 and status = $2` is represented along - /// the lines of `["select * from foo where id = ", " and status = ", ""]`. - /// `plain_sql` should always have exactly one more element than `params`. - plain_sql: Vec, - params: Vec, -} - -impl QueryFragment { - /// Validate invariants. Should be run immediately before returning any - /// useful output. - fn gut_checks(&self) { - assert!(self.plain_sql.len() == self.params.len() + 1); - } - - pub fn to_sql(&self, first_param_idx: usize) -> String { - self.gut_checks(); - self.plain_sql - .iter() - .cloned() - .zip((first_param_idx..).map(|n| format!("${n}"))) - .fold( - Vec::with_capacity(2 * self.plain_sql.len()), - |mut acc, pair| { - acc.extend([pair.0, pair.1]); - acc - }, - ) - .get(0..(2 * self.plain_sql.len() - 1)) - .expect("already asserted sufficient length") - .join("") - } - - /// Returns only the parameterized values, in order. - pub fn to_params(&self) -> Vec { - self.gut_checks(); - self.params.clone() - } - - /// Parse from a SQL string with no parameters. - pub fn from_sql(sql: &str) -> Self { - Self { - plain_sql: vec![sql.to_owned()], - params: vec![], - } - } - - /// Convenience function to construct an empty value. - pub fn empty() -> Self { - Self::from_sql("") - } - - /// Parse from a parameter value with no additional SQL. (Renders as `$n`, - /// where`n` is the appropriate parameter index.) - pub fn from_param(param: Datum) -> Self { - Self { - plain_sql: vec!["".to_owned(), "".to_owned()], - params: vec![param], - } - } - - /// Append another query fragment to this one. - pub fn push(&mut self, mut other: QueryFragment) { - let tail = self - .plain_sql - .pop() - .expect("already asserted that vec contains at least 1 item"); - let head = other - .plain_sql - .first() - .expect("already asserted that vec contains at least 1 item"); - self.plain_sql.push(format!("{tail}{head}")); - for value in other.plain_sql.drain(1..) { - self.plain_sql.push(value); - } - self.params.append(&mut other.params); - } - - /// Combine multiple QueryFragments with a separator, similar to - /// [`Vec::join`]. - pub fn join>(fragments: I, sep: Self) -> Self { - let mut acc = QueryFragment::from_sql(""); - let mut iter = fragments.into_iter(); - let mut fragment = match iter.next() { - Some(value) => value, - None => return acc, - }; - for next_fragment in iter { - acc.push(fragment); - acc.push(sep.clone()); - fragment = next_fragment; - } - acc.push(fragment); - acc - } - - /// Convenience method equivalent to: - /// `QueryFragment::concat(fragments, QueryFragment::from_sql(""))` - pub fn concat>(fragments: I) -> Self { - Self::join(fragments, Self::from_sql("")) - } - - /// Checks whether value is empty. A value is considered empty if the - /// resulting SQL code is 0 characters long. - pub fn is_empty(&self) -> bool { - self.gut_checks(); - self.plain_sql.len() == 1 - && self - .plain_sql - .first() - .expect("already checked that len == 1") - .is_empty() - } -} - -impl From for QueryBuilder<'_, Postgres> { - fn from(value: QueryFragment) -> Self { - value.gut_checks(); - let mut builder = QueryBuilder::new(""); - let mut param_iter = value.params.into_iter(); - for plain_sql in value.plain_sql { - builder.push(plain_sql); - if let Some(param) = param_iter.next() { - param.push_bind_onto(&mut builder); - } - } - builder - } -} +use crate::{datum::Datum, query_builders::QueryFragment}; /// Building block of a syntax tree for a constrained subset of SQL that can be /// statically analyzed, to validate that user-provided expressions perform only diff --git a/phono-models/src/lib.rs b/phono-models/src/lib.rs index 8e347e5..eda304f 100644 --- a/phono-models/src/lib.rs +++ b/phono-models/src/lib.rs @@ -27,6 +27,7 @@ pub mod language; mod macros; pub mod portal; pub mod presentation; +pub mod query_builders; pub mod service_cred; pub mod user; pub mod workspace; diff --git a/phono-models/src/query_builders.rs b/phono-models/src/query_builders.rs new file mode 100644 index 0000000..c2863f5 --- /dev/null +++ b/phono-models/src/query_builders.rs @@ -0,0 +1,171 @@ +//! Assorted utilities for dynamically constructing and manipulating [`sqlx`] +//! queries. + +use sqlx::{Postgres, QueryBuilder}; + +use crate::datum::Datum; + +/// Representation of a partial, parameterized SQL query. Allows callers to +/// build queries iteratively and dynamically, handling parameter numbering +/// (`$1`, `$2`, `$3`, ...) automatically. +/// +/// This is similar to [`sqlx::QueryBuilder`], except that [`QueryFragment`] +/// objects are composable and may be concatenated to each other. +#[derive(Clone, Debug, PartialEq)] +pub struct QueryFragment { + /// SQL string, split wherever there is a query parameter. For example, + /// `select * from foo where id = $1 and status = $2` is represented along + /// the lines of `["select * from foo where id = ", " and status = ", ""]`. + /// `plain_sql` should always have exactly one more element than `params`. + plain_sql: Vec, + params: Vec, +} + +impl QueryFragment { + /// Validate invariants. Should be run immediately before returning any + /// useful output. + fn gut_checks(&self) { + assert!(self.plain_sql.len() == self.params.len() + 1); + } + + /// Parse from a SQL string with no parameters. + pub fn from_sql(sql: &str) -> Self { + Self { + plain_sql: vec![sql.to_owned()], + params: vec![], + } + } + + /// Convenience function to construct an empty value. + pub fn empty() -> Self { + Self::from_sql("") + } + + /// Parse from a parameter value with no additional SQL. (Renders as `$n`, + /// where`n` is the appropriate parameter index.) + pub fn from_param(param: Datum) -> Self { + Self { + plain_sql: vec!["".to_owned(), "".to_owned()], + params: vec![param], + } + } + + /// Append another query fragment to this one. + pub fn push(&mut self, mut other: QueryFragment) { + let tail = self + .plain_sql + .pop() + .expect("already asserted that vec contains at least 1 item"); + let head = other + .plain_sql + .first() + .expect("already asserted that vec contains at least 1 item"); + self.plain_sql.push(format!("{tail}{head}")); + for value in other.plain_sql.drain(1..) { + self.plain_sql.push(value); + } + self.params.append(&mut other.params); + } + + /// Combine multiple QueryFragments with a separator, similar to + /// [`Vec::join`]. + pub fn join>(fragments: I, sep: Self) -> Self { + let mut acc = QueryFragment::from_sql(""); + let mut iter = fragments.into_iter(); + let mut fragment = match iter.next() { + Some(value) => value, + None => return acc, + }; + for next_fragment in iter { + acc.push(fragment); + acc.push(sep.clone()); + fragment = next_fragment; + } + acc.push(fragment); + acc + } + + /// Convenience method equivalent to: + /// `QueryFragment::concat(fragments, QueryFragment::from_sql(""))` + pub fn concat>(fragments: I) -> Self { + Self::join(fragments, Self::from_sql("")) + } + + /// Checks whether value is empty. A value is considered empty if the + /// resulting SQL code is 0 characters long. + pub fn is_empty(&self) -> bool { + self.gut_checks(); + self.plain_sql.len() == 1 + && self + .plain_sql + .first() + .expect("already checked that len == 1") + .is_empty() + } +} + +impl From for QueryBuilder<'_, Postgres> { + fn from(value: QueryFragment) -> Self { + value.gut_checks(); + let mut builder = QueryBuilder::new(""); + let mut param_iter = value.params.into_iter(); + for plain_sql in value.plain_sql { + builder.push(plain_sql); + if let Some(param) = param_iter.next() { + param.push_bind_onto(&mut builder); + } + } + builder + } +} + +/// Helper type to make it easier to build and reason about multiple related SQL +/// queries. +#[derive(Clone, Debug)] +pub struct SelectQuery { + /// Query fragment following (not including) "select ". + pub selection: QueryFragment, + + /// Query fragment following (not including) "from ". + pub source: QueryFragment, + + /// Query fragment following (not including) "where ", or empty if not + /// applicable. + pub filters: QueryFragment, + + /// Query fragment following (not including) "order by ", or empty if not + /// applicable. + pub order: QueryFragment, + + /// Query fragment following (not including) "limit ", or empty if not + /// applicable. + pub limit: QueryFragment, +} + +impl From for QueryFragment { + fn from(value: SelectQuery) -> Self { + let mut result = QueryFragment::from_sql("select "); + result.push(value.selection); + result.push(QueryFragment::from_sql(" from ")); + result.push(value.source); + if !value.filters.is_empty() { + result.push(QueryFragment::from_sql(" where ")); + result.push(value.filters); + } + if !value.order.is_empty() { + result.push(QueryFragment::from_sql(" order by ")); + result.push(value.order); + } + if !value.limit.is_empty() { + result.push(QueryFragment::from_sql(" limit ")); + result.push(value.limit); + } + result + } +} + +impl From for QueryBuilder<'_, Postgres> { + fn from(value: SelectQuery) -> Self { + QueryFragment::from(value).into() + } +} diff --git a/phono-server/src/routes/relations_single/get_data_handler.rs b/phono-server/src/routes/relations_single/get_data_handler.rs index d1bb500..66faa34 100644 --- a/phono-server/src/routes/relations_single/get_data_handler.rs +++ b/phono-server/src/routes/relations_single/get_data_handler.rs @@ -11,12 +11,13 @@ use phono_backends::{ use phono_models::{ accessors::{Accessor, Actor, portal::PortalAccessor}, datum::Datum, - expression::{PgExpressionAny, QueryFragment}, + expression::PgExpressionAny, field::Field, + query_builders::{QueryFragment, SelectQuery}, }; use serde::{Deserialize, Serialize}; use sqlx::{ - Postgres, QueryBuilder, + QueryBuilder, postgres::{PgRow, types::Oid}, }; use tracing::debug; @@ -107,7 +108,7 @@ pub(super) async fn get( field_info }; - let main_data_query = GetDataQuery { + let main_data_query = SelectQuery { selection: QueryFragment::from_sql( &pkey_attrs .iter() @@ -154,7 +155,7 @@ pub(super) async fn get( limit: QueryFragment::from_param(Datum::Numeric(Some(FRONTEND_ROW_LIMIT.into()))), }; - let count_query = GetDataQuery { + let count_query = SelectQuery { selection: QueryFragment::from_sql("count(*)"), order: QueryFragment::empty(), limit: QueryFragment::empty(), @@ -214,42 +215,3 @@ pub(super) async fn get( }) .into_response()) } - -/// Helper type to make it easier to build and reason about multiple related SQL -/// queries. -#[derive(Clone, Debug)] -struct GetDataQuery { - selection: QueryFragment, - source: QueryFragment, - filters: QueryFragment, - order: QueryFragment, - limit: QueryFragment, -} - -impl From for QueryFragment { - fn from(value: GetDataQuery) -> Self { - let mut result = QueryFragment::from_sql("select "); - result.push(value.selection); - result.push(QueryFragment::from_sql(" from ")); - result.push(value.source); - if !value.filters.is_empty() { - result.push(QueryFragment::from_sql(" where ")); - result.push(value.filters); - } - if !value.order.is_empty() { - result.push(QueryFragment::from_sql(" order by ")); - result.push(value.order); - } - if !value.limit.is_empty() { - result.push(QueryFragment::from_sql(" limit ")); - result.push(value.limit); - } - result - } -} - -impl From for QueryBuilder<'_, Postgres> { - fn from(value: GetDataQuery) -> Self { - QueryFragment::from(value).into() - } -}