diff --git a/Cargo.lock b/Cargo.lock index 128a8916..792a119a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1572,6 +1572,7 @@ dependencies = [ "http", "indexmap 2.2.5", "itertools 0.10.5", + "lazy_static", "mongodb", "mongodb-agent-common", "mongodb-support", diff --git a/crates/configuration/src/schema/database.rs b/crates/configuration/src/schema/database.rs index ce4ce146..0abca261 100644 --- a/crates/configuration/src/schema/database.rs +++ b/crates/configuration/src/schema/database.rs @@ -13,7 +13,7 @@ pub struct Collection { /// The name of a type declared in `objectTypes` that describes the fields of this collection. /// The type name may be the same as the collection name. pub r#type: String, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } @@ -65,7 +65,7 @@ impl Type { #[serde(rename_all = "camelCase")] pub struct ObjectType { pub fields: BTreeMap, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } @@ -88,7 +88,7 @@ impl ObjectType { #[serde(rename_all = "camelCase")] pub struct ObjectField { pub r#type: Type, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } diff --git a/crates/dc-api-test-helpers/src/field.rs b/crates/dc-api-test-helpers/src/field.rs index 076d1674..548bc099 100644 --- a/crates/dc-api-test-helpers/src/field.rs +++ b/crates/dc-api-test-helpers/src/field.rs @@ -20,6 +20,19 @@ macro_rules! column { }; } +#[macro_export] +macro_rules! relation_field { + ($relationship:literal => $name:literal, $query:expr) => { + ( + $name.into(), + dc_api_types::Field::Relationship { + relationship: $relationship.to_owned(), + query: Box::new($query.into()), + }, + ) + }; +} + #[macro_export()] macro_rules! nested_object_field { ($column:literal, $query:expr) => { diff --git a/crates/dc-api-test-helpers/src/query.rs b/crates/dc-api-test-helpers/src/query.rs index d79af05d..714586d3 100644 --- a/crates/dc-api-test-helpers/src/query.rs +++ b/crates/dc-api-test-helpers/src/query.rs @@ -30,6 +30,11 @@ impl QueryBuilder { self.predicate = Some(predicate); self } + + pub fn order_by(mut self, order_by: OrderBy) -> Self { + self.order_by = Some(Some(order_by)); + self + } } impl From for Query { diff --git a/crates/dc-api-test-helpers/src/query_request.rs b/crates/dc-api-test-helpers/src/query_request.rs index 70195802..fe398f2a 100644 --- a/crates/dc-api-test-helpers/src/query_request.rs +++ b/crates/dc-api-test-helpers/src/query_request.rs @@ -31,6 +31,11 @@ impl QueryRequestBuilder { self.query = Some(query.into()); self } + + pub fn relationships(mut self, relationships: impl Into>) -> Self { + self.relationships = Some(relationships.into()); + self + } } impl From for QueryRequest { diff --git a/crates/mongodb-agent-common/src/query/column_ref.rs b/crates/mongodb-agent-common/src/query/column_ref.rs index b2e2040b..85255bcd 100644 --- a/crates/mongodb-agent-common/src/query/column_ref.rs +++ b/crates/mongodb-agent-common/src/query/column_ref.rs @@ -1,4 +1,4 @@ -use dc_api_types::comparison_column::ColumnSelector; +use dc_api_types::ComparisonColumn; use crate::{ interface_types::MongoAgentError, @@ -11,18 +11,22 @@ use crate::{ /// /// evaluating them as expressions. pub fn column_ref( - column_name: &ColumnSelector, + column: &ComparisonColumn, collection_name: Option<&str>, ) -> Result { + if column.path.as_ref().map(|path| !path.is_empty()).unwrap_or(false) { + return Err(MongoAgentError::NotImplemented("comparisons against root query table columns")) + } + let reference = if let Some(collection) = collection_name { // This assumes that a related collection has been brought into scope by a $lookup stage. format!( "{}.{}", safe_name(collection)?, - safe_column_selector(column_name)? + safe_column_selector(&column.name)? ) } else { - format!("{}", safe_column_selector(column_name)?) + format!("{}", safe_column_selector(&column.name)?) }; Ok(reference) } diff --git a/crates/mongodb-agent-common/src/query/make_selector.rs b/crates/mongodb-agent-common/src/query/make_selector.rs index 170eba54..d969eebc 100644 --- a/crates/mongodb-agent-common/src/query/make_selector.rs +++ b/crates/mongodb-agent-common/src/query/make_selector.rs @@ -77,7 +77,7 @@ fn make_selector_helper( value, } => { let mongo_op = ComparisonFunction::try_from(operator)?; - let col = column_ref(&column.name, in_table)?; + let col = column_ref(column, in_table)?; let comparison_value = match value { ComparisonValue::AnotherColumnComparison { .. } => Err( MongoAgentError::NotImplemented("comparisons between columns"), @@ -117,7 +117,7 @@ fn make_selector_helper( }) .collect::>()?; Ok(doc! { - column_ref(&column.name, in_table)?: { + column_ref(column, in_table)?: { mongo_op: values } }) @@ -129,11 +129,11 @@ fn make_selector_helper( // value is null or is absent. Checking for type 10 returns true if the value is // null, but false if it is absent. Ok(doc! { - column_ref(&column.name, in_table)?: { "$type": 10 } + column_ref(column, in_table)?: { "$type": 10 } }) } UnaryComparisonOperator::CustomUnaryComparisonOperator(op) => { - let col = column_ref(&column.name, in_table)?; + let col = column_ref(column, in_table)?; if op == "$exists" { Ok(doc! { col: { "$exists": true } }) } else { diff --git a/crates/mongodb-connector/Cargo.toml b/crates/mongodb-connector/Cargo.toml index 36a21468..0ee015c7 100644 --- a/crates/mongodb-connector/Cargo.toml +++ b/crates/mongodb-connector/Cargo.toml @@ -14,6 +14,7 @@ futures = "^0.3" http = "^0.2" indexmap = { version = "2.1.0", features = ["serde"] } itertools = "^0.10" +lazy_static = "^1.4.0" mongodb = "2.8" mongodb-agent-common = { path = "../mongodb-agent-common" } mongodb-support = { path = "../mongodb-support" } diff --git a/crates/mongodb-connector/src/api_type_conversions/conversion_error.rs b/crates/mongodb-connector/src/api_type_conversions/conversion_error.rs index e05bbace..2ef88b9a 100644 --- a/crates/mongodb-connector/src/api_type_conversions/conversion_error.rs +++ b/crates/mongodb-connector/src/api_type_conversions/conversion_error.rs @@ -15,6 +15,21 @@ pub enum ConversionError { #[error("Unknown scalar type, \"{0}\"")] UnknownScalarType(String), + #[error("Unknown object type, \"{0}\"")] + UnknownObjectType(String), + + #[error("Unknown field \"{field_name}\" in object type \"{object_type}\"")] + UnknownObjectTypeField { object_type: String, field_name: String }, + + #[error("Unknown collection, \"{0}\"")] + UnknownCollection(String), + + #[error("Unknown relationship, \"{0}\"")] + UnknownRelationship(String), + + #[error("Unknown aggregate function, \"{aggregate_function}\" in scalar type \"{scalar_type}\"")] + UnknownAggregateFunction { scalar_type: String, aggregate_function: String }, + #[error("Query referenced a function, \"{0}\", but it has not been defined")] UnspecifiedFunction(String), diff --git a/crates/mongodb-connector/src/api_type_conversions/helpers.rs b/crates/mongodb-connector/src/api_type_conversions/helpers.rs index 043283a4..ef500a63 100644 --- a/crates/mongodb-connector/src/api_type_conversions/helpers.rs +++ b/crates/mongodb-connector/src/api_type_conversions/helpers.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use ndc_sdk::models::{self as v3, ComparisonOperatorDefinition, ScalarType}; +use ndc_sdk::models::{self as v3}; use super::ConversionError; @@ -12,18 +12,3 @@ pub fn lookup_relationship<'a>( .get(relationship) .ok_or_else(|| ConversionError::UnspecifiedRelation(relationship.to_owned())) } - -pub fn lookup_operator_definition( - scalar_types: &BTreeMap, - type_name: &str, - operator: &str, -) -> Result { - let scalar_type = scalar_types - .get(type_name) - .ok_or_else(|| ConversionError::UnknownScalarType(type_name.to_owned()))?; - let operator = scalar_type - .comparison_operators - .get(operator) - .ok_or_else(|| ConversionError::UnknownComparisonOperator(operator.to_owned()))?; - Ok(operator.clone()) -} diff --git a/crates/mongodb-connector/src/api_type_conversions/query_request.rs b/crates/mongodb-connector/src/api_type_conversions/query_request.rs index f431e8cc..aff3ee37 100644 --- a/crates/mongodb-connector/src/api_type_conversions/query_request.rs +++ b/crates/mongodb-connector/src/api_type_conversions/query_request.rs @@ -1,37 +1,89 @@ -use std::{ - collections::{BTreeMap, HashMap}, - ops::Deref, -}; +use std::collections::{BTreeMap, HashMap}; +use configuration::{schema, Schema, WithNameRef}; use dc_api_types::{self as v2, ColumnSelector, Target}; use indexmap::IndexMap; use itertools::Itertools; -use ndc_sdk::models::{self as v3, FunctionInfo, ScalarType}; +use ndc_sdk::models::{self as v3}; use super::{ - helpers::{lookup_operator_definition, lookup_relationship}, + helpers::lookup_relationship, query_traversal::{query_traversal, Node, TraversalStep}, ConversionError, }; -const UNKNOWN_TYPE: &str = "unknown"; - #[derive(Clone, Debug)] -pub struct QueryContext { - pub functions: Vec, - pub scalar_types: BTreeMap, +pub struct QueryContext<'a> { + pub functions: Vec, + pub scalar_types: &'a BTreeMap, + pub schema: &'a Schema, +} + +impl QueryContext<'_> { + fn find_collection(&self, collection_name: &str) -> Result<&schema::Collection, ConversionError> { + self + .schema + .collections + .get(collection_name) + .ok_or_else(|| ConversionError::UnknownCollection(collection_name.to_string())) + } + + fn find_object_type<'a>(&'a self, object_type_name: &'a str) -> Result, ConversionError> { + let object_type = self + .schema + .object_types + .get(object_type_name) + .ok_or_else(|| ConversionError::UnknownObjectType(object_type_name.to_string()))?; + + Ok(WithNameRef { name: object_type_name, value: object_type }) + } + + fn find_scalar_type(&self, scalar_type_name: &str) -> Result<&v3::ScalarType, ConversionError> { + self.scalar_types + .get(scalar_type_name) + .ok_or_else(|| ConversionError::UnknownScalarType(scalar_type_name.to_owned())) + } + + fn find_comparison_operator_definition(&self, scalar_type_name: &str, operator: &str) -> Result<&v3::ComparisonOperatorDefinition, ConversionError> { + let scalar_type = self.find_scalar_type(scalar_type_name)?; + let operator = scalar_type + .comparison_operators + .get(operator) + .ok_or_else(|| ConversionError::UnknownComparisonOperator(operator.to_owned()))?; + Ok(operator) + } +} + +fn find_object_field<'a>(object_type: &'a WithNameRef, field_name: &str) -> Result<&'a schema::ObjectField, ConversionError> { + object_type + .value + .fields + .get(field_name) + .ok_or_else(|| ConversionError::UnknownObjectTypeField { + object_type: object_type.name.to_string(), + field_name: field_name.to_string(), + }) } pub fn v3_to_v2_query_request( context: &QueryContext, request: v3::QueryRequest, ) -> Result { + let collection = context.find_collection(&request.collection)?; + let collection_object_type = context.find_object_type(&collection.r#type)?; + Ok(v2::QueryRequest { relationships: v3_to_v2_relationships(&request)?, target: Target::TTable { name: vec![request.collection], }, - query: Box::new(v3_to_v2_query(context, request.query)?), + query: Box::new(v3_to_v2_query( + context, + &request.collection_relationships, + &collection_object_type, + request.query, + &collection_object_type, + )?), // We are using v2 types that have been augmented with a `variables` field (even though // that is not part of the v2 API). For queries translated from v3 we use `variables` @@ -41,7 +93,13 @@ pub fn v3_to_v2_query_request( }) } -fn v3_to_v2_query(context: &QueryContext, query: v3::Query) -> Result { +fn v3_to_v2_query( + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + query: v3::Query, + collection_object_type: &WithNameRef, +) -> Result { let aggregates: Option>> = query .aggregates .map(|aggregates| -> Result<_, ConversionError> { @@ -55,19 +113,33 @@ fn v3_to_v2_query(context: &QueryContext, query: v3::Query) -> Result> = query .order_by .map(|order_by| -> Result<_, ConversionError> { - Ok(v2::OrderBy { - elements: order_by + let (elements, relations) = + order_by .elements .into_iter() - .map(v3_to_v2_order_by_element) - .collect::>()?, - relations: Default::default(), - }) + .map(|order_by_element| v3_to_v2_order_by_element(context, collection_relationships, root_collection_object_type, collection_object_type, order_by_element)) + .collect::, ConversionError>>()? + .into_iter() + .try_fold( + (Vec::::new(), HashMap::::new()), + |(mut acc_elems, mut acc_rels), (elem, rels)| { + acc_elems.push(elem); + merge_order_by_relations(&mut acc_rels, rels)?; + Ok((acc_elems, acc_rels)) + } + )?; + Ok(v2::OrderBy { elements, relations }) }) .transpose()? .map(Some); @@ -84,13 +156,30 @@ fn v3_to_v2_query(context: &QueryContext, query: v3::Query) -> Result, rels2: HashMap) -> Result<(), ConversionError> { + for (relationship_name, relation2) in rels2 { + if let Some(relation1) = rels1.get_mut(&relationship_name) { + if relation1.r#where != relation2.r#where { + // v2 does not support navigating the same relationship more than once across multiple + // order by elements and having different predicates used on the same relationship in + // different order by elements. This appears to be technically supported by NDC. + return Err(ConversionError::NotImplemented("Relationships used in order by elements cannot contain different predicates when used more than once")) + } + merge_order_by_relations(&mut relation1.subrelations, relation2.subrelations)?; + } else { + rels1.insert(relationship_name, relation2); + } + } + Ok(()) +} + fn v3_to_v2_aggregate( - functions: &[FunctionInfo], + functions: &[v3::FunctionInfo], aggregate: v3::Aggregate, ) -> Result { match aggregate { @@ -128,13 +217,21 @@ fn type_to_type_name(t: &v3::Type) -> Result { fn v3_to_v2_fields( context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, v3_fields: Option>, ) -> Result>>, ConversionError> { let v2_fields: Option>> = v3_fields .map(|fields| { fields .into_iter() - .map(|(name, field)| Ok((name, v3_to_v2_field(context, field)?))) + .map(|(name, field)| { + Ok(( + name, + v3_to_v2_field(context, collection_relationships, root_collection_object_type, object_type, field)?, + )) + }) .collect::>() }) .transpose()? @@ -142,56 +239,114 @@ fn v3_to_v2_fields( Ok(v2_fields) } -fn v3_to_v2_field(context: &QueryContext, field: v3::Field) -> Result { +fn v3_to_v2_field( + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, + field: v3::Field, +) -> Result { match field { - v3::Field::Column { column, fields } => match fields { - None => Ok(v2::Field::Column { + v3::Field::Column { column, fields } => { + let object_type_field = find_object_field(object_type, column.as_ref())?; + v3_to_v2_nested_field( + context, + collection_relationships, + root_collection_object_type, column, - column_type: UNKNOWN_TYPE.to_owned(), // TODO: is there a better option? - }), - Some(nested_field) => v3_to_v2_nested_field(context, column, nested_field), - }, + &object_type_field.r#type, + fields, + ) + } v3::Field::Relationship { query, relationship, arguments: _, - } => Ok(v2::Field::Relationship { - query: Box::new(v3_to_v2_query(context, *query)?), - relationship, - }), + } => { + let v3_relationship = lookup_relationship(collection_relationships, &relationship)?; + let collection = context.find_collection(&v3_relationship.target_collection)?; + let collection_object_type = context.find_object_type(&collection.r#type)?; + Ok(v2::Field::Relationship { + query: Box::new(v3_to_v2_query( + context, + collection_relationships, + root_collection_object_type, + *query, + &collection_object_type, + )?), + relationship, + }) + } } } fn v3_to_v2_nested_field( context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, column: String, - nested_field: v3::NestedField, + schema_type: &schema::Type, + nested_field: Option, ) -> Result { - match nested_field { - v3::NestedField::Object(nested_object) => { - let mut query = v2::Query::new(); - query.fields = v3_to_v2_fields(context, Some(nested_object.fields))?; - Ok(v2::Field::NestedObject { + match schema_type { + schema::Type::Any => { + Ok(v2::Field::Column { column, - query: Box::new(query), + column_type: mongodb_support::ANY_TYPE_NAME.to_string(), }) } - v3::NestedField::Array(nested_array) => { - let field = - v3_to_v2_nested_field(context, column, nested_array.fields.deref().to_owned())?; + schema::Type::Scalar(bson_scalar_type) => { + Ok(v2::Field::Column { + column, + column_type: bson_scalar_type.graphql_name(), + }) + }, + schema::Type::Nullable(underlying_type) => v3_to_v2_nested_field(context, collection_relationships, root_collection_object_type, column, underlying_type, nested_field), + schema::Type::ArrayOf(element_type) => { + let inner_nested_field = match nested_field { + None => Ok(None), + Some(v3::NestedField::Object(_nested_object)) => Err(ConversionError::TypeMismatch("Expected an array nested field selection, but got an object nested field selection instead".into())), + Some(v3::NestedField::Array(nested_array)) => Ok(Some(*nested_array.fields)), + }?; + let nested_v2_field = v3_to_v2_nested_field(context, collection_relationships, root_collection_object_type, column, element_type, inner_nested_field)?; Ok(v2::Field::NestedArray { - field: Box::new(field), + field: Box::new(nested_v2_field), limit: None, offset: None, r#where: None, }) - } + }, + schema::Type::Object(object_type_name) => { + match nested_field { + None => { + Ok(v2::Field::Column { + column, + column_type: object_type_name.clone(), + }) + }, + Some(v3::NestedField::Object(nested_object)) => { + let object_type = context.find_object_type(object_type_name.as_ref())?; + let mut query = v2::Query::new(); + query.fields = v3_to_v2_fields(context, collection_relationships, root_collection_object_type, &object_type, Some(nested_object.fields))?; + Ok(v2::Field::NestedObject { + column, + query: Box::new(query), + }) + }, + Some(v3::NestedField::Array(_nested_array)) => + Err(ConversionError::TypeMismatch("Expected an array nested field selection, but got an object nested field selection instead".into())), + } + }, } } fn v3_to_v2_order_by_element( + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, elem: v3::OrderByElement, -) -> Result { +) -> Result<(v2::OrderByElement, HashMap), ConversionError> { let (target, target_path) = match elem.target { v3::OrderByTarget::Column { name, path } => ( v2::OrderByTarget::Column { @@ -203,53 +358,90 @@ fn v3_to_v2_order_by_element( column, function, path, - } => ( - v2::OrderByTarget::SingleColumnAggregate { + } => { + let end_of_relationship_path_object_type = path + .last() + .map(|last_path_element| { + let relationship = lookup_relationship(collection_relationships, &last_path_element.relationship)?; + let target_collection = context.find_collection(&relationship.target_collection)?; + context.find_object_type(&target_collection.r#type) + }) + .transpose()?; + let target_object_type = end_of_relationship_path_object_type.as_ref().unwrap_or(object_type); + let object_field = find_object_field(target_object_type, &column)?; + let scalar_type_name = get_scalar_type_name(&object_field.r#type)?; + let scalar_type = context.find_scalar_type(&scalar_type_name)?; + let aggregate_function = scalar_type.aggregate_functions.get(&function).ok_or_else(|| ConversionError::UnknownAggregateFunction { scalar_type: scalar_type_name, aggregate_function: function.clone() })?; + let result_type = type_to_type_name(&aggregate_function.result_type)?; + let target = v2::OrderByTarget::SingleColumnAggregate { column, function, - result_type: UNKNOWN_TYPE.to_owned(), // TODO: is there a better option? - }, - path, - ), + result_type, + }; + (target, path) + }, v3::OrderByTarget::StarCountAggregate { path } => { (v2::OrderByTarget::StarCountAggregate {}, path) } }; - Ok(v2::OrderByElement { + let (target_path, relations) = v3_to_v2_target_path(context, collection_relationships, root_collection_object_type, target_path)?; + let order_by_element = v2::OrderByElement { order_direction: match elem.order_direction { v3::OrderDirection::Asc => v2::OrderDirection::Asc, v3::OrderDirection::Desc => v2::OrderDirection::Desc, }, target, - target_path: v3_to_v2_target_path(target_path)?, - }) + target_path, + }; + Ok((order_by_element, relations)) } -// TODO: We should capture the predicate expression for each path element, and modify the agent to -// apply those predicates. This will involve modifying the dc_api_types to accept this data (even -// though the v2 API does not include this information - we will make sure serialization remains -// v2-compatible). This will be done in an upcoming PR. -fn v3_to_v2_target_path(path: Vec) -> Result, ConversionError> { - fn is_expression_non_empty(expression: &v3::Expression) -> bool { - match expression { - v3::Expression::And { expressions } => !expressions.is_empty(), - v3::Expression::Or { expressions } => !expressions.is_empty(), - _ => true, - } - } - if path - .iter() - .any(|path_element| match &path_element.predicate { - Some(pred) => is_expression_non_empty(pred), - None => false, - }) - { - Err(ConversionError::NotImplemented( - "The MongoDB connector does not currently support predicates on references through relations", - )) - } else { - Ok(path.into_iter().map(|elem| elem.relationship).collect()) +fn v3_to_v2_target_path( + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + path: Vec +) -> Result<(Vec, HashMap), ConversionError> { + let mut v2_path = vec![]; + let v2_relations = v3_to_v2_target_path_step::>(context, collection_relationships, root_collection_object_type, path.into_iter(), &mut v2_path)?; + Ok((v2_path, v2_relations)) +} + +fn v3_to_v2_target_path_step>( + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + mut path_iter: T::IntoIter, + v2_path: &mut Vec +) -> Result, ConversionError> { + let mut v2_relations = HashMap::new(); + + if let Some(path_element) = path_iter.next() { + v2_path.push(path_element.relationship.clone()); + + let where_expr = path_element + .predicate + .map(|expression| { + let v3_relationship = lookup_relationship(collection_relationships, &path_element.relationship)?; + let target_collection = context.find_collection(&v3_relationship.target_collection)?; + let target_object_type = context.find_object_type(&target_collection.r#type)?; + let v2_expression = v3_to_v2_expression(context, collection_relationships, root_collection_object_type, &target_object_type, *expression)?; + Ok(Box::new(v2_expression)) + }) + .transpose()?; + + let subrelations = v3_to_v2_target_path_step::(context, collection_relationships, root_collection_object_type, path_iter, v2_path)?; + + v2_relations.insert( + path_element.relationship, + v2::OrderByRelation { + r#where: where_expr, + subrelations, + } + ); } + + Ok(v2_relations) } /// Like v2, a v3 QueryRequest has a map of Relationships. Unlike v2, v3 does not indicate the @@ -350,28 +542,31 @@ fn v3_to_v2_relationships( } fn v3_to_v2_expression( - scalar_types: &BTreeMap, + context: &QueryContext, + collection_relationships: &BTreeMap, + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, expression: v3::Expression, ) -> Result { match expression { v3::Expression::And { expressions } => Ok(v2::Expression::And { expressions: expressions .into_iter() - .map(|expr| v3_to_v2_expression(scalar_types, expr)) + .map(|expr| v3_to_v2_expression(context, collection_relationships, root_collection_object_type, object_type, expr)) .collect::>()?, }), v3::Expression::Or { expressions } => Ok(v2::Expression::Or { expressions: expressions .into_iter() - .map(|expr| v3_to_v2_expression(scalar_types, expr)) + .map(|expr| v3_to_v2_expression(context, collection_relationships, root_collection_object_type, object_type, expr)) .collect::>()?, }), v3::Expression::Not { expression } => Ok(v2::Expression::Not { - expression: Box::new(v3_to_v2_expression(scalar_types, *expression)?), + expression: Box::new(v3_to_v2_expression(context, collection_relationships, root_collection_object_type, object_type, *expression)?), }), v3::Expression::UnaryComparisonOperator { column, operator } => { Ok(v2::Expression::ApplyUnaryComparison { - column: v3_to_v2_comparison_target(column)?, + column: v3_to_v2_comparison_target(root_collection_object_type, object_type, column)?, operator: match operator { v3::UnaryComparisonOperator::IsNull => v2::UnaryComparisonOperator::IsNull, }, @@ -381,89 +576,121 @@ fn v3_to_v2_expression( column, operator, value, - } => v3_to_v2_binary_comparison(scalar_types, column, operator, value), - v3::Expression::Exists { - in_collection, - predicate, - } => Ok(v2::Expression::Exists { - in_table: match in_collection { - v3::ExistsInCollection::Related { - relationship, - arguments: _, - } => v2::ExistsInTable::RelatedTable { relationship }, - v3::ExistsInCollection::Unrelated { - collection, - arguments: _, - } => v2::ExistsInTable::UnrelatedTable { - table: vec![collection], + } => v3_to_v2_binary_comparison(context, root_collection_object_type, object_type, column, operator, value), + v3::Expression::Exists { in_collection, predicate, } => { + let (in_table, collection_object_type) = match in_collection { + v3::ExistsInCollection::Related { relationship, arguments: _ } => { + let v3_relationship = lookup_relationship(collection_relationships, &relationship)?; + let v3_collection = context.find_collection(&v3_relationship.target_collection)?; + let collection_object_type = context.find_object_type(&v3_collection.r#type)?; + let in_table = v2::ExistsInTable::RelatedTable { relationship }; + Ok((in_table, collection_object_type)) }, - }, - r#where: Box::new(if let Some(predicate) = predicate { - v3_to_v2_expression(scalar_types, *predicate)? - } else { - // empty expression - v2::Expression::Or { - expressions: vec![], - } - }), - }), + v3::ExistsInCollection::Unrelated { collection, arguments: _ } => { + let v3_collection = context.find_collection(&collection)?; + let collection_object_type = context.find_object_type(&v3_collection.r#type)?; + let in_table = v2::ExistsInTable::UnrelatedTable { table: vec![collection] }; + Ok((in_table, collection_object_type)) + }, + }?; + Ok(v2::Expression::Exists { + in_table, + r#where: Box::new(if let Some(predicate) = predicate { + v3_to_v2_expression(context, collection_relationships, root_collection_object_type, &collection_object_type, *predicate)? + } else { + // empty expression + v2::Expression::Or { + expressions: vec![], + } + }), + }) + }, } } // TODO: NDC-393 - What do we need to do to handle array comparisons like `in`?. v3 now combines // scalar and array comparisons, v2 separates them fn v3_to_v2_binary_comparison( - scalar_types: &BTreeMap, + context: &QueryContext, + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, column: v3::ComparisonTarget, operator: String, value: v3::ComparisonValue, ) -> Result { - // TODO: NDC-310 look up real type here - let fake_type = "String"; - let operator_definition = lookup_operator_definition(scalar_types, fake_type, &operator)?; + let comparison_column = v3_to_v2_comparison_target(root_collection_object_type, object_type, column)?; + let operator_definition = context.find_comparison_operator_definition(&comparison_column.column_type, &operator)?; let operator = match operator_definition { v3::ComparisonOperatorDefinition::Equal => v2::BinaryComparisonOperator::Equal, _ => v2::BinaryComparisonOperator::CustomBinaryComparisonOperator(operator), }; Ok(v2::Expression::ApplyBinaryComparison { - column: v3_to_v2_comparison_target(column)?, + value: v3_to_v2_comparison_value(root_collection_object_type, object_type, comparison_column.column_type.clone(), value)?, + column: comparison_column, operator, - value: v3_to_v2_comparison_value(value)?, }) } +fn get_scalar_type_name(schema_type: &schema::Type) -> Result { + match schema_type { + schema::Type::Any => Ok(mongodb_support::ANY_TYPE_NAME.to_string()), + schema::Type::Scalar(scalar_type_name) => Ok(scalar_type_name.graphql_name()), + schema::Type::Object(object_name_name) => Err(ConversionError::TypeMismatch(format!("Expected a scalar type, got the object type {object_name_name}"))), + schema::Type::ArrayOf(element_type) => Err(ConversionError::TypeMismatch(format!("Expected a scalar type, got an array of {element_type:?}"))), + schema::Type::Nullable(underlying_type) => get_scalar_type_name(underlying_type), + } +} + fn v3_to_v2_comparison_target( + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, target: v3::ComparisonTarget, ) -> Result { match target { v3::ComparisonTarget::Column { name, path } => { - let path = v3_to_v2_target_path(path)?; + let object_field = find_object_field(object_type, &name)?; + let scalar_type_name = get_scalar_type_name(&object_field.r#type)?; + if !path.is_empty() { + // This is not supported in the v2 model. ComparisonColumn.path accepts only two values: + // []/None for the current table, and ["*"] for the RootCollectionColumn (handled below) + Err(ConversionError::NotImplemented( + "The MongoDB connector does not currently support comparisons against columns from related tables", + )) + } else { + Ok(v2::ComparisonColumn { + column_type: scalar_type_name, + name: ColumnSelector::Column(name), + path: None, + }) + } + } + v3::ComparisonTarget::RootCollectionColumn { name } => { + let object_field = find_object_field(root_collection_object_type, &name)?; + let scalar_type_name = get_scalar_type_name(&object_field.r#type)?; Ok(v2::ComparisonColumn { - column_type: UNKNOWN_TYPE.to_owned(), + column_type: scalar_type_name, name: ColumnSelector::Column(name), - path: if path.is_empty() { None } else { Some(path) }, + path: Some(vec!["$".to_owned()]), }) - } - v3::ComparisonTarget::RootCollectionColumn { name } => Ok(v2::ComparisonColumn { - column_type: UNKNOWN_TYPE.to_owned(), - name: ColumnSelector::Column(name), - path: Some(vec!["$".to_owned()]), - }), + }, } } fn v3_to_v2_comparison_value( + root_collection_object_type: &WithNameRef, + object_type: &WithNameRef, + comparison_column_scalar_type: String, value: v3::ComparisonValue, ) -> Result { match value { v3::ComparisonValue::Column { column } => { Ok(v2::ComparisonValue::AnotherColumnComparison { - column: v3_to_v2_comparison_target(column)?, + column: v3_to_v2_comparison_target(root_collection_object_type, object_type, column)?, }) } v3::ComparisonValue::Scalar { value } => Ok(v2::ComparisonValue::ScalarValueComparison { value, - value_type: UNKNOWN_TYPE.to_owned(), + value_type: comparison_column_scalar_type, }), v3::ComparisonValue::Variable { name } => Ok(v2::ComparisonValue::Variable { name }), } @@ -479,12 +706,13 @@ where #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::collections::{BTreeMap, HashMap}; + use configuration::{schema, Schema}; use dc_api_test_helpers::{self as v2, source, table_relationships, target}; + use mongodb_support::BsonScalarType; use ndc_sdk::models::{ - ComparisonOperatorDefinition, OrderByElement, OrderByTarget, OrderDirection, ScalarType, - Type, + AggregateFunctionDefinition, ComparisonOperatorDefinition, OrderByElement, OrderByTarget, OrderDirection, ScalarType, Type }; use ndc_test_helpers::*; use pretty_assertions::assert_eq; @@ -634,6 +862,13 @@ mod tests { #[test] fn translates_root_column_references() -> Result<(), anyhow::Error> { + let scalar_types = make_scalar_types(); + let schema = make_flat_schema(); + let query_context = QueryContext { + functions: vec![], + scalar_types: &scalar_types, + schema: &schema, + }; let query = query_request() .collection("authors") .query(query().fields([field!("last_name")]).predicate(exists( @@ -643,29 +878,25 @@ mod tests { binop("_regex", target!("title"), value!("Functional.*")), ]), ))) - .relationships([( - "author_articles", - relationship("articles", [("id", "author_id")]), - )]) .into(); - let v2_request = v3_to_v2_query_request(&query_context(), query)?; + let v2_request = v3_to_v2_query_request(&query_context, query)?; let expected = v2::query_request() .target(["authors"]) .query( v2::query() - .fields([v2::column!("last_name": "unknown")]) + .fields([v2::column!("last_name": "String")]) .predicate(v2::exists_unrelated( ["articles"], v2::and([ v2::equal( - v2::compare!("author_id": "unknown"), - v2::column_value!(["$"], "id": "unknown"), + v2::compare!("author_id": "Int"), + v2::column_value!(["$"], "id": "Int"), ), v2::binop( "_regex", - v2::compare!("title": "unknown"), - v2::value!(json!("Functional.*"), "unknown"), + v2::compare!("title": "String"), + v2::value!(json!("Functional.*"), "String"), ), ]), )), @@ -676,8 +907,137 @@ mod tests { Ok(()) } + #[test] + fn translates_relationships_in_fields_predicates_and_orderings() -> Result<(), anyhow::Error> { + let scalar_types = make_scalar_types(); + let schema = make_flat_schema(); + let query_context = QueryContext { + functions: vec![], + scalar_types: &scalar_types, + schema: &schema, + }; + let query = query_request() + .collection("authors") + .query( + query() + .fields([ + field!("last_name"), + relation_field!( + "author_articles" => "articles", + query().fields([field!("title"), field!("year")]) + ) + ]) + .predicate(exists( + related!("author_articles"), + binop("_regex", target!("title"), value!("Functional.*")), + )) + .order_by(vec![ + OrderByElement { + order_direction: OrderDirection::Asc, + target: OrderByTarget::SingleColumnAggregate { + column: "year".into(), + function: "avg".into(), + path: vec![ + path_element("author_articles").into() + ], + }, + }, + OrderByElement { + order_direction: OrderDirection::Desc, + target: OrderByTarget::Column { + name: "id".into(), + path: vec![], + }, + } + ]) + ) + .relationships([( + "author_articles", + relationship("articles", [("id", "author_id")]), + )]) + .into(); + let v2_request = v3_to_v2_query_request(&query_context, query)?; + + let expected = v2::query_request() + .target(["authors"]) + .query( + v2::query() + .fields([ + v2::column!("last_name": "String"), + v2::relation_field!( + "author_articles" => "articles", + v2::query() + .fields([ + v2::column!("title": "String"), + v2::column!("year": "Int")] + ) + ) + ]) + .predicate(v2::exists( + "author_articles", + v2::binop( + "_regex", + v2::compare!("title": "String"), + v2::value!(json!("Functional.*"), "String"), + ), + )) + .order_by( + dc_api_types::OrderBy { + elements: vec![ + dc_api_types::OrderByElement { + order_direction: dc_api_types::OrderDirection::Asc, + target: dc_api_types::OrderByTarget::SingleColumnAggregate { + column: "year".into(), + function: "avg".into(), + result_type: "Float".into() + }, + target_path: vec!["author_articles".into()], + }, + dc_api_types::OrderByElement { + order_direction: dc_api_types::OrderDirection::Desc, + target: dc_api_types::OrderByTarget::Column { column: v2::select!("id") }, + target_path: vec![], + } + ], + relations: HashMap::from([( + "author_articles".into(), + dc_api_types::OrderByRelation { + r#where: None, + subrelations: HashMap::new(), + } + )]) + } + ), + ) + .relationships(vec![ + table_relationships( + source("authors"), + [ + ( + "author_articles", + v2::relationship( + target("articles"), + [(v2::select!("id"), v2::select!("author_id"))], + ) + ), + ], + ) + ]) + .into(); + + assert_eq!(v2_request, expected); + Ok(()) + } + #[test] fn translates_nested_fields() -> Result<(), anyhow::Error> { + let scalar_types = make_scalar_types(); + let schema = make_nested_schema(); + let query_context = QueryContext { + functions: vec![], + scalar_types: &scalar_types, + schema: &schema, + }; let query_request = query_request() .collection("authors") .query(query().fields([ @@ -686,14 +1046,14 @@ mod tests { field!("author_array_of_arrays" => "array_of_arrays", array!(array!(object!([field!("article_title" => "title")])))) ])) .into(); - let v2_request = v3_to_v2_query_request(&query_context(), query_request)?; + let v2_request = v3_to_v2_query_request(&query_context, query_request)?; let expected = v2::query_request() .target(["authors"]) .query(v2::query().fields([ - v2::nested_object!("author_address" => "address", v2::query().fields([v2::column!("address_country" => "country": "unknown")])), - v2::nested_array!("author_articles", v2::nested_object_field!("articles", v2::query().fields([v2::column!("article_title" => "title": "unknown")]))), - v2::nested_array!("author_array_of_arrays", v2::nested_array_field!(v2::nested_object_field!("array_of_arrays", v2::query().fields([v2::column!("article_title" => "title": "unknown")])))) + v2::nested_object!("author_address" => "address", v2::query().fields([v2::column!("address_country" => "country": "String")])), + v2::nested_array!("author_articles", v2::nested_object_field!("articles", v2::query().fields([v2::column!("article_title" => "title": "String")]))), + v2::nested_array!("author_array_of_arrays", v2::nested_array_field!(v2::nested_object_field!("array_of_arrays", v2::query().fields([v2::column!("article_title" => "title": "String")])))) ])) .into(); @@ -701,10 +1061,9 @@ mod tests { Ok(()) } - fn query_context() -> QueryContext { - QueryContext { - functions: vec![], - scalar_types: BTreeMap::from([( + fn make_scalar_types() -> BTreeMap { + BTreeMap::from([ + ( "String".to_owned(), ScalarType { aggregate_functions: Default::default(), @@ -720,7 +1079,174 @@ mod tests { ), ]), }, - )]), + ), + ( + "Int".to_owned(), + ScalarType { + aggregate_functions: BTreeMap::from([ + ( + "avg".into(), + AggregateFunctionDefinition { + result_type: Type::Named { + name: "Float".into() // Different result type to the input scalar type + } + } + ) + ]), + comparison_operators: BTreeMap::from([ + ("_eq".to_owned(), ComparisonOperatorDefinition::Equal), + ]), + }, + ) + ]) + } + + fn make_flat_schema() -> Schema { + Schema { + collections: BTreeMap::from([ + ( + "authors".into(), + schema::Collection { + description: None, + r#type: "Author".into() + } + ), + ( + "articles".into(), + schema::Collection { + description: None, + r#type: "Article".into() + } + ), + ]), + object_types: BTreeMap::from([ + ( + "Author".into(), + schema::ObjectType { + description: None, + fields: BTreeMap::from([ + ( + "id".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::Int) + } + ), + ( + "last_name".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::String) + } + ), + ]), + } + ), + ( + "Article".into(), + schema::ObjectType { + description: None, + fields: BTreeMap::from([ + ( + "author_id".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::Int) + } + ), + ( + "title".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::String) + } + ), + ( + "year".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Nullable(Box::new(schema::Type::Scalar(BsonScalarType::Int))) + } + ), + ]), + } + ), + ]), + } + } + + fn make_nested_schema() -> Schema { + Schema { + collections: BTreeMap::from([ + ( + "authors".into(), + schema::Collection { + description: None, + r#type: "Author".into() + } + ) + ]), + object_types: BTreeMap::from([ + ( + "Author".into(), + schema::ObjectType { + description: None, + fields: BTreeMap::from([ + ( + "address".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Object("Address".into()) + } + ), + ( + "articles".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::ArrayOf(Box::new(schema::Type::Object("Article".into()))) + } + ), + ( + "array_of_arrays".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::ArrayOf(Box::new(schema::Type::ArrayOf(Box::new(schema::Type::Object("Article".into()))))) + } + ), + ]), + } + ), + ( + "Address".into(), + schema::ObjectType { + description: None, + fields: BTreeMap::from([ + ( + "country".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::String) + } + ), + ]), + } + ), + ( + "Article".into(), + schema::ObjectType { + description: None, + fields: BTreeMap::from([ + ( + "title".into(), + schema::ObjectField { + description: None, + r#type: schema::Type::Scalar(BsonScalarType::String) + } + ), + ]), + } + ), + ]), } } } diff --git a/crates/mongodb-connector/src/mongo_connector.rs b/crates/mongodb-connector/src/mongo_connector.rs index 77f16cc5..65df53c2 100644 --- a/crates/mongodb-connector/src/mongo_connector.rs +++ b/crates/mongodb-connector/src/mongo_connector.rs @@ -23,8 +23,7 @@ use crate::{ api_type_conversions::{ v2_to_v3_explain_response, v2_to_v3_query_response, v3_to_v2_query_request, QueryContext, }, - capabilities::scalar_types, - error_mapping::{mongo_agent_error_to_explain_error, mongo_agent_error_to_query_error}, + error_mapping::{mongo_agent_error_to_explain_error, mongo_agent_error_to_query_error}, schema, }; use crate::{capabilities::mongo_capabilities_response, mutation::handle_mutation_request}; @@ -86,14 +85,15 @@ impl Connector for MongoConnector { } async fn query_explain( - _configuration: &Self::Configuration, + configuration: &Self::Configuration, state: &Self::State, request: QueryRequest, ) -> Result, ExplainError> { let v2_request = v3_to_v2_query_request( &QueryContext { functions: vec![], - scalar_types: scalar_types(), + scalar_types: &schema::SCALAR_TYPES, + schema: &configuration.schema, }, request, )?; @@ -122,14 +122,15 @@ impl Connector for MongoConnector { } async fn query( - _configuration: &Self::Configuration, + configuration: &Self::Configuration, state: &Self::State, request: QueryRequest, ) -> Result, QueryError> { let v2_request = v3_to_v2_query_request( &QueryContext { functions: vec![], - scalar_types: scalar_types(), + scalar_types: &schema::SCALAR_TYPES, + schema: &configuration.schema, }, request, )?; diff --git a/crates/mongodb-connector/src/schema.rs b/crates/mongodb-connector/src/schema.rs index f0512fe2..f3e9f715 100644 --- a/crates/mongodb-connector/src/schema.rs +++ b/crates/mongodb-connector/src/schema.rs @@ -1,4 +1,5 @@ use std::collections::BTreeMap; +use lazy_static::lazy_static; use configuration::{ native_queries::{self, NativeQuery}, @@ -8,6 +9,10 @@ use ndc_sdk::{connector, models}; use crate::capabilities; +lazy_static! { + pub static ref SCALAR_TYPES: BTreeMap = capabilities::scalar_types(); +} + pub async fn get_schema( config: &Configuration, ) -> Result { @@ -32,7 +37,7 @@ pub async fn get_schema( Ok(models::SchemaResponse { collections, object_types, - scalar_types: capabilities::scalar_types(), + scalar_types: SCALAR_TYPES.clone(), functions, procedures, }) diff --git a/fixtures/connector/chinook/schema.json b/fixtures/connector/chinook/schema.json deleted file mode 100644 index b2c96ec0..00000000 --- a/fixtures/connector/chinook/schema.json +++ /dev/null @@ -1,555 +0,0 @@ -{ - "collections": { - "Album": { - "type": "Album", - "description": null - }, - "Artist": { - "type": "Artist", - "description": null - }, - "Customer": { - "type": "Customer", - "description": null - }, - "Employee": { - "type": "Employee", - "description": null - }, - "Genre": { - "type": "Genre", - "description": null - }, - "Invoice": { - "type": "Invoice", - "description": null - }, - "InvoiceLine": { - "type": "InvoiceLine", - "description": null - }, - "MediaType": { - "type": "MediaType", - "description": null - }, - "Playlist": { - "type": "Playlist", - "description": null - }, - "PlaylistTrack": { - "type": "PlaylistTrack", - "description": null - }, - "Track": { - "type": "Track", - "description": null - } - }, - "objectTypes": { - "Album": { - "fields": { - "AlbumId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "ArtistId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Title": { - "type": { - "scalar": "string" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Artist": { - "fields": { - "ArtistId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Name": { - "type": { - "scalar": "string" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Customer": { - "fields": { - "Address": { - "type": { - "scalar": "string" - }, - "description": null - }, - "City": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Company": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Country": { - "type": { - "scalar": "string" - }, - "description": null - }, - "CustomerId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Email": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Fax": { - "type": { - "scalar": "string" - }, - "description": null - }, - "FirstName": { - "type": { - "scalar": "string" - }, - "description": null - }, - "LastName": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Phone": { - "type": { - "scalar": "string" - }, - "description": null - }, - "PostalCode": { - "type": { - "scalar": "string" - }, - "description": null - }, - "State": { - "type": { - "scalar": "string" - }, - "description": null - }, - "SupportRepId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Employee": { - "fields": { - "Address": { - "type": { - "scalar": "string" - }, - "description": null - }, - "BirthDate": { - "type": { - "scalar": "string" - }, - "description": null - }, - "City": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Country": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Email": { - "type": { - "scalar": "string" - }, - "description": null - }, - "EmployeeId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Fax": { - "type": { - "scalar": "string" - }, - "description": null - }, - "FirstName": { - "type": { - "scalar": "string" - }, - "description": null - }, - "HireDate": { - "type": { - "scalar": "string" - }, - "description": null - }, - "LastName": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Phone": { - "type": { - "scalar": "string" - }, - "description": null - }, - "PostalCode": { - "type": { - "scalar": "string" - }, - "description": null - }, - "ReportsTo": { - "type": { - "scalar": "string" - }, - "description": null - }, - "State": { - "type": { - "scalar": "string" - }, - "description": null - }, - "Title": { - "type": { - "scalar": "string" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Genre": { - "fields": { - "GenreId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Name": { - "type": { - "scalar": "string" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Invoice": { - "fields": { - "BillingAddress": { - "type": { - "scalar": "string" - }, - "description": null - }, - "BillingCity": { - "type": { - "scalar": "string" - }, - "description": null - }, - "BillingCountry": { - "type": { - "scalar": "string" - }, - "description": null - }, - "BillingPostalCode": { - "type": { - "scalar": "string" - }, - "description": null - }, - "BillingState": { - "type": { - "scalar": "string" - }, - "description": null - }, - "CustomerId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "InvoiceDate": { - "type": { - "scalar": "string" - }, - "description": null - }, - "InvoiceId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Total": { - "type": { - "scalar": "double" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "InvoiceLine": { - "fields": { - "InvoiceId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "InvoiceLineId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Quantity": { - "type": { - "scalar": "int" - }, - "description": null - }, - "TrackId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "UnitPrice": { - "type": { - "scalar": "double" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "MediaType": { - "fields": { - "MediaTypeId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Name": { - "type": { - "scalar": "string" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Playlist": { - "fields": { - "Name": { - "type": { - "scalar": "string" - }, - "description": null - }, - "PlaylistId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "PlaylistTrack": { - "fields": { - "PlaylistId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "TrackId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - }, - "Track": { - "fields": { - "AlbumId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Bytes": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Composer": { - "type": { - "scalar": "string" - }, - "description": null - }, - "GenreId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "MediaTypeId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Milliseconds": { - "type": { - "scalar": "int" - }, - "description": null - }, - "Name": { - "type": { - "scalar": "string" - }, - "description": null - }, - "TrackId": { - "type": { - "scalar": "int" - }, - "description": null - }, - "UnitPrice": { - "type": { - "scalar": "double" - }, - "description": null - }, - "_id": { - "type": { - "scalar": "objectId" - }, - "description": null - } - }, - "description": null - } - } -} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Album.json b/fixtures/connector/chinook/schema/Album.json new file mode 100644 index 00000000..9ccc9974 --- /dev/null +++ b/fixtures/connector/chinook/schema/Album.json @@ -0,0 +1,37 @@ +{ + "name": "Album", + "collections": { + "Album": { + "type": "Album" + } + }, + "objectTypes": { + "Album": { + "fields": { + "AlbumId": { + "type": { + "scalar": "int" + } + }, + "ArtistId": { + "type": { + "scalar": "int" + } + }, + "Title": { + "type": { + "scalar": "string" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Album" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Artist.json b/fixtures/connector/chinook/schema/Artist.json new file mode 100644 index 00000000..3f19aec5 --- /dev/null +++ b/fixtures/connector/chinook/schema/Artist.json @@ -0,0 +1,34 @@ +{ + "name": "Artist", + "collections": { + "Artist": { + "type": "Artist" + } + }, + "objectTypes": { + "Artist": { + "fields": { + "ArtistId": { + "type": { + "scalar": "int" + } + }, + "Name": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Artist" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Customer.json b/fixtures/connector/chinook/schema/Customer.json new file mode 100644 index 00000000..61156790 --- /dev/null +++ b/fixtures/connector/chinook/schema/Customer.json @@ -0,0 +1,105 @@ +{ + "name": "Customer", + "collections": { + "Customer": { + "type": "Customer" + } + }, + "objectTypes": { + "Customer": { + "fields": { + "Address": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "City": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "Company": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "Country": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "CustomerId": { + "type": { + "scalar": "int" + } + }, + "Email": { + "type": { + "scalar": "string" + } + }, + "Fax": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "FirstName": { + "type": { + "scalar": "string" + } + }, + "LastName": { + "type": { + "scalar": "string" + } + }, + "Phone": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "PostalCode": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "State": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "SupportRepId": { + "type": { + "nullable": { + "scalar": "int" + } + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Customer" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Employee.json b/fixtures/connector/chinook/schema/Employee.json new file mode 100644 index 00000000..679e1576 --- /dev/null +++ b/fixtures/connector/chinook/schema/Employee.json @@ -0,0 +1,121 @@ +{ + "name": "Employee", + "collections": { + "Employee": { + "type": "Employee" + } + }, + "objectTypes": { + "Employee": { + "fields": { + "Address": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "BirthDate": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "City": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "Country": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "Email": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "EmployeeId": { + "type": { + "scalar": "int" + } + }, + "Fax": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "FirstName": { + "type": { + "scalar": "string" + } + }, + "HireDate": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "LastName": { + "type": { + "scalar": "string" + } + }, + "Phone": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "PostalCode": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "ReportsTo": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "State": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "Title": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Employee" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Genre.json b/fixtures/connector/chinook/schema/Genre.json new file mode 100644 index 00000000..c3e07a3f --- /dev/null +++ b/fixtures/connector/chinook/schema/Genre.json @@ -0,0 +1,34 @@ +{ + "name": "Genre", + "collections": { + "Genre": { + "type": "Genre" + } + }, + "objectTypes": { + "Genre": { + "fields": { + "GenreId": { + "type": { + "scalar": "int" + } + }, + "Name": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Genre" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Invoice.json b/fixtures/connector/chinook/schema/Invoice.json new file mode 100644 index 00000000..65d3587a --- /dev/null +++ b/fixtures/connector/chinook/schema/Invoice.json @@ -0,0 +1,77 @@ +{ + "name": "Invoice", + "collections": { + "Invoice": { + "type": "Invoice" + } + }, + "objectTypes": { + "Invoice": { + "fields": { + "BillingAddress": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "BillingCity": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "BillingCountry": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "BillingPostalCode": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "BillingState": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "CustomerId": { + "type": { + "scalar": "int" + } + }, + "InvoiceDate": { + "type": { + "scalar": "string" + } + }, + "InvoiceId": { + "type": { + "scalar": "int" + } + }, + "Total": { + "type": { + "scalar": "double" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Invoice" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/InvoiceLine.json b/fixtures/connector/chinook/schema/InvoiceLine.json new file mode 100644 index 00000000..93ce306d --- /dev/null +++ b/fixtures/connector/chinook/schema/InvoiceLine.json @@ -0,0 +1,47 @@ +{ + "name": "InvoiceLine", + "collections": { + "InvoiceLine": { + "type": "InvoiceLine" + } + }, + "objectTypes": { + "InvoiceLine": { + "fields": { + "InvoiceId": { + "type": { + "scalar": "int" + } + }, + "InvoiceLineId": { + "type": { + "scalar": "int" + } + }, + "Quantity": { + "type": { + "scalar": "int" + } + }, + "TrackId": { + "type": { + "scalar": "int" + } + }, + "UnitPrice": { + "type": { + "scalar": "double" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection InvoiceLine" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/MediaType.json b/fixtures/connector/chinook/schema/MediaType.json new file mode 100644 index 00000000..a3811166 --- /dev/null +++ b/fixtures/connector/chinook/schema/MediaType.json @@ -0,0 +1,34 @@ +{ + "name": "MediaType", + "collections": { + "MediaType": { + "type": "MediaType" + } + }, + "objectTypes": { + "MediaType": { + "fields": { + "MediaTypeId": { + "type": { + "scalar": "int" + } + }, + "Name": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection MediaType" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Playlist.json b/fixtures/connector/chinook/schema/Playlist.json new file mode 100644 index 00000000..3d22bd7a --- /dev/null +++ b/fixtures/connector/chinook/schema/Playlist.json @@ -0,0 +1,34 @@ +{ + "name": "Playlist", + "collections": { + "Playlist": { + "type": "Playlist" + } + }, + "objectTypes": { + "Playlist": { + "fields": { + "Name": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "PlaylistId": { + "type": { + "scalar": "int" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Playlist" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/PlaylistTrack.json b/fixtures/connector/chinook/schema/PlaylistTrack.json new file mode 100644 index 00000000..649b0cd5 --- /dev/null +++ b/fixtures/connector/chinook/schema/PlaylistTrack.json @@ -0,0 +1,32 @@ +{ + "name": "PlaylistTrack", + "collections": { + "PlaylistTrack": { + "type": "PlaylistTrack" + } + }, + "objectTypes": { + "PlaylistTrack": { + "fields": { + "PlaylistId": { + "type": { + "scalar": "int" + } + }, + "TrackId": { + "type": { + "scalar": "int" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection PlaylistTrack" + } + } +} \ No newline at end of file diff --git a/fixtures/connector/chinook/schema/Track.json b/fixtures/connector/chinook/schema/Track.json new file mode 100644 index 00000000..b241f229 --- /dev/null +++ b/fixtures/connector/chinook/schema/Track.json @@ -0,0 +1,75 @@ +{ + "name": "Track", + "collections": { + "Track": { + "type": "Track" + } + }, + "objectTypes": { + "Track": { + "fields": { + "AlbumId": { + "type": { + "nullable": { + "scalar": "int" + } + } + }, + "Bytes": { + "type": { + "nullable": { + "scalar": "int" + } + } + }, + "Composer": { + "type": { + "nullable": { + "scalar": "string" + } + } + }, + "GenreId": { + "type": { + "nullable": { + "scalar": "int" + } + } + }, + "MediaTypeId": { + "type": { + "scalar": "int" + } + }, + "Milliseconds": { + "type": { + "scalar": "int" + } + }, + "Name": { + "type": { + "scalar": "string" + } + }, + "TrackId": { + "type": { + "scalar": "int" + } + }, + "UnitPrice": { + "type": { + "scalar": "double" + } + }, + "_id": { + "type": { + "nullable": { + "scalar": "objectId" + } + } + } + }, + "description": "Object type for collection Track" + } + } +} \ No newline at end of file