diff --git a/Cargo.lock b/Cargo.lock index 794e18089..469dc03e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "copy_dir" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543d1dd138ef086e2ff05e3a48cf9da045da2033d16f8538fd76b86cd49b2ca3" +dependencies = [ + "walkdir", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1709,9 +1718,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openapi-generator" @@ -2120,6 +2129,7 @@ version = "0.5.0" dependencies = [ "schemars", "serde", + "serde_json", ] [[package]] @@ -2428,6 +2438,15 @@ dependencies = [ "safe-regex-compiler", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.22" @@ -3083,6 +3102,7 @@ dependencies = [ "anyhow", "axum", "axum-test-helper", + "copy_dir", "env_logger", "hyper", "jsonschema", @@ -3574,6 +3594,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3710,6 +3740,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/changelog.md b/changelog.md index 3e2f56c75..c024c6e50 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Support for reading Native Query SQL from files. + ([#372](https://github.com/hasura/ndc-postgres/pull/372)) + ### Changed - Do not print information about when the cli decides not to write to a file. diff --git a/crates/configuration/src/configuration.rs b/crates/configuration/src/configuration.rs index 4d55ae26c..a8c215662 100644 --- a/crates/configuration/src/configuration.rs +++ b/crates/configuration/src/configuration.rs @@ -60,14 +60,29 @@ pub async fn parse_configuration( environment: impl Environment, ) -> Result { let configuration_file = configuration_dir.as_ref().join(CONFIGURATION_FILENAME); - let configuration_file_contents = fs::read_to_string(&configuration_file).await?; - let configuration: version3::RawConfiguration = + + let configuration_file_contents = + fs::read_to_string(&configuration_file) + .await + .map_err(|err| { + Error::IoErrorButStringified(format!("{}: {}", &configuration_file.display(), err)) + })?; + let mut configuration: version3::RawConfiguration = serde_json::from_str(&configuration_file_contents).map_err(|error| Error::ParseError { file_path: configuration_file.clone(), line: error.line(), column: error.column(), message: error.to_string(), })?; + // look for native query sql file references and read from disk. + for native_query_sql in configuration.metadata.native_queries.0.values_mut() { + native_query_sql.sql = metadata::NativeQuerySqlEither::NativeQuerySql( + native_query_sql + .sql + .from_external(configuration_dir.as_ref()) + .map_err(Error::IoErrorButStringified)?, + ); + } let connection_uri = match configuration.connection_settings.connection_uri { ConnectionUri(Secret::Plain(uri)) => Ok(uri), diff --git a/crates/configuration/src/error.rs b/crates/configuration/src/error.rs index a1a0c8586..3d2015672 100644 --- a/crates/configuration/src/error.rs +++ b/crates/configuration/src/error.rs @@ -21,6 +21,10 @@ pub enum Error { file_path: std::path::PathBuf, message: String, }, + + #[error("I/O error: {0}")] + IoErrorButStringified(String), + #[error("I/O error: {0}")] IoError(#[from] std::io::Error), } diff --git a/crates/connectors/ndc-postgres/src/connector.rs b/crates/connectors/ndc-postgres/src/connector.rs index f714ccc9a..877d00c9d 100644 --- a/crates/connectors/ndc-postgres/src/connector.rs +++ b/crates/connectors/ndc-postgres/src/connector.rs @@ -253,6 +253,9 @@ impl ConnectorSetup for PostgresSetup { ])) } configuration::Error::IoError(inner) => connector::ParseError::IoError(inner), + configuration::Error::IoErrorButStringified(inner) => { + connector::ParseError::Other(inner.into()) + } }) // Note that we don't log validation errors, because they are part of the normal business diff --git a/crates/query-engine/metadata/Cargo.toml b/crates/query-engine/metadata/Cargo.toml index bd368a0b5..a3c2d5194 100644 --- a/crates/query-engine/metadata/Cargo.toml +++ b/crates/query-engine/metadata/Cargo.toml @@ -10,3 +10,6 @@ workspace = true [dependencies] schemars = { version = "0.8.16", features = ["smol_str"] } serde = { version = "1.0.197", features = ["derive"] } + +[dev-dependencies] +serde_json = "1.0.114" diff --git a/crates/query-engine/metadata/src/metadata/native_queries.rs b/crates/query-engine/metadata/src/metadata/native_queries.rs index 54d21836d..304d6380d 100644 --- a/crates/query-engine/metadata/src/metadata/native_queries.rs +++ b/crates/query-engine/metadata/src/metadata/native_queries.rs @@ -5,6 +5,7 @@ use super::database::*; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; +use std::fs; // Types @@ -20,7 +21,7 @@ pub struct NativeQueryInfo { /// SQL expression to use for the Native Query. /// We can interpolate values using `{{variable_name}}` syntax, /// such as `SELECT * FROM authors WHERE name = {{author_name}}` - pub sql: NativeQuerySql, + pub sql: NativeQuerySqlEither, /// Columns returned by the Native Query pub columns: BTreeMap, #[serde(default)] @@ -46,6 +47,149 @@ pub struct ReadOnlyColumnInfo { pub description: Option, } +/// This type contains information that still needs to be resolved. +/// After deserializing, we expect the value to be "external", +/// and after a subsequent step where we read from files, +/// they should all be converted to NativeQuerySql. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(from = "NativeQuerySqlExternal")] +#[serde(into = "NativeQuerySqlExternal")] +pub enum NativeQuerySqlEither { + NativeQuerySql(NativeQuerySql), + NativeQuerySqlExternal(NativeQuerySqlExternal), +} + +impl NativeQuerySqlEither { + /// Extract the actual native query sql from this type. + /// If this happens before reading a file, it will fail with an error. + pub fn sql(self) -> Result { + match self { + NativeQuerySqlEither::NativeQuerySql(NativeQuerySql::Inline { sql }) => Ok(sql), + NativeQuerySqlEither::NativeQuerySql(NativeQuerySql::FromFile { sql, .. }) => Ok(sql), + NativeQuerySqlEither::NativeQuerySqlExternal(NativeQuerySqlExternal::Inline { + inline, + }) => Ok(inline), + NativeQuerySqlEither::NativeQuerySqlExternal( + NativeQuerySqlExternal::InlineUntagged(inline), + ) => Ok(inline), + NativeQuerySqlEither::NativeQuerySqlExternal(NativeQuerySqlExternal::File { + .. + }) => Err("not all native query sql files were read during parsing".to_string()), + } + } +} + +impl From for NativeQuerySqlEither { + /// We use this to deserialize. + fn from(value: NativeQuerySqlExternal) -> Self { + NativeQuerySqlEither::NativeQuerySqlExternal(value) + } +} + +impl From for NativeQuerySqlExternal { + /// We use this to serialize. + fn from(value: NativeQuerySqlEither) -> Self { + match value { + NativeQuerySqlEither::NativeQuerySqlExternal(value) => value, + NativeQuerySqlEither::NativeQuerySql(value) => value.into(), + } + } +} + +/// A Native Query SQL after file resolution. +/// This is the underlying type of the `NativeQuerySqlEither` variant with the same name +/// that is expected in the metadata when translating requests. A subsequent phase after de-serializing +/// Should convert NativeQuerySqlExternal values to values of this type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NativeQuerySql { + FromFile { + file: std::path::PathBuf, + sql: NativeQueryParts, + }, + Inline { + sql: NativeQueryParts, + }, +} + +impl NativeQuerySql { + /// Extract the native query sql expression. + pub fn sql(self) -> NativeQueryParts { + match self { + NativeQuerySql::Inline { sql } => sql, + NativeQuerySql::FromFile { sql, .. } => sql, + } + } +} + +// We use this type as an intermediate representation for serialization/deserialization +// of native query sql location/expression. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +/// Native Query SQL location. +pub enum NativeQuerySqlExternal { + /// Refer to an external Native Query SQL file. + File { + /// Relative path to a sql file. + file: std::path::PathBuf, + }, + /// Inline Native Query SQL string. + Inline { + /// An inline Native Query SQL string. + inline: NativeQueryParts, + }, + InlineUntagged( + /// An inline Native Query SQL string. + NativeQueryParts, + ), +} + +impl NativeQuerySqlEither { + /// Convert an external native query sql type to NativeQuerySql, + /// including reading files from disk. + pub fn from_external( + &self, + absolute_configuration_directory: &std::path::Path, + ) -> Result { + match self { + // unexpected we get this, but ok. + NativeQuerySqlEither::NativeQuerySql(value) => Ok(value.clone()), + NativeQuerySqlEither::NativeQuerySqlExternal(external) => match external { + NativeQuerySqlExternal::File { file } => { + parse_native_query_from_file(absolute_configuration_directory.join(file)) + } + NativeQuerySqlExternal::Inline { inline } => Ok(NativeQuerySql::Inline { + sql: inline.clone(), + }), + NativeQuerySqlExternal::InlineUntagged(inline) => Ok(NativeQuerySql::Inline { + sql: inline.clone(), + }), + }, + } + } +} + +impl From for NativeQuerySqlExternal { + /// used for deserialization. + fn from(value: NativeQuerySql) -> Self { + match value { + NativeQuerySql::Inline { sql } => NativeQuerySqlExternal::Inline { inline: sql }, + NativeQuerySql::FromFile { file, .. } => NativeQuerySqlExternal::File { file }, + } + } +} + +impl JsonSchema for NativeQuerySqlEither { + fn schema_name() -> String { + "NativeQuerySql".to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + NativeQuerySqlExternal::json_schema(gen) + } +} + /// A part of a Native Query text, either raw text or a parameter. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeQueryPart { @@ -55,19 +199,24 @@ pub enum NativeQueryPart { Parameter(String), } -/// A Native Query SQL after parsing. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NativeQuerySql(pub Vec); +/// A Native Query SQL parts after parsing. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String")] +#[serde(into = "String")] +pub struct NativeQueryParts(pub Vec); -// Serialization +impl From for NativeQueryParts { + /// Used for de-serialization. + fn from(value: String) -> Self { + parse_native_query(&value) + } +} -impl Serialize for NativeQuerySql { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { +impl From for String { + /// Used for serialization. + fn from(value: NativeQueryParts) -> Self { let mut sql: String = String::new(); - for part in self.0.iter() { + for part in value.0.iter() { match part { NativeQueryPart::Text(text) => sql.push_str(text.as_str()), NativeQueryPart::Parameter(param) => { @@ -75,42 +224,13 @@ impl Serialize for NativeQuerySql { } } } - serializer.serialize_str(&sql) - } -} - -struct NQVisitor; - -impl<'de> serde::de::Visitor<'de> for NQVisitor { - type Value = NativeQuerySql; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("A Native Query SQL") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - // todo: write a better parser - let sql = parse_native_query(value); - - Ok(NativeQuerySql(sql)) + sql } } -impl<'de> Deserialize<'de> for NativeQuerySql { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_str(NQVisitor) - } -} - -impl JsonSchema for NativeQuerySql { +impl JsonSchema for NativeQueryParts { fn schema_name() -> String { - "Native_query_sql".to_string() + "InlineNativeQuerySql".to_string() } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { @@ -120,9 +240,18 @@ impl JsonSchema for NativeQuerySql { // Parsing -/// Parse a native query into parts where variables have the -/// syntax `{{}}`. -fn parse_native_query(string: &str) -> Vec { +/// Read a file a parse it into native query parts. +pub fn parse_native_query_from_file(file: std::path::PathBuf) -> Result { + let contents: String = match fs::read_to_string(&file) { + Ok(ok) => Ok(ok), + Err(err) => Err(format!("{}: {}", &file.display(), err)), + }?; + let sql = parse_native_query(&contents); + Ok(NativeQuerySql::FromFile { file, sql }) +} + +/// Parse a native query into parts where variables have the syntax `{{}}`. +fn parse_native_query(string: &str) -> NativeQueryParts { let vec: Vec> = string .split("{{") .map(|part| match part.split_once("}}") { @@ -139,18 +268,20 @@ fn parse_native_query(string: &str) -> Vec { } }) .collect(); - vec.concat() + NativeQueryParts(vec.concat()) } +// tests + #[cfg(test)] mod tests { - use super::{parse_native_query, NativeQueryPart}; + use super::{parse_native_query, NativeQueryPart, NativeQueryParts, NativeQuerySqlExternal}; #[test] fn no_parameters() { assert_eq!( parse_native_query("select 1"), - vec![NativeQueryPart::Text("select 1".to_string())] + NativeQueryParts(vec![NativeQueryPart::Text("select 1".to_string())]) ); } @@ -158,11 +289,11 @@ mod tests { fn one_parameter() { assert_eq!( parse_native_query("select * from t where {{name}} = name"), - vec![ + NativeQueryParts(vec![ NativeQueryPart::Text("select * from t where ".to_string()), NativeQueryPart::Parameter("name".to_string()), NativeQueryPart::Text(" = name".to_string()), - ] + ]) ); } @@ -170,14 +301,14 @@ mod tests { fn multiple_parameters() { assert_eq!( parse_native_query("select * from t where id = {{id}} and {{name}} = {{other_name}}"), - vec![ + NativeQueryParts(vec![ NativeQueryPart::Text("select * from t where id = ".to_string()), NativeQueryPart::Parameter("id".to_string()), NativeQueryPart::Text(" and ".to_string()), NativeQueryPart::Parameter("name".to_string()), NativeQueryPart::Text(" = ".to_string()), NativeQueryPart::Parameter("other_name".to_string()), - ] + ]) ); } @@ -185,11 +316,31 @@ mod tests { fn one_parameter_and_curly_text() { assert_eq!( parse_native_query("select * from t where {{name}} = '{name}'"), - vec![ + NativeQueryParts(vec![ NativeQueryPart::Text("select * from t where ".to_string()), NativeQueryPart::Parameter("name".to_string()), NativeQueryPart::Text(" = '{name}'".to_string()), - ] + ]) + ); + } + + #[test] + fn parse_inline_untagged() { + assert_eq!( + serde_json::from_str::(r#""select 1""#).unwrap(), + NativeQuerySqlExternal::InlineUntagged(NativeQueryParts(vec![NativeQueryPart::Text( + "select 1".to_string() + )])) + ); + } + + #[test] + fn parse_inline_tagged() { + assert_eq!( + serde_json::from_str::(r#"{ "inline": "select 1" }"#).unwrap(), + NativeQuerySqlExternal::Inline { + inline: NativeQueryParts(vec![NativeQueryPart::Text("select 1".to_string())]) + } ); } } diff --git a/crates/query-engine/translation/src/translation/query/native_queries.rs b/crates/query-engine/translation/src/translation/query/native_queries.rs index 2fd4153e9..675cb7fd7 100644 --- a/crates/query-engine/translation/src/translation/query/native_queries.rs +++ b/crates/query-engine/translation/src/translation/query/native_queries.rs @@ -31,6 +31,8 @@ pub fn translate(env: &Env, state: State) -> Result = native_query .info .sql + .sql() + .map_err(Error::InternalError)? .0 .into_iter() .map(|part| match part { diff --git a/crates/tests/databases-tests/src/citus/configuration_tests.rs b/crates/tests/databases-tests/src/citus/configuration_tests.rs index e69eb3173..13272ab1e 100644 --- a/crates/tests/databases-tests/src/citus/configuration_tests.rs +++ b/crates/tests/databases-tests/src/citus/configuration_tests.rs @@ -23,4 +23,5 @@ async fn configuration_conforms_to_the_schema() { common::CHINOOK_NDC_METADATA_PATH, ) .await + .unwrap() } diff --git a/crates/tests/databases-tests/src/cockroach/configuration_tests.rs b/crates/tests/databases-tests/src/cockroach/configuration_tests.rs index e69eb3173..13272ab1e 100644 --- a/crates/tests/databases-tests/src/cockroach/configuration_tests.rs +++ b/crates/tests/databases-tests/src/cockroach/configuration_tests.rs @@ -23,4 +23,5 @@ async fn configuration_conforms_to_the_schema() { common::CHINOOK_NDC_METADATA_PATH, ) .await + .unwrap() } diff --git a/crates/tests/databases-tests/src/postgres/configuration_tests.rs b/crates/tests/databases-tests/src/postgres/configuration_tests.rs index 6beb6ae7b..3133ec3d3 100644 --- a/crates/tests/databases-tests/src/postgres/configuration_tests.rs +++ b/crates/tests/databases-tests/src/postgres/configuration_tests.rs @@ -37,6 +37,7 @@ async fn configuration_v3_conforms_to_the_schema() { common::CHINOOK_NDC_METADATA_PATH, ) .await + .unwrap() } #[tokio::test] diff --git a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_configuration_schema.snap b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_configuration_schema.snap index 76fd0dc95..fe3eb72fe 100644 --- a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_configuration_schema.snap +++ b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_configuration_schema.snap @@ -799,7 +799,7 @@ expression: schema "description": "SQL expression to use for the Native Query. We can interpolate values using `{{variable_name}}` syntax, such as `SELECT * FROM authors WHERE name = {{author_name}}`", "allOf": [ { - "$ref": "#/definitions/Native_query_sql" + "$ref": "#/definitions/NativeQuerySql" } ] }, @@ -831,7 +831,45 @@ expression: schema } } }, - "Native_query_sql": { + "NativeQuerySql": { + "description": "Native Query SQL location.", + "anyOf": [ + { + "description": "Refer to an external Native Query SQL file.", + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "description": "Relative path to a sql file.", + "type": "string" + } + } + }, + { + "description": "Inline Native Query SQL string.", + "type": "object", + "required": [ + "inline" + ], + "properties": { + "inline": { + "description": "An inline Native Query SQL string.", + "allOf": [ + { + "$ref": "#/definitions/InlineNativeQuerySql" + } + ] + } + } + }, + { + "$ref": "#/definitions/InlineNativeQuerySql" + } + ] + }, + "InlineNativeQuerySql": { "type": "string" }, "ReadOnlyColumnInfo": { diff --git a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_rawconfiguration_v3_schema.snap b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_rawconfiguration_v3_schema.snap index b7de07cbd..3b0c77908 100644 --- a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_rawconfiguration_v3_schema.snap +++ b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__configuration_tests__get_rawconfiguration_v3_schema.snap @@ -787,7 +787,7 @@ expression: schema "description": "SQL expression to use for the Native Query. We can interpolate values using `{{variable_name}}` syntax, such as `SELECT * FROM authors WHERE name = {{author_name}}`", "allOf": [ { - "$ref": "#/definitions/Native_query_sql" + "$ref": "#/definitions/NativeQuerySql" } ] }, @@ -819,7 +819,45 @@ expression: schema } } }, - "Native_query_sql": { + "NativeQuerySql": { + "description": "Native Query SQL location.", + "anyOf": [ + { + "description": "Refer to an external Native Query SQL file.", + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "description": "Relative path to a sql file.", + "type": "string" + } + } + }, + { + "description": "Inline Native Query SQL string.", + "type": "object", + "required": [ + "inline" + ], + "properties": { + "inline": { + "description": "An inline Native Query SQL string.", + "allOf": [ + { + "$ref": "#/definitions/InlineNativeQuerySql" + } + ] + } + } + }, + { + "$ref": "#/definitions/InlineNativeQuerySql" + } + ] + }, + "InlineNativeQuerySql": { "type": "string" }, "ReadOnlyColumnInfo": { diff --git a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__openapi_tests__openapi__up_to_date_generated_schema.snap b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__openapi_tests__openapi__up_to_date_generated_schema.snap index 1598fcb42..e75341467 100644 --- a/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__openapi_tests__openapi__up_to_date_generated_schema.snap +++ b/crates/tests/databases-tests/src/postgres/snapshots/databases_tests__postgres__openapi_tests__openapi__up_to_date_generated_schema.snap @@ -781,7 +781,7 @@ expression: generated_schema_json "description": "SQL expression to use for the Native Query. We can interpolate values using `{{variable_name}}` syntax, such as `SELECT * FROM authors WHERE name = {{author_name}}`", "allOf": [ { - "$ref": "#/components/schemas/Native_query_sql" + "$ref": "#/components/schemas/NativeQuerySql" } ] }, @@ -811,7 +811,45 @@ expression: generated_schema_json } } }, - "Native_query_sql": { + "NativeQuerySql": { + "description": "Native Query SQL location.", + "anyOf": [ + { + "description": "Refer to an external Native Query SQL file.", + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "description": "Relative path to a sql file.", + "type": "string" + } + } + }, + { + "description": "Inline Native Query SQL string.", + "type": "object", + "required": [ + "inline" + ], + "properties": { + "inline": { + "description": "An inline Native Query SQL string.", + "allOf": [ + { + "$ref": "#/components/schemas/InlineNativeQuerySql" + } + ] + } + } + }, + { + "$ref": "#/components/schemas/InlineNativeQuerySql" + } + ] + }, + "InlineNativeQuerySql": { "type": "string" }, "ReadOnlyColumnInfo": { diff --git a/crates/tests/tests-common/Cargo.toml b/crates/tests/tests-common/Cargo.toml index dbeec40df..2a95a2290 100644 --- a/crates/tests/tests-common/Cargo.toml +++ b/crates/tests/tests-common/Cargo.toml @@ -35,3 +35,4 @@ tokio = { version = "1.36.0", features = ["full"] } tokio-postgres = "0.7.10" tracing = "0.1.40" uuid = {version = "1.7.0", features = [ "v4", "fast-rng", "macro-diagnostics" ]} +copy_dir = "0.1.3" diff --git a/crates/tests/tests-common/src/common_tests/configuration_v3_tests.rs b/crates/tests/tests-common/src/common_tests/configuration_v3_tests.rs index 335b6b957..e1839b329 100644 --- a/crates/tests/tests-common/src/common_tests/configuration_v3_tests.rs +++ b/crates/tests/tests-common/src/common_tests/configuration_v3_tests.rs @@ -58,19 +58,23 @@ pub async fn configure_initial_configuration_is_unchanged( .expect("configuration::introspect") } -pub async fn configuration_conforms_to_the_schema(chinook_ndc_metadata_path: impl AsRef) { +pub async fn configuration_conforms_to_the_schema( + chinook_ndc_metadata_path: impl AsRef, +) -> anyhow::Result<()> { check_value_conforms_to_schema::( read_configuration(chinook_ndc_metadata_path).await.unwrap(), ); + Ok(()) } async fn read_configuration( chinook_ndc_metadata_path: impl AsRef, ) -> anyhow::Result { - let contents = fs::read_to_string( - get_path_from_project_root(chinook_ndc_metadata_path).join("configuration.json"), - ) - .await?; + let absolute_configuration_directory = get_path_from_project_root(chinook_ndc_metadata_path); + + let contents = + fs::read_to_string(absolute_configuration_directory.join("configuration.json")).await?; + let mut multi_version: serde_json::Value = serde_json::from_str(&contents)?; // We assume the stored NDC metadata file to be in the newest version, so to be able to make diff --git a/crates/tests/tests-common/src/ndc_metadata/configuration.rs b/crates/tests/tests-common/src/ndc_metadata/configuration.rs index 7e417906f..ceb27c17b 100644 --- a/crates/tests/tests-common/src/ndc_metadata/configuration.rs +++ b/crates/tests/tests-common/src/ndc_metadata/configuration.rs @@ -15,30 +15,68 @@ use super::helpers::get_path_from_project_root; pub async fn copy_ndc_metadata_with_new_postgres_url( main_ndc_metadata_path: impl AsRef, new_connection_uri: &str, - new_ndc_metadata_path: impl AsRef, + temp_deploys_path: std::path::PathBuf, + db_name: &str, ) -> anyhow::Result<()> { - let ndc_metadata_path = - get_path_from_project_root(main_ndc_metadata_path).join("configuration.json"); + let ndc_metadata_dir_path = get_path_from_project_root(main_ndc_metadata_path); + let ndc_metadata_path = ndc_metadata_dir_path.join("configuration.json"); + + let new_ndc_metadata = serde_json::from_str::( + &std::fs::read_to_string(&ndc_metadata_path) + .map_err(|err| anyhow::anyhow!("{}: {}", &ndc_metadata_path.display(), err))?, + ) + .map_err(|err| anyhow::anyhow!("{}: {}", &ndc_metadata_path.display(), err))?; - let new_ndc_metadata: RawConfiguration = - serde_json::from_str(&fs::read_to_string(ndc_metadata_path).await?)?; let new_ndc_metadata = set_connection_uri(new_ndc_metadata, new_connection_uri.into()); - let new_ndc_metadata_dir = get_path_from_project_root(new_ndc_metadata_path); - fs::create_dir_all(&new_ndc_metadata_dir).await?; + let temp_deploys_path = get_path_from_project_root(temp_deploys_path); + + // make sure the directory where all temp deploys are copied to exists + fs::create_dir_all(&temp_deploys_path) + .await + .map_err(|err| anyhow::anyhow!("{}: {}", &temp_deploys_path.display(), err))?; + + let new_ndc_metadata_dir = temp_deploys_path.join(db_name); + + copy_dir::copy_dir(&ndc_metadata_dir_path, &new_ndc_metadata_dir).map_err(|err| { + anyhow::anyhow!( + r#"copy_dir failed. +from: {} +to: {} +error: {}"#, + &ndc_metadata_dir_path.display(), + &new_ndc_metadata_dir.display(), + err + ) + })?; + let new_ndc_metadata_path = new_ndc_metadata_dir.join("configuration.json"); fs::write( - get_path_from_project_root(new_ndc_metadata_path), + get_path_from_project_root(&new_ndc_metadata_path), serde_json::to_string_pretty(&new_ndc_metadata)?, ) - .await?; + .await + .map_err(|err| { + anyhow::anyhow!( + "{}: {}", + &get_path_from_project_root(new_ndc_metadata_path).display(), + err + ) + })?; Ok(()) } /// Erase test NDC metadata file created at `ndc_metadata_path` pub async fn delete_ndc_metadata(ndc_metadata_path: impl AsRef) -> anyhow::Result<()> { let absolute_path = get_path_from_project_root(ndc_metadata_path); - fs::remove_dir_all(absolute_path).await?; + fs::remove_dir_all(&absolute_path).await.map_err(|err| { + anyhow::anyhow!( + "{}: {}", + &get_path_from_project_root(absolute_path).display(), + err + ) + })?; + Ok(()) } diff --git a/crates/tests/tests-common/src/ndc_metadata/mod.rs b/crates/tests/tests-common/src/ndc_metadata/mod.rs index c932d1199..d27c8a3d5 100644 --- a/crates/tests/tests-common/src/ndc_metadata/mod.rs +++ b/crates/tests/tests-common/src/ndc_metadata/mod.rs @@ -22,14 +22,18 @@ impl FreshDeployment { ) -> anyhow::Result { let (db_name, new_connection_uri) = database::create_fresh_database(connection_uri).await; - let new_ndc_metadata_path = PathBuf::from("static/temp-deploys").join(&db_name); + let temp_deploys_path = PathBuf::from("static/temp-deploys"); configuration::copy_ndc_metadata_with_new_postgres_url( ndc_metadata_path, &new_connection_uri, - &new_ndc_metadata_path, + temp_deploys_path, + &db_name, ) .await?; + + let new_ndc_metadata_path = PathBuf::from("static/temp-deploys").join(&db_name); + Ok(FreshDeployment { db_name, ndc_metadata_path: new_ndc_metadata_path, diff --git a/crates/tests/tests-common/src/router.rs b/crates/tests/tests-common/src/router.rs index 781cde31d..c2de3c5cf 100644 --- a/crates/tests/tests-common/src/router.rs +++ b/crates/tests/tests-common/src/router.rs @@ -21,7 +21,8 @@ pub async fn create_router( connection_uri.to_string(), )]); let setup = PostgresSetup::new(environment); - let state = ndc_sdk::default_main::init_server_state(setup, absolute_configuration_directory) + + let state = ndc_sdk::default_main::init_server_state(setup, &absolute_configuration_directory) .await .unwrap(); diff --git a/justfile b/justfile index 0350c5b77..429146618 100644 --- a/justfile +++ b/justfile @@ -51,7 +51,7 @@ dev: start-dependencies OTEL_SERVICE_NAME=ndc-postgres \ cargo watch -i "**/snapshots/*" \ -c \ - -x 'test -p query-engine-sql -p query-engine-translation -p databases-tests --features postgres' \ + -x 'test -p query-engine-metadata -p query-engine-sql -p query-engine-translation -p databases-tests --features postgres' \ -x clippy \ -x 'run --bin ndc-postgres -- serve --configuration {{POSTGRES_V3_CHINOOK_NDC_METADATA}}' diff --git a/static/citus/v3-chinook-ndc-metadata/configuration.json b/static/citus/v3-chinook-ndc-metadata/configuration.json index a4fe04f36..1c641ab9f 100644 --- a/static/citus/v3-chinook-ndc-metadata/configuration.json +++ b/static/citus/v3-chinook-ndc-metadata/configuration.json @@ -841,7 +841,7 @@ }, "nativeQueries": { "address_identity_function": { - "sql": "SELECT {{address}} as result", + "sql": { "inline": "SELECT {{address}} as result" }, "columns": { "result": { "name": "result", @@ -865,7 +865,9 @@ "description": "A native query used to test support for composite types" }, "album_by_title": { - "sql": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -913,7 +915,9 @@ "description": null }, "array_reverse": { - "sql": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)", + "sql": { + "inline": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)" + }, "columns": { "reversed": { "name": "reversed", @@ -941,7 +945,9 @@ "description": "A native query used to test support for arrays as inputs" }, "array_series": { - "sql": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr", + "sql": { + "inline": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr" + }, "columns": { "series": { "name": "series", @@ -983,7 +989,7 @@ "description": "A native query used to test support for arrays" }, "artist": { - "sql": "SELECT * FROM public.\"Artist\"", + "sql": { "inline": "SELECT * FROM public.\"Artist\"" }, "columns": { "ArtistId": { "name": "ArtistId", @@ -1006,7 +1012,9 @@ "description": null }, "artist_below_id": { - "sql": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}" + }, "columns": { "id": { "name": "ArtistId", @@ -1038,7 +1046,9 @@ "description": null }, "count_elements": { - "sql": "SELECT array_length({{array_argument}}, 1) as result", + "sql": { + "inline": "SELECT array_length({{array_argument}}, 1) as result" + }, "columns": { "result": { "name": "result", @@ -1064,7 +1074,9 @@ "description": "A native query used to test support array-valued variables" }, "delete_playlist_track": { - "sql": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *", + "sql": { + "inline": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *" + }, "columns": { "PlaylistId": { "name": "PlaylistId", @@ -1097,7 +1109,9 @@ "isProcedure": true }, "insert_album": { - "sql": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -1154,7 +1168,9 @@ "isProcedure": true }, "insert_artist": { - "sql": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *" + }, "columns": { "ArtistId": { "name": "ArtistId", @@ -1195,7 +1211,9 @@ "isProcedure": true }, "make_person": { - "sql": "SELECT ROW({{name}}, {{address}})::person as result", + "sql": { + "inline": "SELECT ROW({{name}}, {{address}})::person as result" + }, "columns": { "result": { "name": "result", @@ -1227,7 +1245,9 @@ "description": "A native query used to test support for composite types" }, "summarize_organizations": { - "sql": "SELECT 'The organization ' || org.name || ' has ' || no_committees::text || ' committees, ' || 'the largest of which has ' || max_members || ' members.' AS result FROM (SELECT orgs.* FROM unnest({{organizations}}) as orgs) AS org JOIN LATERAL ( SELECT count(committee.*) AS no_committees, max(members_agg.no_members) AS max_members FROM unnest(org.committees) AS committee JOIN LATERAL ( SELECT count(*) as no_members FROM unnest(committee.members) AS members) AS members_agg ON true) AS coms ON true", + "sql": { + "inline": "SELECT 'The organization ' || org.name || ' has ' || no_committees::text || ' committees, ' || 'the largest of which has ' || max_members || ' members.' AS result FROM (SELECT orgs.* FROM unnest({{organizations}}) as orgs) AS org JOIN LATERAL ( SELECT count(committee.*) AS no_committees, max(members_agg.no_members) AS max_members FROM unnest(org.committees) AS committee JOIN LATERAL ( SELECT count(*) as no_members FROM unnest(committee.members) AS members) AS members_agg ON true) AS coms ON true" + }, "columns": { "result": { "name": "result", @@ -1253,7 +1273,9 @@ "description": "A native query used to test support array-valued variables" }, "value_types": { - "sql": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid", + "sql": { + "inline": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid" + }, "columns": { "bool": { "name": "bool", diff --git a/static/cockroach/v3-chinook-ndc-metadata/configuration.json b/static/cockroach/v3-chinook-ndc-metadata/configuration.json index 8ebb22c4c..276327239 100644 --- a/static/cockroach/v3-chinook-ndc-metadata/configuration.json +++ b/static/cockroach/v3-chinook-ndc-metadata/configuration.json @@ -826,7 +826,7 @@ }, "nativeQueries": { "address_identity_function": { - "sql": "SELECT {{address}} as result", + "sql": { "inline": "SELECT {{address}} as result" }, "columns": { "result": { "name": "result", @@ -850,7 +850,9 @@ "description": "A native query used to test support for composite types" }, "album_by_title": { - "sql": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -898,7 +900,9 @@ "description": null }, "array_reverse": { - "sql": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)", + "sql": { + "inline": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)" + }, "columns": { "reversed": { "name": "reversed", @@ -926,7 +930,9 @@ "description": "A native query used to test support for arrays as inputs" }, "array_series": { - "sql": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr", + "sql": { + "inline": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr" + }, "columns": { "series": { "name": "series", @@ -968,7 +974,7 @@ "description": "A native query used to test support for arrays" }, "artist": { - "sql": "SELECT * FROM public.\"Artist\"", + "sql": { "inline": "SELECT * FROM public.\"Artist\"" }, "columns": { "ArtistId": { "name": "ArtistId", @@ -991,7 +997,9 @@ "description": null }, "artist_below_id": { - "sql": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}" + }, "columns": { "id": { "name": "ArtistId", @@ -1023,7 +1031,9 @@ "description": null }, "count_elements": { - "sql": "SELECT array_length({{array_argument}}, 1) as result", + "sql": { + "inline": "SELECT array_length({{array_argument}}, 1) as result" + }, "columns": { "result": { "name": "result", @@ -1049,7 +1059,9 @@ "description": "A native query used to test support array-valued variables" }, "delete_playlist_track": { - "sql": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *", + "sql": { + "inline": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *" + }, "columns": { "PlaylistId": { "name": "PlaylistId", @@ -1082,7 +1094,9 @@ "isProcedure": true }, "insert_album": { - "sql": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -1139,7 +1153,9 @@ "isProcedure": true }, "insert_artist": { - "sql": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *" + }, "columns": { "ArtistId": { "name": "ArtistId", @@ -1180,7 +1196,9 @@ "isProcedure": true }, "value_types": { - "sql": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid", + "sql": { + "inline": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid" + }, "columns": { "bool": { "name": "bool", diff --git a/static/postgres/v3-chinook-ndc-metadata/configuration.json b/static/postgres/v3-chinook-ndc-metadata/configuration.json index 132d78b29..89039abc8 100644 --- a/static/postgres/v3-chinook-ndc-metadata/configuration.json +++ b/static/postgres/v3-chinook-ndc-metadata/configuration.json @@ -1098,7 +1098,7 @@ }, "nativeQueries": { "address_identity_function": { - "sql": "SELECT {{address}} as result", + "sql": { "inline": "SELECT {{address}} as result" }, "columns": { "result": { "name": "result", @@ -1122,7 +1122,9 @@ "description": "A native query used to test support for composite types" }, "album_by_title": { - "sql": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Album\" WHERE \"Title\" LIKE {{title}} AND \"AlbumId\" < {{id}}" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -1170,7 +1172,9 @@ "description": null }, "array_reverse": { - "sql": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)", + "sql": { + "inline": "SELECT array_agg(t.x) as reversed FROM (SELECT x FROM unnest({{array}}) WITH ORDINALITY AS t(x,ix) ORDER BY t.ix DESC) as t(x)" + }, "columns": { "reversed": { "name": "reversed", @@ -1198,7 +1202,9 @@ "description": "A native query used to test support for arrays as inputs" }, "array_series": { - "sql": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr", + "sql": { + "inline": "SELECT 3 as three, array_agg(arr.series) AS series FROM (SELECT generate_series({{from}},{{to}}) AS series) AS arr" + }, "columns": { "series": { "name": "series", @@ -1240,7 +1246,7 @@ "description": "A native query used to test support for arrays" }, "artist": { - "sql": "SELECT * FROM public.\"Artist\"", + "sql": { "inline": "SELECT * FROM public.\"Artist\"" }, "columns": { "ArtistId": { "name": "ArtistId", @@ -1263,7 +1269,9 @@ "description": null }, "artist_below_id": { - "sql": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}", + "sql": { + "inline": "SELECT * FROM public.\"Artist\" WHERE \"ArtistId\" < {{id}}" + }, "columns": { "id": { "name": "ArtistId", @@ -1295,7 +1303,9 @@ "description": null }, "count_elements": { - "sql": "SELECT array_length({{array_argument}}, 1) as result", + "sql": { + "inline": "SELECT array_length({{array_argument}}, 1) as result" + }, "columns": { "result": { "name": "result", @@ -1321,7 +1331,9 @@ "description": "A native query used to test support array-valued variables" }, "delete_playlist_track": { - "sql": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *", + "sql": { + "inline": "DELETE FROM public.\"PlaylistTrack\" WHERE \"TrackId\" = {{track_id}} RETURNING *" + }, "columns": { "PlaylistId": { "name": "PlaylistId", @@ -1354,7 +1366,9 @@ "isProcedure": true }, "insert_album": { - "sql": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Album\" VALUES({{id}}, {{title}}, {{artist_id}}) RETURNING *" + }, "columns": { "AlbumId": { "name": "AlbumId", @@ -1411,7 +1425,9 @@ "isProcedure": true }, "insert_artist": { - "sql": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *", + "sql": { + "inline": "INSERT INTO public.\"Artist\" VALUES ({{id}}, {{name}}) RETURNING *" + }, "columns": { "ArtistId": { "name": "ArtistId", @@ -1452,7 +1468,9 @@ "isProcedure": true }, "make_person": { - "sql": "SELECT ROW({{name}}, {{address}})::person as result", + "sql": { + "inline": "SELECT ROW({{name}}, {{address}})::person as result" + }, "columns": { "result": { "name": "result", @@ -1484,7 +1502,7 @@ "description": "A native query used to test support for composite types" }, "summarize_organizations": { - "sql": "SELECT 'The organization ' || org.name || ' has ' || no_committees::text || ' committees, ' || 'the largest of which has ' || max_members || ' members.' AS result FROM (SELECT orgs.* FROM unnest({{organizations}}) as orgs) AS org JOIN LATERAL ( SELECT count(committee.*) AS no_committees, max(members_agg.no_members) AS max_members FROM unnest(org.committees) AS committee JOIN LATERAL ( SELECT count(*) as no_members FROM unnest(committee.members) AS members) AS members_agg ON true) AS coms ON true", + "sql": { "file": "./native_queries/summarize_organizations.sql" }, "columns": { "result": { "name": "result", @@ -1510,7 +1528,9 @@ "description": "A native query used to test support array-valued variables" }, "value_types": { - "sql": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid", + "sql": { + "inline": "SELECT {{bool}} as bool, {{int4}} as int4, {{int2}} as int2, {{int8}} as int8, {{float4}} as float4, {{float8}} as \"float8\", {{numeric}} as numeric, {{char}} as char, {{varchar}} as \"varchar\", {{text}} as text, {{date}} as date, {{time}} as time, {{timetz}} as timetz, {{timestamp}} as timestamp, {{timestamptz}} as timestamptz, {{uuid}} as uuid" + }, "columns": { "bool": { "name": "bool", diff --git a/static/postgres/v3-chinook-ndc-metadata/native_queries/summarize_organizations.sql b/static/postgres/v3-chinook-ndc-metadata/native_queries/summarize_organizations.sql new file mode 100644 index 000000000..58c5e163d --- /dev/null +++ b/static/postgres/v3-chinook-ndc-metadata/native_queries/summarize_organizations.sql @@ -0,0 +1,22 @@ +SELECT + 'The organization ' || org.name || ' has ' || no_committees :: text || ' committees, ' || 'the largest of which has ' || max_members || ' members.' AS result +FROM + ( + SELECT + orgs.* + FROM + unnest({{organizations}}) AS orgs + ) AS org + JOIN LATERAL ( + SELECT + count(committee.*) AS no_committees, + max(members_agg.no_members) AS max_members + FROM + unnest(org.committees) AS committee + JOIN LATERAL ( + SELECT + count(*) AS no_members + FROM + unnest(committee.members) AS members + ) AS members_agg ON TRUE + ) AS coms ON TRUE