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 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<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
|
||||
}
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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::{
|
||||
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<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