diff --git a/Cargo.toml b/Cargo.toml index 0421a32e274..57eb13c4698 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,9 +33,9 @@ axum = "0.7.5" chrono = "0.4.38" clap = { version = "4.5.4", features = ["derive", "env"] } derivative = "2.2.0" -diesel = { version = "2.2.4", features = ["postgres", "serde_json", "numeric", "r2d2", "chrono", "uuid"] } +diesel = { version = "2.2.4", features = ["postgres", "serde_json", "numeric", "r2d2", "chrono", "uuid", "i-implement-a-third-party-backend-and-opt-into-breaking-changes"] } diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } -diesel-dynamic-schema = "0.2.1" +diesel-dynamic-schema = { version = "0.2.1", features = ["postgres"] } diesel_derives = "2.1.4" diesel_migrations = "2.1.0" graph = { path = "./graph" } diff --git a/graph/src/data/store/scalar/bigdecimal.rs b/graph/src/data/store/scalar/bigdecimal.rs index 27af887851f..b8b62f573fb 100644 --- a/graph/src/data/store/scalar/bigdecimal.rs +++ b/graph/src/data/store/scalar/bigdecimal.rs @@ -1,6 +1,6 @@ use diesel::deserialize::FromSqlRow; use diesel::expression::AsExpression; -use num_bigint; +use num_bigint::{self, ToBigInt}; use num_traits::FromPrimitive; use serde::{self, Deserialize, Serialize}; use stable_hash::{FieldAddress, StableHash}; @@ -10,8 +10,8 @@ use std::fmt::{self, Display, Formatter}; use std::ops::{Add, Div, Mul, Sub}; use std::str::FromStr; +use crate::anyhow::anyhow; use crate::runtime::gas::{Gas, GasSizeOf}; - use old_bigdecimal::BigDecimal as OldBigDecimal; pub use old_bigdecimal::ToPrimitive; @@ -60,6 +60,26 @@ impl BigDecimal { self.0.as_bigint_and_exponent() } + pub fn is_integer(&self) -> bool { + self.0.is_integer() + } + + /// Convert this `BigDecimal` to a `BigInt` if it is an integer, and + /// return an error if it is not. Also return an error if the integer + /// would use too many digits as definied by `BigInt::new` + pub fn to_bigint(&self) -> Result { + if !self.is_integer() { + return Err(anyhow!( + "Cannot convert non-integer `BigDecimal` to `BigInt`: {:?}", + self + )); + } + let bi = self.0.to_bigint().ok_or_else(|| { + anyhow!("The implementation of `to_bigint` for `OldBigDecimal` always returns `Some`") + })?; + BigInt::new(bi) + } + pub fn digits(&self) -> u64 { self.0.digits() } diff --git a/graph/src/data/store/scalar/bytes.rs b/graph/src/data/store/scalar/bytes.rs index dd76cb29589..585b548f931 100644 --- a/graph/src/data/store/scalar/bytes.rs +++ b/graph/src/data/store/scalar/bytes.rs @@ -1,3 +1,5 @@ +use diesel::deserialize::FromSql; +use diesel::pg::PgValue; use diesel::serialize::ToSql; use hex; use serde::{self, Deserialize, Serialize}; @@ -115,3 +117,9 @@ impl ToSql for Bytes { <_ as ToSql>::to_sql(self.as_slice(), &mut out.reborrow()) } } + +impl FromSql for Bytes { + fn from_sql(value: PgValue) -> diesel::deserialize::Result { + as FromSql>::from_sql(value).map(Bytes::from) + } +} diff --git a/graph/src/data/store/scalar/timestamp.rs b/graph/src/data/store/scalar/timestamp.rs index 13d71f354a6..0bbf72e36e5 100644 --- a/graph/src/data/store/scalar/timestamp.rs +++ b/graph/src/data/store/scalar/timestamp.rs @@ -1,4 +1,6 @@ use chrono::{DateTime, Utc}; +use diesel::deserialize::FromSql; +use diesel::pg::PgValue; use diesel::serialize::ToSql; use serde::{self, Deserialize, Serialize}; use stable_hash::StableHash; @@ -107,3 +109,10 @@ impl GasSizeOf for Timestamp { Some(Gas::new(std::mem::size_of::().saturating_into())) } } + +impl FromSql for Timestamp { + fn from_sql(value: PgValue) -> diesel::deserialize::Result { + as FromSql>::from_sql(value) + .map(Timestamp) + } +} diff --git a/graph/src/data/value.rs b/graph/src/data/value.rs index b4ede7540a4..af2629a1f18 100644 --- a/graph/src/data/value.rs +++ b/graph/src/data/value.rs @@ -115,6 +115,24 @@ impl PartialEq<&str> for Word { } } +impl PartialEq for Word { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq for Word { + fn eq(&self, other: &String) -> bool { + self.as_str() == other + } +} + +impl PartialEq for String { + fn eq(&self, other: &Word) -> bool { + self.as_str() == other.as_str() + } +} + impl PartialEq for &str { fn eq(&self, other: &Word) -> bool { self == &other.as_str() diff --git a/graph/src/data_source/causality_region.rs b/graph/src/data_source/causality_region.rs index bc8fc89cef2..489247c1b9b 100644 --- a/graph/src/data_source/causality_region.rs +++ b/graph/src/data_source/causality_region.rs @@ -4,6 +4,7 @@ use diesel::{ serialize::{Output, ToSql}, sql_types::Integer, }; +use diesel_derives::AsExpression; use std::fmt; use crate::components::subgraph::Entity; @@ -20,7 +21,10 @@ use crate::derive::CacheWeight; /// This necessary for determinism because offchain data sources don't have a deterministic order of /// execution, for example an IPFS file may become available at any point in time. The isolation /// rules make the indexing result reproducible, given a set of available files. -#[derive(Debug, CacheWeight, Copy, Clone, PartialEq, Eq, FromSqlRow, Hash, PartialOrd, Ord)] +#[derive( + Debug, CacheWeight, Copy, Clone, PartialEq, Eq, FromSqlRow, Hash, PartialOrd, Ord, AsExpression, +)] +#[diesel(sql_type = Integer)] pub struct CausalityRegion(i32); impl fmt::Display for CausalityRegion { diff --git a/store/postgres/src/block_range.rs b/store/postgres/src/block_range.rs index 1d81eac5e81..f05c4e73869 100644 --- a/store/postgres/src/block_range.rs +++ b/store/postgres/src/block_range.rs @@ -165,13 +165,6 @@ impl<'a> BlockRangeColumn<'a> { } } } - - pub fn block(&self) -> BlockNumber { - match self { - BlockRangeColumn::Mutable { block, .. } => *block, - BlockRangeColumn::Immutable { block, .. } => *block, - } - } } impl<'a> BlockRangeColumn<'a> { @@ -227,13 +220,6 @@ impl<'a> BlockRangeColumn<'a> { } } - pub fn column_name(&self) -> &str { - match self { - BlockRangeColumn::Mutable { .. } => BLOCK_RANGE_COLUMN, - BlockRangeColumn::Immutable { .. } => BLOCK_COLUMN, - } - } - /// Output the qualified name of the block range column pub fn name(&self, out: &mut AstPass) { match self { @@ -280,14 +266,6 @@ impl<'a> BlockRangeColumn<'a> { } } - /// Output the name of the block range column without the table prefix - pub(crate) fn bare_name(&self, out: &mut AstPass) { - match self { - BlockRangeColumn::Mutable { .. } => out.push_sql(BLOCK_RANGE_COLUMN), - BlockRangeColumn::Immutable { .. } => out.push_sql(BLOCK_COLUMN), - } - } - /// Output an expression that matches all rows that have been changed /// after `block` (inclusive) pub(crate) fn changed_since<'b>(&'b self, out: &mut AstPass<'_, 'b, Pg>) -> QueryResult<()> { diff --git a/store/postgres/src/primary.rs b/store/postgres/src/primary.rs index 6017fc093ec..b13d1608bf3 100644 --- a/store/postgres/src/primary.rs +++ b/store/postgres/src/primary.rs @@ -296,6 +296,12 @@ impl Borrow for Namespace { } } +impl Borrow for &Namespace { + fn borrow(&self) -> &str { + &self.0 + } +} + /// A marker that an `i32` references a deployment. Values of this type hold /// the primary key from the `deployment_schemas` table #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, AsExpression, FromSqlRow)] diff --git a/store/postgres/src/relational.rs b/store/postgres/src/relational.rs index 593ad386889..860449bd42a 100644 --- a/store/postgres/src/relational.rs +++ b/store/postgres/src/relational.rs @@ -14,16 +14,20 @@ mod ddl_tests; #[cfg(test)] mod query_tests; +pub(crate) mod dsl; pub(crate) mod index; mod prune; mod rollup; +pub(crate) mod value; use diesel::deserialize::FromSql; use diesel::pg::Pg; use diesel::serialize::{Output, ToSql}; use diesel::sql_types::Text; use diesel::{connection::SimpleConnection, Connection}; -use diesel::{debug_query, sql_query, OptionalExtension, PgConnection, QueryResult, RunQueryDsl}; +use diesel::{ + debug_query, sql_query, OptionalExtension, PgConnection, QueryDsl, QueryResult, RunQueryDsl, +}; use graph::blockchain::BlockTime; use graph::cheap_clone::CheapClone; use graph::components::store::write::{RowGroup, WriteChunk}; @@ -50,6 +54,7 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; +use crate::relational::value::{FromOidRow, OidRow}; use crate::relational_queries::{ ConflictingEntitiesData, ConflictingEntitiesQuery, FindChangesQuery, FindDerivedQuery, FindPossibleDeletionsQuery, ReturnedEntityData, @@ -58,10 +63,10 @@ use crate::{ primary::{Namespace, Site}, relational_queries::{ ClampRangeQuery, EntityData, EntityDeletion, FilterCollection, FilterQuery, FindManyQuery, - FindQuery, InsertQuery, RevertClampQuery, RevertRemoveQuery, + InsertQuery, RevertClampQuery, RevertRemoveQuery, }, }; -use graph::components::store::DerivedEntityQuery; +use graph::components::store::{AttributeNames, DerivedEntityQuery}; use graph::data::store::{Id, IdList, IdType, BYTES_SCALAR}; use graph::data::subgraph::schema::POI_TABLE; use graph::prelude::{ @@ -172,6 +177,12 @@ impl From for SqlName { } } +impl From for Word { + fn from(name: SqlName) -> Self { + Word::from(name.0) + } +} + impl fmt::Display for SqlName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) @@ -184,6 +195,12 @@ impl Borrow for &SqlName { } } +impl PartialEq for SqlName { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} + impl FromSql for SqlName { fn from_sql(bytes: diesel::pg::PgValue) -> diesel::deserialize::Result { >::from_sql(bytes).map(|s| SqlName::verbatim(s)) @@ -361,9 +378,11 @@ impl Layout { } let table_name = SqlName::verbatim(POI_TABLE.to_owned()); + let nsp = catalog.site.namespace.clone(); Table { object: poi_type.to_owned(), qualified_name: SqlName::qualified_name(&catalog.site.namespace, &table_name), + nsp, name: table_name, columns, // The position of this table in all the tables for this layout; this @@ -469,11 +488,19 @@ impl Layout { key: &EntityKey, block: BlockNumber, ) -> Result, StoreError> { - let table = self.table_for_entity(&key.entity_type)?; - FindQuery::new(table.as_ref(), key, block) - .get_result::(conn) + let table = self.table_for_entity(&key.entity_type)?.dsl_table(); + let columns = table.selected_columns::(&AttributeNames::All, None)?; + + let query = table + .select_cols(&columns) + .filter(table.id_eq(&key.entity_id)) + .filter(table.at_block(block)) + .filter(table.belongs_to_causality_region(key.causality_region)); + + query + .get_result::(conn) .optional()? - .map(|entity_data| entity_data.deserialize_with_layout(self, None)) + .map(|row| Entity::from_oid_row(row, &self.input_schema, &columns)) .transpose() } @@ -1348,6 +1375,21 @@ impl Column { }) } + pub fn pseudo_column(name: &str, column_type: ColumnType) -> Column { + let field_type = q::Type::NamedType(column_type.to_string()); + let name = SqlName::verbatim(name.to_string()); + let field = Word::from(name.as_str()); + Column { + name, + field, + field_type, + column_type, + fulltext_fields: None, + is_reference: false, + use_prefix_comparison: false, + } + } + fn new_fulltext(def: &FulltextDefinition) -> Result { SqlName::check_valid_identifier(&def.name, "attribute")?; let sql_name = SqlName::from(def.name.as_str()); @@ -1440,6 +1482,9 @@ pub struct Table { /// `Stats_hour`, not the overall aggregation type `Stats`. pub object: EntityType, + /// The namespace in which the table lives + nsp: Namespace, + /// The name of the database table for this type ('thing'), snakecased /// version of `object` pub name: SqlName, @@ -1494,10 +1539,11 @@ impl Table { .collect::, StoreError>>()?; let qualified_name = SqlName::qualified_name(&catalog.site.namespace, &table_name); let immutable = defn.is_immutable(); - + let nsp = catalog.site.namespace.clone(); let table = Table { object: defn.cheap_clone(), name: table_name, + nsp, qualified_name, // Default `is_account_like` to `false`; the caller should call // `refresh` after constructing the layout, but that requires a @@ -1516,6 +1562,7 @@ impl Table { pub fn new_like(&self, namespace: &Namespace, name: &SqlName) -> Arc { let other = Table { object: self.object.clone(), + nsp: self.nsp.clone(), name: name.clone(), qualified_name: SqlName::qualified_name(namespace, name), columns: self.columns.clone(), @@ -1590,6 +1637,10 @@ impl Table { &crate::block_range::BLOCK_RANGE_COLUMN_SQL } } + + pub fn dsl_table(&self) -> dsl::Table<'_> { + dsl::Table::new(self) + } } #[derive(Clone)] diff --git a/store/postgres/src/relational/dsl.rs b/store/postgres/src/relational/dsl.rs new file mode 100644 index 00000000000..b11b0858a5d --- /dev/null +++ b/store/postgres/src/relational/dsl.rs @@ -0,0 +1,781 @@ +//! Helpers for creating relational queries using diesel. A lot of this code +//! is copied from `diesel_dynamic_schema` and adapted to our data +//! structures, especially the `Table` and `Column` types. + +use std::marker::PhantomData; + +use diesel::backend::Backend; +use diesel::dsl::sql; +use diesel::expression::{expression_types, is_aggregate, TypedExpressionType, ValidGrouping}; +use diesel::pg::Pg; +use diesel::query_builder::{ + AsQuery, AstPass, BoxedSelectStatement, FromClause, Query, QueryFragment, QueryId, + SelectStatement, +}; +use diesel::query_dsl::methods::SelectDsl; +use diesel::query_source::QuerySource; + +use diesel::sql_types::{ + Array, BigInt, Binary, Bool, Integer, Nullable, Numeric, SingleValue, Text, Timestamptz, + Untyped, +}; +use diesel::{AppearsOnTable, Expression, QueryDsl, QueryResult, SelectableExpression}; +use diesel_dynamic_schema::DynamicSelectClause; +use graph::components::store::{AttributeNames, BlockNumber, StoreError, BLOCK_NUMBER_MAX}; +use graph::data::store::{Id, IdType, ID}; +use graph::data_source::CausalityRegion; +use graph::prelude::{lazy_static, ENV_VARS}; + +use crate::relational::ColumnType; +use crate::relational_queries::PARENT_ID; + +use super::value::FromOidRow; +use super::Column as RelColumn; +use super::SqlName; +use super::{BLOCK_COLUMN, BLOCK_RANGE_COLUMN}; + +const TYPENAME: &str = "__typename"; + +lazy_static! { + pub static ref TYPENAME_SQL: SqlName = TYPENAME.into(); + pub static ref VID_SQL: SqlName = "vid".into(); + pub static ref PARENT_SQL: SqlName = PARENT_ID.into(); + pub static ref TYPENAME_COL: RelColumn = RelColumn::pseudo_column(TYPENAME, ColumnType::String); + pub static ref VID_COL: RelColumn = RelColumn::pseudo_column("vid", ColumnType::Int8); + pub static ref BLOCK_COL: RelColumn = RelColumn::pseudo_column(BLOCK_COLUMN, ColumnType::Int8); + // The column type is a placeholder, we can't deserialize in4range; but + // we also never try to use it when we get data from the database + pub static ref BLOCK_RANGE_COL: RelColumn = + RelColumn::pseudo_column(BLOCK_RANGE_COLUMN, ColumnType::Bytes); + pub static ref PARENT_STRING_COL: RelColumn = RelColumn::pseudo_column(PARENT_ID, ColumnType::String); + pub static ref PARENT_BYTES_COL: RelColumn = RelColumn::pseudo_column(PARENT_ID, ColumnType::Bytes); + pub static ref PARENT_INT_COL: RelColumn = RelColumn::pseudo_column(PARENT_ID, ColumnType::Int8); + + pub static ref META_COLS: [&'static RelColumn; 2] = [&*TYPENAME_COL, &*VID_COL]; +} + +#[doc(hidden)] +/// A dummy expression. +pub struct DummyExpression; + +impl DummyExpression { + pub(crate) fn new() -> Self { + DummyExpression + } +} + +impl SelectableExpression for DummyExpression {} + +impl AppearsOnTable for DummyExpression {} + +impl Expression for DummyExpression { + type SqlType = expression_types::NotSelectable; +} + +impl ValidGrouping<()> for DummyExpression { + type IsAggregate = is_aggregate::No; +} + +/// A fixed size string for the table alias. We want to make sure that +/// converting these to `&str` doesn't allocate and that they are small +/// enough that the `Table` struct is only 16 bytes and can be `Copy` +#[derive(Debug, Clone, Copy)] +pub struct ChildAliasStr { + alias: [u8; 4], +} + +impl ChildAliasStr { + fn new(idx: u8) -> Self { + let c = 'i' as u8; + let alias = if idx == 0 { + [c, 0, 0, 0] + } else if idx < 10 { + let ones = char::from_digit(idx as u32, 10).unwrap() as u8; + [c, ones, 0, 0] + } else if idx < 100 { + let tens = char::from_digit((idx / 10) as u32, 10).unwrap() as u8; + let ones = char::from_digit((idx % 10) as u32, 10).unwrap() as u8; + [c, tens, ones, 0] + } else { + let hundreds = char::from_digit((idx / 100) as u32, 10).unwrap() as u8; + let idx = idx % 100; + let tens = char::from_digit((idx / 10) as u32, 10).unwrap() as u8; + let ones = char::from_digit((idx % 10) as u32, 10).unwrap() as u8; + [c, hundreds, tens, ones] + }; + ChildAliasStr { alias } + } + + fn as_str(&self) -> &str { + let alias = if self.alias[1] == 0 { + return "i"; + } else if self.alias[2] == 0 { + &self.alias[..2] + } else if self.alias[3] == 0 { + &self.alias[..3] + } else { + &self.alias + }; + unsafe { std::str::from_utf8_unchecked(alias) } + } +} + +/// A table alias. We use `c` as the main table alias and `i`, `i1`, `i2`, +/// ... for child tables. The fact that we use these specific letters is +/// historical and doesn't have any meaning. +#[derive(Debug, Clone, Copy)] +pub enum Alias { + Main, + Child(ChildAliasStr), +} + +impl Alias { + fn as_str(&self) -> &str { + match self { + Alias::Main => "c", + Alias::Child(idx) => idx.as_str(), + } + } + + fn child(idx: u8) -> Self { + Alias::Child(ChildAliasStr::new(idx)) + } +} + +#[test] +fn alias() { + assert_eq!(Alias::Main.as_str(), "c"); + assert_eq!(Alias::Child(ChildAliasStr::new(0)).as_str(), "i"); + assert_eq!(Alias::Child(ChildAliasStr::new(1)).as_str(), "i1"); + assert_eq!(Alias::Child(ChildAliasStr::new(10)).as_str(), "i10"); + assert_eq!(Alias::Child(ChildAliasStr::new(100)).as_str(), "i100"); + assert_eq!(Alias::Child(ChildAliasStr::new(255)).as_str(), "i255"); +} + +#[derive(Debug, Clone, Copy)] +/// A wrapper around the `super::Table` struct that provides helper +/// functions for generating SQL queries +pub struct Table<'a> { + /// The metadata for this table + pub meta: &'a super::Table, + alias: Alias, +} + +impl<'a> Table<'a> { + pub(crate) fn new(meta: &'a super::Table) -> Self { + Self { + meta, + alias: Alias::Main, + } + } + + /// Change the alias for this table to be a child table. + pub fn child(mut self, idx: u8) -> Self { + self.alias = Alias::child(idx); + self + } + + /// Reference a column in this table and use the correct SQL type `ST` + fn bind(&self, name: &str) -> Option> { + self.column(name).map(|c| c.bind()) + } + + /// Reference a column without regard to the underlying SQL type. This + /// is useful if just the name of the column qualified with the table + /// name/alias is needed + pub fn column(&self, name: &str) -> Option> { + self.meta + .columns + .iter() + .chain(META_COLS.into_iter()) + .find(|c| &c.name == name) + .map(|c| Column::new(self.clone(), c)) + } + + pub fn name(&self) -> &str { + &self.meta.name + } + + pub fn column_for_field(&self, field: &str) -> Result, StoreError> { + self.meta + .column_for_field(field) + .map(|column| Column::new(*self, column)) + } + + pub fn primary_key(&self) -> Column<'a> { + Column::new(*self, self.meta.primary_key()) + } + + /// Return a filter expression that generates the SQL for `id = $id` + pub fn id_eq(&'a self, id: &'a Id) -> IdEq<'a> { + IdEq::new(*self, id) + } + + /// Return an expression that generates the SQL for `block_range @> + /// $block` or `block = $block` depending on whether the table is + /// mutable or not + pub fn at_block(&self, block: BlockNumber) -> AtBlock<'a> { + AtBlock::new(*self, block) + } + + /// The block column for this table for places where the just the + /// qualified name is needed + pub fn block_column(&self) -> BlockColumn<'a> { + BlockColumn::new(*self) + } + + /// An expression that is true if the entity has changed since `block` + pub fn changed_since(&self, block: BlockNumber) -> ChangedSince<'a> { + let column = self.block_column(); + ChangedSince { column, block } + } + + /// Return an expression that generates the SQL for `causality_region = + /// $cr` if the table uses causality regions + pub fn belongs_to_causality_region( + &'a self, + cr: CausalityRegion, + ) -> BelongsToCausalityRegion<'a> { + BelongsToCausalityRegion::new(*self, cr) + } + + /// Produce a list of the columns that should be selected for a query + /// based on `column_names`. The result needs to be used both to create + /// the actual select statement with `Self::select_cols` and to decode + /// query results with `FromOidRow`. + pub fn selected_columns( + &self, + column_names: &'a AttributeNames, + parent_type: Option, + ) -> Result, StoreError> { + let mut cols = Vec::new(); + if T::WITH_INTERNAL_KEYS { + cols.push(&*TYPENAME_COL); + } + + match column_names { + AttributeNames::All => cols.extend(self.meta.columns.iter()), + AttributeNames::Select(names) => { + let pk = self.meta.primary_key(); + cols.push(pk); + let mut names: Vec<_> = names.iter().filter(|name| *name != &*ID).collect(); + names.sort(); + for name in names { + let column = self.meta.column_for_field(&name)?; + cols.push(column); + } + } + }; + + if T::WITH_INTERNAL_KEYS { + match parent_type { + Some(IdType::String) => cols.push(&*PARENT_STRING_COL), + Some(IdType::Bytes) => cols.push(&*PARENT_BYTES_COL), + Some(IdType::Int8) => cols.push(&*PARENT_INT_COL), + None => (), + } + } + + if T::WITH_SYSTEM_COLUMNS { + cols.push(&*VID_COL); + if self.meta.immutable { + cols.push(&*BLOCK_COL); + } else { + // TODO: We can't deserialize in4range + cols.push(&*BLOCK_RANGE_COL); + } + } + Ok(cols) + } + + /// Create a Diesel select statement that selects the columns in + /// `columns`. Use to generate a query via + /// `table.select_cols(columns).filter(...)`. For a full example, see + /// `Layout::find` + pub fn select_cols( + &'a self, + columns: &[&'a RelColumn], + ) -> BoxedSelectStatement<'a, Untyped, FromClause>, Pg> { + type SelectClause<'b> = DynamicSelectClause<'b, Pg, Table<'b>>; + + fn add_field<'b, ST: SingleValue + Send>( + select: &mut SelectClause<'b>, + table: &'b Table<'b>, + column: &'b RelColumn, + ) { + let name = &column.name; + + match (column.is_list(), column.is_nullable()) { + (true, true) => select.add_field(table.bind::>>(name).unwrap()), + (true, false) => select.add_field(table.bind::>(name).unwrap()), + (false, true) => select.add_field(table.bind::>(name).unwrap()), + (false, false) => select.add_field(table.bind::(name).unwrap()), + } + } + + fn add_enum_field<'b>( + select: &mut SelectClause<'b>, + table: &'b Table<'b>, + column: &'b RelColumn, + ) { + let name = format!("{}.{}::text", table.alias.as_str(), &column.name); + + match (column.is_list(), column.is_nullable()) { + (true, true) => select.add_field(sql::>>(&name)), + (true, false) => select.add_field(sql::>(&name)), + (false, true) => select.add_field(sql::>(&name)), + (false, false) => select.add_field(sql::(&name)), + } + } + + let mut selection = DynamicSelectClause::new(); + for column in columns { + if column.name == TYPENAME_COL.name { + selection.add_field(sql::(&format!( + "'{}' as __typename", + self.meta.object.typename() + ))); + continue; + } + match column.column_type { + ColumnType::Boolean => add_field::(&mut selection, self, column), + ColumnType::BigDecimal => add_field::(&mut selection, self, column), + ColumnType::BigInt => add_field::(&mut selection, self, column), + ColumnType::Bytes => add_field::(&mut selection, self, column), + ColumnType::Int => add_field::(&mut selection, self, column), + ColumnType::Int8 => add_field::(&mut selection, self, column), + ColumnType::Timestamp => add_field::(&mut selection, self, column), + ColumnType::String => add_field::(&mut selection, self, column), + ColumnType::TSVector(_) => add_field::(&mut selection, self, column), + ColumnType::Enum(_) => add_enum_field(&mut selection, self, column), + }; + } + >>::select(*self, selection).into_boxed() + } +} + +/// Generate the SQL to use a table in the `from` clause, complete with +/// giving the table an alias +#[derive(Debug, Clone, Copy)] +pub struct FromTable<'a>(Table<'a>); + +impl<'a, DB> QueryFragment for FromTable<'a> +where + DB: Backend, +{ + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + + out.push_identifier(self.0.meta.nsp.as_str())?; + out.push_sql("."); + out.push_identifier(&self.0.meta.name)?; + out.push_sql(" as "); + out.push_sql(self.0.alias.as_str()); + Ok(()) + } +} + +impl std::fmt::Display for Table<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} as {}", self.meta.name, self.alias.as_str()) + } +} + +impl std::fmt::Display for FromTable<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'a> QuerySource for Table<'a> { + type FromClause = FromTable<'a>; + type DefaultSelection = DummyExpression; + + fn from_clause(&self) -> FromTable<'a> { + FromTable(*self) + } + + fn default_selection(&self) -> Self::DefaultSelection { + DummyExpression::new() + } +} + +impl<'a> AsQuery for Table<'a> +where + SelectStatement>: Query, +{ + type SqlType = expression_types::NotSelectable; + type Query = SelectStatement>; + + fn as_query(self) -> Self::Query { + SelectStatement::simple(self) + } +} + +impl<'a> diesel::Table for Table<'a> +where + Self: QuerySource + AsQuery, +{ + type PrimaryKey = DummyExpression; + type AllColumns = DummyExpression; + + fn primary_key(&self) -> Self::PrimaryKey { + DummyExpression::new() + } + + fn all_columns() -> Self::AllColumns { + DummyExpression::new() + } +} + +impl<'a, DB> QueryFragment for Table<'a> +where + DB: Backend, +{ + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + + out.push_sql(self.alias.as_str()); + Ok(()) + } +} + +impl<'a> QueryId for Table<'a> { + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; +} + +/// Generated by `Table.id_eq` +pub struct IdEq<'a> { + table: Table<'a>, + id: &'a Id, +} + +impl<'a> IdEq<'a> { + fn new(table: Table<'a>, id: &'a Id) -> Self { + IdEq { table, id } + } +} + +impl Expression for IdEq<'_> { + type SqlType = Bool; +} + +impl<'a> QueryFragment for IdEq<'a> { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + self.table.walk_ast(out.reborrow())?; + out.push_sql(".id = "); + match self.id { + Id::String(s) => out.push_bind_param::(s.as_str())?, + Id::Bytes(b) => out.push_bind_param::(b)?, + Id::Int8(i) => out.push_bind_param::(i)?, + } + Ok(()) + } +} + +impl ValidGrouping<()> for IdEq<'_> { + type IsAggregate = is_aggregate::No; +} + +impl<'a> AppearsOnTable> for IdEq<'a> {} + +/// Generated by `Table.block_column` +#[derive(Debug, Clone, Copy)] +pub struct BlockColumn<'a> { + table: Table<'a>, +} + +impl<'a> BlockColumn<'a> { + fn new(table: Table<'a>) -> Self { + BlockColumn { table } + } + + fn immutable(&self) -> bool { + self.table.meta.immutable + } + + pub fn name(&self) -> &str { + if self.immutable() { + BLOCK_COLUMN + } else { + BLOCK_RANGE_COLUMN + } + } +} + +impl std::fmt::Display for BlockColumn<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}.{}", self.table.alias.as_str(), self.name()) + } +} + +impl QueryFragment for BlockColumn<'_> { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + self.table.walk_ast(out.reborrow())?; + out.push_sql("."); + out.push_sql(self.name()); + Ok(()) + } +} + +/// Generated by `Table.at_block` +#[derive(Debug, Clone, Copy)] +pub struct AtBlock<'a> { + column: BlockColumn<'a>, + block: BlockNumber, + filters_by_id: bool, +} + +impl<'a> AtBlock<'a> { + fn new(table: Table<'a>, block: BlockNumber) -> Self { + let column = BlockColumn::new(table); + AtBlock { + column, + block, + filters_by_id: false, + } + } + + pub fn filters_by_id(mut self, by_id: bool) -> Self { + self.filters_by_id = by_id; + self + } +} + +impl Expression for AtBlock<'_> { + type SqlType = Bool; +} + +impl<'a> QueryFragment for AtBlock<'a> { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + + if self.column.immutable() { + if self.block == BLOCK_NUMBER_MAX { + // `self.block <= BLOCK_NUMBER_MAX` is always true + out.push_sql("true"); + } else { + self.column.walk_ast(out.reborrow())?; + out.push_sql(" <= "); + out.push_bind_param::(&self.block)?; + } + } else { + // Table is mutable and has a block_range column + self.column.walk_ast(out.reborrow())?; + out.push_sql(" @> "); + out.push_bind_param::(&self.block)?; + + let should_use_brin = + !self.filters_by_id || ENV_VARS.store.use_brin_for_all_query_types; + if self.column.table.meta.is_account_like + && self.block < BLOCK_NUMBER_MAX + && should_use_brin + { + // When block is BLOCK_NUMBER_MAX, these checks would be wrong; we + // don't worry about adding the equivalent in that case since + // we generally only see BLOCK_NUMBER_MAX here for metadata + // queries where block ranges don't matter anyway. + // + // We also don't need to add these if the query already filters by ID, + // because the ideal index is the GiST index on id and block_range. + out.push_sql(" and coalesce(upper("); + self.column.walk_ast(out.reborrow())?; + out.push_sql("), 2147483647) > "); + out.push_bind_param::(&self.block)?; + out.push_sql(" and lower("); + self.column.walk_ast(out.reborrow())?; + out.push_sql(") <= "); + out.push_bind_param::(&self.block)?; + } + } + + Ok(()) + } +} + +impl ValidGrouping<()> for AtBlock<'_> { + type IsAggregate = is_aggregate::No; +} + +impl<'a> AppearsOnTable> for AtBlock<'a> {} + +/// Generated by `Table.changed_since` +#[derive(Debug)] +pub struct ChangedSince<'a> { + column: BlockColumn<'a>, + block: BlockNumber, +} + +impl std::fmt::Display for ChangedSince<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} >= {}", self.column, self.block) + } +} + +impl Expression for ChangedSince<'_> { + type SqlType = Bool; +} + +impl QueryFragment for ChangedSince<'_> { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + if self.column.table.meta.immutable { + self.column.walk_ast(out.reborrow())?; + out.push_sql(" >= "); + out.push_bind_param::(&self.block) + } else { + out.push_sql("lower("); + self.column.walk_ast(out.reborrow())?; + out.push_sql(") >= "); + out.push_bind_param::(&self.block) + } + } +} + +/// Generated by `Table.belongs_to_causality_region` +pub struct BelongsToCausalityRegion<'a> { + table: Table<'a>, + cr: CausalityRegion, +} + +impl<'a> BelongsToCausalityRegion<'a> { + fn new(table: Table<'a>, cr: CausalityRegion) -> Self { + BelongsToCausalityRegion { table, cr } + } +} + +impl Expression for BelongsToCausalityRegion<'_> { + type SqlType = Bool; +} + +impl<'a> QueryFragment for BelongsToCausalityRegion<'a> { + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + + if self.table.meta.has_causality_region { + self.table.walk_ast(out.reborrow())?; + out.push_sql(".causality_region"); + out.push_sql(" = "); + out.push_bind_param::(&self.cr)?; + } else { + out.push_sql("true"); + } + Ok(()) + } +} + +impl ValidGrouping<()> for BelongsToCausalityRegion<'_> { + type IsAggregate = is_aggregate::No; +} + +impl<'a> AppearsOnTable> for BelongsToCausalityRegion<'a> {} + +/// A specific column in a specific table +#[derive(Debug, Clone, Copy)] +pub struct Column<'a> { + table: Table<'a>, + column: &'a super::Column, +} + +impl<'a> Column<'a> { + fn new(table: Table<'a>, column: &'a super::Column) -> Self { + Column { table, column } + } + + /// Bind this column to a specific SQL type for use in contexts where + /// Diesel requires that + pub fn bind(&self) -> BoundColumn<'a, ST> { + BoundColumn::new(self.table, self.column) + } + + pub fn name(&self) -> &'a str { + &self.column.name + } + + pub(crate) fn is_list(&self) -> bool { + self.column.is_list() + } + + pub(crate) fn is_primary_key(&self) -> bool { + self.column.is_primary_key() + } + + pub(crate) fn is_fulltext(&self) -> bool { + self.column.is_fulltext() + } + + pub(crate) fn column_type(&self) -> &'a ColumnType { + &self.column.column_type + } + + pub(crate) fn use_prefix_comparison(&self) -> bool { + self.column.use_prefix_comparison + } +} + +impl std::fmt::Display for Column<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}.{}", self.table.alias.as_str(), self.column.name) + } +} + +impl<'a, DB> QueryFragment for Column<'a> +where + DB: Backend, +{ + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> { + out.unsafe_to_cache_prepared(); + self.table.walk_ast(out.reborrow())?; + out.push_sql("."); + out.push_identifier(&self.column.name)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Copy)] +/// A database table column bound to the SQL type for the column +pub struct BoundColumn<'a, ST> { + column: Column<'a>, + _sql_type: PhantomData, +} + +impl<'a, ST> BoundColumn<'a, ST> { + fn new(table: Table<'a>, column: &'a super::Column) -> Self { + let column = Column::new(table, column); + Self { + column, + _sql_type: PhantomData, + } + } +} + +impl<'a, ST> QueryId for BoundColumn<'a, ST> { + type QueryId = (); + const HAS_STATIC_QUERY_ID: bool = false; +} + +impl<'a, ST, QS> SelectableExpression for BoundColumn<'a, ST> where Self: Expression {} + +impl<'a, ST, QS> AppearsOnTable for BoundColumn<'a, ST> where Self: Expression {} + +impl<'a, ST> Expression for BoundColumn<'a, ST> +where + ST: TypedExpressionType, +{ + type SqlType = ST; +} + +impl<'a, ST> ValidGrouping<()> for BoundColumn<'a, ST> { + type IsAggregate = is_aggregate::No; +} + +impl<'a, ST, DB> QueryFragment for BoundColumn<'a, ST> +where + DB: Backend, +{ + fn walk_ast<'b>(&'b self, out: AstPass<'_, 'b, DB>) -> QueryResult<()> { + self.column.walk_ast(out) + } +} diff --git a/store/postgres/src/relational/query_tests.rs b/store/postgres/src/relational/query_tests.rs index 06a98db4353..7a3286227f6 100644 --- a/store/postgres/src/relational/query_tests.rs +++ b/store/postgres/src/relational/query_tests.rs @@ -49,8 +49,9 @@ fn filter_contains(filter: EntityFilter, sql: &str) { let layout = test_layout(SCHEMA); let table = layout .table_for_entity(&layout.input_schema.entity_type("Thing").unwrap()) - .unwrap(); - let filter = Filter::main(&layout, table.as_ref(), &filter, Default::default()).unwrap(); + .unwrap() + .dsl_table(); + let filter = Filter::main(&layout, table, &filter, Default::default()).unwrap(); let query = debug_query::(&filter); assert!( query.to_string().contains(sql), diff --git a/store/postgres/src/relational/value.rs b/store/postgres/src/relational/value.rs new file mode 100644 index 00000000000..fadcfdcfbca --- /dev/null +++ b/store/postgres/src/relational/value.rs @@ -0,0 +1,263 @@ +//! Helpers to use diesel dynamic schema to retrieve values from Postgres + +use std::num::NonZeroU32; + +use diesel::sql_types::{Array, BigInt, Binary, Bool, Integer, Numeric, Text, Timestamptz}; +use diesel::{deserialize::FromSql, pg::Pg}; +use diesel_dynamic_schema::dynamic_value::{Any, DynamicRow}; + +use graph::{ + components::store::StoreError, + data::{ + store::{ + scalar::{BigDecimal, Bytes, Timestamp}, + Entity, QueryObject, + }, + value::{Object, Word}, + }, + prelude::r, + schema::InputSchema, +}; + +use super::ColumnType; +use crate::relational::Column; + +/// Represent values of the database types we care about as a single value. +/// The deserialization of these values is completely governed by the oid we +/// get from Postgres; in a second step, these values need to be transformed +/// into our internal values using the underlying `ColumnType`. Diesel's API +/// doesn't let us do that in one go, so we do a first transformation into +/// `OidValue` and then use `FromOidValue` to transform guided by the +/// `ColumnType` +#[derive(Debug)] +pub enum OidValue { + String(String), + StringArray(Vec), + Bytes(Bytes), + BytesArray(Vec), + Bool(bool), + BoolArray(Vec), + Int(i32), + Ints(Vec), + Int8(i64), + Int8Array(Vec), + BigDecimal(BigDecimal), + BigDecimalArray(Vec), + Timestamp(Timestamp), + TimestampArray(Vec), + Null, +} + +impl FromSql for OidValue { + fn from_sql(value: diesel::pg::PgValue) -> diesel::deserialize::Result { + const VARCHAR_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1043) }; + const VARCHAR_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1015) }; + const TEXT_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(25) }; + const TEXT_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1009) }; + const BYTEA_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(17) }; + const BYTEA_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1001) }; + const BOOL_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(16) }; + const BOOL_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1000) }; + const INTEGER_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(23) }; + const INTEGER_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1007) }; + const INT8_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(20) }; + const INT8_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1016) }; + const NUMERIC_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1700) }; + const NUMERIC_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1231) }; + const TIMESTAMPTZ_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1184) }; + const TIMESTAMPTZ_ARY_OID: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1185) }; + + match value.get_oid() { + VARCHAR_OID | TEXT_OID => { + >::from_sql(value).map(OidValue::String) + } + VARCHAR_ARY_OID | TEXT_ARY_OID => { + as FromSql, Pg>>::from_sql(value) + .map(OidValue::StringArray) + } + BYTEA_OID => >::from_sql(value).map(OidValue::Bytes), + BYTEA_ARY_OID => as FromSql, Pg>>::from_sql(value) + .map(OidValue::BytesArray), + BOOL_OID => >::from_sql(value).map(OidValue::Bool), + BOOL_ARY_OID => { + as FromSql, Pg>>::from_sql(value).map(OidValue::BoolArray) + } + INTEGER_OID => >::from_sql(value).map(OidValue::Int), + INTEGER_ARY_OID => { + as FromSql, Pg>>::from_sql(value).map(OidValue::Ints) + } + INT8_OID => >::from_sql(value).map(OidValue::Int8), + INT8_ARY_OID => { + as FromSql, Pg>>::from_sql(value).map(OidValue::Int8Array) + } + NUMERIC_OID => { + >::from_sql(value).map(OidValue::BigDecimal) + } + NUMERIC_ARY_OID => as FromSql, Pg>>::from_sql(value) + .map(OidValue::BigDecimalArray), + TIMESTAMPTZ_OID => { + >::from_sql(value).map(OidValue::Timestamp) + } + TIMESTAMPTZ_ARY_OID => { + as FromSql, Pg>>::from_sql(value) + .map(OidValue::TimestampArray) + } + e => Err(format!("Unknown type: {e}").into()), + } + } + + fn from_nullable_sql(bytes: Option) -> diesel::deserialize::Result { + match bytes { + Some(bytes) => Self::from_sql(bytes), + None => Ok(OidValue::Null), + } + } +} + +pub trait FromOidValue: Sized { + fn from_oid_value(value: OidValue, column_type: &ColumnType) -> Result; +} + +impl FromOidValue for r::Value { + fn from_oid_value(value: OidValue, _: &ColumnType) -> Result { + fn as_list(values: Vec, f: F) -> r::Value + where + F: Fn(T) -> r::Value, + { + r::Value::List(values.into_iter().map(f).collect()) + } + + use OidValue as O; + let value = match value { + O::String(s) => Self::String(s), + O::StringArray(s) => as_list(s, Self::String), + O::Bytes(b) => Self::String(b.to_string()), + O::BytesArray(b) => as_list(b, |b| Self::String(b.to_string())), + O::Bool(b) => Self::Boolean(b), + O::BoolArray(b) => as_list(b, Self::Boolean), + O::Int(i) => Self::Int(i as i64), + O::Ints(i) => as_list(i, |i| Self::Int(i as i64)), + O::Int8(i) => Self::String(i.to_string()), + O::Int8Array(i) => as_list(i, |i| Self::String(i.to_string())), + O::BigDecimal(b) => Self::String(b.to_string()), + O::BigDecimalArray(b) => as_list(b, |b| Self::String(b.to_string())), + O::Timestamp(t) => Self::Timestamp(t), + O::TimestampArray(t) => as_list(t, Self::Timestamp), + O::Null => Self::Null, + }; + Ok(value) + } +} + +impl FromOidValue for graph::prelude::Value { + fn from_oid_value(value: OidValue, column_type: &ColumnType) -> Result { + fn as_list(values: Vec, f: F) -> graph::prelude::Value + where + F: Fn(T) -> graph::prelude::Value, + { + graph::prelude::Value::List(values.into_iter().map(f).collect()) + } + + fn as_list_err(values: Vec, f: F) -> Result + where + F: Fn(T) -> Result, + { + values + .into_iter() + .map(f) + .collect::>() + .map(graph::prelude::Value::List) + } + + use OidValue as O; + let value = match value { + O::String(s) => Self::String(s), + O::StringArray(s) => as_list(s, Self::String), + O::Bytes(b) => Self::Bytes(b), + O::BytesArray(b) => as_list(b, Self::Bytes), + O::Bool(b) => Self::Bool(b), + O::BoolArray(b) => as_list(b, Self::Bool), + O::Int(i) => Self::Int(i), + O::Ints(i) => as_list(i, Self::Int), + O::Int8(i) => Self::Int8(i), + O::Int8Array(i) => as_list(i, Self::Int8), + O::BigDecimal(b) => match column_type { + ColumnType::BigDecimal => Self::BigDecimal(b), + ColumnType::BigInt => Self::BigInt(b.to_bigint()?), + _ => unreachable!("only BigInt and BigDecimal are stored as numeric"), + }, + O::BigDecimalArray(b) => match column_type { + ColumnType::BigDecimal => as_list(b, Self::BigDecimal), + ColumnType::BigInt => as_list_err(b, |b| { + b.to_bigint().map(Self::BigInt).map_err(StoreError::from) + })?, + _ => unreachable!("only BigInt and BigDecimal are stored as numeric[]"), + }, + O::Timestamp(t) => Self::Timestamp(t), + O::TimestampArray(t) => as_list(t, Self::Timestamp), + O::Null => Self::Null, + }; + Ok(value) + } +} + +pub type OidRow = DynamicRow; + +pub trait FromOidRow: Sized { + // Should the columns for `__typename` and `g$parent_id` be selected + const WITH_INTERNAL_KEYS: bool; + // Should the system columns for block/block_range and vid be selected + const WITH_SYSTEM_COLUMNS: bool = false; + + fn from_oid_row( + row: DynamicRow, + schema: &InputSchema, + columns: &[&Column], + ) -> Result; +} + +impl FromOidRow for Entity { + const WITH_INTERNAL_KEYS: bool = false; + + fn from_oid_row( + row: DynamicRow, + schema: &InputSchema, + columns: &[&Column], + ) -> Result { + let x = row + .into_iter() + .zip(columns) + .filter(|(value, _)| !matches!(value, OidValue::Null)) + .map(|(value, column)| { + graph::prelude::Value::from_oid_value(value, &column.column_type) + .map(|value| (Word::from(column.field.clone()), value)) + }); + schema.try_make_entity(x).map_err(StoreError::from) + } +} + +impl FromOidRow for QueryObject { + const WITH_INTERNAL_KEYS: bool = true; + + fn from_oid_row( + row: DynamicRow, + _schema: &InputSchema, + columns: &[&Column], + ) -> Result { + let pairs = row + .into_iter() + .zip(columns) + .filter(|(value, _)| !matches!(value, OidValue::Null)) + .map(|(value, column)| -> Result<_, StoreError> { + let name = &column.name; + let value = r::Value::from_oid_value(value, &column.column_type)?; + Ok((Word::from(name.clone()), value)) + }) + .collect::, _>>()?; + let entity = Object::from_iter(pairs); + Ok(QueryObject { + entity, + parent: None, + }) + } +} diff --git a/store/postgres/src/relational_queries.rs b/store/postgres/src/relational_queries.rs index 4626ce0479e..56ad1aafacb 100644 --- a/store/postgres/src/relational_queries.rs +++ b/store/postgres/src/relational_queries.rs @@ -11,6 +11,7 @@ use diesel::query_dsl::RunQueryDsl; use diesel::result::{Error as DieselError, QueryResult}; use diesel::sql_types::Untyped; use diesel::sql_types::{Array, BigInt, Binary, Bool, Int8, Integer, Jsonb, Text, Timestamptz}; +use diesel::QuerySource as _; use graph::components::store::write::{EntityWrite, RowGroup, WriteChunk}; use graph::components::store::{Child as StoreChild, DerivedEntityQuery}; use graph::data::store::{Id, IdType, NULL}; @@ -22,7 +23,7 @@ use graph::prelude::{ EntityLink, EntityOrder, EntityOrderByChild, EntityOrderByChildInfo, EntityRange, EntityWindow, ParentLink, QueryExecutionError, StoreError, Value, ENV_VARS, }; -use graph::schema::{EntityKey, EntityType, FulltextAlgorithm, FulltextConfig, InputSchema}; +use graph::schema::{EntityType, FulltextAlgorithm, FulltextConfig, InputSchema}; use graph::{components::store::AttributeNames, data::store::scalar}; use inflector::Inflector; use itertools::Itertools; @@ -33,8 +34,9 @@ use std::iter::FromIterator; use std::str::FromStr; use std::string::ToString; +use crate::relational::dsl::AtBlock; use crate::relational::{ - Column, ColumnType, Layout, SqlName, Table, BYTE_ARRAY_PREFIX_SIZE, PRIMARY_KEY_COLUMN, + dsl, Column, ColumnType, Layout, SqlName, Table, BYTE_ARRAY_PREFIX_SIZE, PRIMARY_KEY_COLUMN, STRING_PREFIX_SIZE, }; use crate::{ @@ -56,7 +58,7 @@ const SORT_KEY_COLUMN: &str = "sort_key$"; /// The name of the parent_id attribute that we inject into queries. Users /// outside of this module should access the parent id through the /// `QueryObject` struct -const PARENT_ID: &str = "g$parent_id"; +pub(crate) const PARENT_ID: &str = "g$parent_id"; /// Describes at what level a `SELECT` statement is used. enum SelectStatementLevel { @@ -101,34 +103,14 @@ macro_rules! constraint_violation { }} } -/// Conveniences for handling foreign keys depending on whether we are using -/// `IdType::Bytes` or `IdType::String` as the primary key -/// -/// This trait adds some capabilities to `Column` that are very specific to -/// how we generate SQL queries. Using a method like `bind_ids` from this -/// trait on a given column means "send these values to the database in a form -/// that can later be used for comparisons with that column" -trait ForeignKeyClauses { - /// The name of the column - fn name(&self) -> &str; - - /// Generate a clause `{name()} = $id` using the right types to bind `$id` - /// into `out` - fn eq<'b>(&self, id: &'b Id, out: &mut AstPass<'_, 'b, Pg>) -> QueryResult<()> { - out.push_sql(self.name()); - out.push_sql(" = "); - id.push_bind_param(out) - } - - /// Generate a clause - /// `exists (select 1 from unnest($ids) as p(g$id) where id = p.g$id)` - /// using the right types to bind `$ids` into `out` - fn is_in<'b>(&self, ids: &'b IdList, out: &mut AstPass<'_, 'b, Pg>) -> QueryResult<()> { - out.push_sql("exists (select 1 from unnest("); - ids.push_bind_param(out)?; - out.push_sql(") as p(g$id) where id = p.g$id)"); - Ok(()) - } +/// Generate a clause +/// `exists (select 1 from unnest($ids) as p(g$id) where id = p.g$id)` +/// using the right types to bind `$ids` into `out` +fn id_is_in<'b>(ids: &'b IdList, out: &mut AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.push_sql("exists (select 1 from unnest("); + ids.push_bind_param(out)?; + out.push_sql(") as p(g$id) where id = p.g$id)"); + Ok(()) } /// This trait is here to deal with the fact that we can't implement `ToSql` @@ -163,12 +145,6 @@ impl PushBindParam for IdList { } } -impl ForeignKeyClauses for Column { - fn name(&self) -> &str { - self.name.as_str() - } -} - pub trait FromEntityData: Sized { /// Whether to include the internal keys `__typename` and `g$parent_id`. const WITH_INTERNAL_KEYS: bool; @@ -906,7 +882,7 @@ enum PrefixType { } impl PrefixType { - fn new(column: &QualColumn<'_>) -> QueryResult { + fn new(column: &dsl::Column<'_>) -> QueryResult { match column.column_type() { ColumnType::String => Ok(PrefixType::String), ColumnType::Bytes => Ok(PrefixType::Bytes), @@ -923,7 +899,7 @@ impl PrefixType { /// for the column fn push_column_prefix<'b>( self, - column: &'b QualColumn<'_>, + column: &'b dsl::Column<'b>, out: &mut AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { match self { @@ -979,14 +955,14 @@ fn is_large_string(s: &String) -> Result { pub struct PrefixComparison<'a> { op: Comparison, kind: PrefixType, - column: QualColumn<'a>, + column: dsl::Column<'a>, value: QueryValue<'a>, } impl<'a> PrefixComparison<'a> { fn new( op: Comparison, - column: QualColumn<'a>, + column: dsl::Column<'a>, column_type: &'a ColumnType, text: &'a Value, ) -> Result { @@ -1142,23 +1118,21 @@ impl<'a> QueryFragment for PrefixComparison<'a> { /// filtered with `child_filter`` #[derive(Debug)] pub struct QueryChild<'a> { - parent_column: &'a Column, - child_table: &'a Table, - child_column: &'a Column, + parent_column: dsl::Column<'a>, + child_from: dsl::FromTable<'a>, + child_column: dsl::Column<'a>, child_filter: Filter<'a>, derived: bool, - br_column: BlockRangeColumn<'a>, + at_block: dsl::AtBlock<'a>, } impl<'a> QueryChild<'a> { fn new( layout: &'a Layout, - parent_table: &'a Table, + parent_table: dsl::Table<'a>, child: &'a StoreChild, block: BlockNumber, ) -> Result { - const CHILD_PREFIX: &str = "i."; - let StoreChild { attr, entity_type, @@ -1166,7 +1140,7 @@ impl<'a> QueryChild<'a> { derived, } = child; let derived = *derived; - let child_table = layout.table_for_entity(entity_type)?; + let child_table = layout.table_for_entity(entity_type)?.dsl_table().child(0); let (parent_column, child_column) = if derived { // If the parent is derived, the child column is picked based on // the provided attribute and the parent column is the primary @@ -1184,16 +1158,16 @@ impl<'a> QueryChild<'a> { child_table.primary_key(), ) }; - let br_column = BlockRangeColumn::new(child_table, CHILD_PREFIX, block); + let at_block = child_table.at_block(block).filters_by_id(!derived); let child_filter = Filter::new(layout, child_table, filter, block, ColumnQual::Child)?; - + let child_from = child_table.from_clause(); Ok(Self { parent_column, - child_table, + child_from, child_column, child_filter, derived, - br_column, + at_block, }) } } @@ -1204,68 +1178,52 @@ impl<'a> QueryFragment for QueryChild<'a> { let QueryChild { parent_column, - child_table, + child_from, child_column, child_filter, derived, - br_column, + at_block, } = self; let derived = *derived; - let child_prefix = "i."; - let parent_prefix = "c."; - out.push_sql("exists (select 1 from "); - out.push_sql(child_table.qualified_name.as_str()); - out.push_sql(" as i"); + child_from.walk_ast(out.reborrow())?; out.push_sql(" where "); - let mut is_type_c_or_d = false; - // Join tables if derived { if child_column.is_list() { // Type A: c.id = any(i.{parent_field}) - out.push_sql(parent_prefix); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; out.push_sql(" = any("); - out.push_sql(child_prefix); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; out.push_sql(")"); } else { // Type B: c.id = i.{parent_field} - out.push_sql(parent_prefix); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; out.push_sql(" = "); - out.push_sql(child_prefix); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; } } else { - is_type_c_or_d = true; - if parent_column.is_list() { // Type C: i.id = any(c.child_ids) - out.push_sql(child_prefix); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; out.push_sql(" = any("); - out.push_sql(parent_prefix); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; out.push_sql(")"); } else { // Type D: i.id = c.child_id - out.push_sql(child_prefix); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; out.push_sql(" = "); - out.push_sql(parent_prefix); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; } } out.push_sql(" and "); // Match by block - br_column.contains(&mut out, is_type_c_or_d)?; + at_block.walk_ast(out.reborrow())?; out.push_sql(" and "); @@ -1286,13 +1244,6 @@ enum ColumnQual { } impl ColumnQual { - fn with<'a>(&self, column: &'a Column) -> QualColumn<'a> { - match self { - ColumnQual::Main => QualColumn::Main(column), - ColumnQual::Child => QualColumn::Child(column), - } - } - /// Return `true` if we allow a nested child filter. That's allowed as /// long as we are filtering the main table fn allow_child(&self) -> bool { @@ -1301,55 +1252,6 @@ impl ColumnQual { ColumnQual::Child => false, } } - - fn prefix(&self) -> &'static str { - match self { - ColumnQual::Main => "c.", - ColumnQual::Child => "i.", - } - } -} - -/// A qualified column name. This is either `c.{column}` or `i.{column}` -#[derive(Debug)] -pub enum QualColumn<'a> { - Main(&'a Column), - Child(&'a Column), -} -impl QualColumn<'_> { - fn column_type(&self) -> &ColumnType { - &self.column().column_type - } - - fn prefix(&self) -> &str { - match self { - QualColumn::Main(_) => "c.", - QualColumn::Child(_) => "i.", - } - } - - fn column(&self) -> &Column { - match self { - QualColumn::Main(column) => column, - QualColumn::Child(column) => column, - } - } -} - -impl std::fmt::Display for QualColumn<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - QualColumn::Main(column) => write!(f, "{}", column.name), - QualColumn::Child(column) => write!(f, "{}", column.name), - } - } -} - -impl QueryFragment for QualColumn<'_> { - fn walk_ast<'a>(&self, mut out: AstPass<'_, 'a, Pg>) -> QueryResult<()> { - out.push_sql(self.prefix()); - out.push_identifier(self.column().name.as_str()) - } } /// The equivalent of `EntityFilter` with columns resolved and various @@ -1362,29 +1264,29 @@ pub enum Filter<'a> { And(Vec>), Or(Vec>), PrefixCmp(PrefixComparison<'a>), - Cmp(QualColumn<'a>, Comparison, QueryValue<'a>), - In(QualColumn<'a>, Vec>), - NotIn(QualColumn<'a>, Vec>), + Cmp(dsl::Column<'a>, Comparison, QueryValue<'a>), + In(dsl::Column<'a>, Vec>), + NotIn(dsl::Column<'a>, Vec>), Contains { - column: QualColumn<'a>, + column: dsl::Column<'a>, op: ContainsOp, pattern: QueryValue<'a>, }, StartsOrEndsWith { - column: QualColumn<'a>, + column: dsl::Column<'a>, op: &'static str, pattern: String, }, - ChangeBlockGte(BlockRangeColumn<'a>), + ChangeBlockGte(dsl::ChangedSince<'a>), Child(Box>), /// The value is never null for fulltext queries - Fulltext(QualColumn<'a>, QueryValue<'a>), + Fulltext(dsl::Column<'a>, QueryValue<'a>), } impl<'a> Filter<'a> { pub fn main( layout: &'a Layout, - table: &'a Table, + table: dsl::Table<'a>, filter: &'a EntityFilter, block: BlockNumber, ) -> Result { @@ -1393,32 +1295,30 @@ impl<'a> Filter<'a> { fn new( layout: &'a Layout, - table: &'a Table, + table: dsl::Table<'a>, filter: &'a EntityFilter, block: BlockNumber, qual: ColumnQual, ) -> Result { fn column_and_value<'v>( - prefix: ColumnQual, - table: &'v Table, + table: dsl::Table<'v>, attr: &String, value: &'v Value, - ) -> Result<(QualColumn<'v>, QueryValue<'v>), StoreError> { + ) -> Result<(dsl::Column<'v>, QueryValue<'v>), StoreError> { + let column = table.column_for_field(attr)?; + let value = QueryValue::new(value, column.column_type())?; let column = table.column_for_field(attr)?; - let value = QueryValue::new(value, &column.column_type)?; - let column = prefix.with(table.column_for_field(attr)?); Ok((column, value)) } fn starts_or_ends_with<'s>( - qual: ColumnQual, - table: &'s Table, + table: dsl::Table<'s>, attr: &String, value: &Value, op: &'static str, starts_with: bool, ) -> Result, StoreError> { - let column = qual.with(table.column_for_field(attr)?); + let column = table.column_for_field(attr)?; match value { Value::String(s) => { @@ -1451,8 +1351,7 @@ impl<'a> Filter<'a> { } fn cmp<'s>( - qual: ColumnQual, - table: &'s Table, + table: dsl::Table<'s>, attr: &String, op: Comparison, value: &'s Value, @@ -1461,27 +1360,24 @@ impl<'a> Filter<'a> { op.suitable(value)?; - if column.use_prefix_comparison && !value.is_null() { - let column_type = &column.column_type; - let column = qual.with(column); + if column.use_prefix_comparison() && !value.is_null() { + let column_type = column.column_type(); PrefixComparison::new(op, column, column_type, value) .map(|pc| Filter::PrefixCmp(pc)) } else { - let value = QueryValue::new(value, &column.column_type)?; - let column = qual.with(column); + let value = QueryValue::new(value, column.column_type())?; Ok(Filter::Cmp(column, op, value)) } } fn contains<'s>( - qual: ColumnQual, - table: &'s Table, + table: dsl::Table<'s>, attr: &String, op: ContainsOp, value: &'s Value, ) -> Result, StoreError> { let column = table.column_for_field(attr)?; - let pattern = QueryValue::new(value, &column.column_type)?; + let pattern = QueryValue::new(value, column.column_type())?; let pattern = match &pattern.value { SqlValue::String(s) => { if s.starts_with('%') || s.ends_with('%') { @@ -1516,7 +1412,6 @@ impl<'a> Filter<'a> { | SqlValue::Bytes(_) | SqlValue::Binary(_) => pattern, }; - let column = qual.with(column); Ok(Filter::Contains { column, op, @@ -1541,57 +1436,49 @@ impl<'a> Filter<'a> { .map(|f| F::new(layout, table, f, block, qual)) .collect::>()?, )), - Equal(attr, value) => cmp(qual, table, attr, C::Equal, value), - Not(attr, value) => cmp(qual, table, attr, C::NotEqual, value), - GreaterThan(attr, value) => cmp(qual, table, attr, C::Greater, value), - LessThan(attr, value) => cmp(qual, table, attr, C::Less, value), - GreaterOrEqual(attr, value) => cmp(qual, table, attr, C::GreaterOrEqual, value), - LessOrEqual(attr, value) => cmp(qual, table, attr, C::LessOrEqual, value), + Equal(attr, value) => cmp(table, attr, C::Equal, value), + Not(attr, value) => cmp(table, attr, C::NotEqual, value), + GreaterThan(attr, value) => cmp(table, attr, C::Greater, value), + LessThan(attr, value) => cmp(table, attr, C::Less, value), + GreaterOrEqual(attr, value) => cmp(table, attr, C::GreaterOrEqual, value), + LessOrEqual(attr, value) => cmp(table, attr, C::LessOrEqual, value), In(attr, values) => { let column = table.column_for_field(attr.as_str())?; - let values = QueryValue::many(values, &column.column_type)?; - let column = qual.with(column); + let values = QueryValue::many(values, column.column_type())?; Ok(F::In(column, values)) } NotIn(attr, values) => { let column = table.column_for_field(attr.as_str())?; - let values = QueryValue::many(values, &column.column_type)?; - let column = qual.with(column); + let values = QueryValue::many(values, &column.column_type())?; Ok(F::NotIn(column, values)) } - Contains(attr, value) => contains(qual, table, attr, K::Like, value), - ContainsNoCase(attr, value) => contains(qual, table, attr, K::ILike, value), - NotContains(attr, value) => contains(qual, table, attr, K::NotLike, value), - NotContainsNoCase(attr, value) => contains(qual, table, attr, K::NotILike, value), + Contains(attr, value) => contains(table, attr, K::Like, value), + ContainsNoCase(attr, value) => contains(table, attr, K::ILike, value), + NotContains(attr, value) => contains(table, attr, K::NotLike, value), + NotContainsNoCase(attr, value) => contains(table, attr, K::NotILike, value), - StartsWith(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " like ", true) - } + StartsWith(attr, value) => starts_or_ends_with(table, attr, value, " like ", true), StartsWithNoCase(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " ilike ", true) + starts_or_ends_with(table, attr, value, " ilike ", true) } NotStartsWith(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " not like ", true) + starts_or_ends_with(table, attr, value, " not like ", true) } NotStartsWithNoCase(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " not ilike ", true) + starts_or_ends_with(table, attr, value, " not ilike ", true) } - EndsWith(attr, value) => starts_or_ends_with(qual, table, attr, value, " like ", false), + EndsWith(attr, value) => starts_or_ends_with(table, attr, value, " like ", false), EndsWithNoCase(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " ilike ", false) + starts_or_ends_with(table, attr, value, " ilike ", false) } NotEndsWith(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " not like ", false) + starts_or_ends_with(table, attr, value, " not like ", false) } NotEndsWithNoCase(attr, value) => { - starts_or_ends_with(qual, table, attr, value, " not ilike ", false) + starts_or_ends_with(table, attr, value, " not ilike ", false) } - ChangeBlockGte(num) => Ok(F::ChangeBlockGte(BlockRangeColumn::new( - table, - qual.prefix(), - *num, - ))), + ChangeBlockGte(num) => Ok(F::ChangeBlockGte(table.changed_since(*num))), Child(child) => { if !qual.allow_child() { return Err(StoreError::ChildFilterNestingNotSupportedError( @@ -1603,7 +1490,7 @@ impl<'a> Filter<'a> { Ok(F::Child(Box::new(child))) } Fulltext(attr, value) => { - let (column, value) = column_and_value(qual, table, attr, value)?; + let (column, value) = column_and_value(table, attr, value)?; if value.is_null() { return Err(StoreError::UnsupportedFilter( "fulltext".to_owned(), @@ -1637,7 +1524,7 @@ impl<'a> Filter<'a> { } fn cmp<'b>( - column: &'b QualColumn<'b>, + column: &'b dsl::Column<'b>, qv: &'b QueryValue<'b>, op: Comparison, mut out: AstPass<'_, 'b, Pg>, @@ -1660,7 +1547,7 @@ impl<'a> Filter<'a> { } fn fulltext<'b>( - column: &'b QualColumn, + column: &'b dsl::Column<'b>, qv: &'b QueryValue, mut out: AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { @@ -1671,7 +1558,7 @@ impl<'a> Filter<'a> { } fn contains<'b>( - column: &'b QualColumn, + column: &'b dsl::Column<'b>, op: &'b ContainsOp, qv: &'b QueryValue, mut out: AstPass<'_, 'b, Pg>, @@ -1736,7 +1623,7 @@ impl<'a> Filter<'a> { } fn in_array<'b>( - column: &'b QualColumn, + column: &'b dsl::Column<'b>, values: &'b [QueryValue], negated: bool, mut out: AstPass<'_, 'b, Pg>, @@ -1780,8 +1667,8 @@ impl<'a> Filter<'a> { } if have_non_nulls { - if column.column().use_prefix_comparison - && PrefixType::new(column).is_ok() + if column.use_prefix_comparison() + && PrefixType::new(&column).is_ok() && values.iter().all(|v| match &v.value { SqlValue::Text(s) => s.len() < STRING_PREFIX_SIZE, SqlValue::String(s) => s.len() < STRING_PREFIX_SIZE, @@ -1796,7 +1683,7 @@ impl<'a> Filter<'a> { // query optimizer // See PrefixComparison for a more detailed discussion of what // is happening here - PrefixType::new(column)?.push_column_prefix(column, &mut out.reborrow())?; + PrefixType::new(&column)?.push_column_prefix(&column, &mut out.reborrow())?; } else { column.walk_ast(out.reborrow())?; } @@ -1868,12 +1755,12 @@ impl<'a> fmt::Display for Filter<'a> { } => { write!(f, "{column} {op} '{pattern}'") } - ChangeBlockGte(b) => write!(f, "block >= {}", b.block()), + ChangeBlockGte(b) => write!(f, "{}", b), Child(child /* a, et, cf, _ */) => write!( f, "join on {} with {}({})", child.child_column.name(), - child.child_table.name, + child.child_from, child.child_filter ), } @@ -1908,69 +1795,13 @@ impl<'a> QueryFragment for Filter<'a> { out.push_sql(op); out.push_bind_param::(pattern)?; } - ChangeBlockGte(br_column) => br_column.changed_since(&mut out)?, + ChangeBlockGte(changed_since) => changed_since.walk_ast(out.reborrow())?, Child(child) => child.walk_ast(out)?, } Ok(()) } } -/// A query that finds an entity by key. Used during indexing. -/// See also `FindManyQuery`. -#[derive(Debug, Clone)] -pub struct FindQuery<'a> { - table: &'a Table, - key: &'a EntityKey, - br_column: BlockRangeColumn<'a>, -} - -impl<'a> FindQuery<'a> { - pub fn new(table: &'a Table, key: &'a EntityKey, block: BlockNumber) -> Self { - let br_column = BlockRangeColumn::new(table, "e.", block); - Self { - table, - key, - br_column, - } - } -} - -impl<'a> QueryFragment for FindQuery<'a> { - fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { - out.unsafe_to_cache_prepared(); - - // Generate - // select '..' as entity, to_jsonb(e.*) as data - // from schema.table e where id = $1 - out.push_sql("select "); - out.push_bind_param::(self.table.object.as_str())?; - out.push_sql(" as entity, to_jsonb(e.*) as data\n"); - out.push_sql(" from "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" e\n where "); - self.table.primary_key().eq(&self.key.entity_id, &mut out)?; - out.push_sql(" and "); - if self.table.has_causality_region { - out.push_sql("causality_region = "); - out.push_bind_param::(&self.key.causality_region)?; - out.push_sql(" and "); - } - self.br_column.contains(&mut out, true) - } -} - -impl<'a> QueryId for FindQuery<'a> { - type QueryId = (); - - const HAS_STATIC_QUERY_ID: bool = false; -} - -impl<'a> Query for FindQuery<'a> { - type SqlType = Untyped; -} - -impl<'a, Conn> RunQueryDsl for FindQuery<'a> {} - /// Builds a query over a given set of [`Table`]s in an attempt to find updated /// and/or newly inserted entities at a given block number; i.e. such that the /// block range's lower bound is equal to said block number. @@ -2131,9 +1962,7 @@ impl<'a> QueryFragment for FindManyQuery<'a> { out.push_sql(" from "); out.push_sql(table.qualified_name.as_str()); out.push_sql(" e\n where "); - table - .primary_key() - .is_in(&self.ids_for_type[&(table.object.clone(), *cr)], &mut out)?; + id_is_in(&self.ids_for_type[&(table.object.clone(), *cr)], &mut out)?; out.push_sql(" and "); if table.has_causality_region { out.push_sql("causality_region = "); @@ -2534,7 +2363,7 @@ impl ParentIds { /// corresponding table and column #[derive(Debug, Clone)] enum TableLink<'a> { - Direct(&'a Column, ChildMultiplicity), + Direct(dsl::Column<'a>, ChildMultiplicity), /// The `Table` is the parent table Parent(&'a Table, ParentIds), } @@ -2542,7 +2371,7 @@ enum TableLink<'a> { impl<'a> TableLink<'a> { fn new( layout: &'a Layout, - child_table: &'a Table, + child_table: dsl::Table<'a>, link: EntityLink, ) -> Result { match link { @@ -2606,7 +2435,8 @@ impl<'a> ParentLimit<'a> { #[derive(Debug)] pub struct FilterWindow<'a> { /// The table from which we take entities - table: &'a Table, + table: dsl::Table<'a>, + from_table: dsl::FromTable<'a>, /// The overall filter for the entire query query_filter: Option>, /// The parent ids we are interested in. The type in the database @@ -2618,7 +2448,7 @@ pub struct FilterWindow<'a> { /// How to filter by a set of parents link: TableLink<'a>, column_names: AttributeNames, - br_column: BlockRangeColumn<'a>, + at_block: AtBlock<'a>, } impl<'a> FilterWindow<'a> { @@ -2634,7 +2464,7 @@ impl<'a> FilterWindow<'a> { link, column_names, } = window; - let table = layout.table_for_entity(&child_type)?.as_ref(); + let table = layout.table_for_entity(&child_type)?.as_ref().dsl_table(); // Confidence check: ensure that all selected column names exist in the table if let AttributeNames::Select(ref selected_field_names) = column_names { @@ -2647,20 +2477,23 @@ impl<'a> FilterWindow<'a> { .map(|filter| Filter::main(layout, table, filter, block)) .transpose()?; let link = TableLink::new(layout, table, link)?; - let br_column = BlockRangeColumn::new(table, "c.", block); + let at_block = table + .at_block(block) + .filters_by_id(matches!(link, TableLink::Parent(_, _))); Ok(FilterWindow { table, + from_table: table.from_clause(), query_filter, ids, link, column_names, - br_column, + at_block, }) } fn parent_type(&self) -> QueryResult { match &self.link { - TableLink::Direct(column, _) => column.column_type.id_type(), + TableLink::Direct(column, _) => column.column_type().id_type(), TableLink::Parent(parent_table, _) => parent_table.primary_key().column_type.id_type(), } } @@ -2675,7 +2508,7 @@ impl<'a> FilterWindow<'a> { fn children_type_a<'b>( &'b self, - column: &Column, + column: &'b dsl::Column<'b>, is_outer: bool, limit: &'b ParentLimit<'_>, out: &mut AstPass<'_, 'b, Pg>, @@ -2698,12 +2531,12 @@ impl<'a> FilterWindow<'a> { out.push_sql(") as p(id) cross join lateral (select "); write_column_names(&self.column_names, self.table, None, out)?; out.push_sql(" from "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, false)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); - out.push_sql(" and p.id = any(c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(" and p.id = any("); + column.walk_ast(out.reborrow())?; out.push_sql(")"); self.and_filter(out)?; limit.restrict(is_outer, out)?; @@ -2713,7 +2546,7 @@ impl<'a> FilterWindow<'a> { fn child_type_a<'b>( &'b self, - column: &Column, + column: &'b dsl::Column<'b>, is_outer: bool, limit: &'b ParentLimit<'_>, out: &mut AstPass<'_, 'b, Pg>, @@ -2734,16 +2567,16 @@ impl<'a> FilterWindow<'a> { out.push_sql("\n/* child_type_a */ from unnest("); self.ids.push_bind_param(out)?; out.push_sql(") as p(id), "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, false)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); - out.push_sql(" and c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(" and "); + column.walk_ast(out.reborrow())?; out.push_sql(" @> array[p.id]"); if self.ids.len() < ENV_VARS.store.typea_batch_size { - out.push_sql(" and c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(" and "); + column.walk_ast(out.reborrow())?; out.push_sql(" && "); self.ids.push_bind_param(out)?; } @@ -2754,7 +2587,7 @@ impl<'a> FilterWindow<'a> { fn children_type_b<'b>( &'b self, - column: &Column, + column: &'b dsl::Column<'b>, is_outer: bool, limit: &'b ParentLimit<'_>, out: &mut AstPass<'_, 'b, Pg>, @@ -2777,12 +2610,12 @@ impl<'a> FilterWindow<'a> { out.push_sql(") as p(id) cross join lateral (select "); write_column_names(&self.column_names, self.table, None, out)?; out.push_sql(" from "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, false)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); - out.push_sql(" and p.id = c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(" and p.id = "); + column.walk_ast(out.reborrow())?; self.and_filter(out)?; limit.restrict(is_outer, out)?; out.push_sql(") c"); @@ -2791,7 +2624,7 @@ impl<'a> FilterWindow<'a> { fn child_type_b<'b>( &'b self, - column: &Column, + column: &'b dsl::Column<'b>, is_outer: bool, limit: &'b ParentLimit<'_>, out: &mut AstPass<'_, 'b, Pg>, @@ -2807,12 +2640,12 @@ impl<'a> FilterWindow<'a> { out.push_sql("\n/* child_type_b */ from unnest("); self.ids.push_bind_param(out)?; out.push_sql(") as p(id), "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, false)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); - out.push_sql(" and p.id = c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(" and p.id = "); + column.walk_ast(out.reborrow())?; self.and_filter(out)?; limit.single_limit(is_outer, self.ids.len(), out); Ok(()) @@ -2861,9 +2694,9 @@ impl<'a> FilterWindow<'a> { out.push_sql(" cross join lateral (select "); write_column_names(&self.column_names, self.table, None, out)?; out.push_sql(" from "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, true)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); out.push_sql(" and c.id = any(p.child_ids)"); self.and_filter(out)?; @@ -2879,8 +2712,8 @@ impl<'a> FilterWindow<'a> { out.push_sql("from unnest(array[]::text[]) as p(id) cross join (select "); write_column_names(&self.column_names, self.table, None, out)?; out.push_sql(" from "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where false) c"); + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where false) c"); } Ok(()) } @@ -2903,9 +2736,9 @@ impl<'a> FilterWindow<'a> { out.push_sql("), unnest("); child_ids.push_bind_param(out)?; out.push_sql(")) as p(id, child_id), "); - out.push_sql(self.table.qualified_name.as_str()); - out.push_sql(" c where "); - self.br_column.contains(out, true)?; + self.from_table.walk_ast(out.reborrow())?; + out.push_sql(" where "); + self.at_block.walk_ast(out.reborrow())?; limit.filter(is_outer, out); // Include a constraint on the child IDs as a set if the size of the set @@ -2966,7 +2799,7 @@ impl<'a> FilterWindow<'a> { mut out: AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { out.push_sql("select '"); - out.push_sql(self.table.object.as_str()); + out.push_sql(self.table.meta.object.as_str()); out.push_sql("' as entity, c.id, c.vid, p.id::text as "); out.push_sql(&*PARENT_ID); limit @@ -2985,10 +2818,11 @@ impl<'a> FilterWindow<'a> { #[derive(Debug)] pub struct WholeTable<'a> { - table: &'a Table, + table: dsl::Table<'a>, + from_table: dsl::FromTable<'a>, filter: Option>, column_names: AttributeNames, - br_column: BlockRangeColumn<'a>, + at_block: AtBlock<'a>, } impl<'a> WholeTable<'a> { @@ -2999,16 +2833,25 @@ impl<'a> WholeTable<'a> { column_names: AttributeNames, block: BlockNumber, ) -> Result { - let table = layout.table_for_entity(entity_type).map(|rc| rc.as_ref())?; + let table = layout + .table_for_entity(entity_type) + .map(|rc| rc.as_ref())? + .dsl_table(); let filter = entity_filter .map(|filter| Filter::main(layout, table, filter, block)) .transpose()?; - let br_column = BlockRangeColumn::new(table, "c.", block); + + let filters_by_id = { + matches!(filter.as_ref(), Some(Filter::Cmp(column, Comparison::Equal, _)) if column.is_primary_key()) + }; + + let at_block = table.at_block(block).filters_by_id(filters_by_id); Ok(WholeTable { table, + from_table: table.from_clause(), filter, column_names, - br_column, + at_block, }) } } @@ -3030,11 +2873,15 @@ impl<'a> fmt::Display for FilterCollection<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), std::fmt::Error> { fn fmt_table( f: &mut fmt::Formatter, - table: &Table, + table: dsl::Table<'_>, attrs: &AttributeNames, filter: &Option, ) -> Result<(), std::fmt::Error> { - write!(f, "{}[", table.qualified_name.as_str().replace("\\\"", ""))?; + write!( + f, + "{}[", + table.meta.qualified_name.as_str().replace("\\\"", "") + )?; match attrs { AttributeNames::All => write!(f, "*")?, AttributeNames::Select(cols) => write!(f, "{}", cols.iter().join(","))?, @@ -3049,13 +2896,14 @@ impl<'a> fmt::Display for FilterCollection<'a> { fn fmt_window(f: &mut fmt::Formatter, w: &FilterWindow) -> Result<(), std::fmt::Error> { let FilterWindow { table, + from_table: _, query_filter, ids, link, column_names, - br_column: _, + at_block: _, } = w; - fmt_table(f, table, column_names, query_filter)?; + fmt_table(f, *table, column_names, query_filter)?; if !ids.is_empty() { use ChildMultiplicity::*; @@ -3149,7 +2997,7 @@ impl<'a> FilterCollection<'a> { } } - fn first_table(&self) -> Option<&Table> { + fn first_table(&self) -> Option> { match self { FilterCollection::All(entities) => entities.first().map(|wh| wh.table), FilterCollection::SingleWindow(window) => Some(window.table), @@ -3167,10 +3015,10 @@ impl<'a> FilterCollection<'a> { fn all_mutable(&self) -> bool { match self { - FilterCollection::All(entities) => entities.iter().all(|wh| !wh.table.immutable), - FilterCollection::SingleWindow(window) => !window.table.immutable, + FilterCollection::All(entities) => entities.iter().all(|wh| !wh.table.meta.immutable), + FilterCollection::SingleWindow(window) => !window.table.meta.immutable, FilterCollection::MultiWindow(windows, _) => { - windows.iter().all(|window| !window.table.immutable) + windows.iter().all(|window| !window.table.meta.immutable) } } } @@ -3198,60 +3046,73 @@ impl<'a> FilterCollection<'a> { #[derive(Debug, Clone)] pub struct ChildKeyDetails<'a> { /// Column in the parent table that stores the connection between the parent and the child - pub parent_join_column: &'a Column, + pub parent_join_column: dsl::Column<'a>, /// Table representing the child entity - pub child_table: &'a Table, + pub child_table: dsl::Table<'a>, + pub child_from: dsl::FromTable<'a>, /// Column in the child table that stores the connection between the child and the parent - pub child_join_column: &'a Column, + pub child_join_column: dsl::Column<'a>, + pub child_at_block: dsl::AtBlock<'a>, /// Column of the child table that sorting is done on - pub sort_by_column: &'a Column, - /// Prefix for the child table - pub prefix: String, + pub sort_by_column: dsl::Column<'a>, /// Either `asc` or `desc` - pub direction: &'static str, + pub direction: SortDirection, } #[derive(Debug, Clone)] pub struct ChildKeyAndIdSharedDetails<'a> { - /// Table representing the parent entity - pub parent_table: &'a Table, /// Column in the parent table that stores the connection between the parent and the child - pub parent_join_column: &'a Column, + pub parent_join_column: dsl::Column<'a>, /// Table representing the child entity - pub child_table: &'a Table, + pub child_table: dsl::Table<'a>, + pub child_from: dsl::FromTable<'a>, /// Column in the child table that stores the connection between the child and the parent - pub child_join_column: &'a Column, + pub child_join_column: dsl::Column<'a>, + pub child_pk: dsl::Column<'a>, + pub child_br: dsl::BlockColumn<'a>, + pub child_at_block: dsl::AtBlock<'a>, /// Column of the child table that sorting is done on - pub sort_by_column: &'a Column, - /// Prefix for the child table - pub prefix: String, + pub sort_by_column: dsl::Column<'a>, /// Either `asc` or `desc` - pub direction: &'static str, + pub direction: SortDirection, } #[allow(unused)] #[derive(Debug, Clone)] pub struct ChildIdDetails<'a> { - /// Table representing the parent entity - pub parent_table: &'a Table, /// Column in the parent table that stores the connection between the parent and the child - pub parent_join_column: &'a Column, + pub parent_join_column: dsl::Column<'a>, /// Table representing the child entity - pub child_table: &'a Table, + pub child_table: dsl::Table<'a>, + pub child_from: dsl::FromTable<'a>, /// Column in the child table that stores the connection between the child and the parent - pub child_join_column: &'a Column, - /// Prefix for the child table - pub prefix: String, + pub child_join_column: dsl::Column<'a>, + pub child_pk: dsl::Column<'a>, + pub child_br: dsl::BlockColumn<'a>, + pub child_at_block: dsl::AtBlock<'a>, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UseBlockColumn { + Yes, + No, +} +impl UseBlockColumn { + fn block_column<'a>(&self, table: dsl::Table<'a>) -> Option> { + match self { + UseBlockColumn::Yes => Some(table.block_column()), + UseBlockColumn::No => None, + } + } } #[derive(Debug, Clone)] pub enum ChildKey<'a> { Single(ChildKeyDetails<'a>), - Many(Vec>), - IdAsc(ChildIdDetails<'a>, Option>), - IdDesc(ChildIdDetails<'a>, Option>), - ManyIdAsc(Vec>, Option>), - ManyIdDesc(Vec>, Option>), + /// First column is the primary key of the parent + Many(dsl::Column<'a>, Vec>), + Id(SortDirection, ChildIdDetails<'a>, UseBlockColumn), + ManyId(SortDirection, Vec>, UseBlockColumn), } /// Convenience to pass the name of the column to order by around. If `name` @@ -3259,15 +3120,13 @@ pub enum ChildKey<'a> { #[derive(Debug, Clone)] pub enum SortKey<'a> { None, - /// Order by `id asc` - IdAsc(Option>), - /// Order by `id desc` - IdDesc(Option>), + /// Order by `id , [block ]` + Id(SortDirection, Option>), /// Order by some other column; `column` will never be `id` Key { - column: &'a Column, + column: dsl::Column<'a>, value: Option<&'a str>, - direction: &'static str, + direction: SortDirection, }, /// Order by some other column; `column` will never be `id` ChildKey(ChildKey<'a>), @@ -3278,11 +3137,12 @@ impl<'a> fmt::Display for SortKey<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { SortKey::None => write!(f, "none"), - SortKey::IdAsc(Option::None) => write!(f, "{}", PRIMARY_KEY_COLUMN), - SortKey::IdAsc(Some(br)) => write!(f, "{}, {}", PRIMARY_KEY_COLUMN, br.column_name()), - SortKey::IdDesc(Option::None) => write!(f, "{} desc", PRIMARY_KEY_COLUMN), - SortKey::IdDesc(Some(br)) => { - write!(f, "{} desc, {} desc", PRIMARY_KEY_COLUMN, br.column_name()) + SortKey::Id(direction, br) => { + write!(f, "{}{}", PRIMARY_KEY_COLUMN, direction)?; + if let Some(br) = br { + write!(f, ", {} {}", PRIMARY_KEY_COLUMN, br)?; + } + Ok(()) } SortKey::Key { column, @@ -3290,105 +3150,53 @@ impl<'a> fmt::Display for SortKey<'a> { direction, } => write!( f, - "{} {}, {} {}", - column.name.as_str(), - direction, - PRIMARY_KEY_COLUMN, - direction + "{}{}, {}{}", + column, direction, PRIMARY_KEY_COLUMN, direction ), SortKey::ChildKey(child) => match child { ChildKey::Single(details) => write!( f, - "{}.{} {}, {}.{} {}", - details.child_table.name.as_str(), - details.sort_by_column.name.as_str(), + "{}{}, {}{}", + details.sort_by_column, details.direction, - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, + details.child_table.primary_key(), details.direction ), - ChildKey::Many(details) => details.iter().try_for_each(|details| { + ChildKey::Many(_, details) => details.iter().try_for_each(|details| { write!( f, - "{}.{} {}, {}.{} {}", - details.child_table.name.as_str(), - details.sort_by_column.name.as_str(), + "{}{}, {}{}", + details.sort_by_column, details.direction, - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, + details.child_table.primary_key(), details.direction ) }), - ChildKey::ManyIdAsc(details, Option::None) => { + ChildKey::ManyId(direction, details, UseBlockColumn::No) => { details.iter().try_for_each(|details| { - write!( - f, - "{}.{}", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN - ) + write!(f, "{}{direction}", details.child_table.primary_key()) }) } - ChildKey::ManyIdAsc(details, Some(br)) => details.iter().try_for_each(|details| { - write!( - f, - "{}.{}, {}.{}", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, - details.child_table.name.as_str(), - br.column_name() - ) - }), - ChildKey::ManyIdDesc(details, Option::None) => { + ChildKey::ManyId(direction, details, UseBlockColumn::Yes) => { details.iter().try_for_each(|details| { write!( f, - "{}.{} desc", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN + "{}{direction}, {}{direction}", + details.child_table.primary_key(), + details.child_table.block_column() ) }) } - ChildKey::ManyIdDesc(details, Some(br)) => details.iter().try_for_each(|details| { - write!( - f, - "{}.{} desc, {}.{} desc", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, - details.child_table.name.as_str(), - br.column_name() - ) - }), - - ChildKey::IdAsc(details, Option::None) => write!( - f, - "{}.{}", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN - ), - ChildKey::IdAsc(details, Some(br)) => write!( - f, - "{}.{}, {}.{}", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, - details.child_table.name.as_str(), - br.column_name() - ), - ChildKey::IdDesc(details, Option::None) => write!( - f, - "{}.{} desc", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN - ), - ChildKey::IdDesc(details, Some(br)) => { + ChildKey::Id(direction, details, UseBlockColumn::No) => { + write!(f, "{}{}", details.child_table.primary_key(), direction) + } + ChildKey::Id(direction, details, UseBlockColumn::Yes) => { write!( f, - "{}.{} desc, {}.{} desc", - details.child_table.name.as_str(), - PRIMARY_KEY_COLUMN, - details.child_table.name.as_str(), - br.column_name() + "{}{direction}, {}{direction}", + details.child_table.primary_key(), + details.child_br ) } }, @@ -3396,21 +3204,42 @@ impl<'a> fmt::Display for SortKey<'a> { } } -const ASC: &str = "asc"; -const DESC: &str = "desc"; +#[derive(Debug, Clone, Copy)] +pub enum SortDirection { + Asc, + Desc, +} + +impl SortDirection { + /// Generate either `""` or `" desc"`; convenient for SQL generation + /// without needing an additional space to separate it from preceding + /// text + fn as_sql(&self) -> &'static str { + match self { + SortDirection::Asc => "", + SortDirection::Desc => " desc", + } + } +} + +impl std::fmt::Display for SortDirection { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.as_sql()) + } +} impl<'a> SortKey<'a> { fn new( order: EntityOrder, collection: &'a FilterCollection, filter: Option<&'a EntityFilter>, - block: BlockNumber, layout: &'a Layout, + block: BlockNumber, ) -> Result { fn sort_key_from_value<'a>( - column: &'a Column, + column: dsl::Column<'a>, value: &'a Value, - direction: &'static str, + direction: SortDirection, ) -> Result, QueryExecutionError> { let sort_value = value.as_str(); @@ -3422,11 +3251,11 @@ impl<'a> SortKey<'a> { } fn with_key<'a>( - table: &'a Table, + table: dsl::Table<'a>, attribute: String, filter: Option<&'a EntityFilter>, - direction: &'static str, - br_column: Option>, + direction: SortDirection, + use_block_column: UseBlockColumn, ) -> Result, QueryExecutionError> { let column = table.column_for_field(&attribute)?; if column.is_fulltext() { @@ -3443,11 +3272,8 @@ impl<'a> SortKey<'a> { _ => unreachable!(), } } else if column.is_primary_key() { - match direction { - ASC => Ok(SortKey::IdAsc(br_column)), - DESC => Ok(SortKey::IdDesc(br_column)), - _ => unreachable!("direction is 'asc' or 'desc'"), - } + let block_column = use_block_column.block_column(table); + Ok(SortKey::Id(direction, block_column)) } else { Ok(SortKey::Key { column, @@ -3458,14 +3284,16 @@ impl<'a> SortKey<'a> { } fn with_child_object_key<'a>( - parent_table: &'a Table, - child_table: &'a Table, + block: BlockNumber, + parent_table: dsl::Table<'a>, + child_table: dsl::Table<'a>, join_attribute: String, derived: bool, attribute: String, - br_column: Option>, - direction: &'static str, + use_block_column: UseBlockColumn, + direction: SortDirection, ) -> Result, QueryExecutionError> { + let child_table = child_table.child(1); let sort_by_column = child_table.column_for_field(&attribute)?; if sort_by_column.is_fulltext() { Err(QueryExecutionError::NotSupported( @@ -3479,7 +3307,7 @@ impl<'a> SortKey<'a> { graph::constraint_violation!( "Column for a join attribute `{}` of `{}` table not found", join_attribute, - child_table.name.as_str() + child_table.name() ) })?, ), @@ -3490,7 +3318,7 @@ impl<'a> SortKey<'a> { graph::constraint_violation!( "Column for a join attribute `{}` of `{}` table not found", join_attribute, - parent_table.name.as_str() + parent_table.name() ) })?, child_table.primary_key(), @@ -3498,38 +3326,37 @@ impl<'a> SortKey<'a> { }; if sort_by_column.is_primary_key() { - return match direction { - ASC => Ok(SortKey::ChildKey(ChildKey::IdAsc( - ChildIdDetails { - parent_table, - child_table, - parent_join_column: parent_column, - child_join_column: child_column, - prefix: "cc".to_string(), - }, - br_column, - ))), - DESC => Ok(SortKey::ChildKey(ChildKey::IdDesc( - ChildIdDetails { - parent_table, - child_table, - parent_join_column: parent_column, - child_join_column: child_column, - prefix: "cc".to_string(), - }, - br_column, - ))), - _ => unreachable!("direction is 'asc' or 'desc'"), - }; + let child_from = child_table.from_clause(); + let child_pk = child_table.primary_key(); + let child_br = child_table.block_column(); + let child_at_block = child_table.at_block(block); + + return Ok(SortKey::ChildKey(ChildKey::Id( + direction, + ChildIdDetails { + child_table, + child_from, + parent_join_column: parent_column, + child_join_column: child_column, + child_pk, + child_br, + child_at_block, + }, + use_block_column, + ))); } + let child_table = child_table.child(1); + let child_at_block = child_table.at_block(block); + let child_from = child_table.from_clause(); Ok(SortKey::ChildKey(ChildKey::Single(ChildKeyDetails { - child_table, + child_table: child_table.child(1), + child_from, parent_join_column: parent_column, child_join_column: child_column, + child_at_block, // Sort by this column sort_by_column, - prefix: "cc".to_string(), direction, }))) } @@ -3537,16 +3364,21 @@ impl<'a> SortKey<'a> { fn build_children_vec<'a>( layout: &'a Layout, - parent_table: &'a Table, + block: BlockNumber, + parent_table: dsl::Table<'a>, entity_types: Vec, child: EntityOrderByChildInfo, - direction: &'static str, + direction: SortDirection, ) -> Result>, QueryExecutionError> { + assert!(entity_types.len() < 255); return entity_types .iter() .enumerate() .map(|(i, entity_type)| { - let child_table = layout.table_for_entity(entity_type)?; + let child_table = layout + .table_for_entity(entity_type)? + .dsl_table() + .child((i + 1) as u8); let sort_by_column = child_table.column_for_field(&child.sort_by_attribute)?; if sort_by_column.is_fulltext() { Err(QueryExecutionError::NotSupported( @@ -3562,7 +3394,7 @@ impl<'a> SortKey<'a> { graph::constraint_violation!( "Column for a join attribute `{}` of `{}` table not found", child.join_attribute, - child_table.name.as_str() + child_table.name() ) })?, ), @@ -3573,19 +3405,25 @@ impl<'a> SortKey<'a> { graph::constraint_violation!( "Column for a join attribute `{}` of `{}` table not found", child.join_attribute, - parent_table.name.as_str() + parent_table.name() ) })?, child_table.primary_key(), ), }; + let child_pk = child_table.primary_key(); + let child_br = child_table.block_column(); + let child_at_block = child_table.at_block(block); + let child_from = child_table.from_clause(); Ok(ChildKeyAndIdSharedDetails { - parent_table, child_table, + child_from, parent_join_column: parent_column, child_join_column: child_column, - prefix: format!("cc{}", i), + child_pk, + child_br, + child_at_block, sort_by_column, direction, }) @@ -3596,79 +3434,74 @@ impl<'a> SortKey<'a> { fn with_child_interface_key<'a>( layout: &'a Layout, - parent_table: &'a Table, + block: BlockNumber, + parent_table: dsl::Table<'a>, child: EntityOrderByChildInfo, entity_types: Vec, - br_column: Option>, - direction: &'static str, + use_block_column: UseBlockColumn, + direction: SortDirection, ) -> Result, QueryExecutionError> { - if let Some(first_entity) = entity_types.first() { - let child_table = layout.table_for_entity(first_entity)?; - let sort_by_column = child_table.column_for_field(&child.sort_by_attribute)?; + if entity_types.is_empty() { + return Err(QueryExecutionError::ConstraintViolation( + "Cannot order by child interface with no implementing entity types".to_string(), + )); + } - if sort_by_column.is_fulltext() { - Err(QueryExecutionError::NotSupported( - "Sorting by fulltext fields".to_string(), - )) - } else if sort_by_column.is_primary_key() { - if direction == ASC { - Ok(SortKey::ChildKey(ChildKey::ManyIdAsc( - build_children_vec( - layout, - parent_table, - entity_types, - child, - direction, - )? - .iter() - .map(|details| ChildIdDetails { - parent_table: details.parent_table, - child_table: details.child_table, - parent_join_column: details.parent_join_column, - child_join_column: details.child_join_column, - prefix: details.prefix.clone(), - }) - .collect(), - br_column, - ))) - } else { - Ok(SortKey::ChildKey(ChildKey::ManyIdDesc( - build_children_vec( - layout, - parent_table, - entity_types, - child, - direction, - )? - .iter() - .map(|details| ChildIdDetails { - parent_table: details.parent_table, - child_table: details.child_table, - parent_join_column: details.parent_join_column, - child_join_column: details.child_join_column, - prefix: details.prefix.clone(), - }) - .collect(), - br_column, - ))) - } - } else { - Ok(SortKey::ChildKey(ChildKey::Many( - build_children_vec(layout, parent_table, entity_types, child, direction)? - .iter() - .map(|details| ChildKeyDetails { - parent_join_column: details.parent_join_column, - child_table: details.child_table, - child_join_column: details.child_join_column, - sort_by_column: details.sort_by_column, - prefix: details.prefix.clone(), - direction: details.direction, - }) - .collect(), - ))) - } + let first_entity = entity_types.first().unwrap(); + let child_table = layout.table_for_entity(first_entity)?; + let sort_by_column = child_table.column_for_field(&child.sort_by_attribute)?; + + if sort_by_column.is_fulltext() { + Err(QueryExecutionError::NotSupported( + "Sorting by fulltext fields".to_string(), + )) + } else if sort_by_column.is_primary_key() { + Ok(SortKey::ChildKey(ChildKey::ManyId( + direction, + build_children_vec( + layout, + block, + parent_table, + entity_types, + child, + direction, + )? + .iter() + .map(|details| ChildIdDetails { + child_table: details.child_table, + child_from: details.child_from, + parent_join_column: details.parent_join_column, + child_join_column: details.child_join_column, + child_pk: details.child_pk, + child_br: details.child_br, + child_at_block: details.child_at_block, + }) + .collect(), + use_block_column, + ))) } else { - Ok(SortKey::ChildKey(ChildKey::Many(vec![]))) + Ok(SortKey::ChildKey(ChildKey::Many( + parent_table.primary_key(), + build_children_vec( + layout, + block, + parent_table, + entity_types, + child, + direction, + )? + .iter() + .map(|details| ChildKeyDetails { + parent_join_column: details.parent_join_column, + child_table: details.child_table, + child_from: details.child_from, + child_join_column: details.child_join_column, + child_at_block: details.child_at_block, + sort_by_column: details.sort_by_column, + direction: details.direction, + }) + .collect(), + ))) } } @@ -3680,63 +3513,80 @@ impl<'a> SortKey<'a> { .first_table() .expect("an entity query always contains at least one entity type/table"); - let br_column = if collection.all_mutable() && ENV_VARS.store.order_by_block_range { - Some(BlockRangeColumn::new(table, "c.", block)) + let use_block_column = if collection.all_mutable() && ENV_VARS.store.order_by_block_range { + UseBlockColumn::Yes } else { - None + UseBlockColumn::No }; + use SortDirection::*; match order { - EntityOrder::Ascending(attr, _) => with_key(table, attr, filter, ASC, br_column), - EntityOrder::Descending(attr, _) => with_key(table, attr, filter, DESC, br_column), - EntityOrder::Default => Ok(SortKey::IdAsc(br_column)), + EntityOrder::Ascending(attr, _) => with_key(table, attr, filter, Asc, use_block_column), + EntityOrder::Descending(attr, _) => { + with_key(table, attr, filter, Desc, use_block_column) + } + EntityOrder::Default => Ok(SortKey::Id(Asc, use_block_column.block_column(table))), EntityOrder::Unordered => Ok(SortKey::None), EntityOrder::ChildAscending(kind) => match kind { EntityOrderByChild::Object(child, entity_type) => with_child_object_key( + block, table, - layout.table_for_entity(&entity_type)?, + layout.table_for_entity(&entity_type)?.dsl_table(), child.join_attribute, child.derived, child.sort_by_attribute, - br_column, - ASC, + use_block_column, + Asc, + ), + EntityOrderByChild::Interface(child, entity_types) => with_child_interface_key( + layout, + block, + table, + child, + entity_types, + use_block_column, + Asc, ), - EntityOrderByChild::Interface(child, entity_types) => { - with_child_interface_key(layout, table, child, entity_types, br_column, ASC) - } }, EntityOrder::ChildDescending(kind) => match kind { EntityOrderByChild::Object(child, entity_type) => with_child_object_key( + block, table, - layout.table_for_entity(&entity_type)?, + layout.table_for_entity(&entity_type)?.dsl_table(), child.join_attribute, child.derived, child.sort_by_attribute, - br_column, - DESC, + use_block_column, + Desc, + ), + EntityOrderByChild::Interface(child, entity_types) => with_child_interface_key( + layout, + block, + table, + child, + entity_types, + use_block_column, + Desc, ), - EntityOrderByChild::Interface(child, entity_types) => { - with_child_interface_key(layout, table, child, entity_types, br_column, DESC) - } }, } } /// Generate selecting the sort key if it is needed - fn select( - &self, - out: &mut AstPass, + fn select<'b>( + &'b self, + out: &mut AstPass<'_, 'b, Pg>, select_statement_level: SelectStatementLevel, ) -> QueryResult<()> { match self { SortKey::None => {} - SortKey::IdAsc(br_column) | SortKey::IdDesc(br_column) => { + SortKey::Id(_, br_column) => { if let Some(br_column) = br_column { out.push_sql(", "); match select_statement_level { SelectStatementLevel::InnerStatement => { - br_column.name(out); + br_column.walk_ast(out.reborrow())?; out.push_sql(" as "); out.push_sql(SORT_KEY_COLUMN); } @@ -3755,8 +3605,8 @@ impl<'a> SortKey<'a> { match select_statement_level { SelectStatementLevel::InnerStatement => { - out.push_sql(", c."); - out.push_identifier(column.name.as_str())?; + out.push_sql(", "); + column.walk_ast(out.reborrow())?; out.push_sql(" as "); out.push_sql(SORT_KEY_COLUMN); } @@ -3776,9 +3626,7 @@ impl<'a> SortKey<'a> { match select_statement_level { SelectStatementLevel::InnerStatement => { out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - out.push_identifier(child.sort_by_column.name.as_str())?; + child.sort_by_column.walk_ast(out.reborrow())?; } SelectStatementLevel::OuterStatement => { out.push_sql(", "); @@ -3786,36 +3634,27 @@ impl<'a> SortKey<'a> { } } } - ChildKey::Many(children) => { + ChildKey::Many(_, children) => { for child in children.iter() { if child.sort_by_column.is_primary_key() { return Err(constraint_violation!("SortKey::Key never uses 'id'")); } out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - out.push_identifier(child.sort_by_column.name.as_str())?; + child.sort_by_column.walk_ast(out.reborrow())?; } } - ChildKey::ManyIdAsc(children, br_column) - | ChildKey::ManyIdDesc(children, br_column) => { + ChildKey::ManyId(_, children, UseBlockColumn::Yes) => { for child in children.iter() { - if let Some(br_column) = br_column { - out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - br_column.name(out); - } - } - } - ChildKey::IdAsc(child, br_column) | ChildKey::IdDesc(child, br_column) => { - if let Some(br_column) = br_column { out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - br_column.name(out); + child.child_br.walk_ast(out.reborrow())?; } } + ChildKey::ManyId(_, _, UseBlockColumn::No) => { /* nothing to do */ } + ChildKey::Id(_, child, UseBlockColumn::Yes) => { + out.push_sql(", "); + child.child_br.walk_ast(out.reborrow())?; + } + ChildKey::Id(_, _, UseBlockColumn::No) => { /* nothing to do */ } } if let SelectStatementLevel::InnerStatement = select_statement_level { @@ -3836,33 +3675,19 @@ impl<'a> SortKey<'a> { ) -> QueryResult<()> { match self { SortKey::None => Ok(()), - SortKey::IdAsc(br_column) => { + SortKey::Id(direction, br_column) => { out.push_sql("order by "); out.push_identifier(PRIMARY_KEY_COLUMN)?; + out.push_sql(direction.as_sql()); if let Some(br_column) = br_column { if use_sort_key_alias { out.push_sql(", "); out.push_sql(SORT_KEY_COLUMN); } else { out.push_sql(", "); - br_column.bare_name(out); + out.push_sql(br_column.name()); } - } - Ok(()) - } - SortKey::IdDesc(br_column) => { - out.push_sql("order by "); - out.push_identifier(PRIMARY_KEY_COLUMN)?; - out.push_sql(" desc"); - if let Some(br_column) = br_column { - if use_sort_key_alias { - out.push_sql(", "); - out.push_sql(SORT_KEY_COLUMN); - } else { - out.push_sql(", "); - br_column.bare_name(out); - } - out.push_sql(" desc"); + out.push_sql(direction.as_sql()); } Ok(()) } @@ -3872,75 +3697,37 @@ impl<'a> SortKey<'a> { direction, } => { out.push_sql("order by "); - SortKey::sort_expr( - column, - value, - direction, - None, - None, - use_sort_key_alias, - out, - ) + SortKey::sort_expr(column, value, direction, None, use_sort_key_alias, out) } SortKey::ChildKey(child) => { out.push_sql("order by "); match child { ChildKey::Single(child) => SortKey::sort_expr( - child.sort_by_column, + &child.sort_by_column, &None, - child.direction, - Some(&child.prefix), + &child.direction, Some("c"), use_sort_key_alias, out, ), - ChildKey::Many(children) => { - let columns: Vec<(&Column, &str)> = children - .iter() - .map(|child| (child.sort_by_column, child.prefix.as_str())) - .collect(); - SortKey::multi_sort_expr( - columns, - children.first().unwrap().direction, - Some("c"), - out, - ) - } + ChildKey::Many(parent_pk, children) => SortKey::multi_sort_expr( + parent_pk, + children, + &children.first().unwrap().direction, + out, + ), - ChildKey::ManyIdAsc(children, br_column) => { - let prefixes: Vec<&str> = - children.iter().map(|child| child.prefix.as_str()).collect(); - SortKey::multi_sort_id_expr(prefixes, ASC, br_column, out) - } - ChildKey::ManyIdDesc(children, br_column) => { - let prefixes: Vec<&str> = - children.iter().map(|child| child.prefix.as_str()).collect(); - SortKey::multi_sort_id_expr(prefixes, DESC, br_column, out) + ChildKey::ManyId(direction, children, use_block_column) => { + SortKey::multi_sort_id_expr(children, *direction, *use_block_column, out) } - ChildKey::IdAsc(child, br_column) => { - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - out.push_identifier(PRIMARY_KEY_COLUMN)?; - if let Some(br_column) = br_column { - out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - br_column.bare_name(out); - } - Ok(()) - } - ChildKey::IdDesc(child, br_column) => { - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - out.push_identifier(PRIMARY_KEY_COLUMN)?; - out.push_sql(" desc"); - if let Some(br_column) = br_column { + ChildKey::Id(direction, child, use_block_column) => { + child.child_pk.walk_ast(out.reborrow())?; + out.push_sql(direction.as_sql()); + if UseBlockColumn::Yes == *use_block_column { out.push_sql(", "); - out.push_sql(child.prefix.as_str()); - out.push_sql("."); - br_column.bare_name(out); - out.push_sql(" desc"); + child.child_br.walk_ast(out.reborrow())?; + out.push_sql(direction.as_sql()); } Ok(()) } @@ -3967,14 +3754,10 @@ impl<'a> SortKey<'a> { match self { SortKey::None => Ok(()), - SortKey::IdAsc(_) => { - order_by_parent_id(out); - out.push_identifier(PRIMARY_KEY_COLUMN) - } - SortKey::IdDesc(_) => { + SortKey::Id(direction, _) => { order_by_parent_id(out); out.push_identifier(PRIMARY_KEY_COLUMN)?; - out.push_sql(" desc"); + out.push_sql(direction.as_sql()); Ok(()) } SortKey::Key { @@ -3983,15 +3766,7 @@ impl<'a> SortKey<'a> { direction, } => { order_by_parent_id(out); - SortKey::sort_expr( - column, - value, - direction, - None, - None, - use_sort_key_alias, - out, - ) + SortKey::sort_expr(column, value, direction, None, use_sort_key_alias, out) } SortKey::ChildKey(_) => Err(diesel::result::Error::QueryBuilderError( "SortKey::ChildKey cannot be used for parent ordering (yet)".into(), @@ -4002,10 +3777,9 @@ impl<'a> SortKey<'a> { /// Generate /// [name direction,] id fn sort_expr<'b>( - column: &Column, + column: &'b dsl::Column<'b>, value: &'b Option<&str>, - direction: &str, - column_prefix: Option<&str>, + direction: &'b SortDirection, rest_prefix: Option<&str>, use_sort_key_alias: bool, out: &mut AstPass<'_, 'b, Pg>, @@ -4024,7 +3798,7 @@ impl<'a> SortKey<'a> { } } - match &column.column_type { + match column.column_type() { ColumnType::TSVector(config) => { let algorithm = match config.algorithm { FulltextAlgorithm::Rank => "ts_rank(", @@ -4034,9 +3808,7 @@ impl<'a> SortKey<'a> { if use_sort_key_alias { out.push_sql(SORT_KEY_COLUMN); } else { - let name = column.name.as_str(); - push_prefix(column_prefix, out); - out.push_identifier(name)?; + column.walk_ast(out.reborrow())?; } out.push_sql(", to_tsquery("); @@ -4048,41 +3820,38 @@ impl<'a> SortKey<'a> { if use_sort_key_alias { out.push_sql(SORT_KEY_COLUMN); } else { - let name = column.name.as_str(); - push_prefix(column_prefix, out); - out.push_identifier(name)?; + column.walk_ast(out.reborrow())?; } } } - out.push_sql(" "); - out.push_sql(direction); + out.push_sql(direction.as_sql()); out.push_sql(", "); if !use_sort_key_alias { push_prefix(rest_prefix, out); } out.push_identifier(PRIMARY_KEY_COLUMN)?; - out.push_sql(" "); - out.push_sql(direction); + out.push_sql(direction.as_sql()); Ok(()) } /// Generate /// [COALESCE(name1, name2) direction,] id1, id2 - fn multi_sort_expr( - columns: Vec<(&Column, &str)>, - direction: &str, - rest_prefix: Option<&str>, - out: &mut AstPass, + fn multi_sort_expr<'b>( + parent_pk: &'b dsl::Column<'b>, + children: &'b [ChildKeyDetails<'b>], + direction: &'b SortDirection, + out: &mut AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { - for (column, _) in columns.iter() { - if column.is_primary_key() { + for child in children { + let sort_by = &child.sort_by_column; + if sort_by.is_primary_key() { // This shouldn't happen since we'd use SortKey::ManyIdAsc/ManyDesc return Err(constraint_violation!( "multi_sort_expr called with primary key column" )); } - match column.column_type { + match sort_by.column_type() { ColumnType::TSVector(_) => { return Err(constraint_violation!("TSVector is not supported")); } @@ -4090,133 +3859,104 @@ impl<'a> SortKey<'a> { } } - fn push_prefix(prefix: Option<&str>, out: &mut AstPass) { - if let Some(prefix) = prefix { - out.push_sql(prefix); - out.push_sql("."); - } - } - out.push_sql("coalesce("); - for (i, (column, prefix)) in columns.iter().enumerate() { - if i != 0 { + let mut first = true; + for child in children { + if first { + first = false; + } else { out.push_sql(", "); } - let name = column.name.as_str(); - push_prefix(Some(prefix), out); - out.push_identifier(name)?; + child.sort_by_column.walk_ast(out.reborrow())?; } - out.push_sql(") "); + out.push_sql(")"); - out.push_sql(direction); + out.push_sql(direction.as_sql()); out.push_sql(", "); - push_prefix(rest_prefix, out); - out.push_identifier(PRIMARY_KEY_COLUMN)?; - out.push_sql(" "); - out.push_sql(direction); + + parent_pk.walk_ast(out.reborrow())?; + out.push_sql(direction.as_sql()); Ok(()) } /// Generate /// COALESCE(id1, id2) direction, [COALESCE(br_column1, br_column2) direction] - fn multi_sort_id_expr( - prefixes: Vec<&str>, - direction: &str, - br_column: &Option, - out: &mut AstPass, + fn multi_sort_id_expr<'b>( + children: &'b [ChildIdDetails<'b>], + direction: SortDirection, + use_block_column: UseBlockColumn, + out: &mut AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { - fn push_prefix(prefix: Option<&str>, out: &mut AstPass) { - if let Some(prefix) = prefix { - out.push_sql(prefix); - out.push_sql("."); - } - } - out.push_sql("coalesce("); - for (i, prefix) in prefixes.iter().enumerate() { - if i != 0 { + let mut first = true; + for child in children { + if first { + first = false; + } else { out.push_sql(", "); } - push_prefix(Some(prefix), out); - out.push_identifier(PRIMARY_KEY_COLUMN)?; + child.child_join_column.walk_ast(out.reborrow())?; } - out.push_sql(") "); + out.push_sql(")"); - out.push_sql(direction); + out.push_sql(direction.as_sql()); - if let Some(br_column) = br_column { + if UseBlockColumn::Yes == use_block_column { out.push_sql(", coalesce("); - for (i, prefix) in prefixes.iter().enumerate() { - if i != 0 { + let mut first = true; + for child in children { + if first { + first = false; + } else { out.push_sql(", "); } - push_prefix(Some(prefix), out); - br_column.bare_name(out); + child.child_br.walk_ast(out.reborrow())?; } - out.push_sql(") "); - out.push_sql(direction); + out.push_sql(")"); + out.push_sql(direction.as_sql()); } Ok(()) } - fn add_child<'b>( - &self, - block: &'b BlockNumber, - out: &mut AstPass<'_, 'b, Pg>, - ) -> QueryResult<()> { + fn add_child<'b>(&'b self, out: &mut AstPass<'_, 'b, Pg>) -> QueryResult<()> { fn add<'b>( - block: &'b BlockNumber, - child_table: &Table, - child_column: &Column, - parent_column: &Column, - prefix: &str, + child_from: &'b dsl::FromTable<'b>, + child_column: &'b dsl::Column<'b>, + child_at_block: &'b dsl::AtBlock<'b>, + parent_column: &'b dsl::Column<'b>, out: &mut AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { out.push_sql(" left join "); - out.push_sql(child_table.qualified_name.as_str()); - out.push_sql(" as "); - out.push_sql(prefix); + child_from.walk_ast(out.reborrow())?; out.push_sql(" on ("); if child_column.is_list() { // Type C: p.id = any(c.child_ids) - out.push_sql("c."); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; out.push_sql(" = any("); - out.push_sql(prefix); - out.push_sql("."); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; out.push_sql(")"); } else if parent_column.is_list() { // Type A: c.id = any(p.{parent_field}) - out.push_sql(prefix); - out.push_sql("."); - out.push_identifier(child_column.name.as_str())?; - out.push_sql(" = any(c."); - out.push_identifier(parent_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; + out.push_sql(" = any("); + parent_column.walk_ast(out.reborrow())?; out.push_sql(")"); } else { // Type B: c.id = p.{parent_field} - out.push_sql(prefix); - out.push_sql("."); - out.push_identifier(child_column.name.as_str())?; + child_column.walk_ast(out.reborrow())?; out.push_sql(" = "); - out.push_sql("c."); - out.push_identifier(parent_column.name.as_str())?; + parent_column.walk_ast(out.reborrow())?; } out.push_sql(" and "); - out.push_sql(prefix); - out.push_sql("."); - out.push_identifier(BLOCK_RANGE_COLUMN)?; - out.push_sql(" @> "); - out.push_bind_param::(block)?; + child_at_block.walk_ast(out.reborrow())?; out.push_sql(") "); Ok(()) @@ -4226,45 +3966,41 @@ impl<'a> SortKey<'a> { SortKey::ChildKey(nested) => match nested { ChildKey::Single(child) => { add( - block, - child.child_table, - child.child_join_column, - child.parent_join_column, - &child.prefix, + &child.child_from, + &child.child_join_column, + &child.child_at_block, + &child.parent_join_column, out, )?; } - ChildKey::Many(children) => { + ChildKey::Many(_, children) => { for child in children.iter() { add( - block, - child.child_table, - child.child_join_column, - child.parent_join_column, - &child.prefix, + &child.child_from, + &child.child_join_column, + &child.child_at_block, + &child.parent_join_column, out, )?; } } - ChildKey::ManyIdAsc(children, _) | ChildKey::ManyIdDesc(children, _) => { + ChildKey::ManyId(_, children, _) => { for child in children.iter() { add( - block, - child.child_table, - child.child_join_column, - child.parent_join_column, - &child.prefix, + &child.child_from, + &child.child_join_column, + &child.child_at_block, + &child.parent_join_column, out, )?; } } - ChildKey::IdAsc(child, _) | ChildKey::IdDesc(child, _) => { + ChildKey::Id(_, child, _) => { add( - block, - child.child_table, - child.child_join_column, - child.parent_join_column, - &child.prefix, + &child.child_from, + &child.child_join_column, + &child.child_at_block, + &child.parent_join_column, out, )?; } @@ -4349,7 +4085,7 @@ impl<'a> FilterQuery<'a> { query_id: Option, site: &'a Site, ) -> Result { - let sort_key = SortKey::new(order, collection, filter, block, layout)?; + let sort_key = SortKey::new(order, collection, filter, layout, block)?; let range = FilterRange(range); let limit = ParentLimit { sort_key, range }; @@ -4374,18 +4110,13 @@ impl<'a> FilterQuery<'a> { out: &mut AstPass<'_, 'b, Pg>, ) -> QueryResult<()> { out.push_sql("\n from "); - out.push_sql(wh.table.qualified_name.as_str()); - out.push_sql(" c"); + wh.from_table.walk_ast(out.reborrow())?; - self.limit.sort_key.add_child(&self.block, out)?; + self.limit.sort_key.add_child(out)?; out.push_sql("\n where "); - let filters_by_id = { - matches!(wh.filter.as_ref(), Some(Filter::Cmp(column, Comparison::Equal, _)) if column.column().is_primary_key()) - }; - - wh.br_column.contains(out, filters_by_id)?; + wh.at_block.walk_ast(out.reborrow())?; if let Some(filter) = &wh.filter { out.push_sql(" and "); filter.walk_ast(out.reborrow())?; @@ -4394,9 +4125,9 @@ impl<'a> FilterQuery<'a> { Ok(()) } - fn select_entity_and_data(table: &Table, out: &mut AstPass) { + fn select_entity_and_data(table: dsl::Table<'_>, out: &mut AstPass) { out.push_sql("select '"); - out.push_sql(table.object.as_str()); + out.push_sql(table.meta.object.as_str()); out.push_sql("' as entity, to_jsonb(c.*) as data"); } @@ -4493,7 +4224,7 @@ impl<'a> FilterQuery<'a> { // c.vid, // c.${sort_key} out.push_sql("select '"); - out.push_sql(wh.table.object.as_str()); + out.push_sql(wh.table.meta.object.as_str()); out.push_sql("' as entity, c.id, c.vid"); self.limit .sort_key @@ -4518,11 +4249,11 @@ impl<'a> FilterQuery<'a> { .sort_key .select(out, SelectStatementLevel::OuterStatement)?; out.push_sql("\n from "); - out.push_sql(wh.table.qualified_name.as_str()); - out.push_sql(" c,"); + wh.from_table.walk_ast(out.reborrow())?; + out.push_sql(" ,"); out.push_sql(" matches m"); out.push_sql("\n where c.vid = m.vid and m.entity = "); - out.push_bind_param::(wh.table.object.as_str())?; + out.push_bind_param::(wh.table.meta.object.as_str())?; } out.push_sql("\n "); self.limit.sort_key.order_by(out, true)?; @@ -4591,8 +4322,8 @@ impl<'a> FilterQuery<'a> { .iter() .unique_by(|window| { ( - &window.table.qualified_name, - &window.table.object, + &window.table.meta.qualified_name, + &window.table.meta.object, &window.column_names, ) }) @@ -4606,9 +4337,9 @@ impl<'a> FilterQuery<'a> { jsonb_build_object(&window.column_names, "c", window.table, out)?; out.push_sql("|| jsonb_build_object('g$parent_id', m.g$parent_id) as data"); out.push_sql("\n from "); - out.push_sql(window.table.qualified_name.as_str()); - out.push_sql(" c, matches m\n where c.vid = m.vid and m.entity = '"); - out.push_sql(window.table.object.as_str()); + window.from_table.walk_ast(out.reborrow())?; + out.push_sql(", matches m\n where c.vid = m.vid and m.entity = '"); + out.push_sql(window.table.meta.object.as_str()); out.push_sql("'"); } out.push_sql("\n "); @@ -4718,7 +4449,7 @@ impl<'a> QueryFragment for ClampRangeQuery<'a> { self.br_column.clamp(&mut out)?; out.push_sql("\n where "); - self.table.primary_key().is_in(&self.entity_ids, &mut out)?; + id_is_in(&self.entity_ids, &mut out)?; out.push_sql(" and ("); self.br_column.latest(&mut out); out.push_sql(")"); @@ -5043,7 +4774,7 @@ pub struct CopyVid { fn write_column_names( column_names: &AttributeNames, - table: &Table, + table: dsl::Table<'_>, prefix: Option<&str>, out: &mut AstPass, ) -> QueryResult<()> { @@ -5072,7 +4803,7 @@ fn write_column_names( fn jsonb_build_object( column_names: &AttributeNames, table_identifier: &str, - table: &Table, + table: dsl::Table<'_>, out: &mut AstPass, ) -> QueryResult<()> { match column_names { @@ -5107,11 +4838,11 @@ fn jsonb_build_object( /// names, yielding valid SQL names for the given table. fn iter_column_names<'a, 'b>( attribute_names: &'a BTreeSet, - table: &'b Table, + table: dsl::Table<'b>, include_block_range_column: bool, ) -> impl Iterator { let extra = if include_block_range_column { - if table.immutable { + if table.meta.immutable { [BLOCK_COLUMN].iter() } else { [BLOCK_RANGE_COLUMN].iter() @@ -5127,7 +4858,7 @@ fn iter_column_names<'a, 'b>( // Unwrapping: We have already checked that all attribute names exist in table table.column_for_field(attribute_name).unwrap() }) - .map(|column| column.name.as_str()) + .map(|column| column.name()) .chain(BASE_SQL_COLUMNS.iter().copied()) .chain(extra) .sorted()