diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml new file mode 100644 index 0000000..274d119 --- /dev/null +++ b/.github/workflows/build_and_test.yaml @@ -0,0 +1,34 @@ +name: "Test Suite" +on: + pull_request: + +jobs: + test: + name: cargo test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # this defaults to "-D warnings", making warnings fail the entire build. + # setting to empty strng to allow builds with warnings + # todo: consider removing this, and disallowing pushing with warnings? + rustflags: "" + - run: cargo test --all-features + + # Check formatting with rustfmt + formatting: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Ensure rustfmt is installed and setup problem matcher + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt + # this defaults to "-D warnings", making warnings fail the entire build. + # setting to empty strng to allow builds with warnings + # todo: consider removing this, and disallowing pushing with warnings? + rustflags: "" + - name: Rustfmt Check + uses: actions-rust-lang/rustfmt@v1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b89a70f..7ab5bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.9] + +- Change namespaceing to use `.` separator instead of `_`. We assume table names are less likely to contain periods, so this reduces likelyhood of naming conflicts.This will change generated type names and will thus manifest as a breaking change for some users +- Support `Nested` column types correctly, (previously these were treated as essentially Tuple columns) +- Support subfield selection on complex column types. +- Add support for relationships on column subfields, if the path to the subfield does not include lists +- Fix datatype parser: add ability to parse SimpleAggregateFunction and AggregateFunction columns + ## [0.2.8] - Make spans visible to cloud console users (tag spans with `internal.visibility = 'user'`) diff --git a/Cargo.lock b/Cargo.lock index 38d5be7..e895418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -340,7 +340,7 @@ checksum = "97af0562545a7d7f3d9222fcf909963bec36dcb502afaacab98c6ffac8da47ce" [[package]] name = "common" -version = "0.2.8" +version = "0.2.9" dependencies = [ "bytes", "peg", @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "ndc-clickhouse" -version = "0.2.8" +version = "0.2.9" dependencies = [ "async-trait", "bytes", @@ -1074,6 +1074,7 @@ dependencies = [ "ndc-sdk", "prometheus", "reqwest 0.12.3", + "schemars", "serde", "serde_json", "sqlformat", @@ -1084,7 +1085,7 @@ dependencies = [ [[package]] name = "ndc-clickhouse-cli" -version = "0.2.8" +version = "0.2.9" dependencies = [ "clap", "common", diff --git a/Cargo.toml b/Cargo.toml index 7e4d739..d867865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,5 @@ members = [ ] resolver = "2" -package.version = "0.2.8" +package.version = "0.2.9" package.edition = "2021" diff --git a/crates/common/src/clickhouse_parser.rs b/crates/common/src/clickhouse_parser.rs index 8350d42..15e0e4c 100644 --- a/crates/common/src/clickhouse_parser.rs +++ b/crates/common/src/clickhouse_parser.rs @@ -56,6 +56,8 @@ peg::parser! { / map() / tuple() / r#enum() + / aggregate_function() + / simple_aggregate_function() / nothing() rule nullable() -> DT = "Nullable(" t:data_type() ")" { DT::Nullable(Box::new(t)) } rule uint8() -> DT = "UInt8" { DT::UInt8 } @@ -181,6 +183,16 @@ fn can_parse_clickhouse_data_type() { (Some(Identifier::BacktickQuoted("u".to_string())), DT::UInt8), ]), ), + ( + "SimpleAggregateFunction(sum, UInt64)", + DT::SimpleAggregateFunction { + function: AggregateFunctionDefinition { + name: Identifier::Unquoted("sum".to_string()), + parameters: None, + }, + arguments: vec![DT::UInt64], + }, + ), ]; for (s, t) in data_types { diff --git a/crates/common/src/clickhouse_parser/datatype.rs b/crates/common/src/clickhouse_parser/datatype.rs index f42bb11..63c4924 100644 --- a/crates/common/src/clickhouse_parser/datatype.rs +++ b/crates/common/src/clickhouse_parser/datatype.rs @@ -20,6 +20,16 @@ pub enum Identifier { Unquoted(String), } +impl Identifier { + pub fn value(&self) -> &str { + match self { + Identifier::DoubleQuoted(s) + | Identifier::BacktickQuoted(s) + | Identifier::Unquoted(s) => s, + } + } +} + impl Display for Identifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index 6d1677d..329d26d 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -10,6 +10,7 @@ use crate::{ pub struct ServerConfig { /// the connection part of the config is not part of the config file pub connection: ConnectionConfig, + pub namespace_separator: String, pub table_types: BTreeMap, pub tables: BTreeMap, pub queries: BTreeMap, diff --git a/crates/ndc-clickhouse/Cargo.toml b/crates/ndc-clickhouse/Cargo.toml index 059862b..73a2a53 100644 --- a/crates/ndc-clickhouse/Cargo.toml +++ b/crates/ndc-clickhouse/Cargo.toml @@ -14,6 +14,7 @@ reqwest = { version = "0.12.3", features = [ "json", "rustls-tls", ], default-features = false } +schemars = "0.8.16" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" sqlformat = "0.2.3" diff --git a/crates/ndc-clickhouse/src/connector.rs b/crates/ndc-clickhouse/src/connector.rs index d8383b9..daa5f01 100644 --- a/crates/ndc-clickhouse/src/connector.rs +++ b/crates/ndc-clickhouse/src/connector.rs @@ -287,6 +287,9 @@ pub async fn read_server_config( let config = ServerConfig { connection, + // hardcoding separator for now, to avoid prematurely exposing configuration options we may not want to keep + // if we make this configurable, we must default to this separator when the option is not provided + namespace_separator: ".".to_string(), table_types, tables, queries, diff --git a/crates/ndc-clickhouse/src/connector/handler/query.rs b/crates/ndc-clickhouse/src/connector/handler/query.rs index a6ad205..fb1e642 100644 --- a/crates/ndc-clickhouse/src/connector/handler/query.rs +++ b/crates/ndc-clickhouse/src/connector/handler/query.rs @@ -1,9 +1,5 @@ use common::{client::execute_query, config::ServerConfig}; -use ndc_sdk::{ - connector::QueryError, - json_response::JsonResponse, - models::{self, QueryResponse}, -}; +use ndc_sdk::{connector::QueryError, json_response::JsonResponse, models}; use tracing::{Instrument, Level}; use crate::{connector::state::ServerState, sql::QueryBuilder}; diff --git a/crates/ndc-clickhouse/src/connector/handler/schema.rs b/crates/ndc-clickhouse/src/connector/handler/schema.rs index 72578d9..379204f 100644 --- a/crates/ndc-clickhouse/src/connector/handler/schema.rs +++ b/crates/ndc-clickhouse/src/connector/handler/schema.rs @@ -1,7 +1,7 @@ use crate::schema::ClickHouseTypeDefinition; use common::{ clickhouse_parser::{ - datatype::{ClickHouseDataType, Identifier}, + datatype::ClickHouseDataType, parameterized_query::{Parameter, ParameterType, ParameterizedQueryElement}, }, config::ServerConfig, @@ -23,6 +23,7 @@ pub async fn schema( &column_type, &column_alias, &type_name, + &configuration.namespace_separator, ); let (scalars, objects) = type_definition.type_definitions(); @@ -61,6 +62,7 @@ pub async fn schema( &argument_type, &argument_name, table_alias, + &configuration.namespace_separator, ); let (scalars, objects) = type_definition.type_definitions(); @@ -79,19 +81,15 @@ pub async fn schema( for (query_alias, query_config) in &configuration.queries { for element in &query_config.query.elements { if let ParameterizedQueryElement::Parameter(Parameter { name, r#type }) = element { - let argument_alias = match name { - Identifier::DoubleQuoted(n) - | Identifier::BacktickQuoted(n) - | Identifier::Unquoted(n) => n, - }; let data_type = match r#type { ParameterType::Identifier => &ClickHouseDataType::String, ParameterType::DataType(t) => t, }; let type_definition = ClickHouseTypeDefinition::from_query_argument( data_type, - &argument_alias, + name.value(), query_alias, + &configuration.namespace_separator, ); let (scalars, objects) = type_definition.type_definitions(); @@ -123,6 +121,7 @@ pub async fn schema( &argument_type, &argument_name, table_alias, + &configuration.namespace_separator, ); ( argument_name.to_owned(), @@ -164,23 +163,19 @@ pub async fn schema( .filter_map(|element| match element { ParameterizedQueryElement::String(_) => None, ParameterizedQueryElement::Parameter(Parameter { name, r#type }) => { - let argument_alias = match name { - Identifier::DoubleQuoted(n) - | Identifier::BacktickQuoted(n) - | Identifier::Unquoted(n) => n, - }; let data_type = match r#type { ParameterType::Identifier => &ClickHouseDataType::String, ParameterType::DataType(t) => &t, }; let type_definition = ClickHouseTypeDefinition::from_query_argument( data_type, - &argument_alias, + name.value(), query_alias, + &configuration.namespace_separator, ); Some(( - argument_alias.to_owned(), + name.value().to_owned(), models::ArgumentInfo { description: None, argument_type: type_definition.type_identifier(), diff --git a/crates/ndc-clickhouse/src/schema/type_definition.rs b/crates/ndc-clickhouse/src/schema/type_definition.rs index e78f0c7..c39ff38 100644 --- a/crates/ndc-clickhouse/src/schema/type_definition.rs +++ b/crates/ndc-clickhouse/src/schema/type_definition.rs @@ -1,57 +1,50 @@ -use std::collections::BTreeMap; - use common::clickhouse_parser::datatype::{ClickHouseDataType, Identifier, SingleQuotedString}; +use indexmap::IndexMap; use ndc_sdk::models; +use std::iter; use super::{ClickHouseBinaryComparisonOperator, ClickHouseSingleColumnAggregateFunction}; -#[derive(Debug, Clone, strum::Display)] -pub enum ClickHouseScalar { - Bool, - String, - UInt8, - UInt16, - UInt32, - UInt64, - UInt128, - UInt256, - Int8, - Int16, - Int32, - Int64, - Int128, - Int256, - Float32, - Float64, - Decimal, - Decimal32, - Decimal64, - Decimal128, - Decimal256, - Date, - Date32, - DateTime, - DateTime64, - #[strum(to_string = "JSON")] - Json, - #[strum(to_string = "UUID")] - Uuid, - IPv4, - IPv6, - #[strum(to_string = "{name}")] - Enum { - name: String, - variants: Vec, - }, +#[derive(Debug, Clone)] +struct NameSpace<'a> { + separator: &'a str, + path: Vec<&'a str>, } +impl<'a> NameSpace<'a> { + pub fn new(path: Vec<&'a str>, separator: &'a str) -> Self { + Self { separator, path } + } + pub fn value(&self) -> String { + self.path.join(&self.separator) + } + pub fn child(&self, path_element: &'a str) -> Self { + Self { + separator: self.separator, + path: self + .path + .clone() + .into_iter() + .chain(iter::once(path_element)) + .collect(), + } + } +} + +#[derive(Debug, Clone)] +pub struct ClickHouseScalar(ClickHouseDataType); + impl ClickHouseScalar { fn type_name(&self) -> String { - self.to_string() + self.0.to_string() + } + fn cast_type(&self) -> ClickHouseDataType { + // todo: recusively map large number types to string here + self.0.clone() } fn type_definition(&self) -> models::ScalarType { models::ScalarType { - representation: Some(self.json_representation()), + representation: self.json_representation(), aggregate_functions: self .aggregate_functions() .into_iter() @@ -60,7 +53,7 @@ impl ClickHouseScalar { function.to_string(), models::AggregateFunctionDefinition { result_type: models::Type::Named { - name: result_type.type_name(), + name: result_type.to_string(), }, }, ) @@ -108,319 +101,310 @@ impl ClickHouseScalar { .collect(), } } - fn json_representation(&self) -> models::TypeRepresentation { + fn json_representation(&self) -> Option { use models::TypeRepresentation as Rep; - use ClickHouseScalar as ST; - match self { - ST::Bool => Rep::Boolean, - ST::String => Rep::String, - ST::UInt8 => Rep::Integer, - ST::UInt16 => Rep::Integer, - ST::UInt32 => Rep::Integer, - ST::UInt64 => Rep::Integer, - ST::UInt128 => Rep::Integer, - ST::UInt256 => Rep::Integer, - ST::Int8 => Rep::Integer, - ST::Int16 => Rep::Integer, - ST::Int32 => Rep::Integer, - ST::Int64 => Rep::Integer, - ST::Int128 => Rep::Integer, - ST::Int256 => Rep::Integer, - ST::Float32 => Rep::Number, - ST::Float64 => Rep::Number, - ST::Decimal => Rep::Number, - ST::Decimal32 => Rep::String, - ST::Decimal64 => Rep::String, - ST::Decimal128 => Rep::String, - ST::Decimal256 => Rep::String, - ST::Date => Rep::String, - ST::Date32 => Rep::String, - ST::DateTime => Rep::String, - ST::DateTime64 => Rep::String, - ST::Json => Rep::String, - ST::Uuid => Rep::String, - ST::IPv4 => Rep::String, - ST::IPv6 => Rep::String, - ST::Enum { name: _, variants } => Rep::Enum { - one_of: variants.to_owned(), - }, + match &self.0 { + ClickHouseDataType::Bool => Some(Rep::Boolean), + ClickHouseDataType::String => Some(Rep::String), + ClickHouseDataType::UInt8 => Some(Rep::Integer), + ClickHouseDataType::UInt16 => Some(Rep::Integer), + ClickHouseDataType::UInt32 => Some(Rep::Integer), + ClickHouseDataType::UInt64 => Some(Rep::Integer), + ClickHouseDataType::UInt128 => Some(Rep::Integer), + ClickHouseDataType::UInt256 => Some(Rep::Integer), + ClickHouseDataType::Int8 => Some(Rep::Integer), + ClickHouseDataType::Int16 => Some(Rep::Integer), + ClickHouseDataType::Int32 => Some(Rep::Integer), + ClickHouseDataType::Int64 => Some(Rep::Integer), + ClickHouseDataType::Int128 => Some(Rep::Integer), + ClickHouseDataType::Int256 => Some(Rep::Integer), + ClickHouseDataType::Float32 => Some(Rep::Number), + ClickHouseDataType::Float64 => Some(Rep::Number), + ClickHouseDataType::Decimal { .. } => Some(Rep::Number), + ClickHouseDataType::Decimal32 { .. } => Some(Rep::String), + ClickHouseDataType::Decimal64 { .. } => Some(Rep::String), + ClickHouseDataType::Decimal128 { .. } => Some(Rep::String), + ClickHouseDataType::Decimal256 { .. } => Some(Rep::String), + ClickHouseDataType::Date => Some(Rep::String), + ClickHouseDataType::Date32 => Some(Rep::String), + ClickHouseDataType::DateTime { .. } => Some(Rep::String), + ClickHouseDataType::DateTime64 { .. } => Some(Rep::String), + ClickHouseDataType::Json => Some(Rep::String), + ClickHouseDataType::Uuid => Some(Rep::String), + ClickHouseDataType::IPv4 => Some(Rep::String), + ClickHouseDataType::IPv6 => Some(Rep::String), + ClickHouseDataType::Enum(variants) => { + let variants = variants + .iter() + .map(|(SingleQuotedString(variant), _)| variant.to_owned()) + .collect(); + + Some(Rep::Enum { one_of: variants }) + } + _ => None, } } fn aggregate_functions( &self, - ) -> Vec<(ClickHouseSingleColumnAggregateFunction, ClickHouseScalar)> { - use ClickHouseScalar as ST; + ) -> Vec<(ClickHouseSingleColumnAggregateFunction, ClickHouseDataType)> { use ClickHouseSingleColumnAggregateFunction as AF; - match self { - ST::Bool => vec![], - ST::String => vec![], - ST::UInt8 => vec![ - (AF::Max, ST::UInt8), - (AF::Min, ST::UInt8), - (AF::Sum, ST::UInt64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + match self.0 { + ClickHouseDataType::Bool => vec![], + ClickHouseDataType::String => vec![], + ClickHouseDataType::UInt8 => vec![ + (AF::Max, ClickHouseDataType::UInt8), + (AF::Min, ClickHouseDataType::UInt8), + (AF::Sum, ClickHouseDataType::UInt64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), + ], + ClickHouseDataType::UInt16 => vec![ + (AF::Max, ClickHouseDataType::UInt16), + (AF::Min, ClickHouseDataType::UInt16), + (AF::Sum, ClickHouseDataType::UInt64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), + ], + ClickHouseDataType::UInt32 => vec![ + (AF::Max, ClickHouseDataType::UInt32), + (AF::Min, ClickHouseDataType::UInt32), + (AF::Sum, ClickHouseDataType::UInt64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::UInt16 => vec![ - (AF::Max, ST::UInt16), - (AF::Min, ST::UInt16), - (AF::Sum, ST::UInt64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::UInt64 => vec![ + (AF::Max, ClickHouseDataType::UInt64), + (AF::Min, ClickHouseDataType::UInt64), + (AF::Sum, ClickHouseDataType::UInt64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::UInt32 => vec![ - (AF::Max, ST::UInt32), - (AF::Min, ST::UInt32), - (AF::Sum, ST::UInt64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::UInt128 => vec![ + (AF::Max, ClickHouseDataType::UInt128), + (AF::Min, ClickHouseDataType::UInt128), + (AF::Sum, ClickHouseDataType::UInt128), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::UInt64 => vec![ - (AF::Max, ST::UInt64), - (AF::Min, ST::UInt64), - (AF::Sum, ST::UInt64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::UInt256 => vec![ + (AF::Max, ClickHouseDataType::UInt256), + (AF::Min, ClickHouseDataType::UInt256), + (AF::Sum, ClickHouseDataType::UInt256), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::UInt128 => vec![ - (AF::Max, ST::UInt128), - (AF::Min, ST::UInt128), - (AF::Sum, ST::UInt128), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int8 => vec![ + (AF::Max, ClickHouseDataType::Int8), + (AF::Min, ClickHouseDataType::Int8), + (AF::Sum, ClickHouseDataType::Int64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::UInt256 => vec![ - (AF::Max, ST::UInt256), - (AF::Min, ST::UInt256), - (AF::Sum, ST::UInt256), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int16 => vec![ + (AF::Max, ClickHouseDataType::Int16), + (AF::Min, ClickHouseDataType::Int16), + (AF::Sum, ClickHouseDataType::Int64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Int8 => vec![ - (AF::Max, ST::Int8), - (AF::Min, ST::Int8), - (AF::Sum, ST::Int64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int32 => vec![ + (AF::Max, ClickHouseDataType::Int32), + (AF::Min, ClickHouseDataType::Int32), + (AF::Sum, ClickHouseDataType::Int64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Int16 => vec![ - (AF::Max, ST::Int16), - (AF::Min, ST::Int16), - (AF::Sum, ST::Int64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int64 => vec![ + (AF::Max, ClickHouseDataType::Int64), + (AF::Min, ClickHouseDataType::Int64), + (AF::Sum, ClickHouseDataType::Int64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Int32 => vec![ - (AF::Max, ST::Int32), - (AF::Min, ST::Int32), - (AF::Sum, ST::Int64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int128 => vec![ + (AF::Max, ClickHouseDataType::Int128), + (AF::Min, ClickHouseDataType::Int128), + (AF::Sum, ClickHouseDataType::Int128), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Int64 => vec![ - (AF::Max, ST::Int64), - (AF::Min, ST::Int64), - (AF::Sum, ST::Int64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Int256 => vec![ + (AF::Max, ClickHouseDataType::Int256), + (AF::Min, ClickHouseDataType::Int256), + (AF::Sum, ClickHouseDataType::Int256), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Int128 => vec![ - (AF::Max, ST::Int128), - (AF::Min, ST::Int128), - (AF::Sum, ST::Int128), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Float32 => vec![ + (AF::Max, ClickHouseDataType::Float64), + (AF::Min, ClickHouseDataType::Float32), + (AF::Sum, ClickHouseDataType::Float32), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float32), + (AF::StddevSamp, ClickHouseDataType::Float32), + (AF::VarPop, ClickHouseDataType::Float32), + (AF::VarSamp, ClickHouseDataType::Float32), ], - ST::Int256 => vec![ - (AF::Max, ST::Int256), - (AF::Min, ST::Int256), - (AF::Sum, ST::Int256), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Float64 => vec![ + (AF::Max, ClickHouseDataType::Float64), + (AF::Min, ClickHouseDataType::Float64), + (AF::Sum, ClickHouseDataType::Float64), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Float32 => vec![ - (AF::Max, ST::Float64), - (AF::Min, ST::Float32), - (AF::Sum, ST::Float32), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float32), - (AF::StddevSamp, ST::Float32), - (AF::VarPop, ST::Float32), - (AF::VarSamp, ST::Float32), + ClickHouseDataType::Decimal { .. } => vec![ + (AF::Max, self.0.to_owned()), + (AF::Min, self.0.to_owned()), + (AF::Sum, self.0.to_owned()), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Float64 => vec![ - (AF::Max, ST::Float64), - (AF::Min, ST::Float64), - (AF::Sum, ST::Float64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Decimal32 { .. } => vec![ + (AF::Max, self.0.to_owned()), + (AF::Min, self.0.to_owned()), + (AF::Sum, self.0.to_owned()), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Decimal => vec![ - (AF::Max, ST::Decimal), - (AF::Min, ST::Decimal), - (AF::Sum, ST::Decimal), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Decimal64 { .. } => vec![ + (AF::Max, self.0.to_owned()), + (AF::Min, self.0.to_owned()), + (AF::Sum, self.0.to_owned()), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Decimal32 => vec![ - (AF::Max, ST::Decimal32), - (AF::Min, ST::Decimal32), - (AF::Sum, ST::Decimal32), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Decimal128 { .. } => vec![ + (AF::Max, self.0.to_owned()), + (AF::Min, self.0.to_owned()), + (AF::Sum, self.0.to_owned()), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Decimal64 => vec![ - (AF::Max, ST::Decimal64), - (AF::Min, ST::Decimal64), - (AF::Sum, ST::Decimal64), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Decimal256 { .. } => vec![ + (AF::Max, self.0.to_owned()), + (AF::Min, self.0.to_owned()), + (AF::Sum, self.0.to_owned()), + (AF::Avg, ClickHouseDataType::Float64), + (AF::StddevPop, ClickHouseDataType::Float64), + (AF::StddevSamp, ClickHouseDataType::Float64), + (AF::VarPop, ClickHouseDataType::Float64), + (AF::VarSamp, ClickHouseDataType::Float64), ], - ST::Decimal128 => vec![ - (AF::Max, ST::Decimal128), - (AF::Min, ST::Decimal128), - (AF::Sum, ST::Decimal128), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Date => vec![ + (AF::Max, ClickHouseDataType::Date), + (AF::Min, ClickHouseDataType::Date), ], - ST::Decimal256 => vec![ - (AF::Max, ST::Decimal256), - (AF::Min, ST::Decimal256), - (AF::Sum, ST::Decimal256), - (AF::Avg, ST::Float64), - (AF::StddevPop, ST::Float64), - (AF::StddevSamp, ST::Float64), - (AF::VarPop, ST::Float64), - (AF::VarSamp, ST::Float64), + ClickHouseDataType::Date32 => vec![ + (AF::Max, ClickHouseDataType::Date32), + (AF::Min, ClickHouseDataType::Date32), ], - ST::Date => vec![(AF::Max, ST::Date), (AF::Min, ST::Date)], - ST::Date32 => vec![(AF::Max, ST::Date32), (AF::Min, ST::Date32)], - ST::DateTime => vec![(AF::Max, ST::DateTime), (AF::Min, ST::DateTime)], - ST::DateTime64 => vec![(AF::Max, ST::DateTime64), (AF::Min, ST::DateTime64)], - ST::Json => vec![], - ST::Uuid => vec![], - ST::IPv4 => vec![], - ST::IPv6 => vec![], - ST::Enum { .. } => vec![], + ClickHouseDataType::DateTime { .. } => { + vec![(AF::Max, self.0.to_owned()), (AF::Min, self.0.to_owned())] + } + ClickHouseDataType::DateTime64 { .. } => { + vec![(AF::Max, self.0.to_owned()), (AF::Min, self.0.to_owned())] + } + _ => vec![], } } fn comparison_operators(&self) -> Vec { use ClickHouseBinaryComparisonOperator as BC; - use ClickHouseScalar as ST; let equality_operators = vec![BC::Eq, BC::NotEq, BC::In, BC::NotIn]; let ordering_operators = vec![BC::Gt, BC::Lt, BC::GtEq, BC::LtEq]; let string_operators = vec![BC::Like, BC::NotLike, BC::ILike, BC::NotILike, BC::Match]; - match self { - ST::Bool => equality_operators, - ST::String => [equality_operators, ordering_operators, string_operators].concat(), - ST::UInt8 | ST::UInt16 | ST::UInt32 | ST::UInt64 | ST::UInt128 | ST::UInt256 => { + match self.0 { + ClickHouseDataType::Bool => equality_operators, + ClickHouseDataType::String => { + [equality_operators, ordering_operators, string_operators].concat() + } + ClickHouseDataType::UInt8 + | ClickHouseDataType::UInt16 + | ClickHouseDataType::UInt32 + | ClickHouseDataType::UInt64 + | ClickHouseDataType::UInt128 + | ClickHouseDataType::UInt256 => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::Int8 + | ClickHouseDataType::Int16 + | ClickHouseDataType::Int32 + | ClickHouseDataType::Int64 + | ClickHouseDataType::Int128 + | ClickHouseDataType::Int256 => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::Float32 | ClickHouseDataType::Float64 => { [equality_operators, ordering_operators].concat() } - ST::Int8 | ST::Int16 | ST::Int32 | ST::Int64 | ST::Int128 | ST::Int256 => { + ClickHouseDataType::Decimal { .. } + | ClickHouseDataType::Decimal32 { .. } + | ClickHouseDataType::Decimal64 { .. } + | ClickHouseDataType::Decimal128 { .. } + | ClickHouseDataType::Decimal256 { .. } => { [equality_operators, ordering_operators].concat() } - ST::Float32 | ST::Float64 => [equality_operators, ordering_operators].concat(), - ST::Decimal | ST::Decimal32 | ST::Decimal64 | ST::Decimal128 | ST::Decimal256 => { + ClickHouseDataType::Date | ClickHouseDataType::Date32 => { [equality_operators, ordering_operators].concat() } - ST::Date | ST::Date32 => [equality_operators, ordering_operators].concat(), - ST::DateTime | ST::DateTime64 => [equality_operators, ordering_operators].concat(), - ST::Json => [equality_operators, ordering_operators].concat(), - ST::Uuid => [equality_operators, ordering_operators].concat(), - ST::IPv4 => [equality_operators, ordering_operators].concat(), - ST::IPv6 => [equality_operators, ordering_operators].concat(), - ST::Enum { .. } => equality_operators, - } - } - /// returns the type we can cast this type to - /// this may not be the same as the underlying real type - /// for examples, enums are cast to strings, and fixed strings cast to strings - fn cast_type(&self) -> ClickHouseDataType { - match self { - ClickHouseScalar::Bool => ClickHouseDataType::Bool, - ClickHouseScalar::String => ClickHouseDataType::String, - ClickHouseScalar::UInt8 => ClickHouseDataType::UInt8, - ClickHouseScalar::UInt16 => ClickHouseDataType::UInt16, - ClickHouseScalar::UInt32 => ClickHouseDataType::UInt32, - ClickHouseScalar::UInt64 => ClickHouseDataType::UInt64, - ClickHouseScalar::UInt128 => ClickHouseDataType::UInt128, - ClickHouseScalar::UInt256 => ClickHouseDataType::UInt256, - ClickHouseScalar::Int8 => ClickHouseDataType::Int8, - ClickHouseScalar::Int16 => ClickHouseDataType::Int16, - ClickHouseScalar::Int32 => ClickHouseDataType::Int32, - ClickHouseScalar::Int64 => ClickHouseDataType::Int64, - ClickHouseScalar::Int128 => ClickHouseDataType::Int128, - ClickHouseScalar::Int256 => ClickHouseDataType::Int256, - ClickHouseScalar::Float32 => ClickHouseDataType::Float32, - ClickHouseScalar::Float64 => ClickHouseDataType::Float64, - ClickHouseScalar::Decimal => ClickHouseDataType::String, - ClickHouseScalar::Decimal32 => ClickHouseDataType::String, - ClickHouseScalar::Decimal64 => ClickHouseDataType::String, - ClickHouseScalar::Decimal128 => ClickHouseDataType::String, - ClickHouseScalar::Decimal256 => ClickHouseDataType::String, - ClickHouseScalar::Date => ClickHouseDataType::String, - ClickHouseScalar::Date32 => ClickHouseDataType::String, - ClickHouseScalar::DateTime => ClickHouseDataType::String, - ClickHouseScalar::DateTime64 => ClickHouseDataType::String, - ClickHouseScalar::Json => ClickHouseDataType::Json, - ClickHouseScalar::Uuid => ClickHouseDataType::Uuid, - ClickHouseScalar::IPv4 => ClickHouseDataType::IPv4, - ClickHouseScalar::IPv6 => ClickHouseDataType::IPv6, - ClickHouseScalar::Enum { .. } => ClickHouseDataType::String, + ClickHouseDataType::DateTime { .. } | ClickHouseDataType::DateTime64 { .. } => { + [equality_operators, ordering_operators].concat() + } + ClickHouseDataType::Json => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::Uuid => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::IPv4 => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::IPv6 => [equality_operators, ordering_operators].concat(), + ClickHouseDataType::Enum { .. } => equality_operators, + _ => vec![], } } } @@ -435,13 +419,7 @@ pub enum ClickHouseTypeDefinition { }, Object { name: String, - fields: BTreeMap, - }, - /// Stand-in for data types that either cannot be represented in graphql, - /// (such as maps or tuples with anonymous memebers) - /// or cannot be known ahead of time (such as the return type of aggregate function columns) - Unknown { - name: String, + fields: IndexMap, }, } @@ -451,116 +429,77 @@ impl ClickHouseTypeDefinition { data_type: &ClickHouseDataType, column_alias: &str, table_alias: &str, + separator: &str, ) -> Self { - let namespace = format!("{table_alias}.{column_alias}"); + let namespace = NameSpace::new(vec![table_alias, column_alias], separator); Self::new(data_type, &namespace) } pub fn from_query_return_type( data_type: &ClickHouseDataType, field_alias: &str, query_alias: &str, + separator: &str, ) -> Self { - let namespace = format!("{query_alias}.{field_alias}"); + let namespace = NameSpace::new(vec![query_alias, field_alias], separator); Self::new(data_type, &namespace) } pub fn from_query_argument( data_type: &ClickHouseDataType, argument_alias: &str, query_alias: &str, + separator: &str, ) -> Self { - let namespace = format!("{query_alias}.arg.{argument_alias}"); + let namespace = NameSpace::new(vec![query_alias, "_arg", argument_alias], separator); Self::new(data_type, &namespace) } - fn new(data_type: &ClickHouseDataType, namespace: &str) -> Self { + fn new(data_type: &ClickHouseDataType, namespace: &NameSpace) -> Self { match data_type { ClickHouseDataType::Nullable(inner) => Self::Nullable { inner: Box::new(Self::new(inner, namespace)), }, - ClickHouseDataType::Bool => Self::Scalar(ClickHouseScalar::Bool), ClickHouseDataType::String | ClickHouseDataType::FixedString(_) => { - Self::Scalar(ClickHouseScalar::String) + Self::Scalar(ClickHouseScalar(ClickHouseDataType::String)) } - ClickHouseDataType::UInt8 => Self::Scalar(ClickHouseScalar::UInt8), - ClickHouseDataType::UInt16 => Self::Scalar(ClickHouseScalar::UInt16), - ClickHouseDataType::UInt32 => Self::Scalar(ClickHouseScalar::UInt32), - ClickHouseDataType::UInt64 => Self::Scalar(ClickHouseScalar::UInt64), - ClickHouseDataType::UInt128 => Self::Scalar(ClickHouseScalar::UInt128), - ClickHouseDataType::UInt256 => Self::Scalar(ClickHouseScalar::UInt256), - ClickHouseDataType::Int8 => Self::Scalar(ClickHouseScalar::Int8), - ClickHouseDataType::Int16 => Self::Scalar(ClickHouseScalar::Int16), - ClickHouseDataType::Int32 => Self::Scalar(ClickHouseScalar::Int32), - ClickHouseDataType::Int64 => Self::Scalar(ClickHouseScalar::Int64), - ClickHouseDataType::Int128 => Self::Scalar(ClickHouseScalar::Int128), - ClickHouseDataType::Int256 => Self::Scalar(ClickHouseScalar::Int256), - ClickHouseDataType::Float32 => Self::Scalar(ClickHouseScalar::Float32), - ClickHouseDataType::Float64 => Self::Scalar(ClickHouseScalar::Float64), - ClickHouseDataType::Decimal { .. } => Self::Scalar(ClickHouseScalar::Decimal), - ClickHouseDataType::Decimal32 { .. } => Self::Scalar(ClickHouseScalar::Decimal32), - ClickHouseDataType::Decimal64 { .. } => Self::Scalar(ClickHouseScalar::Decimal64), - ClickHouseDataType::Decimal128 { .. } => Self::Scalar(ClickHouseScalar::Decimal128), - ClickHouseDataType::Decimal256 { .. } => Self::Scalar(ClickHouseScalar::Decimal256), - ClickHouseDataType::Date => Self::Scalar(ClickHouseScalar::Date), - ClickHouseDataType::Date32 => Self::Scalar(ClickHouseScalar::Date32), - ClickHouseDataType::DateTime { .. } => Self::Scalar(ClickHouseScalar::DateTime), - ClickHouseDataType::DateTime64 { .. } => Self::Scalar(ClickHouseScalar::DateTime64), - ClickHouseDataType::Json => Self::Scalar(ClickHouseScalar::Json), - ClickHouseDataType::Uuid => Self::Scalar(ClickHouseScalar::Uuid), - ClickHouseDataType::IPv4 => Self::Scalar(ClickHouseScalar::IPv4), - ClickHouseDataType::IPv6 => Self::Scalar(ClickHouseScalar::IPv6), ClickHouseDataType::LowCardinality(inner) => Self::new(inner, namespace), ClickHouseDataType::Nested(entries) => { - let mut fields = BTreeMap::new(); + let mut fields = IndexMap::new(); for (name, field_data_type) in entries { - let field_name = match name { - Identifier::DoubleQuoted(n) => n, - Identifier::BacktickQuoted(n) => n, - Identifier::Unquoted(n) => n, - }; - - let field_namespace = format!("{namespace}_{field_name}"); + let field_namespace = namespace.child(name.value()); let field_definition = Self::new(field_data_type, &field_namespace); if fields - .insert(field_name.to_owned(), field_definition) + .insert(name.value().to_owned(), field_definition) .is_some() { // on duplicate field names, fall back to unknown type - return Self::Unknown { - name: namespace.to_owned(), - }; + return Self::Scalar(ClickHouseScalar(data_type.to_owned())); } } - Self::Object { - name: namespace.to_owned(), - fields, + Self::Array { + element_type: Box::new(Self::Object { + name: namespace.value(), + fields, + }), } } ClickHouseDataType::Array(element) => Self::Array { element_type: Box::new(Self::new(element, namespace)), }, - ClickHouseDataType::Map { .. } => Self::Unknown { - name: namespace.to_owned(), - }, ClickHouseDataType::Tuple(entries) => { - let mut fields = BTreeMap::new(); + let mut fields = IndexMap::new(); for (name, field_data_type) in entries { let field_name = if let Some(name) = name { - match name { - Identifier::DoubleQuoted(n) => n, - Identifier::BacktickQuoted(n) => n, - Identifier::Unquoted(n) => n, - } + name.value() } else { - return Self::Unknown { - name: namespace.to_owned(), - }; + // anonymous tuples treated as scalar types + return Self::Scalar(ClickHouseScalar(data_type.to_owned())); }; - let field_namespace = format!("{namespace}.{field_name}"); + let field_namespace = namespace.child(&field_name); let field_definition = Self::new(field_data_type, &field_namespace); @@ -569,27 +508,15 @@ impl ClickHouseTypeDefinition { .is_some() { // on duplicate field names, fall back to unknown type - return Self::Unknown { - name: namespace.to_owned(), - }; + return Self::Scalar(ClickHouseScalar(data_type.to_owned())); } } Self::Object { - name: namespace.to_owned(), + name: namespace.value(), fields, } } - ClickHouseDataType::Enum(variants) => { - let name = namespace.to_owned(); - let variants = variants - .iter() - .map(|(SingleQuotedString(variant), _)| variant.to_owned()) - .collect(); - - Self::Scalar(ClickHouseScalar::Enum { name, variants }) - } - ClickHouseDataType::SimpleAggregateFunction { function: _, arguments, @@ -597,9 +524,7 @@ impl ClickHouseTypeDefinition { if let (Some(data_type), 1) = (arguments.first(), arguments.len()) { Self::new(data_type, namespace) } else { - Self::Unknown { - name: namespace.to_owned(), - } + Self::Scalar(ClickHouseScalar(data_type.to_owned())) } } ClickHouseDataType::AggregateFunction { @@ -608,26 +533,18 @@ impl ClickHouseTypeDefinition { } => { let arg_len = arguments.len(); let first = arguments.first(); - let agg_fn_name = match &function.name { - Identifier::DoubleQuoted(n) => n, - Identifier::BacktickQuoted(n) => n, - Identifier::Unquoted(n) => n, - }; if let (Some(data_type), 1) = (first, arg_len) { Self::new(data_type, namespace) - } else if let (Some(data_type), 2, "anyIf") = (first, arg_len, agg_fn_name.as_str()) + } else if let (Some(data_type), 2, "anyIf") = + (first, arg_len, function.name.value()) { Self::new(data_type, namespace) } else { - Self::Unknown { - name: namespace.to_owned(), - } + Self::Scalar(ClickHouseScalar(data_type.to_owned())) } } - ClickHouseDataType::Nothing => Self::Unknown { - name: namespace.to_owned(), - }, + _ => Self::Scalar(ClickHouseScalar(data_type.to_owned())), } } pub fn type_identifier(&self) -> models::Type { @@ -644,9 +561,31 @@ impl ClickHouseTypeDefinition { ClickHouseTypeDefinition::Object { name, fields: _ } => models::Type::Named { name: name.to_owned(), }, - ClickHouseTypeDefinition::Unknown { name } => models::Type::Named { - name: name.to_owned(), - }, + } + } + pub fn cast_type(&self) -> ClickHouseDataType { + match self { + ClickHouseTypeDefinition::Scalar(scalar) => scalar.cast_type(), + ClickHouseTypeDefinition::Nullable { inner } => { + ClickHouseDataType::Nullable(Box::new(inner.cast_type())) + } + ClickHouseTypeDefinition::Array { element_type } => { + ClickHouseDataType::Array(Box::new(element_type.cast_type())) + } + ClickHouseTypeDefinition::Object { name: _, fields } => { + ClickHouseDataType::Tuple( + fields + .iter() + .map(|(key, value)| { + // todo: prevent issues where the key contains unescaped double quotes + ( + Some(Identifier::DoubleQuoted(key.to_owned())), + value.cast_type(), + ) + }) + .collect(), + ) + } } } /// returns the schema type definitions for this type @@ -696,48 +635,25 @@ impl ClickHouseTypeDefinition { (scalar_type_definitions, object_type_definitions) } - ClickHouseTypeDefinition::Unknown { name } => { - let definition = models::ScalarType { - representation: None, - aggregate_functions: BTreeMap::new(), - comparison_operators: BTreeMap::new(), - }; - (vec![(name.to_owned(), definition)], vec![]) - } - } - } - pub fn cast_type(&self) -> ClickHouseDataType { - match self { - ClickHouseTypeDefinition::Scalar(scalar) => scalar.cast_type(), - ClickHouseTypeDefinition::Nullable { inner } => { - ClickHouseDataType::Nullable(Box::new(inner.cast_type())) - } - ClickHouseTypeDefinition::Array { element_type } => { - ClickHouseDataType::Array(Box::new(element_type.cast_type())) - } - ClickHouseTypeDefinition::Object { name: _, fields } => { - ClickHouseDataType::Nested( - fields - .iter() - .map(|(key, value)| { - // todo: prevent issues where the key contains unescaped double quotes - (Identifier::DoubleQuoted(key.to_owned()), value.cast_type()) - }) - .collect(), - ) - } - ClickHouseTypeDefinition::Unknown { .. } => ClickHouseDataType::Json, } } pub fn aggregate_functions( &self, - ) -> Vec<(ClickHouseSingleColumnAggregateFunction, ClickHouseScalar)> { + ) -> Vec<(ClickHouseSingleColumnAggregateFunction, ClickHouseDataType)> { match self { ClickHouseTypeDefinition::Scalar(scalar) => scalar.aggregate_functions(), ClickHouseTypeDefinition::Nullable { inner } => inner.aggregate_functions(), ClickHouseTypeDefinition::Array { .. } => vec![], ClickHouseTypeDefinition::Object { .. } => vec![], - ClickHouseTypeDefinition::Unknown { .. } => vec![], + } + } + /// the underlying non-nullable type, with any wrapping nullable variants removed + pub fn non_nullable(&self) -> &Self { + match self { + ClickHouseTypeDefinition::Nullable { inner } => inner.non_nullable(), + ClickHouseTypeDefinition::Scalar(_) => self, + ClickHouseTypeDefinition::Array { .. } => self, + ClickHouseTypeDefinition::Object { .. } => self, } } } diff --git a/crates/ndc-clickhouse/src/sql/query_builder.rs b/crates/ndc-clickhouse/src/sql/query_builder.rs index 4907c55..86bfbc5 100644 --- a/crates/ndc-clickhouse/src/sql/query_builder.rs +++ b/crates/ndc-clickhouse/src/sql/query_builder.rs @@ -1,27 +1,25 @@ -use std::str::FromStr; +mod collection_context; +mod comparison_column; +mod error; +mod typecasting; +use self::{collection_context::CollectionContext, typecasting::RowsetTypeString}; +use super::ast::*; +use crate::schema::{ + ClickHouseBinaryComparisonOperator, ClickHouseSingleColumnAggregateFunction, + ClickHouseTypeDefinition, +}; use common::{ clickhouse_parser::{ - datatype::{ClickHouseDataType, Identifier}, - parameterized_query::ParameterizedQueryElement, + datatype::ClickHouseDataType, parameterized_query::ParameterizedQueryElement, }, config::ServerConfig, }; -use indexmap::IndexMap; - -mod collection_context; -mod comparison_column; -mod error; -mod typecasting; - use comparison_column::ComparisonColumn; pub use error::QueryBuilderError; +use indexmap::IndexMap; use ndc_sdk::models; - -use self::{collection_context::CollectionContext, typecasting::RowsetTypeString}; - -use super::ast::*; -use crate::schema::{ClickHouseBinaryComparisonOperator, ClickHouseSingleColumnAggregateFunction}; +use std::{collections::BTreeMap, iter, str::FromStr}; pub struct QueryBuilder<'r, 'c> { request: &'r models::QueryRequest, @@ -58,6 +56,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { &self.request.collection_relationships, &self.configuration, )? + .into_cast_type() .to_string(), )) .into_arg(), @@ -298,28 +297,74 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { relkeys: &Vec<&String>, query: &models::Query, ) -> Result { + let (table, mut base_joins) = if self.request.variables.is_some() { + let table = ObjectName(vec![Ident::new_quoted("_vars")]) + .into_table_factor() + .alias("_vars"); + + let joins = vec![Join { + relation: self.collection_ident(current_collection)?.alias("_origin"), + join_operator: JoinOperator::CrossJoin, + }]; + (table, joins) + } else { + let table = self.collection_ident(current_collection)?.alias("_origin"); + (table, vec![]) + }; + let mut select = vec![]; if let Some(fields) = &query.fields { + let mut rel_index = 0; for (alias, field) in fields { - let expr = match field { + match field { models::Field::Column { column, fields } => { - if fields.is_some() { - return Err(QueryBuilderError::NotSupported( - "nested field selector".into(), - )); + let data_type = self.column_data_type(column, current_collection)?; + let column_definition = ClickHouseTypeDefinition::from_table_column( + &data_type, + &column, + current_collection.alias(), + &self.configuration.namespace_separator, + ); + + let column_ident = + vec![Ident::new_quoted("_origin"), self.column_ident(column)]; + + if let Some((expr, mut joins)) = self.column_accessor( + column_ident, + &column_definition, + false, + fields.as_ref(), + &mut rel_index, + )? { + select.push(expr.into_select(Some(format!("_field_{alias}")))); + base_joins.append(&mut joins); + } else { + let expr = Expr::CompoundIdentifier(vec![ + Ident::new_quoted("_origin"), + self.column_ident(column), + ]); + select.push(expr.into_select(Some(format!("_field_{alias}")))); } - Expr::CompoundIdentifier(vec![ - Ident::new_quoted("_origin"), - self.column_ident(column, current_collection)?, - ]) } - models::Field::Relationship { .. } => Expr::CompoundIdentifier(vec![ - Ident::new_quoted(format!("_rel_{alias}")), - Ident::new_quoted("_rowset"), - ]), - }; - select.push(expr.into_select(Some(format!("_field_{alias}")))) + models::Field::Relationship { + query, + relationship, + arguments, + } => { + let (expr, join) = self.field_relationship( + alias, + &mut rel_index, + &vec![Ident::new_quoted("_origin")], + query, + relationship, + arguments, + )?; + + select.push(expr.into_select(Some(format!("_field_{alias}")))); + base_joins.push(join); + } + } } } @@ -330,7 +375,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { { let expr = Expr::CompoundIdentifier(vec![ Ident::new_quoted("_origin"), - self.column_ident(column, current_collection)?, + self.column_ident(column), ]); select.push(expr.into_select(Some(format!("_agg_{alias}")))) } @@ -341,7 +386,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { select.push( Expr::CompoundIdentifier(vec![ Ident::new_quoted("_origin"), - self.column_ident(relkey, current_collection)?, + self.column_ident(relkey), ]) .into_select(Some(format!("_relkey_{relkey}"))), ) @@ -361,90 +406,6 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { select.push(Expr::Value(Value::Null).into_select::(None)) } - let (table, mut base_joins) = if self.request.variables.is_some() { - let table = ObjectName(vec![Ident::new_quoted("_vars")]) - .into_table_factor() - .alias("_vars"); - - let joins = vec![Join { - relation: self.collection_ident(current_collection)?.alias("_origin"), - join_operator: JoinOperator::CrossJoin, - }]; - (table, joins) - } else { - let table = self.collection_ident(current_collection)?.alias("_origin"); - (table, vec![]) - }; - - if let Some(fields) = &query.fields { - for (alias, field) in fields { - if let models::Field::Relationship { - query, - relationship, - arguments, - } = field - { - let relationship = self.collection_relationship(relationship)?; - let relationship_collection = - CollectionContext::from_relationship(&relationship, arguments); - - let mut join_expr = relationship - .column_mapping - .iter() - .map(|(source_col, target_col)| { - Ok(Expr::BinaryOp { - left: Expr::CompoundIdentifier(vec![ - Ident::new_quoted("_origin"), - self.column_ident(source_col, current_collection)?, - ]) - .into_box(), - op: BinaryOperator::Eq, - right: Expr::CompoundIdentifier(vec![ - Ident::new_quoted(format!("_rel_{alias}")), - Ident::new_quoted(format!("_relkey_{target_col}")), - ]) - .into_box(), - }) - }) - .collect::, QueryBuilderError>>()?; - - if self.request.variables.is_some() { - join_expr.push(Expr::BinaryOp { - left: Expr::CompoundIdentifier(vec![ - Ident::new_quoted("_vars"), - Ident::new_quoted("_varset_id"), - ]) - .into_box(), - op: BinaryOperator::Eq, - right: Expr::CompoundIdentifier(vec![ - Ident::new_quoted(format!("_rel_{alias}")), - Ident::new_quoted("_varset_id"), - ]) - .into_box(), - }) - } - - let join_operator = join_expr - .into_iter() - .reduce(and_reducer) - .map(|expr| JoinOperator::LeftOuter(JoinConstraint::On(expr))) - .unwrap_or(JoinOperator::CrossJoin); - - let relkeys = relationship.column_mapping.values().collect(); - - let join = Join { - relation: self - .rowset_subquery(&relationship_collection, &relkeys, query)? - .into_table_factor() - .alias(format!("_rel_{alias}")), - join_operator, - }; - - base_joins.push(join) - } - } - } - let (predicate, predicate_joins) = if let Some(predicate) = &query.predicate { self.filter_expression( predicate, @@ -458,10 +419,75 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { (None, vec![]) }; + let (order_by_exprs, order_by_joins) = self.order_by(&query.order_by)?; + + let joins = base_joins + .into_iter() + .chain(predicate_joins) + .chain(order_by_joins) + .collect(); + + let from = vec![table.into_table_with_joins(joins)]; + + let mut limit_by_cols = relkeys + .iter() + .map(|relkey| { + Ok(Expr::CompoundIdentifier(vec![ + Ident::new_quoted("_origin"), + self.column_ident(relkey), + ])) + }) + .collect::, QueryBuilderError>>()?; + + if self.request.variables.is_some() { + limit_by_cols.push(Expr::CompoundIdentifier(vec![ + Ident::new_quoted("_vars"), + Ident::new_quoted("_varset_id"), + ])); + } + + let (limit_by, limit, offset) = if limit_by_cols.is_empty() { + ( + None, + query.limit.map(|limit| limit as u64), + query.offset.map(|offset| offset as u64), + ) + } else { + let limit_by = match (query.limit, query.offset) { + (None, None) => None, + (None, Some(offset)) => { + Some(LimitByExpr::new(None, Some(offset as u64), limit_by_cols)) + } + (Some(limit), None) => { + Some(LimitByExpr::new(Some(limit as u64), None, limit_by_cols)) + } + (Some(limit), Some(offset)) => Some(LimitByExpr::new( + Some(limit as u64), + Some(offset as u64), + limit_by_cols, + )), + }; + + (limit_by, None, None) + }; + + Ok(Query::new() + .select(select) + .from(from) + .predicate(predicate) + .order_by(order_by_exprs) + .limit_by(limit_by) + .limit(limit) + .offset(offset)) + } + fn order_by( + &self, + order_by: &Option, + ) -> Result<(Vec, Vec), QueryBuilderError> { let mut order_by_exprs = vec![]; let mut order_by_joins = vec![]; - if let Some(order_by) = &query.order_by { + if let Some(order_by) = &order_by { let mut order_by_index = 0; for element in &order_by.elements { @@ -469,7 +495,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { models::OrderByTarget::Column { name, path } if path.is_empty() => { let expr = Expr::CompoundIdentifier(vec![ Ident::new_quoted("_origin"), - self.column_ident(name, current_collection)?, + self.column_ident(name), ]); let asc = match &element.order_direction { models::OrderDirection::Asc => Some(true), @@ -493,6 +519,16 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { let relationship = self.collection_relationship(&first_element.relationship)?; + + if let ( + models::RelationshipType::Array, + models::OrderByTarget::Column { .. }, + ) = (&relationship.relationship_type, &element.target) + { + return Err(QueryBuilderError::NotSupported( + "order by column across array relationship".to_string(), + )); + } let relationship_collection = CollectionContext::from_relationship( relationship, &first_element.arguments, @@ -509,17 +545,17 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { select.push( Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident(target_col, &relationship_collection)?, + self.column_ident(target_col), ]) .into_select(Some(format!("_relkey_{target_col}"))), ); group_by.push(Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident(target_col, &relationship_collection)?, + self.column_ident(target_col), ])); limit_by.push(Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident(target_col, &relationship_collection)?, + self.column_ident(target_col), ])); } @@ -580,7 +616,6 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { } let mut last_join_alias = join_alias; - let mut last_collection_context = relationship_collection; for path_element in path.iter().skip(1) { let join_alias = @@ -589,6 +624,17 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { let relationship = self.collection_relationship(&path_element.relationship)?; + + if let ( + models::RelationshipType::Array, + models::OrderByTarget::Column { .. }, + ) = (&relationship.relationship_type, &element.target) + { + return Err(QueryBuilderError::NotSupported( + "order by column across array relationship".to_string(), + )); + } + let relationship_collection = CollectionContext::from_relationship( relationship, &path_element.arguments, @@ -601,19 +647,13 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(Expr::BinaryOp { left: Expr::CompoundIdentifier(vec![ last_join_alias.clone(), - self.column_ident( - source_col, - &last_collection_context, - )?, + self.column_ident(source_col), ]) .into_box(), op: BinaryOperator::Eq, right: Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident( - target_col, - &relationship_collection, - )?, + self.column_ident(target_col), ]) .into_box(), }) @@ -655,16 +695,16 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { } last_join_alias = join_alias; - last_collection_context = relationship_collection; } match &element.target { models::OrderByTarget::Column { name, path: _ } => { let column = Expr::CompoundIdentifier(vec![ last_join_alias, - self.column_ident(name, &last_collection_context)?, + self.column_ident(name), ]); - select.push(column.into_select(Some("_order_by_value"))) + group_by.push(column.clone()); + select.push(column.into_select(Some("_order_by_value"))); } models::OrderByTarget::SingleColumnAggregate { column, @@ -673,21 +713,20 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { } => { let column = Expr::CompoundIdentifier(vec![ last_join_alias, - self.column_ident(column, &last_collection_context)?, + self.column_ident(column), ]); select.push( aggregate_function(function)? .as_expr(column) .into_select(Some("_order_by_value")), - ) + ); } models::OrderByTarget::StarCountAggregate { path: _ } => { - if select.is_empty() { - select.push( - Expr::Value(Value::Number("1".to_string())) - .into_select(Some("_order_by_value")), - ) - } + let count = Function::new_unquoted("COUNT") + .args(vec![FunctionArgExpr::Wildcard.into_arg()]); + select.push( + count.into_expr().into_select(Some("_order_by_value")), + ); } } @@ -718,7 +757,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(Expr::BinaryOp { left: Expr::CompoundIdentifier(vec![ Ident::new_quoted("_origin"), - self.column_ident(source_col, ¤t_collection)?, + self.column_ident(source_col), ]) .into_box(), op: BinaryOperator::Eq, @@ -780,64 +819,85 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { } } - let joins = base_joins - .into_iter() - .chain(predicate_joins) - .chain(order_by_joins) - .collect(); + Ok((order_by_exprs, order_by_joins)) + } + fn field_relationship( + &self, + field_alias: &str, + name_index: &mut u32, + target_path: &Vec, + query: &models::Query, + relationship: &str, + arguments: &BTreeMap, + ) -> Result<(Expr, Join), QueryBuilderError> { + let join_alias = format!("_rel_{name_index}_{field_alias}"); + *name_index += 1; - let from = vec![table.into_table_with_joins(joins)]; + let relationship = self.collection_relationship(relationship)?; + let relationship_collection = + CollectionContext::from_relationship(&relationship, &arguments); - let mut limit_by_cols = relkeys + let mut join_expr = relationship + .column_mapping .iter() - .map(|relkey| { - Ok(Expr::CompoundIdentifier(vec![ - Ident::new_quoted("_origin"), - self.column_ident(relkey, current_collection)?, - ])) + .map(|(source_col, target_col)| { + Ok(Expr::BinaryOp { + left: Expr::CompoundIdentifier( + target_path + .clone() + .into_iter() + .chain(iter::once(Ident::new_quoted(source_col))) + .collect(), + ) + .into_box(), + op: BinaryOperator::Eq, + right: Expr::CompoundIdentifier(vec![ + Ident::new_quoted(&join_alias), + Ident::new_quoted(format!("_relkey_{target_col}")), + ]) + .into_box(), + }) }) .collect::, QueryBuilderError>>()?; if self.request.variables.is_some() { - limit_by_cols.push(Expr::CompoundIdentifier(vec![ - Ident::new_quoted("_vars"), - Ident::new_quoted("_varset_id"), - ])); + join_expr.push(Expr::BinaryOp { + left: Expr::CompoundIdentifier(vec![ + Ident::new_quoted("_vars"), + Ident::new_quoted("_varset_id"), + ]) + .into_box(), + op: BinaryOperator::Eq, + right: Expr::CompoundIdentifier(vec![ + Ident::new_quoted(&join_alias), + Ident::new_quoted("_varset_id"), + ]) + .into_box(), + }) } - let (limit_by, limit, offset) = if limit_by_cols.is_empty() { - ( - None, - query.limit.map(|limit| limit as u64), - query.offset.map(|offset| offset as u64), - ) - } else { - let limit_by = match (query.limit, query.offset) { - (None, None) => None, - (None, Some(offset)) => { - Some(LimitByExpr::new(None, Some(offset as u64), limit_by_cols)) - } - (Some(limit), None) => { - Some(LimitByExpr::new(Some(limit as u64), None, limit_by_cols)) - } - (Some(limit), Some(offset)) => Some(LimitByExpr::new( - Some(limit as u64), - Some(offset as u64), - limit_by_cols, - )), - }; + let join_operator = join_expr + .into_iter() + .reduce(and_reducer) + .map(|expr| JoinOperator::LeftOuter(JoinConstraint::On(expr))) + .unwrap_or(JoinOperator::CrossJoin); - (limit_by, None, None) + let relkeys = relationship.column_mapping.values().collect(); + + let join = Join { + relation: self + .rowset_subquery(&relationship_collection, &relkeys, query)? + .into_table_factor() + .alias(&join_alias), + join_operator, }; - Ok(Query::new() - .select(select) - .from(from) - .predicate(predicate) - .order_by(order_by_exprs) - .limit_by(limit_by) - .limit(limit) - .offset(offset)) + let expr = Expr::CompoundIdentifier(vec![ + Ident::new_quoted(&join_alias), + Ident::new_quoted("_rowset"), + ]); + + Ok((expr, join)) } fn filter_expression( &self, @@ -1016,7 +1076,6 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { in_collection, predicate, current_join_alias, - current_collection, name_index, ), } @@ -1026,7 +1085,6 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { in_collection: &models::ExistsInCollection, expression: &Option>, previous_join_alias: &Ident, - previous_collection: &CollectionContext, name_index: &mut u32, ) -> Result<(Expr, Vec), QueryBuilderError> { let exists_join_ident = Ident::new_quoted(format!("_exists_{}", name_index)); @@ -1099,13 +1157,13 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { select.push( Expr::CompoundIdentifier(vec![ subquery_origin_alias.clone(), - self.column_ident(target_col, &target_collection)?, + self.column_ident(target_col), ]) .into_select(Some(format!("_relkey_{target_col}"))), ); limit_by.push(Expr::CompoundIdentifier(vec![ subquery_origin_alias.clone(), - self.column_ident(target_col, &target_collection)?, + self.column_ident(target_col), ])); } } @@ -1152,7 +1210,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { .map(|(source_col, target_col)| { let left = Expr::CompoundIdentifier(vec![ previous_join_alias.clone(), - self.column_ident(source_col, previous_collection)?, + self.column_ident(source_col), ]) .into_box(); let right = Expr::CompoundIdentifier(vec![ @@ -1254,16 +1312,13 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { select.push( Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident( - target_col, - &relationship_collection, - )?, + self.column_ident(target_col), ]) .into_select(Some(format!("_relkey_{target_col}"))), ); group_by.push(Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident(target_col, &relationship_collection)?, + self.column_ident(target_col), ])) } @@ -1342,19 +1397,13 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(Expr::BinaryOp { left: Expr::CompoundIdentifier(vec![ last_join_alias.clone(), - self.column_ident( - source_col, - &last_collection_context, - )?, + self.column_ident(source_col), ]) .into_box(), op: BinaryOperator::Eq, right: Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident( - target_col, - &relationship_collection, - )?, + self.column_ident(target_col), ]) .into_box(), }) @@ -1403,10 +1452,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Function::new_unquoted("groupArray") .args(vec![Expr::CompoundIdentifier(vec![ last_join_alias, - self.column_ident( - comparison_column_name, - &last_collection_context, - )?, + self.column_ident(comparison_column_name), ]) .into_arg()]) .into_expr() @@ -1438,7 +1484,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(Expr::BinaryOp { left: Expr::CompoundIdentifier(vec![ previous_join_alias.clone(), - self.column_ident(source_col, ¤t_collection)?, + self.column_ident(source_col), ]) .into_box(), op: BinaryOperator::Eq, @@ -1526,19 +1572,13 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(Expr::BinaryOp { left: Expr::CompoundIdentifier(vec![ last_join_alias.clone(), - self.column_ident( - source_col, - &last_collection_context, - )?, + self.column_ident(source_col), ]) .into_box(), op: BinaryOperator::Eq, right: Expr::CompoundIdentifier(vec![ join_alias.clone(), - self.column_ident( - target_col, - &relationship_collection, - )?, + self.column_ident(target_col), ]) .into_box(), }) @@ -1585,7 +1625,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { let column_ident = Expr::CompoundIdentifier(vec![ last_join_alias, - self.column_ident(comparison_column_name, &last_collection_context)?, + self.column_ident(comparison_column_name), ]); Ok(ComparisonColumn::new_flat( @@ -1601,7 +1641,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { } else { let column_ident = Expr::CompoundIdentifier(vec![ current_join_alias.clone(), - self.column_ident(comparison_column_name, current_collection)?, + self.column_ident(comparison_column_name), ]); Ok(ComparisonColumn::new_simple( column_ident, @@ -1613,7 +1653,7 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { if current_is_origin { let column_ident = Expr::CompoundIdentifier(vec![ current_join_alias.clone(), - self.column_ident(name, current_collection)?, + self.column_ident(name), ]); Ok(ComparisonColumn::new_simple( column_ident, @@ -1798,26 +1838,18 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { ParameterizedQueryElement::String(s) => { Ok(NativeQueryElement::String(s.to_owned())) } - ParameterizedQueryElement::Parameter(p) => { - let arg_alias = match &p.name { - Identifier::DoubleQuoted(n) - | Identifier::BacktickQuoted(n) - | Identifier::Unquoted(n) => n, - }; - - get_argument(arg_alias) - .transpose()? - .map(|value| { - NativeQueryElement::Parameter(Parameter::new( - value.into(), - p.r#type.clone(), - )) - }) - .ok_or_else(|| QueryBuilderError::MissingNativeQueryArgument { - query: collection.alias().to_owned(), - argument: arg_alias.to_owned(), - }) - } + ParameterizedQueryElement::Parameter(p) => get_argument(p.name.value()) + .transpose()? + .map(|value| { + NativeQueryElement::Parameter(Parameter::new( + value.into(), + p.r#type.clone(), + )) + }) + .ok_or_else(|| QueryBuilderError::MissingNativeQueryArgument { + query: collection.alias().to_owned(), + argument: p.name.value().to_owned(), + }), }) .collect::>()?; @@ -1828,12 +1860,8 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { )) } } - fn column_ident( - &self, - column_alias: &str, - _collection: &CollectionContext, - ) -> Result { - Ok(Ident::new_quoted(column_alias)) + fn column_ident(&self, column_alias: &str) -> Ident { + Ident::new_quoted(column_alias) } fn column_data_type( &self, @@ -1865,6 +1893,153 @@ impl<'r, 'c> QueryBuilder<'r, 'c> { Ok(column_type.to_owned()) } + fn column_accessor( + &self, + column_ident: Vec, + type_definition: &ClickHouseTypeDefinition, + traversed_array: bool, + field_selector: Option<&models::NestedField>, + rel_index: &mut u32, + ) -> Result)>, QueryBuilderError> { + if let Some(fields) = field_selector { + match fields { + models::NestedField::Array(inner) => { + let element_type = match type_definition.non_nullable() { + ClickHouseTypeDefinition::Array { element_type } => &**element_type, + _ => { + return Err(QueryBuilderError::ColumnTypeMismatch { + expected: "Array".to_string(), + got: type_definition.cast_type().to_string(), + }); + } + }; + + let ident = Ident::new_unquoted("_value"); + + if let Some((expr, joins)) = self.column_accessor( + vec![ident.clone()], + element_type, + true, + Some(&inner.fields), + rel_index, + )? { + if !joins.is_empty() { + return Err(QueryBuilderError::Unexpected("column accessor should not return relationship joins after array traversal".to_string())); + } + + let expr = Function::new_unquoted("arrayMap") + .args(vec![ + Lambda::new(vec![ident], expr).into_expr().into_arg(), + Expr::CompoundIdentifier(column_ident).into_arg(), + ]) + .into_expr(); + + Ok(Some((expr, joins))) + } else { + Ok(None) + } + } + models::NestedField::Object(inner) => { + let object_type_fields = match type_definition.non_nullable() { + ClickHouseTypeDefinition::Object { name: _, fields } => fields, + _ => { + return Err(QueryBuilderError::ColumnTypeMismatch { + expected: "Tuple/Object".to_string(), + got: type_definition.cast_type().to_string(), + }); + } + }; + + let chain_ident = |i: &str| -> Vec { + column_ident + .clone() + .into_iter() + .chain(iter::once(Ident::new_quoted(i))) + .collect() + }; + + let mut required_joins = vec![]; + let mut column_accessors = vec![]; + let mut required_columns = vec![]; + let mut accessor_required = false; + for (alias, field) in &inner.fields { + match field { + models::Field::Column { column, fields } => { + required_columns.push(column); + + let type_definition = + object_type_fields.get(column).ok_or_else(|| { + QueryBuilderError::UnknownSubField { + field_name: column.to_owned(), + data_type: type_definition.cast_type().to_string(), + } + })?; + + let column_ident = chain_ident(column); + if let Some((expr, mut joins)) = self.column_accessor( + column_ident.clone(), + type_definition, + traversed_array, + fields.as_ref(), + rel_index, + )? { + accessor_required = true; + required_joins.append(&mut joins); + column_accessors.push(expr); + } else { + column_accessors.push(Expr::CompoundIdentifier(column_ident)); + } + } + models::Field::Relationship { + query, + relationship, + arguments, + } => { + if traversed_array { + return Err(QueryBuilderError::NotSupported( + "Relationships with fields nested in arrays".to_string(), + )); + } + + let (expr, join) = self.field_relationship( + alias, + rel_index, + &column_ident, + query, + relationship, + arguments, + )?; + + required_joins.push(join); + column_accessors.push(expr); + } + } + } + + let all_columns: Vec<_> = object_type_fields.keys().collect(); + if !accessor_required + && required_joins.is_empty() + && required_columns == all_columns + { + Ok(None) + } else { + let expr = Function::new_unquoted("tuple") + .args( + column_accessors + .into_iter() + .map(|expr| expr.into_arg()) + .collect(), + ) + .into_expr(); + + Ok(Some((expr, required_joins))) + } + } + } + } else { + Ok(None) + } + } } fn aggregate_function( diff --git a/crates/ndc-clickhouse/src/sql/query_builder/error.rs b/crates/ndc-clickhouse/src/sql/query_builder/error.rs index 5fdb78b..4339229 100644 --- a/crates/ndc-clickhouse/src/sql/query_builder/error.rs +++ b/crates/ndc-clickhouse/src/sql/query_builder/error.rs @@ -4,7 +4,7 @@ use ndc_sdk::connector::{ExplainError, QueryError}; use super::typecasting::TypeStringError; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum QueryBuilderError { /// A relationship referenced in the query is missing from the collection_relationships map MissingRelationship(String), @@ -20,6 +20,11 @@ pub enum QueryBuilderError { UnknownTableType(String), /// A column was referenced but not found in configuration UnknownColumn(String, String), + /// A field was referenced but not found in configuration + UnknownSubField { + field_name: String, + data_type: String, + }, /// Unable to serialize variables into a json string CannotSerializeVariables(String), /// An unknown single column aggregate function was referenced @@ -32,6 +37,8 @@ pub enum QueryBuilderError { Unexpected(String), /// There was an issue creating typecasting strings Typecasting(TypeStringError), + /// Column type did not match type asserted by request + ColumnTypeMismatch { expected: String, got: String }, } impl fmt::Display for QueryBuilderError { @@ -56,6 +63,12 @@ impl fmt::Display for QueryBuilderError { QueryBuilderError::UnknownColumn(c, t) => { write!(f, "Unable to find column {c} for table {t} in config") } + QueryBuilderError::UnknownSubField { + field_name, + data_type, + } => { + write!(f, "Unknown field {field_name} in type {data_type}") + } QueryBuilderError::CannotSerializeVariables(e) => { write!(f, "Unable to serialize variables into a json string: {e}") } @@ -68,6 +81,9 @@ impl fmt::Display for QueryBuilderError { QueryBuilderError::NotSupported(e) => write!(f, "Not supported: {e}"), QueryBuilderError::Unexpected(e) => write!(f, "Unexpected: {e}"), QueryBuilderError::Typecasting(e) => write!(f, "Typecasting: {e}"), + QueryBuilderError::ColumnTypeMismatch { expected, got } => { + write!(f, "Column Type Mismatch: expected {expected}, got {got}") + } } } } @@ -84,11 +100,13 @@ impl From for QueryError { | QueryBuilderError::UnknownQueryArgument { .. } | QueryBuilderError::UnknownTableType(_) | QueryBuilderError::UnknownColumn(_, _) + | QueryBuilderError::UnknownSubField { .. } | QueryBuilderError::CannotSerializeVariables(_) | QueryBuilderError::UnknownSingleColumnAggregateFunction(_) | QueryBuilderError::UnknownBinaryComparisonOperator(_) - | QueryBuilderError::Typecasting(_) => { - QueryError::UnprocessableContent(value.to_string()) + | QueryBuilderError::Typecasting(_) + | QueryBuilderError::ColumnTypeMismatch { .. } => { + QueryError::InvalidRequest(value.to_string()) } QueryBuilderError::NotSupported(_) => { QueryError::UnsupportedOperation(value.to_string()) @@ -108,11 +126,13 @@ impl From for ExplainError { | QueryBuilderError::UnknownQueryArgument { .. } | QueryBuilderError::UnknownTableType(_) | QueryBuilderError::UnknownColumn(_, _) + | QueryBuilderError::UnknownSubField { .. } | QueryBuilderError::CannotSerializeVariables(_) | QueryBuilderError::UnknownSingleColumnAggregateFunction(_) | QueryBuilderError::UnknownBinaryComparisonOperator(_) - | QueryBuilderError::Typecasting(_) => { - ExplainError::UnprocessableContent(value.to_string()) + | QueryBuilderError::Typecasting(_) + | QueryBuilderError::ColumnTypeMismatch { .. } => { + ExplainError::InvalidRequest(value.to_string()) } QueryBuilderError::NotSupported(_) => { ExplainError::UnsupportedOperation(value.to_string()) diff --git a/crates/ndc-clickhouse/src/sql/query_builder/typecasting.rs b/crates/ndc-clickhouse/src/sql/query_builder/typecasting.rs index 5514cbb..1c16e8e 100644 --- a/crates/ndc-clickhouse/src/sql/query_builder/typecasting.rs +++ b/crates/ndc-clickhouse/src/sql/query_builder/typecasting.rs @@ -1,8 +1,11 @@ use std::{collections::BTreeMap, fmt::Display, str::FromStr}; -use common::{clickhouse_parser::datatype::ClickHouseDataType, config::ServerConfig}; +use common::{ + clickhouse_parser::datatype::{ClickHouseDataType, Identifier}, + config::ServerConfig, +}; use indexmap::IndexMap; -use ndc_sdk::models; +use ndc_sdk::models::{self, NestedField}; use crate::schema::{ClickHouseSingleColumnAggregateFunction, ClickHouseTypeDefinition}; @@ -10,20 +13,22 @@ use super::QueryBuilderError; /// Tuple(rows , aggregates ) pub struct RowsetTypeString { - rows: Option, + rows: Option, aggregates: Option, } /// Tuple("a1" T1, "a2" T2) pub struct AggregatesTypeString { - aggregates: Vec<(String, String)>, + aggregates: Vec<(String, ClickHouseDataType)>, } /// Tuple("f1" T1, "f2" ) -pub struct RowsTypeString { +pub struct RowTypeString { fields: Vec<(String, FieldTypeString)>, } pub enum FieldTypeString { Relationship(RowsetTypeString), - Column(String), + Array(Box), + Object(Vec<(String, FieldTypeString)>), + Scalar(ClickHouseDataType), } impl RowsetTypeString { @@ -34,7 +39,7 @@ impl RowsetTypeString { config: &ServerConfig, ) -> Result { let rows = if let Some(fields) = &query.fields { - Some(RowsTypeString::new( + Some(RowTypeString::new( table_alias, fields, relationships, @@ -51,10 +56,36 @@ impl RowsetTypeString { Ok(Self { rows, aggregates }) } + pub fn into_cast_type(self) -> ClickHouseDataType { + match (self.rows, self.aggregates) { + (None, None) => ClickHouseDataType::Map { + key: Box::new(ClickHouseDataType::Nothing), + value: Box::new(ClickHouseDataType::Nothing), + }, + (None, Some(aggregates)) => ClickHouseDataType::Tuple(vec![( + Some(Identifier::Unquoted("aggregates".to_string())), + aggregates.into_cast_type(), + )]), + (Some(rows), None) => ClickHouseDataType::Tuple(vec![( + Some(Identifier::Unquoted("rows".to_string())), + ClickHouseDataType::Array(Box::new(rows.into_cast_type())), + )]), + (Some(rows), Some(aggregates)) => ClickHouseDataType::Tuple(vec![ + ( + Some(Identifier::Unquoted("rows".to_string())), + ClickHouseDataType::Array(Box::new(rows.into_cast_type())), + ), + ( + Some(Identifier::Unquoted("aggregates".to_string())), + aggregates.into_cast_type(), + ), + ]), + } + } } impl AggregatesTypeString { - pub fn new( + fn new( table_alias: &str, aggregates: &IndexMap, config: &ServerConfig, @@ -64,7 +95,7 @@ impl AggregatesTypeString { .iter() .map(|(alias, aggregate)| match aggregate { models::Aggregate::StarCount {} | models::Aggregate::ColumnCount { .. } => { - Ok((alias.to_string(), "UInt32".to_string())) + Ok((alias.to_string(), ClickHouseDataType::UInt32)) } models::Aggregate::SingleColumn { column: column_alias, @@ -75,6 +106,7 @@ impl AggregatesTypeString { &column_type, column_alias, table_alias, + &config.namespace_separator, ); let aggregate_function = @@ -100,16 +132,31 @@ impl AggregatesTypeString { function: function.to_owned(), })?; - Ok((alias.to_string(), result_type.to_string())) + Ok((alias.to_owned(), result_type.to_owned())) } }) .collect::, _>>()?, }) } + fn into_cast_type(self) -> ClickHouseDataType { + if self.aggregates.is_empty() { + ClickHouseDataType::Map { + key: Box::new(ClickHouseDataType::Nothing), + value: Box::new(ClickHouseDataType::Nothing), + } + } else { + ClickHouseDataType::Tuple( + self.aggregates + .into_iter() + .map(|(alias, t)| (Some(Identifier::DoubleQuoted(alias)), t)) + .collect(), + ) + } + } } -impl RowsTypeString { - pub fn new( +impl RowTypeString { + fn new( table_alias: &str, fields: &IndexMap, relationships: &BTreeMap, @@ -126,18 +173,20 @@ impl RowsTypeString { column: column_alias, fields, } => { - if fields.is_some() { - return Err(TypeStringError::NotSupported( - "subfield selector".into(), - )); - } let column_type = get_column(column_alias, table_alias, config)?; let type_definition = ClickHouseTypeDefinition::from_table_column( &column_type, column_alias, table_alias, + &config.namespace_separator, ); - FieldTypeString::Column(type_definition.cast_type().to_string()) + + FieldTypeString::new( + &type_definition, + fields.as_ref(), + relationships, + config, + )? } models::Field::Relationship { query, @@ -166,70 +215,166 @@ impl RowsTypeString { .collect::, _>>()?, }) } -} - -impl Display for RowsetTypeString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match (&self.rows, &self.aggregates) { - (None, None) => write!(f, "Map(Nothing, Nothing)"), - (None, Some(aggregates)) => write!(f, "Tuple(aggregates {aggregates})"), - (Some(rows), None) => write!(f, "Tuple(rows {rows})"), - (Some(rows), Some(aggregates)) => { - write!(f, "Tuple(rows {rows}, aggregates {aggregates})") + fn into_cast_type(self) -> ClickHouseDataType { + if self.fields.is_empty() { + ClickHouseDataType::Map { + key: Box::new(ClickHouseDataType::Nothing), + value: Box::new(ClickHouseDataType::Nothing), } + } else { + ClickHouseDataType::Tuple( + self.fields + .into_iter() + .map(|(alias, field)| { + ( + Some(Identifier::DoubleQuoted(alias)), + field.into_cast_type(), + ) + }) + .collect(), + ) } } } -impl Display for AggregatesTypeString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.aggregates.is_empty() { - write!(f, "Map(Nothing, Nothing)") - } else { - write!(f, "Tuple(")?; - let mut first = true; - for (alias, t) in &self.aggregates { - if first { - first = false; - } else { - write!(f, ", ")?; +impl FieldTypeString { + fn new( + type_definition: &ClickHouseTypeDefinition, + fields: Option<&NestedField>, + relationships: &BTreeMap, + config: &ServerConfig, + ) -> Result { + if let Some(fields) = fields { + match (type_definition.non_nullable(), fields) { + ( + ClickHouseTypeDefinition::Array { element_type }, + NestedField::Array(subfield_selector), + ) => { + let type_definition = &**element_type; + let fields = Some(&*subfield_selector.fields); + let underlying_typestring = + FieldTypeString::new(type_definition, fields, relationships, config)?; + Ok(FieldTypeString::Array(Box::new(underlying_typestring))) } + ( + ClickHouseTypeDefinition::Object { name: _, fields }, + NestedField::Object(subfield_selector), + ) => { + let subfields = subfield_selector + .fields + .iter() + .map(|(alias, field)| { + match field { + models::Field::Column { + column, + fields: subfield_selector, + } => { + let type_definition = fields.get(column).ok_or_else(|| { + TypeStringError::MissingNestedField { + field_name: column.to_owned(), + object_type: type_definition.cast_type().to_string(), + } + })?; - write!(f, "\"{alias}\" {t}")?; - } + Ok(( + alias.to_owned(), + FieldTypeString::new( + &type_definition, + subfield_selector.as_ref(), + relationships, + config, + )?, + )) + } + models::Field::Relationship { + query, + relationship, + arguments: _, + } => { + let relationship = + relationships.get(relationship).ok_or_else(|| { + TypeStringError::MissingRelationship( + relationship.to_owned(), + ) + })?; - write!(f, ")") - } - } -} -impl Display for RowsTypeString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.fields.is_empty() { - write!(f, "Array(Map(Nothing, Nothing))") - } else { - write!(f, "Array(Tuple(")?; - let mut first = true; + let table_alias = &relationship.target_collection; - for (alias, field) in &self.fields { - if first { - first = false; - } else { - write!(f, ", ")?; + Ok(( + alias.to_owned(), + FieldTypeString::Relationship(RowsetTypeString::new( + table_alias, + query, + relationships, + config, + )?), + )) + } + } + // Ok((alias, FieldTypeString::new(type_definition, fields))) + }) + .collect::>()?; + Ok(FieldTypeString::Object(subfields)) } - - write!(f, "\"{alias}\" ")?; - - match field { - FieldTypeString::Column(c) => { - write!(f, "{c}")?; - } - FieldTypeString::Relationship(r) => { - write!(f, "{r}")?; - } + (ClickHouseTypeDefinition::Scalar(_), NestedField::Object(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Object".to_owned(), + got: type_definition.cast_type().to_string(), + }) + } + (ClickHouseTypeDefinition::Scalar(_), NestedField::Array(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Array".to_owned(), + got: type_definition.cast_type().to_string(), + }) + } + (ClickHouseTypeDefinition::Nullable { .. }, NestedField::Object(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Object".to_owned(), + got: type_definition.cast_type().to_string(), + }) + } + (ClickHouseTypeDefinition::Nullable { .. }, NestedField::Array(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Array".to_owned(), + got: type_definition.cast_type().to_string(), + }) + } + (ClickHouseTypeDefinition::Array { .. }, NestedField::Object(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Object".to_owned(), + got: type_definition.cast_type().to_string(), + }) + } + (ClickHouseTypeDefinition::Object { .. }, NestedField::Array(_)) => { + Err(TypeStringError::NestedFieldTypeMismatch { + expected: "Array".to_owned(), + got: type_definition.cast_type().to_string(), + }) } } - - write!(f, "))") + } else { + Ok(FieldTypeString::Scalar(type_definition.cast_type())) + } + } + fn into_cast_type(self) -> ClickHouseDataType { + match self { + FieldTypeString::Relationship(rel) => rel.into_cast_type(), + FieldTypeString::Array(inner) => { + ClickHouseDataType::Array(Box::new(inner.into_cast_type())) + } + FieldTypeString::Object(fields) => ClickHouseDataType::Tuple( + fields + .into_iter() + .map(|(alias, field)| { + ( + Some(Identifier::DoubleQuoted(alias)), + field.into_cast_type(), + ) + }) + .collect(), + ), + FieldTypeString::Scalar(inner) => inner, } } } @@ -273,7 +418,7 @@ fn get_column<'a>( Ok(column) } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum TypeStringError { UnknownTable { table: String, @@ -293,6 +438,14 @@ pub enum TypeStringError { }, MissingRelationship(String), NotSupported(String), + NestedFieldTypeMismatch { + expected: String, + got: String, + }, + MissingNestedField { + field_name: String, + object_type: String, + }, } impl Display for TypeStringError { @@ -311,6 +464,8 @@ impl Display for TypeStringError { } => write!(f, "Unknown aggregate function: {function} for column {column} of type: {data_type} in table {table}"), TypeStringError::MissingRelationship(rel) => write!(f, "Missing relationship: {rel}"), TypeStringError::NotSupported(feature) => write!(f, "Not supported: {feature}"), + TypeStringError::NestedFieldTypeMismatch { expected, got } => write!(f, "Nested field selector type mismatch, expected: {expected}, got {got}"), + TypeStringError::MissingNestedField { field_name, object_type } => write!(f, "Missing field {field_name} in object type {object_type}"), } } } diff --git a/crates/ndc-clickhouse/tests/query_builder.rs b/crates/ndc-clickhouse/tests/query_builder.rs new file mode 100644 index 0000000..894742c --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder.rs @@ -0,0 +1,307 @@ +use common::config_file::ServerConfigFile; +use ndc_clickhouse::sql::QueryBuilderError; +use ndc_sdk::models; +use schemars::schema_for; +use std::error::Error; +use tokio::fs; + +mod test_utils { + use common::config::ServerConfig; + use ndc_clickhouse::{ + connector::read_server_config, + sql::{QueryBuilder, QueryBuilderError}, + }; + use ndc_sdk::models; + use std::{env, error::Error, path::PathBuf}; + use tokio::fs; + + /// when running tests locally, this can be set to true to update reference files + /// this allows us to view diffs between commited samples and fresh samples + /// we don't want that behavior when running CI, so this value should be false in commited code + const UPDATE_GENERATED_SQL: bool = false; + + fn base_path(schema_dir: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("query_builder") + .join(schema_dir) + } + fn tests_dir_path(schema_dir: &str, group_dir: &str) -> PathBuf { + base_path(schema_dir).join(group_dir) + } + fn config_dir_path(schema_dir: &str) -> PathBuf { + base_path(schema_dir).join("_config") + } + async fn read_mock_configuration(schema_dir: &str) -> Result> { + // set mock values for required env vars, we won't be reading these anyways + env::set_var("CLICKHOUSE_URL", ""); + env::set_var("CLICKHOUSE_USERNAME", ""); + env::set_var("CLICKHOUSE_PASSWORD", ""); + let config_dir = config_dir_path(schema_dir); + let configuration = read_server_config(config_dir).await?; + Ok(configuration) + } + async fn read_request( + schema_dir: &str, + group_dir: &str, + test_name: &str, + ) -> Result> { + let request_path = + tests_dir_path(schema_dir, group_dir).join(format!("{test_name}.request.json")); + + let file_content = fs::read_to_string(request_path).await?; + let request: models::QueryRequest = serde_json::from_str(&file_content)?; + + Ok(request) + } + async fn read_expected_sql( + schema_dir: &str, + group_dir: &str, + test_name: &str, + ) -> Result> { + let statement_path = + tests_dir_path(schema_dir, group_dir).join(format!("{test_name}.statement.sql")); + let expected_statement = fs::read_to_string(&statement_path).await?; + Ok(expected_statement) + } + async fn write_expected_sql( + schema_dir: &str, + group_dir: &str, + test_name: &str, + generated_statement: &str, + ) -> Result<(), Box> { + let statement_path = + tests_dir_path(schema_dir, group_dir).join(format!("{test_name}.statement.sql")); + let pretty_statement = pretty_print_sql(&generated_statement); + fs::write(&statement_path, &pretty_statement).await?; + Ok(()) + } + fn pretty_print_sql(query: &str) -> String { + use sqlformat::{format, FormatOptions, Indent, QueryParams}; + let params = QueryParams::None; + let options = FormatOptions { + indent: Indent::Spaces(2), + uppercase: false, + lines_between_queries: 1, + }; + + format(query, ¶ms, options) + } + fn generate_sql( + configuration: &ServerConfig, + request: &models::QueryRequest, + ) -> Result { + let generated_statement = pretty_print_sql( + &QueryBuilder::new(&request, &configuration) + .build()? + .to_unsafe_sql_string(), + ); + Ok(generated_statement) + } + pub async fn test_generated_sql( + schema_dir: &str, + group_dir: &str, + test_name: &str, + ) -> Result<(), Box> { + let configuration = read_mock_configuration(schema_dir).await?; + let request = read_request(schema_dir, group_dir, test_name).await?; + + let generated_sql = generate_sql(&configuration, &request)?; + + if UPDATE_GENERATED_SQL { + write_expected_sql(schema_dir, group_dir, test_name, &generated_sql).await?; + } else { + let expected_sql = read_expected_sql(schema_dir, group_dir, test_name).await?; + + assert_eq!(generated_sql, expected_sql); + } + + Ok(()) + } + pub async fn test_error( + schema_dir: &str, + group_dir: &str, + test_name: &str, + err: QueryBuilderError, + ) -> Result<(), Box> { + let configuration = read_mock_configuration(schema_dir).await?; + let request = read_request(schema_dir, group_dir, test_name).await?; + + let result = generate_sql(&configuration, &request); + + assert_eq!(result, Err(err)); + + Ok(()) + } +} + +#[tokio::test] +#[ignore] +async fn update_json_schema() -> Result<(), Box> { + fs::write( + "./tests/query_builder/request.schema.json", + serde_json::to_string_pretty(&schema_for!(models::QueryRequest))?, + ) + .await?; + fs::write( + "./tests/query_builder/configuration.schema.json", + serde_json::to_string_pretty(&schema_for!(ServerConfigFile))?, + ) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod simple_queries { + use super::*; + + async fn test_generated_sql(name: &str) -> Result<(), Box> { + super::test_utils::test_generated_sql("chinook", "01_simple_queries", name).await + } + + #[tokio::test] + async fn select_rows() -> Result<(), Box> { + test_generated_sql("01_select_rows").await + } + #[tokio::test] + async fn with_predicate() -> Result<(), Box> { + test_generated_sql("02_with_predicate").await + } + #[tokio::test] + async fn larger_predicate() -> Result<(), Box> { + test_generated_sql("03_larger_predicate").await + } + #[tokio::test] + async fn limit() -> Result<(), Box> { + test_generated_sql("04_limit").await + } + #[tokio::test] + async fn offset() -> Result<(), Box> { + test_generated_sql("05_offset").await + } + #[tokio::test] + async fn limit_offset() -> Result<(), Box> { + test_generated_sql("06_limit_offset").await + } + + #[tokio::test] + async fn order_by() -> Result<(), Box> { + test_generated_sql("07_order_by").await + } + #[tokio::test] + async fn predicate_limit_offset_order_by() -> Result<(), Box> { + test_generated_sql("08_predicate_limit_offset_order_by").await + } +} + +#[cfg(test)] +mod relationships { + use super::*; + + async fn test_generated_sql(name: &str) -> Result<(), Box> { + super::test_utils::test_generated_sql("chinook", "02_relationships", name).await + } + + #[tokio::test] + async fn object_relationship() -> Result<(), Box> { + test_generated_sql("01_object_relationship").await + } + #[tokio::test] + async fn array_relationship() -> Result<(), Box> { + test_generated_sql("02_array_relationship").await + } + #[tokio::test] + async fn parent_predicate() -> Result<(), Box> { + test_generated_sql("03_parent_predicate").await + } + #[tokio::test] + async fn child_predicate() -> Result<(), Box> { + test_generated_sql("04_child_predicate").await + } + #[tokio::test] + async fn traverse_relationship_in_predicate() -> Result<(), Box> { + test_generated_sql("05_traverse_relationship_in_predicate").await + } + #[tokio::test] + async fn traverse_relationship_in_order_by() -> Result<(), Box> { + test_generated_sql("06_traverse_relationship_in_order_by").await + } + #[tokio::test] + async fn order_by_aggregate_across_relationships() -> Result<(), Box> { + test_generated_sql("07_order_by_aggregate_across_relationships").await + } +} + +#[cfg(test)] +mod variables { + use super::*; + + async fn test_generated_sql(name: &str) -> Result<(), Box> { + super::test_utils::test_generated_sql("chinook", "03_variables", name).await + } + + #[tokio::test] + async fn simple_predicate() -> Result<(), Box> { + test_generated_sql("01_simple_predicate").await + } + #[tokio::test] + async fn empty_variable_sets() -> Result<(), Box> { + test_generated_sql("02_empty_variable_sets").await + } +} + +#[cfg(test)] +mod native_queries { + use super::*; + + async fn test_generated_sql(name: &str) -> Result<(), Box> { + super::test_utils::test_generated_sql("star_schema", "01_native_queries", name).await + } + + #[tokio::test] + async fn native_query() -> Result<(), Box> { + test_generated_sql("01_native_query").await + } +} + +#[cfg(test)] +mod field_selector { + use super::*; + + async fn test_generated_sql(name: &str) -> Result<(), Box> { + super::test_utils::test_generated_sql("complex_columns", "field_selector", name).await + } + async fn test_error(name: &str, err: QueryBuilderError) -> Result<(), Box> { + super::test_utils::test_error("complex_columns", "field_selector", name, err).await + } + + #[tokio::test] + async fn generate_column_accessor() -> Result<(), Box> { + test_generated_sql("01_generate_column_accessor").await + } + #[tokio::test] + async fn skip_if_not_required() -> Result<(), Box> { + test_generated_sql("02_skip_if_not_required").await + } + #[tokio::test] + async fn support_relationships_on_nested_field() -> Result<(), Box> { + test_generated_sql("03_support_relationships_on_nested_field").await + } + /// We do not support relationships on nested fileds if an array has been traversed + #[tokio::test] + async fn error_on_relationships_on_array_nested_field() -> Result<(), Box> { + let err = QueryBuilderError::NotSupported( + "Relationships with fields nested in arrays".to_string(), + ); + test_error("04_error_on_relationships_on_array_nested_field", err).await + } + #[tokio::test] + async fn complex_example() -> Result<(), Box> { + test_generated_sql("05_complex_example").await + } + #[tokio::test] + async fn no_useless_nested_accessors() -> Result<(), Box> { + test_generated_sql("06_no_useless_nested_accessors").await + } +} diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.request.json new file mode 100644 index 0000000..44df377 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.request.json @@ -0,0 +1,25 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.statement.sql new file mode 100644 index 0000000..6d3e6f9 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/01_select_rows.statement.sql @@ -0,0 +1,31 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.request.json new file mode 100644 index 0000000..a2d33af --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.request.json @@ -0,0 +1,48 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "scalar", + "value": "1" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.statement.sql new file mode 100644 index 0000000..f3a8cb4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/02_with_predicate.statement.sql @@ -0,0 +1,33 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + WHERE + "_origin"."ArtistId" = '1' + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.request.json new file mode 100644 index 0000000..a41e8fe --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.request.json @@ -0,0 +1,84 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "scalar", + "value": "1" + } + } + ] + }, + { + "type": "not", + "expression": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "scalar", + "value": "2" + } + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.statement.sql new file mode 100644 index 0000000..146636b --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/03_larger_predicate.statement.sql @@ -0,0 +1,36 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + WHERE + ( + "_origin"."ArtistId" = '1' + AND NOT ("_origin"."ArtistId" = '2') + ) + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.request.json new file mode 100644 index 0000000..acb33fa --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.request.json @@ -0,0 +1,26 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "limit": 10 + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.statement.sql new file mode 100644 index 0000000..3063baa --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/04_limit.statement.sql @@ -0,0 +1,33 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + LIMIT + 10 + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.request.json new file mode 100644 index 0000000..63d6984 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.request.json @@ -0,0 +1,26 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "offset": 10 + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.statement.sql new file mode 100644 index 0000000..c54bca3 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/05_offset.statement.sql @@ -0,0 +1,31 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" OFFSET 10 + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.request.json new file mode 100644 index 0000000..b69b6c4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.request.json @@ -0,0 +1,27 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "limit": 10, + "offset": 10 + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.statement.sql new file mode 100644 index 0000000..20d8f04 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/06_limit_offset.statement.sql @@ -0,0 +1,33 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + LIMIT + 10 OFFSET 10 + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.request.json new file mode 100644 index 0000000..27b8008 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.request.json @@ -0,0 +1,37 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "order_by": { + "elements": [ + { + "order_direction": "asc", + "target": { + "type": "column", + "name": "ArtistId", + "path": [] + } + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.statement.sql new file mode 100644 index 0000000..d9df4f4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/07_order_by.statement.sql @@ -0,0 +1,33 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + ORDER BY + "_origin"."ArtistId" ASC + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.request.json new file mode 100644 index 0000000..c1275db --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.request.json @@ -0,0 +1,62 @@ +{ + "$schema": "../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "limit": 10, + "offset": 10, + "order_by": { + "elements": [ + { + "order_direction": "asc", + "target": { + "type": "column", + "name": "ArtistId", + "path": [] + } + } + ] + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_gt", + "value": { + "type": "scalar", + "value": "10" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.statement.sql new file mode 100644 index 0000000..3e74d6d --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/01_simple_queries/08_predicate_limit_offset_order_by.statement.sql @@ -0,0 +1,37 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title" + FROM + "Chinook"."Album" AS "_origin" + WHERE + "_origin"."ArtistId" > '10' + ORDER BY + "_origin"."ArtistId" ASC + LIMIT + 10 OFFSET 10 + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.request.json new file mode 100644 index 0000000..99c578e --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.request.json @@ -0,0 +1,53 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + }, + "Artist": { + "type": "relationship", + "query": { + "fields": { + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {} + } + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]": { + "column_mapping": { + "ArtistId": "ArtistId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Artist", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.statement.sql new file mode 100644 index 0000000..80d4b9a --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/01_object_relationship.statement.sql @@ -0,0 +1,53 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Artist" Tuple(rows Array(Tuple("artistId" Int32, "name" Nullable(String)))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Artist" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Artist"."_rowset" AS "_field_Artist" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple("_row"."_field_artistId", "_row"."_field_name") + ) + ) AS "_rowset", + "_row"."_relkey_ArtistId" AS "_relkey_ArtistId" + FROM + ( + SELECT + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Name" AS "_field_name", + "_origin"."ArtistId" AS "_relkey_ArtistId" + FROM + "Chinook"."Artist" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_ArtistId" + ) AS "_rel_0_Artist" ON "_origin"."ArtistId" = "_rel_0_Artist"."_relkey_ArtistId" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.request.json new file mode 100644 index 0000000..895b14a --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.request.json @@ -0,0 +1,58 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + }, + "Tracks": { + "type": "relationship", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + }, + "unitPrice": { + "type": "column", + "column": "UnitPrice", + "fields": null + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]", + "arguments": {} + } + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "array", + "target_collection": "Chinook_Track", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement copy.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement copy.sql new file mode 100644 index 0000000..ebf6a7d --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement copy.sql @@ -0,0 +1,58 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Tracks" Tuple(rows Array(Tuple("trackId" Int32, "name" String, "unitPrice" Float64))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Tracks" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Tracks"."_rowset" AS "_field_Tracks" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_unitPrice" + ) + ) + ) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_origin"."UnitPrice" AS "_field_unitPrice", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Track" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Tracks" ON "_origin"."AlbumId" = "_rel_0_Tracks"."_relkey_AlbumId" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement.sql new file mode 100644 index 0000000..ebf6a7d --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/02_array_relationship.statement.sql @@ -0,0 +1,58 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Tracks" Tuple(rows Array(Tuple("trackId" Int32, "name" String, "unitPrice" Float64))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Tracks" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Tracks"."_rowset" AS "_field_Tracks" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_unitPrice" + ) + ) + ) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_origin"."UnitPrice" AS "_field_unitPrice", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Track" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Tracks" ON "_origin"."AlbumId" = "_rel_0_Tracks"."_relkey_AlbumId" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.request.json new file mode 100644 index 0000000..8065a62 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.request.json @@ -0,0 +1,81 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + }, + "Tracks": { + "type": "relationship", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + }, + "unitPrice": { + "type": "column", + "column": "UnitPrice", + "fields": null + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]", + "arguments": {} + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_gt", + "value": { + "type": "scalar", + "value": "10" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "array", + "target_collection": "Chinook_Track", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.statement.sql new file mode 100644 index 0000000..374b8b7 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/03_parent_predicate.statement.sql @@ -0,0 +1,60 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Tracks" Tuple(rows Array(Tuple("trackId" Int32, "name" String, "unitPrice" Float64))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Tracks" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Tracks"."_rowset" AS "_field_Tracks" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_unitPrice" + ) + ) + ) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_origin"."UnitPrice" AS "_field_unitPrice", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Track" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Tracks" ON "_origin"."AlbumId" = "_rel_0_Tracks"."_relkey_AlbumId" + WHERE + "_origin"."ArtistId" > '10' + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.request.json new file mode 100644 index 0000000..929e20d --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.request.json @@ -0,0 +1,104 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + }, + "Tracks": { + "type": "relationship", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + }, + "unitPrice": { + "type": "column", + "column": "UnitPrice", + "fields": null + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "TrackId", + "path": [] + }, + "operator": "_gt", + "value": { + "type": "scalar", + "value": "10" + } + } + ] + } + ] + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]", + "arguments": {} + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_gt", + "value": { + "type": "scalar", + "value": "10" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "array", + "target_collection": "Chinook_Track", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.statement.sql new file mode 100644 index 0000000..eea2e1e --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/04_child_predicate.statement.sql @@ -0,0 +1,62 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Tracks" Tuple(rows Array(Tuple("trackId" Int32, "name" String, "unitPrice" Float64))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Tracks" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Tracks"."_rowset" AS "_field_Tracks" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_unitPrice" + ) + ) + ) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_origin"."UnitPrice" AS "_field_unitPrice", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Track" AS "_origin" + WHERE + "_origin"."TrackId" > '10' + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Tracks" ON "_origin"."AlbumId" = "_rel_0_Tracks"."_relkey_AlbumId" + WHERE + "_origin"."ArtistId" > '10' + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.request.json new file mode 100644 index 0000000..75521ff --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.request.json @@ -0,0 +1,102 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + }, + "Tracks": { + "type": "relationship", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + }, + "unitPrice": { + "type": "column", + "column": "UnitPrice", + "fields": null + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]", + "arguments": {} + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "exists", + "in_collection": { + "type": "related", + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {} + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "Name", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "scalar", + "value": "AC/DC" + } + } + ] + } + ] + } + } + ] + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]": { + "column_mapping": { + "ArtistId": "ArtistId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Artist", + "arguments": {} + }, + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Tracks\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "array", + "target_collection": "Chinook_Track", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.statement.sql new file mode 100644 index 0000000..ab269b8 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/05_traverse_relationship_in_predicate.statement.sql @@ -0,0 +1,71 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String, "Tracks" Tuple(rows Array(Tuple("trackId" Int32, "name" String, "unitPrice" Float64))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title", + "_row"."_field_Tracks" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_rel_0_Tracks"."_rowset" AS "_field_Tracks" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_unitPrice" + ) + ) + ) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_origin"."UnitPrice" AS "_field_unitPrice", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Track" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Tracks" ON "_origin"."AlbumId" = "_rel_0_Tracks"."_relkey_AlbumId" + LEFT JOIN ( + SELECT + TRUE AS "_exists_0", + "_exists_1"."ArtistId" AS "_relkey_ArtistId" + FROM + "Chinook"."Artist" AS "_exists_1" + WHERE + "_exists_1"."Name" = 'AC/DC' + LIMIT + 1 BY "_exists_1"."ArtistId" + ) AS "_exists_0" ON "_origin"."ArtistId" = "_exists_0"."_relkey_ArtistId" + WHERE + "_exists_0"."_exists_0" = TRUE + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.request.json new file mode 100644 index 0000000..38b4a79 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.request.json @@ -0,0 +1,97 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Track", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + }, + "Album": { + "type": "relationship", + "query": { + "fields": { + "Artist": { + "type": "relationship", + "query": { + "fields": { + "name": { + "type": "column", + "column": "Name", + "fields": null + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {} + } + } + }, + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]", + "arguments": {} + } + }, + "order_by": { + "elements": [ + { + "order_direction": "asc", + "target": { + "type": "column", + "name": "Name", + "path": [ + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + }, + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + } + ] + } + }, + { + "order_direction": "asc", + "target": { + "type": "column", + "name": "Name", + "path": [] + } + } + ] + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]": { + "column_mapping": { + "ArtistId": "ArtistId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Artist", + "arguments": {} + }, + "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Album", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.statement.sql new file mode 100644 index 0000000..0839ad6 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/06_traverse_relationship_in_order_by.statement.sql @@ -0,0 +1,80 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("trackId" Int32, "name" String, "Album" Tuple(rows Array(Tuple("Artist" Tuple(rows Array(Tuple("name" Nullable(String))))))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_trackId", + "_row"."_field_name", + "_row"."_field_Album" + ) + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name", + "_rel_0_Album"."_rowset" AS "_field_Album" + FROM + "Chinook"."Track" AS "_origin" + LEFT JOIN ( + SELECT + tuple(groupArray(tuple("_row"."_field_Artist"))) AS "_rowset", + "_row"."_relkey_AlbumId" AS "_relkey_AlbumId" + FROM + ( + SELECT + "_rel_0_Artist"."_rowset" AS "_field_Artist", + "_origin"."AlbumId" AS "_relkey_AlbumId" + FROM + "Chinook"."Album" AS "_origin" + LEFT JOIN ( + SELECT + tuple(groupArray(tuple("_row"."_field_name"))) AS "_rowset", + "_row"."_relkey_ArtistId" AS "_relkey_ArtistId" + FROM + ( + SELECT + "_origin"."Name" AS "_field_name", + "_origin"."ArtistId" AS "_relkey_ArtistId" + FROM + "Chinook"."Artist" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_ArtistId" + ) AS "_rel_0_Artist" ON "_origin"."ArtistId" = "_rel_0_Artist"."_relkey_ArtistId" + ) AS "_row" + GROUP BY + "_row"."_relkey_AlbumId" + ) AS "_rel_0_Album" ON "_origin"."AlbumId" = "_rel_0_Album"."_relkey_AlbumId" + LEFT JOIN ( + SELECT + "_order_by_0"."AlbumId" AS "_relkey_AlbumId", + "_order_by_1"."Name" AS "_order_by_value" + FROM + "Chinook"."Album" AS "_order_by_0" + JOIN "Chinook"."Artist" AS "_order_by_1" ON "_order_by_0"."ArtistId" = "_order_by_1"."ArtistId" + WHERE + TRUE + AND TRUE + GROUP BY + "_order_by_0"."AlbumId", + "_order_by_1"."Name" + LIMIT + 1 BY "_order_by_0"."AlbumId" + ) AS "_order_by_0" ON "_origin"."AlbumId" = "_order_by_0"."_relkey_AlbumId" + ORDER BY + "_order_by_0"."_order_by_value" ASC, + "_origin"."Name" ASC + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.request.json new file mode 100644 index 0000000..5c30d2b --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.request.json @@ -0,0 +1,116 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Track", + "query": { + "fields": { + "trackId": { + "type": "column", + "column": "TrackId", + "fields": null + }, + "name": { + "type": "column", + "column": "Name", + "fields": null + } + }, + "order_by": { + "elements": [ + { + "order_direction": "asc", + "target": { + "type": "column", + "name": "Name", + "path": [ + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + }, + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + } + ] + } + }, + { + "order_direction": "asc", + "target": { + "type": "star_count_aggregate", + "path": [ + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + }, + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + } + ] + } + }, + { + "order_direction": "asc", + "target": { + "type": "single_column_aggregate", + "column": "Name", + "function": "max", + "path": [ + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + }, + { + "relationship": "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]", + "arguments": {}, + "predicate": { + "type": "and", + "expressions": [] + } + } + ] + } + } + ] + } + }, + "arguments": {}, + "collection_relationships": { + "[{\"subgraph\":\"app\",\"name\":\"ChinookAlbum\"},\"Artist\"]": { + "column_mapping": { + "ArtistId": "ArtistId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Artist", + "arguments": {} + }, + "[{\"subgraph\":\"app\",\"name\":\"ChinookTrack\"},\"Album\"]": { + "column_mapping": { + "AlbumId": "AlbumId" + }, + "relationship_type": "object", + "target_collection": "Chinook_Album", + "arguments": {} + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.statement.sql new file mode 100644 index 0000000..db6585a --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/02_relationships/07_order_by_aggregate_across_relationships.statement.sql @@ -0,0 +1,76 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("trackId" Int32, "name" String)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple("_row"."_field_trackId", "_row"."_field_name") + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."TrackId" AS "_field_trackId", + "_origin"."Name" AS "_field_name" + FROM + "Chinook"."Track" AS "_origin" + LEFT JOIN ( + SELECT + "_order_by_0"."AlbumId" AS "_relkey_AlbumId", + "_order_by_1"."Name" AS "_order_by_value" + FROM + "Chinook"."Album" AS "_order_by_0" + JOIN "Chinook"."Artist" AS "_order_by_1" ON "_order_by_0"."ArtistId" = "_order_by_1"."ArtistId" + WHERE + TRUE + AND TRUE + GROUP BY + "_order_by_0"."AlbumId", + "_order_by_1"."Name" + LIMIT + 1 BY "_order_by_0"."AlbumId" + ) AS "_order_by_0" ON "_origin"."AlbumId" = "_order_by_0"."_relkey_AlbumId" + LEFT JOIN ( + SELECT + "_order_by_0"."AlbumId" AS "_relkey_AlbumId", + COUNT(*) AS "_order_by_value" + FROM + "Chinook"."Album" AS "_order_by_0" + JOIN "Chinook"."Artist" AS "_order_by_1" ON "_order_by_0"."ArtistId" = "_order_by_1"."ArtistId" + WHERE + TRUE + AND TRUE + GROUP BY + "_order_by_0"."AlbumId" + LIMIT + 1 BY "_order_by_0"."AlbumId" + ) AS "_order_by_1" ON "_origin"."AlbumId" = "_order_by_1"."_relkey_AlbumId" + LEFT JOIN ( + SELECT + "_order_by_0"."AlbumId" AS "_relkey_AlbumId", + max("_order_by_1"."Name") AS "_order_by_value" + FROM + "Chinook"."Album" AS "_order_by_0" + JOIN "Chinook"."Artist" AS "_order_by_1" ON "_order_by_0"."ArtistId" = "_order_by_1"."ArtistId" + WHERE + TRUE + AND TRUE + GROUP BY + "_order_by_0"."AlbumId" + LIMIT + 1 BY "_order_by_0"."AlbumId" + ) AS "_order_by_2" ON "_origin"."AlbumId" = "_order_by_2"."_relkey_AlbumId" + ORDER BY + "_order_by_0"."_order_by_value" ASC, + "_order_by_1"."_order_by_value" ASC, + "_order_by_2"."_order_by_value" ASC + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.request.json new file mode 100644 index 0000000..eacd229 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.request.json @@ -0,0 +1,53 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "variables": [ + { + "ArtistId": 1 + } + ], + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "variable", + "name": "ArtistId" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.statement.sql new file mode 100644 index 0000000..522acc5 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/01_simple_predicate.statement.sql @@ -0,0 +1,50 @@ +WITH "_vars" AS ( + SELECT + * + FROM + format( + JSONColumns, + '{"_varset_id":[1],"_var_ArtistId":[1]}' + ) +) +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + "_vars" AS "_vars" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset", + "_row"."_varset_id" AS "_varset_id" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_vars"."_varset_id" AS "_varset_id" + FROM + "_vars" AS "_vars" + CROSS JOIN "Chinook"."Album" AS "_origin" + WHERE + "_origin"."ArtistId" = "_vars"."_var_ArtistId" + ) AS "_row" + GROUP BY + "_row"."_varset_id" + ) AS "_rowset" ON "_vars"."_varset_id" = "_rowset"."_varset_id" +ORDER BY + "_vars"."_varset_id" ASC FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.request.json b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.request.json new file mode 100644 index 0000000..18327db --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.request.json @@ -0,0 +1,49 @@ +{ + "$schema": "../../request.schema.json", + "collection": "Chinook_Album", + "variables": [], + "query": { + "fields": { + "albumId": { + "type": "column", + "column": "AlbumId", + "fields": null + }, + "artistId": { + "type": "column", + "column": "ArtistId", + "fields": null + }, + "title": { + "type": "column", + "column": "Title", + "fields": null + } + }, + "predicate": { + "type": "and", + "expressions": [ + { + "type": "and", + "expressions": [ + { + "type": "binary_comparison_operator", + "column": { + "type": "column", + "name": "ArtistId", + "path": [] + }, + "operator": "_eq", + "value": { + "type": "variable", + "name": "ArtistId" + } + } + ] + } + ] + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.statement.sql b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.statement.sql new file mode 100644 index 0000000..930ddb6 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/03_variables/02_empty_variable_sets.statement.sql @@ -0,0 +1,47 @@ +WITH "_vars" AS ( + SELECT + * + FROM + format(JSONColumns, '{"_varset_id":[]}') +) +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("albumId" Int32, "artistId" Int32, "title" String)))' + ) + ) + ) AS "rowsets" +FROM + "_vars" AS "_vars" + LEFT JOIN ( + SELECT + tuple( + groupArray( + tuple( + "_row"."_field_albumId", + "_row"."_field_artistId", + "_row"."_field_title" + ) + ) + ) AS "_rowset", + "_row"."_varset_id" AS "_varset_id" + FROM + ( + SELECT + "_origin"."AlbumId" AS "_field_albumId", + "_origin"."ArtistId" AS "_field_artistId", + "_origin"."Title" AS "_field_title", + "_vars"."_varset_id" AS "_varset_id" + FROM + "_vars" AS "_vars" + CROSS JOIN "Chinook"."Album" AS "_origin" + WHERE + "_origin"."ArtistId" = "_vars"."_var_ArtistId" + ) AS "_row" + GROUP BY + "_row"."_varset_id" + ) AS "_rowset" ON "_vars"."_varset_id" = "_rowset"."_varset_id" +ORDER BY + "_vars"."_varset_id" ASC FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/chinook/_config/configuration.json b/crates/ndc-clickhouse/tests/query_builder/chinook/_config/configuration.json new file mode 100644 index 0000000..9e67ca3 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/chinook/_config/configuration.json @@ -0,0 +1,246 @@ +{ + "$schema": "../../configuration.schema.json", + "tables": { + "Chinook_Album": { + "name": "Album", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "AlbumId", + "columns": [ + "AlbumId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "AlbumId": "Int32", + "ArtistId": "Int32", + "Title": "String" + } + } + }, + "Chinook_Artist": { + "name": "Artist", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "ArtistId", + "columns": [ + "ArtistId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "ArtistId": "Int32", + "Name": "Nullable(String)" + } + } + }, + "Chinook_Customer": { + "name": "Customer", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "CustomerId", + "columns": [ + "CustomerId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "Address": "Nullable(String)", + "City": "Nullable(String)", + "Company": "Nullable(String)", + "Country": "Nullable(String)", + "CustomerId": "Int32", + "Email": "String", + "Fax": "Nullable(String)", + "FirstName": "String", + "LastName": "String", + "Phone": "Nullable(String)", + "PostalCode": "Nullable(String)", + "State": "Nullable(String)", + "SupportRepId": "Nullable(Int32)" + } + } + }, + "Chinook_Employee": { + "name": "Employee", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "EmployeeId", + "columns": [ + "EmployeeId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "Address": "Nullable(String)", + "BirthDate": "Nullable(Date32)", + "City": "Nullable(String)", + "Country": "Nullable(String)", + "Email": "Nullable(String)", + "EmployeeId": "Int32", + "Fax": "Nullable(String)", + "FirstName": "String", + "HireDate": "Nullable(Date32)", + "LastName": "String", + "Phone": "Nullable(String)", + "PostalCode": "Nullable(String)", + "ReportsTo": "Nullable(Int32)", + "State": "Nullable(String)", + "Title": "Nullable(String)" + } + } + }, + "Chinook_Genre": { + "name": "Genre", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "GenreId", + "columns": [ + "GenreId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "GenreId": "Int32", + "Name": "Nullable(String)" + } + } + }, + "Chinook_Invoice": { + "name": "Invoice", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "InvoiceId", + "columns": [ + "InvoiceId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "BillingAddress": "Nullable(String)", + "BillingCity": "Nullable(String)", + "BillingCountry": "Nullable(String)", + "BillingPostalCode": "Nullable(String)", + "BillingState": "Nullable(String)", + "CustomerId": "Int32", + "InvoiceDate": "DateTime64(9)", + "InvoiceId": "Int32", + "Total": "Float64" + } + } + }, + "Chinook_InvoiceLine": { + "name": "InvoiceLine", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "InvoiceLineId", + "columns": [ + "InvoiceLineId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "InvoiceId": "Int32", + "InvoiceLineId": "Int32", + "Quantity": "Int32", + "TrackId": "Int32", + "UnitPrice": "Float64" + } + } + }, + "Chinook_MediaType": { + "name": "MediaType", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "MediaTypeId", + "columns": [ + "MediaTypeId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "MediaTypeId": "Int32", + "Name": "Nullable(String)" + } + } + }, + "Chinook_Playlist": { + "name": "Playlist", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "PlaylistId", + "columns": [ + "PlaylistId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "Name": "Nullable(String)", + "PlaylistId": "Int32" + } + } + }, + "Chinook_PlaylistTrack": { + "name": "PlaylistTrack", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "PlaylistId, TrackId", + "columns": [ + "PlaylistId", + "TrackId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "PlaylistId": "Int32", + "TrackId": "Int32" + } + } + }, + "Chinook_Track": { + "name": "Track", + "schema": "Chinook", + "comment": "", + "primary_key": { + "name": "TrackId", + "columns": [ + "TrackId" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "AlbumId": "Nullable(Int32)", + "Bytes": "Nullable(Int32)", + "Composer": "Nullable(String)", + "GenreId": "Nullable(Int32)", + "MediaTypeId": "Int32", + "Milliseconds": "Int32", + "Name": "String", + "TrackId": "Int32", + "UnitPrice": "Float64" + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/_config/configuration.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/_config/configuration.json new file mode 100644 index 0000000..4751783 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/_config/configuration.json @@ -0,0 +1,32 @@ +{ + "$schema": "../../configuration.schema.json", + "tables": { + "TableOne": { + "name": "Table1", + "schema": "Schema1", + "return_type": { + "kind": "definition", + "columns": { + "ColumnA": "String", + "ColumnB": "Array(Tuple(field1 String, field2 String))", + "ColumnC": "Nested(field1 String, field1 String)", + "ColumnD": "Tuple(child Tuple(id UInt32, name String))", + "ColumnE": "Tuple(child Array(Tuple(id UInt32, name String)))", + "ColumnF": "Tuple(child Tuple(id UInt32, name String, toys Nested(id UInt32, name String)))", + "ColumnG": "Tuple(a Nullable(String), b Map(String, String), c Array(Tuple(a String, b Tuple(String, String))), d Tuple(a String, b String))" + } + } + }, + "TableTwo": { + "name": "Table2", + "schema": "Schema1", + "return_type": { + "kind": "definition", + "columns": { + "Id": "UInt32", + "Name": "String" + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.request.json new file mode 100644 index 0000000..2131ee4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.request.json @@ -0,0 +1,26 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": {}, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnB", + "fields": { + "type": "array", + "fields": { + "type": "object", + "fields": { + "subfield1": { + "type": "column", + "column": "field1" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.statement.sql b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.statement.sql new file mode 100644 index 0000000..dae2c71 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/01_generate_column_accessor.statement.sql @@ -0,0 +1,24 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("field1" Array(Tuple("subfield1" String)))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple(groupArray(tuple("_row"."_field_field1"))) AS "_rowset" + FROM + ( + SELECT + arrayMap( + (_value) -> tuple(_value."field1"), + "_origin"."ColumnB" + ) AS "_field_field1" + FROM + "Schema1"."Table1" AS "_origin" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.request.json new file mode 100644 index 0000000..9c62245 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.request.json @@ -0,0 +1,30 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": {}, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnB", + "fields": { + "type": "array", + "fields": { + "type": "object", + "fields": { + "subfield1": { + "type": "column", + "column": "field1" + }, + "subfield2": { + "type": "column", + "column": "field2" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.statement.sql b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.statement.sql new file mode 100644 index 0000000..6e6bb2f --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/02_skip_if_not_required.statement.sql @@ -0,0 +1,21 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("field1" Array(Tuple("subfield1" String, "subfield2" String)))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple(groupArray(tuple("_row"."_field_field1"))) AS "_rowset" + FROM + ( + SELECT + "_origin"."ColumnB" AS "_field_field1" + FROM + "Schema1"."Table1" AS "_origin" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.request.json new file mode 100644 index 0000000..41b63b4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.request.json @@ -0,0 +1,62 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": { + "rel1": { + "arguments": {}, + "column_mapping": { + "id": "Id" + }, + "relationship_type": "object", + "target_collection": "TableTwo" + } + }, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnA" + }, + "field2": { + "type": "column", + "column": "ColumnD", + "fields": { + "type": "object", + "fields": { + "child": { + "type": "column", + "column": "child", + "fields": { + "type": "object", + "fields": { + "id": { + "type": "column", + "column": "id" + }, + "name": { + "type": "column", + "column": "name" + }, + "child": { + "type": "relationship", + "arguments": {}, + "relationship": "rel1", + "query": { + "fields": { + "name": { + "type": "column", + "column": "Name" + } + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.statement.sql b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.statement.sql new file mode 100644 index 0000000..c925ede --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/03_support_relationships_on_nested_field.statement.sql @@ -0,0 +1,47 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("field1" String, "field2" Tuple("child" Tuple("id" UInt32, "name" String, "child" Tuple(rows Array(Tuple("name" String))))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple("_row"."_field_field1", "_row"."_field_field2") + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."ColumnA" AS "_field_field1", + tuple( + tuple( + "_origin"."ColumnD"."child"."id", + "_origin"."ColumnD"."child"."name", + "_rel_0_child"."_rowset" + ) + ) AS "_field_field2" + FROM + "Schema1"."Table1" AS "_origin" + LEFT JOIN ( + SELECT + tuple(groupArray(tuple("_row"."_field_name"))) AS "_rowset", + "_row"."_relkey_Id" AS "_relkey_Id" + FROM + ( + SELECT + "_origin"."Name" AS "_field_name", + "_origin"."Id" AS "_relkey_Id" + FROM + "Schema1"."Table2" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_Id" + ) AS "_rel_0_child" ON "_origin"."ColumnD"."child"."id" = "_rel_0_child"."_relkey_Id" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/04_error_on_relationships_on_array_nested_field.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/04_error_on_relationships_on_array_nested_field.request.json new file mode 100644 index 0000000..969d459 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/04_error_on_relationships_on_array_nested_field.request.json @@ -0,0 +1,66 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": { + "rel1": { + "arguments": {}, + "column_mapping": { + "id": "Id" + }, + "relationship_type": "object", + "target_collection": "TableTwo" + } + }, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnA" + }, + "field2": { + "type": "column", + "column": "ColumnE", + "fields": { + "type": "object", + "fields": { + "child": { + "type": "column", + "column": "child", + "fields": { + "type": "array", + "fields": { + "type": "object", + "fields": { + "id": { + "type": "column", + "column": "id" + }, + "name": { + "type": "column", + "column": "name" + }, + "child": { + "type": "relationship", + "arguments": {}, + "relationship": "rel1", + "query": { + "fields": { + "name": { + "type": "column", + "column": "Name" + } + } + } + } + } + } + + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.request.json new file mode 100644 index 0000000..8cb0d79 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.request.json @@ -0,0 +1,79 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": { + "rel1": { + "arguments": {}, + "column_mapping": { + "id": "Id" + }, + "relationship_type": "object", + "target_collection": "TableTwo" + } + }, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnA" + }, + "field2": { + "type": "column", + "column": "ColumnF", + "fields": { + "type": "object", + "fields": { + "child": { + "type": "column", + "column": "child", + "fields": { + "type": "object", + "fields": { + "id": { + "type": "column", + "column": "id" + }, + "name": { + "type": "column", + "column": "name" + }, + "child": { + "type": "relationship", + "arguments": {}, + "relationship": "rel1", + "query": { + "fields": { + "name": { + "type": "column", + "column": "Name" + } + } + } + }, + "toys": { + "type": "column", + "column": "toys", + "fields": { + "type": "array", + "fields": { + "type": "object", + "fields": { + "name": { + "type": "column", + "column": "name" + } + } + } + } + } + } + + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.statement.sql b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.statement.sql new file mode 100644 index 0000000..5ce92c5 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/05_complex_example.statement.sql @@ -0,0 +1,51 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("field1" String, "field2" Tuple("child" Tuple("id" UInt32, "name" String, "child" Tuple(rows Array(Tuple("name" String))), "toys" Array(Tuple("name" String)))))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple("_row"."_field_field1", "_row"."_field_field2") + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."ColumnA" AS "_field_field1", + tuple( + tuple( + "_origin"."ColumnF"."child"."id", + "_origin"."ColumnF"."child"."name", + "_rel_0_child"."_rowset", + arrayMap( + (_value) -> tuple(_value."name"), + "_origin"."ColumnF"."child"."toys" + ) + ) + ) AS "_field_field2" + FROM + "Schema1"."Table1" AS "_origin" + LEFT JOIN ( + SELECT + tuple(groupArray(tuple("_row"."_field_name"))) AS "_rowset", + "_row"."_relkey_Id" AS "_relkey_Id" + FROM + ( + SELECT + "_origin"."Name" AS "_field_name", + "_origin"."Id" AS "_relkey_Id" + FROM + "Schema1"."Table2" AS "_origin" + ) AS "_row" + GROUP BY + "_row"."_relkey_Id" + ) AS "_rel_0_child" ON "_origin"."ColumnF"."child"."id" = "_rel_0_child"."_relkey_Id" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.request.json b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.request.json new file mode 100644 index 0000000..dcd295e --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.request.json @@ -0,0 +1,73 @@ +{ + "$schema": "../request.schema.json", + "collection": "TableOne", + "collection_relationships": { + "rel1": { + "arguments": {}, + "column_mapping": { + "id": "Id" + }, + "relationship_type": "object", + "target_collection": "TableTwo" + } + }, + "arguments": {}, + "query": { + "fields": { + "field1": { + "type": "column", + "column": "ColumnA" + }, + "field2": { + "type": "column", + "column": "ColumnG", + "fields": { + "type": "object", + "fields": { + "b": { + "type": "column", + "column": "b" + }, + "c": { + "type": "column", + "column": "c", + "fields": { + "type": "array", + "fields": { + "type": "object", + "fields": { + "a": { + "type": "column", + "column": "a" + }, + "b": { + "type": "column", + "column": "b" + } + } + } + } + }, + "d": { + "type": "column", + "column": "d", + "fields": { + "type": "object", + "fields": { + "a": { + "type": "column", + "column": "a" + }, + "b": { + "type": "column", + "column": "b" + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.statement.sql b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.statement.sql new file mode 100644 index 0000000..4e3d084 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/complex_columns/field_selector/06_no_useless_nested_accessors.statement.sql @@ -0,0 +1,30 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("field1" String, "field2" Tuple("b" Map(String, String), "c" Array(Tuple("a" String, "b" Tuple(String, String))), "d" Tuple("a" String, "b" String)))))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple( + groupArray( + tuple("_row"."_field_field1", "_row"."_field_field2") + ) + ) AS "_rowset" + FROM + ( + SELECT + "_origin"."ColumnA" AS "_field_field1", + tuple( + "_origin"."ColumnG"."b", + "_origin"."ColumnG"."c", + "_origin"."ColumnG"."d" + ) AS "_field_field2" + FROM + "Schema1"."Table1" AS "_origin" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/configuration.schema.json b/crates/ndc-clickhouse/tests/query_builder/configuration.schema.json new file mode 100644 index 0000000..fd0ad11 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/configuration.schema.json @@ -0,0 +1,208 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ServerConfigFile", + "description": "the main configuration file", + "type": "object", + "required": [ + "$schema" + ], + "properties": { + "$schema": { + "type": "string" + }, + "tables": { + "description": "A list of tables available in this database\n\nThe map key is a unique table alias that defaults to defaults to \"_\", except for tables in the \"default\" schema where the table name is used This is the name exposed to the engine, and may be configured by users. When the configuration is updated, the table is identified by name and schema, and changes to the alias are preserved.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/TableConfigFile" + } + }, + "queries": { + "description": "Optionally define custom parameterized queries here Note the names must not match table names", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/ParameterizedQueryConfigFile" + } + } + }, + "definitions": { + "TableConfigFile": { + "type": "object", + "required": [ + "name", + "return_type", + "schema" + ], + "properties": { + "name": { + "description": "The table name", + "type": "string" + }, + "schema": { + "description": "The table schema", + "type": "string" + }, + "comment": { + "description": "Comments are sourced from the database table comment", + "type": [ + "string", + "null" + ] + }, + "primary_key": { + "anyOf": [ + { + "$ref": "#/definitions/PrimaryKey" + }, + { + "type": "null" + } + ] + }, + "arguments": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "return_type": { + "description": "The map key is a column alias identifying the table and may be customized. It defaults to the table name. When the configuration is updated, the column is identified by name, and changes to the alias are preserved.", + "allOf": [ + { + "$ref": "#/definitions/ReturnType" + } + ] + } + } + }, + "PrimaryKey": { + "type": "object", + "required": [ + "columns", + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "columns": { + "description": "The names of columns in this primary key", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ReturnType": { + "oneOf": [ + { + "description": "A custom return type definition The keys are column names, the values are parsable clichouse datatypes", + "type": "object", + "required": [ + "columns", + "kind" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "definition" + ] + }, + "columns": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + { + "description": "the same as the return type for another table", + "type": "object", + "required": [ + "kind", + "table_name" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "table_reference" + ] + }, + "table_name": { + "description": "the table alias must match a key in `tables`, and the query must return the same type as that table alternatively, the alias may reference another parameterized query which has a return type definition,", + "type": "string" + } + } + }, + { + "description": "The same as the return type for another query", + "type": "object", + "required": [ + "kind", + "query_name" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "query_reference" + ] + }, + "query_name": { + "description": "the table alias must match a key in `tables`, and the query must return the same type as that table alternatively, the alias may reference another parameterized query which has a return type definition,", + "type": "string" + } + } + } + ] + }, + "ParameterizedQueryConfigFile": { + "type": "object", + "required": [ + "exposed_as", + "file", + "return_type" + ], + "properties": { + "exposed_as": { + "description": "Whether this query should be exposed as a procedure (mutating) or collection (non-mutating)", + "allOf": [ + { + "$ref": "#/definitions/ParameterizedQueryExposedAs" + } + ] + }, + "comment": { + "description": "A comment that will be exposed in the schema", + "type": [ + "string", + "null" + ] + }, + "file": { + "description": "A relative path to a sql file", + "type": "string" + }, + "return_type": { + "description": "Either a type definition for the return type for this query, or a reference to another return type: either a table's alias, or another query's alias. If another query, that query must have a return type definition.", + "allOf": [ + { + "$ref": "#/definitions/ReturnType" + } + ] + } + } + }, + "ParameterizedQueryExposedAs": { + "type": "string", + "enum": [ + "collection", + "procedure" + ] + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/request.schema.json b/crates/ndc-clickhouse/tests/query_builder/request.schema.json new file mode 100644 index 0000000..76930b7 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/request.schema.json @@ -0,0 +1,908 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Query Request", + "description": "This is the request body of the query POST endpoint", + "type": "object", + "required": [ + "arguments", + "collection", + "collection_relationships", + "query" + ], + "properties": { + "collection": { + "description": "The name of a collection", + "type": "string" + }, + "query": { + "description": "The query syntax tree", + "allOf": [ + { + "$ref": "#/definitions/Query" + } + ] + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Argument" + } + }, + "collection_relationships": { + "description": "Any relationships between collections involved in the query request", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Relationship" + } + }, + "variables": { + "description": "One set of named variables for each rowset to fetch. Each variable set should be subtituted in turn, and a fresh set of rows returned.", + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "definitions": { + "Query": { + "title": "Query", + "type": "object", + "properties": { + "aggregates": { + "description": "Aggregate fields of the query", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/Aggregate" + } + }, + "fields": { + "description": "Fields of the query", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/definitions/Field" + } + }, + "limit": { + "description": "Optionally limit to N results", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "offset": { + "description": "Optionally offset from the Nth result", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "order_by": { + "anyOf": [ + { + "$ref": "#/definitions/OrderBy" + }, + { + "type": "null" + } + ] + }, + "predicate": { + "anyOf": [ + { + "$ref": "#/definitions/Expression" + }, + { + "type": "null" + } + ] + } + } + }, + "Aggregate": { + "title": "Aggregate", + "oneOf": [ + { + "type": "object", + "required": [ + "column", + "distinct", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column_count" + ] + }, + "column": { + "description": "The column to apply the count aggregate function to", + "type": "string" + }, + "distinct": { + "description": "Whether or not only distinct items should be counted", + "type": "boolean" + } + } + }, + { + "type": "object", + "required": [ + "column", + "function", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single_column" + ] + }, + "column": { + "description": "The column to apply the aggregation function to", + "type": "string" + }, + "function": { + "description": "Single column aggregate function name.", + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "star_count" + ] + } + } + } + ] + }, + "Field": { + "title": "Field", + "oneOf": [ + { + "type": "object", + "required": [ + "column", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column" + ] + }, + "column": { + "type": "string" + }, + "fields": { + "description": "When the type of the column is a (possibly-nullable) array or object, the caller can request a subset of the complete column data, by specifying fields to fetch here. If omitted, the column data will be fetched in full.", + "anyOf": [ + { + "$ref": "#/definitions/NestedField" + }, + { + "type": "null" + } + ] + } + } + }, + { + "type": "object", + "required": [ + "arguments", + "query", + "relationship", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "relationship" + ] + }, + "query": { + "$ref": "#/definitions/Query" + }, + "relationship": { + "description": "The name of the relationship to follow for the subquery", + "type": "string" + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RelationshipArgument" + } + } + } + } + ] + }, + "NestedField": { + "title": "NestedField", + "oneOf": [ + { + "title": "NestedObject", + "type": "object", + "required": [ + "fields", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "object" + ] + }, + "fields": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Field" + } + } + } + }, + { + "title": "NestedArray", + "type": "object", + "required": [ + "fields", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "array" + ] + }, + "fields": { + "$ref": "#/definitions/NestedField" + } + } + } + ] + }, + "RelationshipArgument": { + "title": "Relationship Argument", + "oneOf": [ + { + "description": "The argument is provided by reference to a variable", + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "variable" + ] + }, + "name": { + "type": "string" + } + } + }, + { + "description": "The argument is provided as a literal value", + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "literal" + ] + }, + "value": true + } + }, + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column" + ] + }, + "name": { + "type": "string" + } + } + } + ] + }, + "OrderBy": { + "title": "Order By", + "type": "object", + "required": [ + "elements" + ], + "properties": { + "elements": { + "description": "The elements to order by, in priority order", + "type": "array", + "items": { + "$ref": "#/definitions/OrderByElement" + } + } + } + }, + "OrderByElement": { + "title": "Order By Element", + "type": "object", + "required": [ + "order_direction", + "target" + ], + "properties": { + "order_direction": { + "$ref": "#/definitions/OrderDirection" + }, + "target": { + "$ref": "#/definitions/OrderByTarget" + } + } + }, + "OrderDirection": { + "title": "Order Direction", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "OrderByTarget": { + "title": "Order By Target", + "oneOf": [ + { + "type": "object", + "required": [ + "name", + "path", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column" + ] + }, + "name": { + "description": "The name of the column", + "type": "string" + }, + "path": { + "description": "Any relationships to traverse to reach this column", + "type": "array", + "items": { + "$ref": "#/definitions/PathElement" + } + } + } + }, + { + "type": "object", + "required": [ + "column", + "function", + "path", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "single_column_aggregate" + ] + }, + "column": { + "description": "The column to apply the aggregation function to", + "type": "string" + }, + "function": { + "description": "Single column aggregate function name.", + "type": "string" + }, + "path": { + "description": "Non-empty collection of relationships to traverse", + "type": "array", + "items": { + "$ref": "#/definitions/PathElement" + } + } + } + }, + { + "type": "object", + "required": [ + "path", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "star_count_aggregate" + ] + }, + "path": { + "description": "Non-empty collection of relationships to traverse", + "type": "array", + "items": { + "$ref": "#/definitions/PathElement" + } + } + } + } + ] + }, + "PathElement": { + "title": "Path Element", + "type": "object", + "required": [ + "arguments", + "relationship" + ], + "properties": { + "relationship": { + "description": "The name of the relationship to follow", + "type": "string" + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RelationshipArgument" + } + }, + "predicate": { + "description": "A predicate expression to apply to the target collection", + "anyOf": [ + { + "$ref": "#/definitions/Expression" + }, + { + "type": "null" + } + ] + } + } + }, + "Expression": { + "title": "Expression", + "oneOf": [ + { + "type": "object", + "required": [ + "expressions", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "and" + ] + }, + "expressions": { + "type": "array", + "items": { + "$ref": "#/definitions/Expression" + } + } + } + }, + { + "type": "object", + "required": [ + "expressions", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "or" + ] + }, + "expressions": { + "type": "array", + "items": { + "$ref": "#/definitions/Expression" + } + } + } + }, + { + "type": "object", + "required": [ + "expression", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "not" + ] + }, + "expression": { + "$ref": "#/definitions/Expression" + } + } + }, + { + "type": "object", + "required": [ + "column", + "operator", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "unary_comparison_operator" + ] + }, + "column": { + "$ref": "#/definitions/ComparisonTarget" + }, + "operator": { + "$ref": "#/definitions/UnaryComparisonOperator" + } + } + }, + { + "type": "object", + "required": [ + "column", + "operator", + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "binary_comparison_operator" + ] + }, + "column": { + "$ref": "#/definitions/ComparisonTarget" + }, + "operator": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/ComparisonValue" + } + } + }, + { + "type": "object", + "required": [ + "in_collection", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "exists" + ] + }, + "in_collection": { + "$ref": "#/definitions/ExistsInCollection" + }, + "predicate": { + "anyOf": [ + { + "$ref": "#/definitions/Expression" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "ComparisonTarget": { + "title": "Comparison Target", + "oneOf": [ + { + "type": "object", + "required": [ + "name", + "path", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column" + ] + }, + "name": { + "description": "The name of the column", + "type": "string" + }, + "path": { + "description": "Any relationships to traverse to reach this column", + "type": "array", + "items": { + "$ref": "#/definitions/PathElement" + } + } + } + }, + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "root_collection_column" + ] + }, + "name": { + "description": "The name of the column", + "type": "string" + } + } + } + ] + }, + "UnaryComparisonOperator": { + "title": "Unary Comparison Operator", + "type": "string", + "enum": [ + "is_null" + ] + }, + "ComparisonValue": { + "title": "Comparison Value", + "oneOf": [ + { + "type": "object", + "required": [ + "column", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "column" + ] + }, + "column": { + "$ref": "#/definitions/ComparisonTarget" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "scalar" + ] + }, + "value": true + } + }, + { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "variable" + ] + }, + "name": { + "type": "string" + } + } + } + ] + }, + "ExistsInCollection": { + "title": "Exists In Collection", + "oneOf": [ + { + "type": "object", + "required": [ + "arguments", + "relationship", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "related" + ] + }, + "relationship": { + "type": "string" + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RelationshipArgument" + } + } + } + }, + { + "type": "object", + "required": [ + "arguments", + "collection", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "unrelated" + ] + }, + "collection": { + "description": "The name of a collection", + "type": "string" + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RelationshipArgument" + } + } + } + } + ] + }, + "Argument": { + "title": "Argument", + "oneOf": [ + { + "description": "The argument is provided by reference to a variable", + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "variable" + ] + }, + "name": { + "type": "string" + } + } + }, + { + "description": "The argument is provided as a literal value", + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "literal" + ] + }, + "value": true + } + } + ] + }, + "Relationship": { + "title": "Relationship", + "type": "object", + "required": [ + "arguments", + "column_mapping", + "relationship_type", + "target_collection" + ], + "properties": { + "column_mapping": { + "description": "A mapping between columns on the source collection to columns on the target collection", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "relationship_type": { + "$ref": "#/definitions/RelationshipType" + }, + "target_collection": { + "description": "The name of a collection", + "type": "string" + }, + "arguments": { + "description": "Values to be provided to any collection arguments", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/RelationshipArgument" + } + } + } + }, + "RelationshipType": { + "title": "Relationship Type", + "type": "string", + "enum": [ + "object", + "array" + ] + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.request.json b/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.request.json new file mode 100644 index 0000000..077877a --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.request.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../request.schema.json", + "collection": "q11", + "query": { + "fields": { + "revenue": { + "type": "column", + "column": "revenue", + "fields": null + } + } + }, + "arguments": {}, + "collection_relationships": {} +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.statement.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.statement.sql new file mode 100644 index 0000000..2568396 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/01_native_queries/01_native_query.statement.sql @@ -0,0 +1,30 @@ +SELECT + toJSONString( + groupArray( + cast( + "_rowset"."_rowset", + 'Tuple(rows Array(Tuple("revenue" UInt64)))' + ) + ) + ) AS "rowsets" +FROM + ( + SELECT + tuple(groupArray(tuple("_row"."_field_revenue"))) AS "_rowset" + FROM + ( + SELECT + "_origin"."revenue" AS "_field_revenue" + FROM + ( + SELECT + sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue + FROM + star.lineorder_flat + WHERE + toYear(LO_ORDERDATE) = 1993 + AND LO_DISCOUNT BETWEEN 1 AND 3 + AND LO_QUANTITY < 25 + ) AS "_origin" + ) AS "_row" + ) AS "_rowset" FORMAT TabSeparatedRaw; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/configuration.json b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/configuration.json new file mode 100644 index 0000000..01dfa0d --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/configuration.json @@ -0,0 +1,322 @@ +{ + "$schema": "../../configuration.schema.json", + "tables": { + "star_customer": { + "name": "customer", + "schema": "star", + "comment": "", + "primary_key": { + "name": "C_CUSTKEY", + "columns": [ + "C_CUSTKEY" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "C_ADDRESS": "String", + "C_CITY": "LowCardinality(String)", + "C_CUSTKEY": "UInt32", + "C_MKTSEGMENT": "LowCardinality(String)", + "C_NAME": "String", + "C_NATION": "LowCardinality(String)", + "C_PHONE": "String", + "C_REGION": "LowCardinality(String)" + } + } + }, + "star_lineorder": { + "name": "lineorder", + "schema": "star", + "comment": "", + "primary_key": { + "name": "LO_ORDERDATE, LO_ORDERKEY", + "columns": [ + "LO_ORDERKEY", + "LO_ORDERDATE" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "LO_COMMITDATE": "Date", + "LO_CUSTKEY": "UInt32", + "LO_DISCOUNT": "UInt8", + "LO_EXTENDEDPRICE": "UInt32", + "LO_LINENUMBER": "UInt8", + "LO_ORDERDATE": "Date", + "LO_ORDERKEY": "UInt32", + "LO_ORDERPRIORITY": "LowCardinality(String)", + "LO_ORDTOTALPRICE": "UInt32", + "LO_PARTKEY": "UInt32", + "LO_QUANTITY": "UInt8", + "LO_REVENUE": "UInt32", + "LO_SHIPMODE": "LowCardinality(String)", + "LO_SHIPPRIORITY": "UInt8", + "LO_SUPPKEY": "UInt32", + "LO_SUPPLYCOST": "UInt32", + "LO_TAX": "UInt8" + } + } + }, + "star_lineorder_flat": { + "name": "lineorder_flat", + "schema": "star", + "comment": "", + "primary_key": { + "name": "LO_ORDERDATE, LO_ORDERKEY", + "columns": [ + "LO_ORDERKEY", + "LO_ORDERDATE" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "C_ADDRESS": "String", + "C_CITY": "LowCardinality(String)", + "C_MKTSEGMENT": "LowCardinality(String)", + "C_NAME": "String", + "C_NATION": "LowCardinality(String)", + "C_PHONE": "String", + "C_REGION": "LowCardinality(String)", + "LO_COMMITDATE": "Date", + "LO_CUSTKEY": "UInt32", + "LO_DISCOUNT": "UInt8", + "LO_EXTENDEDPRICE": "UInt32", + "LO_LINENUMBER": "UInt8", + "LO_ORDERDATE": "Date", + "LO_ORDERKEY": "UInt32", + "LO_ORDERPRIORITY": "LowCardinality(String)", + "LO_ORDTOTALPRICE": "UInt32", + "LO_PARTKEY": "UInt32", + "LO_QUANTITY": "UInt8", + "LO_REVENUE": "UInt32", + "LO_SHIPMODE": "LowCardinality(String)", + "LO_SHIPPRIORITY": "UInt8", + "LO_SUPPKEY": "UInt32", + "LO_SUPPLYCOST": "UInt32", + "LO_TAX": "UInt8", + "P_BRAND": "LowCardinality(String)", + "P_CATEGORY": "LowCardinality(String)", + "P_COLOR": "LowCardinality(String)", + "P_CONTAINER": "LowCardinality(String)", + "P_MFGR": "LowCardinality(String)", + "P_NAME": "String", + "P_SIZE": "UInt8", + "P_TYPE": "LowCardinality(String)", + "S_ADDRESS": "String", + "S_CITY": "LowCardinality(String)", + "S_NAME": "String", + "S_NATION": "LowCardinality(String)", + "S_PHONE": "String", + "S_REGION": "LowCardinality(String)" + } + } + }, + "star_part": { + "name": "part", + "schema": "star", + "comment": "", + "primary_key": { + "name": "P_PARTKEY", + "columns": [ + "P_PARTKEY" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "P_BRAND": "LowCardinality(String)", + "P_CATEGORY": "LowCardinality(String)", + "P_COLOR": "LowCardinality(String)", + "P_CONTAINER": "LowCardinality(String)", + "P_MFGR": "LowCardinality(String)", + "P_NAME": "String", + "P_PARTKEY": "UInt32", + "P_SIZE": "UInt8", + "P_TYPE": "LowCardinality(String)" + } + } + }, + "star_supplier": { + "name": "supplier", + "schema": "star", + "comment": "", + "primary_key": { + "name": "S_SUPPKEY", + "columns": [ + "S_SUPPKEY" + ] + }, + "return_type": { + "kind": "definition", + "columns": { + "S_ADDRESS": "String", + "S_CITY": "LowCardinality(String)", + "S_NAME": "String", + "S_NATION": "LowCardinality(String)", + "S_PHONE": "String", + "S_REGION": "LowCardinality(String)", + "S_SUPPKEY": "UInt32" + } + } + } + }, + "queries": { + "q11": { + "exposed_as": "collection", + "file": "./queries/q1.1.sql", + "return_type": { + "kind": "definition", + "columns": { + "revenue": "UInt64" + } + } + }, + "q12": { + "exposed_as": "collection", + "file": "./queries/q1.2.sql", + "return_type": { + "kind": "query_reference", + "query_name": "q11" + } + }, + "q13": { + "exposed_as": "collection", + "file": "./queries/q1.3.sql", + "return_type": { + "kind": "definition", + "columns": { + "revenue": "UInt64" + } + } + }, + "q21": { + "exposed_as": "collection", + "file": "./queries/q2.1.sql", + "return_type": { + "kind": "definition", + "columns": { + "P_BRAND": "LowCardinality(String)", + "sum(LO_REVENUE)": "UInt64", + "year": "UInt16" + } + } + }, + "q22": { + "exposed_as": "collection", + "file": "./queries/q2.2.sql", + "return_type": { + "kind": "definition", + "columns": { + "P_BRAND": "LowCardinality(String)", + "sum(LO_REVENUE)": "UInt64", + "year": "UInt16" + } + } + }, + "q23": { + "exposed_as": "collection", + "file": "./queries/q2.3.sql", + "return_type": { + "kind": "definition", + "columns": { + "P_BRAND": "LowCardinality(String)", + "sum(LO_REVENUE)": "UInt64", + "year": "UInt16" + } + } + }, + "q31": { + "exposed_as": "collection", + "file": "./queries/q3.1.sql", + "return_type": { + "kind": "definition", + "columns": { + "C_NATION": "LowCardinality(String)", + "S_NATION": "LowCardinality(String)", + "revenue": "UInt64", + "year": "UInt16" + } + } + }, + "q32": { + "exposed_as": "collection", + "file": "./queries/q3.2.sql", + "return_type": { + "kind": "definition", + "columns": { + "C_NATION": "LowCardinality(String)", + "S_NATION": "LowCardinality(String)", + "revenue": "UInt64", + "year": "UInt16" + } + } + }, + "q33": { + "exposed_as": "collection", + "file": "./queries/q3.3.sql", + "return_type": { + "kind": "definition", + "columns": { + "C_NATION": "LowCardinality(String)", + "S_NATION": "LowCardinality(String)", + "revenue": "UInt64", + "year": "UInt16" + } + } + }, + "q34": { + "exposed_as": "collection", + "file": "./queries/q3.4.sql", + "return_type": { + "kind": "definition", + "columns": { + "C_NATION": "LowCardinality(String)", + "S_NATION": "LowCardinality(String)", + "revenue": "UInt64", + "year": "UInt16" + } + } + }, + "q41": { + "exposed_as": "collection", + "file": "./queries/q4.1.sql", + "return_type": { + "kind": "definition", + "columns": { + "C_NATION": "LowCardinality(String)", + "profit": "Int64", + "year": "UInt16" + } + } + }, + "q42": { + "exposed_as": "collection", + "file": "./queries/q4.2.sql", + "return_type": { + "kind": "definition", + "columns": { + "P_CATEGORY": "LowCardinality(String)", + "S_NATION": "LowCardinality(String)", + "profit": "Int64", + "year": "UInt16" + } + } + }, + "q43": { + "exposed_as": "collection", + "file": "./queries/q4.3.sql", + "return_type": { + "kind": "definition", + "columns": { + "P_BRAND": "LowCardinality(String)", + "S_CITY": "LowCardinality(String)", + "profit": "Int64", + "year": "UInt16" + } + } + } + } +} \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.1.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.1.sql new file mode 100644 index 0000000..dcfcde3 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.1.sql @@ -0,0 +1,3 @@ +SELECT sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue +FROM star.lineorder_flat +WHERE toYear(LO_ORDERDATE) = 1993 AND LO_DISCOUNT BETWEEN 1 AND 3 AND LO_QUANTITY < 25; diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.2.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.2.sql new file mode 100644 index 0000000..98a12f8 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.2.sql @@ -0,0 +1,3 @@ +SELECT sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue +FROM star.lineorder_flat +WHERE toYYYYMM(LO_ORDERDATE) = 199401 AND LO_DISCOUNT BETWEEN 4 AND 6 AND LO_QUANTITY BETWEEN 26 AND 35; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.3.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.3.sql new file mode 100644 index 0000000..4d38223 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q1.3.sql @@ -0,0 +1,5 @@ + +SELECT sum(LO_EXTENDEDPRICE * LO_DISCOUNT) AS revenue +FROM star.lineorder_flat +WHERE toISOWeek(LO_ORDERDATE) = 6 AND toYear(LO_ORDERDATE) = 1994 + AND LO_DISCOUNT BETWEEN 5 AND 7 AND LO_QUANTITY BETWEEN 26 AND 35; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.1.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.1.sql new file mode 100644 index 0000000..71e6fb3 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.1.sql @@ -0,0 +1,12 @@ +SELECT + sum(LO_REVENUE), + toYear(LO_ORDERDATE) AS year, + P_BRAND +FROM star.lineorder_flat +WHERE P_CATEGORY = 'MFGR#12' AND S_REGION = 'AMERICA' +GROUP BY + year, + P_BRAND +ORDER BY + year, + P_BRAND; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.2.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.2.sql new file mode 100644 index 0000000..6c72561 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.2.sql @@ -0,0 +1,12 @@ +SELECT + sum(LO_REVENUE), + toYear(LO_ORDERDATE) AS year, + P_BRAND +FROM star.lineorder_flat +WHERE P_BRAND >= 'MFGR#2221' AND P_BRAND <= 'MFGR#2228' AND S_REGION = 'ASIA' +GROUP BY + year, + P_BRAND +ORDER BY + year, + P_BRAND; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.3.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.3.sql new file mode 100644 index 0000000..b024286 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q2.3.sql @@ -0,0 +1,12 @@ +SELECT + sum(LO_REVENUE), + toYear(LO_ORDERDATE) AS year, + P_BRAND +FROM star.lineorder_flat +WHERE P_BRAND = 'MFGR#2239' AND S_REGION = 'EUROPE' +GROUP BY + year, + P_BRAND +ORDER BY + year, + P_BRAND; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.1.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.1.sql new file mode 100644 index 0000000..97b9fa9 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.1.sql @@ -0,0 +1,14 @@ +SELECT + C_NATION, + S_NATION, + toYear(LO_ORDERDATE) AS year, + sum(LO_REVENUE) AS revenue +FROM star.lineorder_flat +WHERE C_REGION = 'ASIA' AND S_REGION = 'ASIA' AND year >= 1992 AND year <= 1997 +GROUP BY + C_NATION, + S_NATION, + year +ORDER BY + year ASC, + revenue DESC; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.2.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.2.sql new file mode 100644 index 0000000..7d13f58 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.2.sql @@ -0,0 +1,14 @@ +SELECT + C_CITY, + S_CITY, + toYear(LO_ORDERDATE) AS year, + sum(LO_REVENUE) AS revenue +FROM star.lineorder_flat +WHERE C_NATION = 'UNITED STATES' AND S_NATION = 'UNITED STATES' AND year >= 1992 AND year <= 1997 +GROUP BY + C_CITY, + S_CITY, + year +ORDER BY + year ASC, + revenue DESC; diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.3.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.3.sql new file mode 100644 index 0000000..f4f8ffc --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.3.sql @@ -0,0 +1,14 @@ +SELECT + C_CITY, + S_CITY, + toYear(LO_ORDERDATE) AS year, + sum(LO_REVENUE) AS revenue +FROM star.lineorder_flat +WHERE (C_CITY = 'UNITED KI1' OR C_CITY = 'UNITED KI5') AND (S_CITY = 'UNITED KI1' OR S_CITY = 'UNITED KI5') AND year >= 1992 AND year <= 1997 +GROUP BY + C_CITY, + S_CITY, + year +ORDER BY + year ASC, + revenue DESC; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.4.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.4.sql new file mode 100644 index 0000000..b4745cc --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q3.4.sql @@ -0,0 +1,15 @@ + +SELECT + C_CITY, + S_CITY, + toYear(LO_ORDERDATE) AS year, + sum(LO_REVENUE) AS revenue +FROM star.lineorder_flat +WHERE (C_CITY = 'UNITED KI1' OR C_CITY = 'UNITED KI5') AND (S_CITY = 'UNITED KI1' OR S_CITY = 'UNITED KI5') AND toYYYYMM(LO_ORDERDATE) = 199712 +GROUP BY + C_CITY, + S_CITY, + year +ORDER BY + year ASC, + revenue DESC; diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.1.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.1.sql new file mode 100644 index 0000000..8cc0068 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.1.sql @@ -0,0 +1,12 @@ +SELECT + toYear(LO_ORDERDATE) AS year, + C_NATION, + sum(LO_REVENUE - LO_SUPPLYCOST) AS profit +FROM star.lineorder_flat +WHERE C_REGION = 'AMERICA' AND S_REGION = 'AMERICA' AND (P_MFGR = 'MFGR#1' OR P_MFGR = 'MFGR#2') +GROUP BY + year, + C_NATION +ORDER BY + year ASC, + C_NATION ASC; diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.2.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.2.sql new file mode 100644 index 0000000..3c0b1ed --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.2.sql @@ -0,0 +1,15 @@ +SELECT + toYear(LO_ORDERDATE) AS year, + S_NATION, + P_CATEGORY, + sum(LO_REVENUE - LO_SUPPLYCOST) AS profit +FROM star.lineorder_flat +WHERE C_REGION = 'AMERICA' AND S_REGION = 'AMERICA' AND (year = 1997 OR year = 1998) AND (P_MFGR = 'MFGR#1' OR P_MFGR = 'MFGR#2') +GROUP BY + year, + S_NATION, + P_CATEGORY +ORDER BY + year ASC, + S_NATION ASC, + P_CATEGORY ASC; \ No newline at end of file diff --git a/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.3.sql b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.3.sql new file mode 100644 index 0000000..ae49fa4 --- /dev/null +++ b/crates/ndc-clickhouse/tests/query_builder/star_schema/_config/queries/q4.3.sql @@ -0,0 +1,16 @@ +SELECT + toYear(LO_ORDERDATE) AS year, + S_CITY, + P_BRAND, + sum(LO_REVENUE - LO_SUPPLYCOST) AS profit +FROM star.lineorder_flat +WHERE S_NATION = 'UNITED STATES' AND (year = 1997 OR year = 1998) AND P_CATEGORY = 'MFGR#14' +GROUP BY + year, + S_CITY, + P_BRAND +ORDER BY + year ASC, + S_CITY ASC, + P_BRAND ASC; +