Skip to content

Introduce configuration version 2 #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
51e844b
Revert "Revert exposed changes in preparation for controlled version …
plcplc Dec 4, 2023
207bd7e
restore version1 configuration.sql
plcplc Dec 4, 2023
0b6d8e6
Adding version distinction to all the things
plcplc Dec 4, 2023
9184fac
fixup merge of deployments
plcplc Dec 11, 2023
0ee80b0
fixup merge deployments
plcplc Dec 11, 2023
1b32e0f
Port older native queries to v2
plcplc Dec 11, 2023
ce271e2
phew
plcplc Dec 11, 2023
9058eef
Workaround to support integer literal version 1
plcplc Dec 11, 2023
5d86b09
linting
plcplc Dec 11, 2023
2a97caf
linting
plcplc Dec 11, 2023
807c3d6
Preserve schema of v1
plcplc Dec 11, 2023
3914d3a
rebasing on origin/main
plcplc Dec 11, 2023
5be1cb0
Deplyoment snapshot
plcplc Dec 11, 2023
26cee2e
update aurora gitignore
plcplc Dec 11, 2023
c1adfb1
Add changelog entry
plcplc Dec 11, 2023
2c0bd58
Update CI actions for aurora
plcplc Dec 11, 2023
0796fb9
remove bit_xor from deployments to appease tests on older postgres ve…
plcplc Dec 12, 2023
cf75eae
Update crates/connectors/ndc-postgres/src/configuration/version1.rs
plcplc Dec 12, 2023
9436440
Update crates/connectors/ndc-postgres/src/configuration/version2.rs
plcplc Dec 12, 2023
746a3e4
Revert "remove bit_xor from deployments to appease tests on older pos…
plcplc Dec 12, 2023
418ec99
API docs for public functions
plcplc Dec 12, 2023
794df2d
Put custom trait implementations in a separate file
plcplc Dec 12, 2023
ec5b9be
Apply deployment configuration tests only on Postgres Current
plcplc Dec 12, 2023
035b9c1
Merge branch 'main' into plc/issues/NDAT-1065
plcplc Dec 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 278 additions & 48 deletions crates/connectors/ndc-postgres/src/configuration.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,69 @@
//! Configuration for the connector.

mod version1;
pub mod version1;
pub mod version2;

use std::collections::BTreeSet;
use std::boxed::Box;
use std::fmt;

use ndc_sdk::connector;
use query_engine_metadata::metadata;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

pub use version1::{
configure, metadata_to_current, validate_raw_configuration, Configuration, ConnectionUri,
PoolSettings, RawConfiguration, ResolvedSecret,
};
pub use version2::{occurring_scalar_types, ConnectionUri, PoolSettings, ResolvedSecret};

pub const CURRENT_VERSION: u32 = 1;
/// Initial configuration, just enough to connect to a database and elaborate a full
/// 'Configuration'.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "version")]
#[serde(try_from = "RawConfigurationCompat")]
#[serde(into = "RawConfigurationCompat")]
pub enum RawConfiguration {
// Until https://github.com/serde-rs/serde/pull/2525 is merged enum tags have to be strings.
#[serde(rename = "1")]
Version1(version1::RawConfiguration),
#[serde(rename = "2")]
Version2(version2::RawConfiguration),
}

impl RawConfiguration {
pub fn empty() -> Self {
RawConfiguration::Version2(version2::RawConfiguration::empty())
}
}

/// User configuration, elaborated from a 'RawConfiguration'.
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Configuration {
pub config: RawConfiguration,
}

pub async fn configure(
args: RawConfiguration,
) -> Result<RawConfiguration, connector::UpdateConfigurationError> {
match args {
RawConfiguration::Version1(v1) => {
Ok(RawConfiguration::Version1(version1::configure(v1).await?))
}
RawConfiguration::Version2(v2) => {
Ok(RawConfiguration::Version2(version2::configure(v2).await?))
}
}
}
pub async fn validate_raw_configuration(
config: RawConfiguration,
) -> Result<Configuration, connector::ValidateError> {
match config {
RawConfiguration::Version1(v1) => Ok(Configuration {
config: RawConfiguration::Version1(version1::validate_raw_configuration(v1).await?),
}),
RawConfiguration::Version2(v2) => Ok(Configuration {
config: RawConfiguration::Version2(version2::validate_raw_configuration(v2).await?),
}),
}
}

/// A configuration type, tailored to the needs of the query/mutation/explain methods (i.e., those
/// not to do with configuration management).
Expand All @@ -25,54 +77,232 @@ pub const CURRENT_VERSION: u32 = 1;
#[derive(Debug)]
pub struct RuntimeConfiguration {
pub metadata: metadata::Metadata,
pub pool_settings: version1::PoolSettings,
pub connection_uri: String,
}

/// Apply the common interpretations on the Configuration API type into an RuntimeConfiguration.
pub fn as_runtime_configuration(config: &Configuration) -> RuntimeConfiguration {
match &config.config {
RawConfiguration::Version1(v1_config) => RuntimeConfiguration {
metadata: version1::metadata_to_current(&v1_config.metadata),
pool_settings: v1_config.pool_settings.clone(),
connection_uri: match &v1_config.connection_uri {
ConnectionUri::Uri(ResolvedSecret(uri)) => uri.clone(),
},
},
RawConfiguration::Version2(v2_config) => RuntimeConfiguration {
metadata: v2_config.metadata.clone(),
pool_settings: v2_config.pool_settings.clone(),
connection_uri: match &v2_config.connection_uri {
ConnectionUri::Uri(ResolvedSecret(uri)) => uri.clone(),
},
},
}
}

// for tests

pub fn set_connection_uri(config: RawConfiguration, connection_uri: String) -> RawConfiguration {
match config {
RawConfiguration::Version1(v1) => RawConfiguration::Version1(version1::RawConfiguration {
connection_uri: ConnectionUri::Uri(ResolvedSecret(connection_uri)),
..v1
}),
RawConfiguration::Version2(v2) => RawConfiguration::Version2(version2::RawConfiguration {
connection_uri: ConnectionUri::Uri(ResolvedSecret(connection_uri)),
..v2
}),
}
}

impl<'a> version1::Configuration {
/// Apply the common interpretations on the Configuration API type into an RuntimeConfiguration.
pub fn as_runtime_configuration(self: &'a Configuration) -> RuntimeConfiguration {
RuntimeConfiguration {
metadata: metadata_to_current(&self.config.metadata),
// The below exists solely to support indicating version 1 with an integer literal.
// Once version 1 is phased out completely, the rest of this file is to be deleted and trait
// implementations for Serialize/Deserialize/JsonSchema reverted to their derived versions.

#[derive(Clone, Deserialize, Serialize)]
pub struct RawConfigurationCompat(serde_json::Value);

impl From<RawConfiguration> for RawConfigurationCompat {
fn from(value: RawConfiguration) -> Self {
let val = match value {
RawConfiguration::Version1(v1) => {
let mut val = serde_json::to_value(v1).unwrap();
let obj = val.as_object_mut().unwrap();

let mut res = serde_json::map::Map::new();
res.insert("version".to_string(), serde_json::json!(1));
res.append(obj);
serde_json::value::to_value(res).unwrap()
}
RawConfiguration::Version2(v2) => {
let mut val = serde_json::to_value(v2).unwrap();
let obj = val.as_object_mut().unwrap();

let mut res = serde_json::map::Map::new();
res.insert("version".to_string(), serde_json::json!("2"));
res.append(obj);
serde_json::value::to_value(res).unwrap()
}
};

RawConfigurationCompat(val)
}
}

// Dump of derivied JsonSchema trait implementation, tweaked to put version as an integer for
// version 1.
//
// Generated with 'cargo expand --theme=gruvbox-light -p ndc-postgres --lib configuration'. This
// should be re-run whenever a new version is added (or removed entirely once version 1 is
// deprecated).
impl schemars::JsonSchema for RawConfiguration {
fn schema_name() -> std::string::String {
"RawConfiguration".to_owned()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ndc_postgres::configuration::RawConfiguration")
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
{
let schema = schemars::schema::Schema::Object(schemars::schema::SchemaObject {
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
one_of: Some(<[_]>::into_vec(Box::new([
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
object: Some(Box::new(schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(
"version".to_owned(),
schemars::schema::Schema::Object(
schemars::schema::SchemaObject {
instance_type: Some(
// Here is the only change,
// String -> Number
schemars::schema::InstanceType::Number.into(),
),
enum_values: Some(<[_]>::into_vec(Box::new([
"1".into()
]))),
..Default::default()
},
),
);
props
},
required: {
let mut required = schemars::Set::new();
required.insert("version".to_owned());
required
},
..Default::default()
})),
..Default::default()
})
.flatten(
<version1::RawConfiguration as schemars::JsonSchema>::json_schema(gen),
),
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
object: Some(Box::new(schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(
"version".to_owned(),
schemars::schema::Schema::Object(
schemars::schema::SchemaObject {
instance_type: Some(
schemars::schema::InstanceType::String.into(),
),
enum_values: Some(<[_]>::into_vec(Box::new([
"2".into()
]))),
..Default::default()
},
),
);
props
},
required: {
let mut required = schemars::Set::new();
required.insert("version".to_owned());
required
},
..Default::default()
})),
..Default::default()
})
.flatten(
<version2::RawConfiguration as schemars::JsonSchema>::json_schema(gen),
),
]))),
..Default::default()
})),
..Default::default()
});
schemars::_private::apply_metadata(
schema,
schemars::schema::Metadata {
description: Some(
"Initial configuration, just enough to connect to a database and elaborate a full 'Configuration'."
.to_owned(),
),
..Default::default()
},
)
}
}
}

/// Collect all the types that can occur in the metadata. This is a bit circumstantial. A better
/// approach is likely to record scalar type names directly in the metadata via configuration.sql.
pub fn occurring_scalar_types(
tables: &metadata::TablesInfo,
native_queries: &metadata::NativeQueries,
) -> BTreeSet<metadata::ScalarType> {
let tables_column_types = tables.0.values().flat_map(|v| {
v.columns
.values()
.map(|c| c.r#type.clone())
.filter_map(some_scalar_type)
});

let native_queries_column_types = native_queries.0.values().flat_map(|v| {
v.columns
.values()
.map(|c| c.r#type.clone())
.filter_map(some_scalar_type)
});

let native_queries_arguments_types = native_queries.0.values().flat_map(|v| {
v.arguments
.values()
.map(|c| c.r#type.clone())
.filter_map(some_scalar_type)
});

tables_column_types
.chain(native_queries_column_types)
.chain(native_queries_arguments_types)
.collect::<BTreeSet<metadata::ScalarType>>()
#[derive(Debug)]
pub enum RawConfigurationCompatError {
JsonError(serde_json::Error),
RawConfigurationCompatError { error_message: String },
}

impl fmt::Display for RawConfigurationCompatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RawConfigurationCompatError::JsonError(e) => write!(f, "{e}"),
RawConfigurationCompatError::RawConfigurationCompatError { error_message } => {
write!(f, "RawConfiguration serialization error: {error_message}")
}
}
}
}

/// Filter predicate that only keeps scalar types.
fn some_scalar_type(typ: metadata::Type) -> Option<metadata::ScalarType> {
match typ {
metadata::Type::ArrayType(_) => None,
metadata::Type::ScalarType(t) => Some(t),
impl From<serde_json::Error> for RawConfigurationCompatError {
fn from(value: serde_json::Error) -> Self {
RawConfigurationCompatError::JsonError(value)
}
}

impl TryFrom<RawConfigurationCompat> for RawConfiguration {
type Error = RawConfigurationCompatError;

fn try_from(value: RawConfigurationCompat) -> Result<Self, Self::Error> {
let version = value.0.get("version").ok_or(
RawConfigurationCompatError::RawConfigurationCompatError {
error_message: "Configuration data did not contain a \"version\" field."
.to_string(),
},
)?;
match version.as_u64() {
Some(1) => Ok(RawConfiguration::Version1(serde_json::from_value(value.0)?)),
Some(n) => Err(RawConfigurationCompatError::RawConfigurationCompatError {
error_message: format!(
"Configuration data version key was an integer literal: {n}. The only supported integer version is 1."
),
}),
None => match version.as_str() {
Some("1") => Ok(RawConfiguration::Version1(serde_json::from_value(value.0)?)),
Some("2") => Ok(RawConfiguration::Version2(serde_json::from_value(value.0)?)),
Some(v) => Err(RawConfigurationCompatError::RawConfigurationCompatError{error_message:
format!("Configuration data version unsupported: \"{v}\". Supported versions are: 1, and \"2\".")}),
None => Err(RawConfigurationCompatError::RawConfigurationCompatError{error_message:
"Configuration data version unsupported. Supported versions are: 1, and \"2\".".to_string()}),
},
}
}
}
Loading