forked from 2sys/phonograph
refactor QueryFragment into a dedicated module
This commit is contained in:
parent
daf0782625
commit
849f981243
4 changed files with 178 additions and 183 deletions
|
|
@ -2,147 +2,8 @@ use std::fmt::Display;
|
||||||
|
|
||||||
use phono_backends::escape_identifier;
|
use phono_backends::escape_identifier;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
|
||||||
|
|
||||||
use crate::datum::Datum;
|
use crate::{datum::Datum, query_builders::QueryFragment};
|
||||||
|
|
||||||
/// 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<String>,
|
|
||||||
params: Vec<Datum>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Datum> {
|
|
||||||
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<I: IntoIterator<Item = Self>>(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<I: IntoIterator<Item = Self>>(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<QueryFragment> 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Building block of a syntax tree for a constrained subset of SQL that can be
|
/// 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
|
/// statically analyzed, to validate that user-provided expressions perform only
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ pub mod language;
|
||||||
mod macros;
|
mod macros;
|
||||||
pub mod portal;
|
pub mod portal;
|
||||||
pub mod presentation;
|
pub mod presentation;
|
||||||
|
pub mod query_builders;
|
||||||
pub mod service_cred;
|
pub mod service_cred;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
|
||||||
171
phono-models/src/query_builders.rs
Normal file
171
phono-models/src/query_builders.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
params: Vec<Datum>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<I: IntoIterator<Item = Self>>(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<I: IntoIterator<Item = Self>>(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<QueryFragment> 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<SelectQuery> 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<SelectQuery> for QueryBuilder<'_, Postgres> {
|
||||||
|
fn from(value: SelectQuery) -> Self {
|
||||||
|
QueryFragment::from(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,13 @@ use phono_backends::{
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor, Actor, portal::PortalAccessor},
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
datum::Datum,
|
datum::Datum,
|
||||||
expression::{PgExpressionAny, QueryFragment},
|
expression::PgExpressionAny,
|
||||||
field::Field,
|
field::Field,
|
||||||
|
query_builders::{QueryFragment, SelectQuery},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
Postgres, QueryBuilder,
|
QueryBuilder,
|
||||||
postgres::{PgRow, types::Oid},
|
postgres::{PgRow, types::Oid},
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
@ -107,7 +108,7 @@ pub(super) async fn get(
|
||||||
field_info
|
field_info
|
||||||
};
|
};
|
||||||
|
|
||||||
let main_data_query = GetDataQuery {
|
let main_data_query = SelectQuery {
|
||||||
selection: QueryFragment::from_sql(
|
selection: QueryFragment::from_sql(
|
||||||
&pkey_attrs
|
&pkey_attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -154,7 +155,7 @@ pub(super) async fn get(
|
||||||
limit: QueryFragment::from_param(Datum::Numeric(Some(FRONTEND_ROW_LIMIT.into()))),
|
limit: QueryFragment::from_param(Datum::Numeric(Some(FRONTEND_ROW_LIMIT.into()))),
|
||||||
};
|
};
|
||||||
|
|
||||||
let count_query = GetDataQuery {
|
let count_query = SelectQuery {
|
||||||
selection: QueryFragment::from_sql("count(*)"),
|
selection: QueryFragment::from_sql("count(*)"),
|
||||||
order: QueryFragment::empty(),
|
order: QueryFragment::empty(),
|
||||||
limit: QueryFragment::empty(),
|
limit: QueryFragment::empty(),
|
||||||
|
|
@ -214,42 +215,3 @@ pub(super) async fn get(
|
||||||
})
|
})
|
||||||
.into_response())
|
.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<GetDataQuery> 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<GetDataQuery> for QueryBuilder<'_, Postgres> {
|
|
||||||
fn from(value: GetDataQuery) -> Self {
|
|
||||||
QueryFragment::from(value).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue