use crate::{BoolExpr, Field, FieldRef, IntoRowValues, Meta, Param, Stmt, ValueExpr};
use crate::{Result, raw};
#[derive(Clone, Debug)]
#[must_use]
#[non_exhaustive]
pub enum Source {
#[non_exhaustive]
Table {
name: &'static str,
alias: Option<String>,
fields: &'static [&'static Meta],
},
#[non_exhaustive]
View {
name: &'static str,
alias: Option<String>,
fields: &'static [&'static Meta],
},
#[non_exhaustive]
Cte {
name: String,
alias: Option<String>,
fields: Vec<Meta>,
},
#[non_exhaustive]
Subquery {
stmt: Box<Stmt>,
alias: String,
fields: Vec<Meta>,
},
#[non_exhaustive]
Raw {
sql: String,
alias: String,
params: Vec<Param>,
fields: Vec<Meta>,
},
#[non_exhaustive]
Function {
name: &'static str,
args: Vec<ValueExpr>,
alias: String,
fields: Vec<Meta>,
ordinality: bool,
},
#[non_exhaustive]
Values {
rows: Vec<Vec<ValueExpr>>,
alias: String,
fields: Vec<Meta>,
},
}
#[derive(Clone, Debug)]
#[must_use]
#[non_exhaustive]
pub struct FunctionSource {
pub(crate) name: &'static str,
pub(crate) args: Vec<ValueExpr>,
pub(crate) alias: String,
pub(crate) fields: Vec<Meta>,
pub(crate) ordinality: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CteMaterialization {
Materialized,
NotMaterialized,
}
impl CteMaterialization {
#[inline]
pub const fn as_sql(self) -> &'static str {
match self {
Self::Materialized => "MATERIALIZED",
Self::NotMaterialized => "NOT MATERIALIZED",
}
}
}
#[derive(Clone, Debug)]
#[must_use]
#[non_exhaustive]
pub struct Cte {
pub(crate) name: String,
pub(crate) recursive: bool,
pub(crate) materialization: Option<CteMaterialization>,
pub(crate) stmt: Box<Stmt>,
pub(crate) fields: Vec<Meta>,
}
#[doc(hidden)]
pub trait IntoFieldMetas {
fn into_field_metas(self) -> Vec<Meta>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum JoinKind {
Inner,
Left,
Right,
Full,
Cross,
}
#[derive(Clone, Debug)]
#[must_use]
#[non_exhaustive]
pub(crate) struct Join {
pub(crate) kind: JoinKind,
pub(crate) source: Source,
pub(crate) on: Option<BoolExpr>,
pub(crate) lateral: bool,
}
impl Source {
#[inline]
pub(crate) fn is_table_or_view(&self) -> bool {
matches!(self, Self::Table { .. } | Self::View { .. })
}
}
#[inline]
pub fn table(name: &'static str, fields: &'static [&'static Meta]) -> Source {
Source::Table {
name,
alias: None,
fields,
}
}
#[inline]
pub fn view(name: &'static str, fields: &'static [&'static Meta]) -> Source {
Source::View {
name,
alias: None,
fields,
}
}
pub fn cte(name: impl Into<String>, stmt: impl Into<Stmt>, fields: impl IntoFieldMetas) -> Cte {
Cte::new(name, stmt, fields)
}
pub fn cte_ref(name: impl Into<String>, fields: impl IntoFieldMetas) -> Source {
Source::Cte {
name: name.into(),
alias: None,
fields: fields.into_field_metas(),
}
}
pub fn subquery(
stmt: impl Into<Stmt>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> Source {
Source::Subquery {
stmt: Box::new(stmt.into()),
alias: alias.into(),
fields: fields.into_field_metas(),
}
}
pub fn raw_source(
sql: impl Into<String>,
alias: impl Into<String>,
params: impl Into<Vec<Param>>,
fields: impl IntoFieldMetas,
) -> Source {
Source::Raw {
sql: sql.into(),
alias: alias.into(),
params: params.into(),
fields: fields.into_field_metas(),
}
}
pub fn function_source(
name: &'static str,
args: impl Into<Vec<ValueExpr>>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
FunctionSource {
name,
args: args.into(),
alias: alias.into(),
fields: fields.into_field_metas(),
ordinality: false,
}
}
pub fn generate_series_source(
start: impl Into<ValueExpr>,
stop: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source(
"generate_series",
vec![start.into(), stop.into()],
alias,
fields,
)
}
pub fn generate_series_step_source(
start: impl Into<ValueExpr>,
stop: impl Into<ValueExpr>,
step: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source(
"generate_series",
vec![start.into(), stop.into(), step.into()],
alias,
fields,
)
}
pub fn unnest_source(
array: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("unnest", vec![array.into()], alias, fields)
}
pub fn generate_subscripts_source(
array: impl Into<ValueExpr>,
dim: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source(
"generate_subscripts",
vec![array.into(), dim.into()],
alias,
fields,
)
}
pub fn regexp_split_to_table_source(
text: impl Into<ValueExpr>,
pattern: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source(
"regexp_split_to_table",
vec![text.into(), pattern.into()],
alias,
fields,
)
}
pub fn json_object_keys_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("json_object_keys", vec![value.into()], alias, fields)
}
pub fn jsonb_object_keys_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("jsonb_object_keys", vec![value.into()], alias, fields)
}
pub fn json_each_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("json_each", vec![value.into()], alias, fields)
}
pub fn jsonb_each_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("jsonb_each", vec![value.into()], alias, fields)
}
pub fn json_array_elements_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("json_array_elements", vec![value.into()], alias, fields)
}
pub fn jsonb_array_elements_source(
value: impl Into<ValueExpr>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> FunctionSource {
function_source("jsonb_array_elements", vec![value.into()], alias, fields)
}
pub fn values_source<R>(
rows: impl IntoIterator<Item = R>,
alias: impl Into<String>,
fields: impl IntoFieldMetas,
) -> Source
where
R: IntoRowValues,
{
Source::Values {
rows: rows
.into_iter()
.map(IntoRowValues::into_row_values)
.collect(),
alias: alias.into(),
fields: fields.into_field_metas(),
}
}
impl IntoFieldMetas for () {
fn into_field_metas(self) -> Vec<Meta> {
Vec::new()
}
}
impl IntoFieldMetas for Meta {
fn into_field_metas(self) -> Vec<Meta> {
vec![self]
}
}
impl IntoFieldMetas for &Meta {
fn into_field_metas(self) -> Vec<Meta> {
vec![*self]
}
}
impl<T> IntoFieldMetas for Field<T> {
fn into_field_metas(self) -> Vec<Meta> {
vec![*self.meta]
}
}
impl<T> IntoFieldMetas for &Field<T> {
fn into_field_metas(self) -> Vec<Meta> {
vec![*self.meta]
}
}
impl<T> IntoFieldMetas for FieldRef<T> {
fn into_field_metas(self) -> Vec<Meta> {
vec![*self.meta]
}
}
impl<T> IntoFieldMetas for &FieldRef<T> {
fn into_field_metas(self) -> Vec<Meta> {
vec![*self.meta]
}
}
impl IntoFieldMetas for Vec<Meta> {
fn into_field_metas(self) -> Vec<Meta> {
self
}
}
impl IntoFieldMetas for &[Meta] {
fn into_field_metas(self) -> Vec<Meta> {
self.to_vec()
}
}
impl<const N: usize> IntoFieldMetas for [Meta; N] {
fn into_field_metas(self) -> Vec<Meta> {
self.into_iter().collect()
}
}
impl<const N: usize> IntoFieldMetas for [&Meta; N] {
fn into_field_metas(self) -> Vec<Meta> {
self.into_iter().copied().collect()
}
}
macro_rules! impl_field_meta_tuple {
($($name:ident),+ $(,)?) => {
impl<$($name),+> IntoFieldMetas for ($($name,)+)
where
$($name: IntoFieldMetas,)+
{
#[allow(non_snake_case)]
fn into_field_metas(self) -> Vec<Meta> {
let ($($name,)+) = self;
let mut fields = Vec::new();
$(fields.extend($name.into_field_metas());)+
fields
}
}
};
}
impl_field_meta_tuple!(A, B);
impl_field_meta_tuple!(A, B, C);
impl_field_meta_tuple!(A, B, C, D);
impl_field_meta_tuple!(A, B, C, D, E);
impl_field_meta_tuple!(A, B, C, D, E, F);
impl_field_meta_tuple!(A, B, C, D, E, F, G);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
impl_field_meta_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
impl JoinKind {
#[inline]
pub const fn as_sql(self) -> &'static str {
match self {
Self::Inner => "JOIN",
Self::Left => "LEFT JOIN",
Self::Right => "RIGHT JOIN",
Self::Full => "FULL JOIN",
Self::Cross => "CROSS JOIN",
}
}
#[inline]
pub const fn requires_condition(self) -> bool {
!matches!(self, Self::Cross)
}
}
impl Join {
pub(crate) fn new(kind: JoinKind, source: impl Into<Source>, on: BoolExpr) -> Self {
Self {
kind,
source: source.into(),
on: Some(on),
lateral: false,
}
}
pub(crate) fn lateral(kind: JoinKind, source: impl Into<Source>, on: BoolExpr) -> Self {
Self {
kind,
source: source.into(),
on: Some(on),
lateral: true,
}
}
pub(crate) fn cross(source: impl Into<Source>) -> Self {
Self {
kind: JoinKind::Cross,
source: source.into(),
on: None,
lateral: false,
}
}
pub(crate) fn cross_lateral(source: impl Into<Source>) -> Self {
Self {
kind: JoinKind::Cross,
source: source.into(),
on: None,
lateral: true,
}
}
pub(crate) fn validate(&self) -> Result<()> {
self.source.validate()?;
match (&self.on, self.kind.requires_condition()) {
(Some(on), _) => on.validate(),
(None, false) => Ok(()),
(None, true) => Err(crate::Error::MissingJoinCondition {
join: self.kind.as_sql(),
}),
}
}
}
impl Cte {
pub fn new(
name: impl Into<String>,
stmt: impl Into<Stmt>,
fields: impl IntoFieldMetas,
) -> Self {
Self {
name: name.into(),
recursive: false,
materialization: None,
stmt: Box::new(stmt.into()),
fields: fields.into_field_metas(),
}
}
#[inline]
pub fn recursive(mut self) -> Self {
self.recursive = true;
self
}
#[inline]
pub fn materialized(mut self) -> Self {
self.materialization = Some(CteMaterialization::Materialized);
self
}
#[inline]
pub fn not_materialized(mut self) -> Self {
self.materialization = Some(CteMaterialization::NotMaterialized);
self
}
#[inline]
pub fn source(&self) -> Source {
cte_ref(self.name.clone(), self.fields.clone())
}
pub(crate) fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(crate::Error::invalid_cte_shape(
self.name.clone(),
"CTE name cannot be empty",
));
}
if let Some(count) = self.stmt.projection_count()
&& !self.fields.is_empty()
&& count != self.fields.len()
{
return Err(crate::Error::invalid_cte_shape(
self.name.clone(),
"field count must match SELECT projection count",
));
}
self.stmt.validate()
}
}
impl From<&Cte> for Source {
fn from(cte: &Cte) -> Self {
cte.source()
}
}
impl FunctionSource {
#[inline]
pub fn with_ordinality(mut self) -> Self {
self.ordinality = true;
self
}
}
impl From<FunctionSource> for Source {
fn from(source: FunctionSource) -> Self {
Self::Function {
name: source.name,
args: source.args,
alias: source.alias,
fields: source.fields,
ordinality: source.ordinality,
}
}
}
impl Source {
#[inline]
pub const fn kind(&self) -> &'static str {
match self {
Self::Table { .. } => "table",
Self::View { .. } => "view",
Self::Cte { .. } => "cte",
Self::Subquery { .. } => "subquery",
Self::Raw { .. } => "raw",
Self::Function { .. } => "function",
Self::Values { .. } => "values",
}
}
pub fn alias(mut self, alias: impl Into<String>) -> Self {
let alias = alias.into();
match &mut self {
Self::Table { alias: current, .. }
| Self::View { alias: current, .. }
| Self::Cte { alias: current, .. } => *current = Some(alias),
Self::Subquery { alias: current, .. }
| Self::Raw { alias: current, .. }
| Self::Function { alias: current, .. }
| Self::Values { alias: current, .. } => *current = alias,
}
self
}
pub(crate) fn explicit_alias(&self) -> Option<&str> {
match self {
Self::Table { alias, .. } | Self::View { alias, .. } | Self::Cte { alias, .. } => {
alias.as_deref()
}
Self::Subquery { alias, .. }
| Self::Raw { alias, .. }
| Self::Function { alias, .. }
| Self::Values { alias, .. } => Some(alias),
}
}
pub(crate) fn for_each_field(&self, mut f: impl FnMut(&Meta)) {
match self {
Self::Table { fields, .. } | Self::View { fields, .. } => {
for field in fields.iter().copied() {
f(field);
}
}
Self::Cte { fields, .. }
| Self::Subquery { fields, .. }
| Self::Raw { fields, .. }
| Self::Function { fields, .. }
| Self::Values { fields, .. } => fields.iter().for_each(f),
}
}
pub(crate) fn field_by_api(&self, api: &str) -> Option<Meta> {
match self {
Self::Table { fields, .. } | Self::View { fields, .. } => fields
.iter()
.copied()
.find(|field| field.api == api)
.copied(),
Self::Cte { fields, .. }
| Self::Subquery { fields, .. }
| Self::Raw { fields, .. }
| Self::Function { fields, .. }
| Self::Values { fields, .. } => fields.iter().find(|field| field.api == api).copied(),
}
}
pub fn validate(&self) -> Result<()> {
match self {
Self::Subquery {
stmt,
alias,
fields,
} => {
stmt.validate_query_statement(
"subquery source must be SELECT, set, or raw statement",
)?;
if let Some(count) = stmt.projection_count()
&& !fields.is_empty()
&& count != fields.len()
{
return Err(crate::Error::InvalidSelectShape {
message: "subquery field count must match SELECT projection count",
});
}
if alias.is_empty() {
return Err(crate::Error::InvalidSelectShape {
message: "subquery alias cannot be empty",
});
}
Ok(())
}
Self::Raw {
sql, alias, params, ..
} => {
validate_source_alias(alias, "raw source alias cannot be empty")?;
raw::validate_bind_count(sql, params.len())
}
Self::Function { args, alias, .. } => {
validate_source_alias(alias, "function source alias cannot be empty")?;
for arg in args {
arg.validate()?;
}
Ok(())
}
Self::Values {
rows,
alias,
fields,
} => {
validate_source_alias(alias, "values source alias cannot be empty")?;
if rows.is_empty() {
return Err(crate::Error::InvalidSelectShape {
message: "values source requires at least one row",
});
}
if fields.is_empty() {
return Err(crate::Error::InvalidSelectShape {
message: "values source fields cannot be empty",
});
}
for row in rows {
if row.len() != fields.len() {
return Err(crate::Error::InvalidSelectShape {
message: "values source row arity must match exposed field count",
});
}
for value in row {
value.validate()?;
}
}
Ok(())
}
Self::Cte { name, alias, .. } => {
if name.is_empty() {
return Err(crate::Error::InvalidSelectShape {
message: "CTE source name cannot be empty",
});
}
validate_optional_source_alias(alias.as_deref(), "source alias cannot be empty")
}
Self::Table { alias, .. } | Self::View { alias, .. } => {
validate_optional_source_alias(alias.as_deref(), "source alias cannot be empty")
}
}
}
}
fn validate_optional_source_alias(alias: Option<&str>, message: &'static str) -> Result<()> {
if matches!(alias, Some("")) {
return Err(crate::Error::InvalidSelectShape { message });
}
Ok(())
}
fn validate_source_alias(alias: &str, message: &'static str) -> Result<()> {
if alias.is_empty() {
return Err(crate::Error::InvalidSelectShape { message });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
CteMaterialization, IntoFieldMetas, Source, cte, cte_ref, function_source,
generate_series_step_source, raw_source, table, values_source,
};
use crate::{Field, Meta, OpSet, Param, select};
static ID_META: Meta = Meta::new("id", "id", "int4").ops(OpSet::ordered());
static FIELDS: [&Meta; 1] = [&ID_META];
const ID: Field<i32> = Field::new(&ID_META);
#[test]
fn into_field_metas_accepts_fields_metadata_and_tuples() {
assert_eq!(().into_field_metas(), Vec::<Meta>::new());
assert_eq!(ID.into_field_metas(), vec![ID_META]);
assert_eq!((&ID_META).into_field_metas(), vec![ID_META]);
assert_eq!((ID, &ID_META).into_field_metas(), vec![ID_META, ID_META]);
assert_eq!(
(
ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID, ID,
)
.into_field_metas()
.len(),
16
);
}
#[test]
fn table_and_view_aliases_are_optional_but_raw_sources_are_always_aliased() {
let table = table("public.users", &FIELDS);
assert_eq!(table.explicit_alias(), None);
assert!(
matches!(table.alias("u"), Source::Table { alias: Some(alias), .. } if alias == "u")
);
let raw = raw_source(
"select ? as id",
"generated",
[Param::typed(1_i32)],
vec![ID_META],
);
assert_eq!(raw.explicit_alias(), Some("generated"));
}
#[test]
fn cte_ref_snapshots_name_and_fields() {
let cte = cte(
"ids",
select(table("public.users", &FIELDS)).column(ID),
vec![ID_META],
);
let source = Source::from(&cte).alias("i");
assert!(matches!(
source,
Source::Cte {
name,
alias: Some(alias),
fields,
} if name == "ids" && alias == "i" && fields == vec![ID_META]
));
}
#[test]
fn cte_validates_field_count_against_select_projection() {
let cte = cte(
"bad_ids",
select(table("public.users", &FIELDS)).column(ID),
vec![ID_META, ID_META],
);
assert!(matches!(
cte.validate().unwrap_err(),
crate::Error::InvalidCteShape(err) if err.name == "bad_ids" && err.message == "field count must match SELECT projection count"
));
}
#[test]
fn subquery_validates_field_count_against_select_projection() {
let source = super::subquery(
select(table("public.users", &FIELDS)).column(ID),
"ids",
vec![ID_META, ID_META],
);
assert!(matches!(
source.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "subquery field count must match SELECT projection count"
));
}
#[test]
fn raw_source_validates_bind_count() {
let source = raw_source(
"select ? as id",
"generated",
Vec::<Param>::new(),
vec![ID_META],
);
assert!(matches!(
source.validate().unwrap_err(),
crate::Error::RawBindMismatch {
placeholders: 1,
binds: 0
}
));
}
#[test]
fn raw_and_function_sources_require_aliases() {
let table = table("public.users", &FIELDS).alias("");
assert!(matches!(
table.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "source alias cannot be empty"
));
let cte = cte_ref("ids", vec![ID_META]).alias("");
assert!(matches!(
cte.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "source alias cannot be empty"
));
let raw = raw_source("select 1 as id", "", Vec::<Param>::new(), vec![ID_META]);
assert!(matches!(
raw.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "raw source alias cannot be empty"
));
let function: Source =
function_source("generate_series", Vec::new(), "", vec![ID_META]).into();
assert!(matches!(
function.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "function source alias cannot be empty"
));
let values = values_source([[1_i32]], "", ID);
assert!(matches!(
values.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "values source alias cannot be empty"
));
}
#[test]
fn function_source_validates_arguments_and_ordinality_is_opt_in() {
let source = generate_series_step_source(1_i32, 3_i32, 1_i32, "g", ID);
assert!(!source.ordinality);
let with_ordinality = source.with_ordinality();
assert!(with_ordinality.ordinality);
let source: Source = with_ordinality.into();
assert!(matches!(
source,
Source::Function {
ordinality: true,
..
}
));
}
#[test]
fn values_source_validates_rows_and_exposed_fields() {
let valid = values_source([[1_i32], [2_i32]], "input", ID);
valid.validate().unwrap();
let empty_rows = values_source(Vec::<[i32; 1]>::new(), "input", ID);
assert!(matches!(
empty_rows.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "values source requires at least one row"
));
let empty_fields = values_source([[1_i32]], "input", ());
assert!(matches!(
empty_fields.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "values source fields cannot be empty"
));
let wrong_arity = values_source([(1_i32, 2_i32)], "input", ID);
assert!(matches!(
wrong_arity.validate().unwrap_err(),
crate::Error::InvalidSelectShape { message }
if message == "values source row arity must match exposed field count"
));
}
#[test]
fn cte_materialization_renders_known_postgres_tokens() {
assert_eq!(CteMaterialization::Materialized.as_sql(), "MATERIALIZED");
assert_eq!(
CteMaterialization::NotMaterialized.as_sql(),
"NOT MATERIALIZED"
);
let source = cte_ref("ids", vec![ID_META]);
assert_eq!(source.kind(), "cte");
}
}