Skip to content

Commit

Permalink
Merge pull request #64 from hasura/djh/add-chinook-generate-script
Browse files Browse the repository at this point in the history
"Introspect" basic comparison operators
  • Loading branch information
danieljharvey authored Nov 13, 2023
2 parents 4617bd9 + c0873ed commit ba3c1ef
Show file tree
Hide file tree
Showing 9 changed files with 3,017 additions and 2,587 deletions.
138 changes: 129 additions & 9 deletions crates/ndc-sqlserver/src/configuration/version1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use tiberius::Query;

const TABLE_CONFIGURATION_QUERY: &str = include_str!("table_configuration.sql");

const TYPES_QUERY: &str = "SELECT name FROM sys.types FOR JSON PATH";

const CURRENT_VERSION: u32 = 1;

/// User configuration.
Expand Down Expand Up @@ -100,28 +102,37 @@ async fn create_mssql_pool(
bb8::Pool::builder().max_size(2).build(mgr).await
}

/// Construct the deployment configuration by introspecting the database.
pub async fn configure(
configuration: &RawConfiguration,
) -> Result<RawConfiguration, connector::UpdateConfigurationError> {
let mssql_pool = create_mssql_pool(configuration).await.unwrap();

async fn select_first_row(
mssql_pool: &bb8::Pool<bb8_tiberius::ConnectionManager>,
query: &str,
) -> tiberius::Row {
let mut connection = mssql_pool.get().await.unwrap();

// let's do a query to check everything is ok
let select = Query::new(TABLE_CONFIGURATION_QUERY);
let select = Query::new(query);

// go!
let stream = select.query(&mut connection).await.unwrap();

// Nothing is fetched, the first result set starts.
let row = stream.into_row().await.unwrap().unwrap();
stream.into_row().await.unwrap().unwrap()
}

/// Construct the deployment configuration by introspecting the database.
pub async fn configure(
configuration: &RawConfiguration,
) -> Result<RawConfiguration, connector::UpdateConfigurationError> {
let mssql_pool = create_mssql_pool(configuration).await.unwrap();

let tables_row = select_first_row(&mssql_pool, TABLE_CONFIGURATION_QUERY).await;

let decoded: Vec<introspection::IntrospectionTable> =
serde_json::from_str(row.get(0).unwrap()).unwrap();
serde_json::from_str(tables_row.get(0).unwrap()).unwrap();

let mut metadata = query_engine_metadata::metadata::Metadata::default();

metadata.comparison_operators = get_comparison_operators(&mssql_pool).await;

metadata.tables = get_tables_info(decoded);

metadata.native_queries = configuration.metadata.native_queries.clone();
Expand All @@ -133,6 +144,115 @@ pub async fn configure(
})
}

#[derive(Deserialize, Debug)]
struct TypeItem {
name: database::ScalarType,
}

// we lookup all types in sys.types, then use our hardcoded ideas about each one to attach
// comparison operators
async fn get_comparison_operators(
mssql_pool: &bb8::Pool<bb8_tiberius::ConnectionManager>,
) -> database::ComparisonOperators {
let types_row = select_first_row(mssql_pool, TYPES_QUERY).await;

let decoded: Vec<TypeItem> = serde_json::from_str(types_row.get(0).unwrap()).unwrap();

let mut comparison_operators = BTreeMap::new();

for type_name in decoded {
comparison_operators.insert(
type_name.name.clone(),
get_comparison_operators_for_type(type_name.name),
);
}
database::ComparisonOperators(comparison_operators)
}

const CHARACTER_STRINGS: [&str; 3] = ["char", "text", "varchar"];
const UNICODE_CHARACTER_STRINGS: [&str; 3] = ["nchar", "ntext", "nvarchar"];
const CANNOT_COMPARE: [&str; 3] = ["text", "ntext", "image"];

// we hard code these, essentially
// we look up available types in `sys.types` but hard code their behaviour by looking them up below
// categories taken from https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql
fn get_comparison_operators_for_type(
type_name: database::ScalarType,
) -> BTreeMap<String, database::ComparisonOperator> {
let mut comparison_operators = BTreeMap::new();

// in ndc-spec, all things can be `==`
comparison_operators.insert(
"_eq".to_string(),
database::ComparisonOperator {
operator_name: "=".to_string(),
argument_type: type_name.clone(),
},
);

// include LIKE and NOT LIKE for string-ish types
if CHARACTER_STRINGS.contains(&type_name.0.as_str())
|| UNICODE_CHARACTER_STRINGS.contains(&type_name.0.as_str())
{
comparison_operators.insert(
"_like".to_string(),
database::ComparisonOperator {
operator_name: "LIKE".to_string(),
argument_type: type_name.clone(),
},
);
comparison_operators.insert(
"_nlike".to_string(),
database::ComparisonOperator {
operator_name: "NOT LIKE".to_string(),
argument_type: type_name.clone(),
},
);
}

// include comparison operators for types that are comparable, according to
// https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comparison-operators-transact-sql?view=sql-server-ver16
if !CANNOT_COMPARE.contains(&type_name.0.as_str()) {
comparison_operators.insert(
"_neq".to_string(),
database::ComparisonOperator {
operator_name: "!=".to_string(),
argument_type: type_name.clone(),
},
);
comparison_operators.insert(
"_lt".to_string(),
database::ComparisonOperator {
operator_name: "<".to_string(),
argument_type: type_name.clone(),
},
);
comparison_operators.insert(
"_gt".to_string(),
database::ComparisonOperator {
operator_name: ">".to_string(),
argument_type: type_name.clone(),
},
);

comparison_operators.insert(
"_gte".to_string(),
database::ComparisonOperator {
operator_name: ">=".to_string(),
argument_type: type_name.clone(),
},
);
comparison_operators.insert(
"_lte".to_string(),
database::ComparisonOperator {
operator_name: "<=".to_string(),
argument_type: type_name,
},
);
}
comparison_operators
}

fn get_tables_info(
introspection_tables: Vec<introspection::IntrospectionTable>,
) -> database::TablesInfo {
Expand Down
71 changes: 71 additions & 0 deletions crates/ndc-sqlserver/tests/configuration_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::fs;
use std::path::{Path, PathBuf};

use ndc_sqlserver::configuration;
use similar_asserts::assert_eq;

const CONNECTION_STRING: &str ="DRIVER={ODBC Driver 18 for SQL Server};SERVER=127.0.0.1,64003;Uid=SA;Database=Chinook;Pwd=Password!";

const CHINOOK_DEPLOYMENT_PATH: &str = "static/chinook-deployment.json";

#[tokio::test]
async fn test_configure_is_idempotent() {
configure_is_idempotent(CONNECTION_STRING, CHINOOK_DEPLOYMENT_PATH).await
}

// Tests that configuration generation has not changed.
//
// This test does not use insta snapshots because it checks the deployment file that is shared with
// other tests.
//
// If you have changed it intentionally, run `just generate-chinook-configuration`.
pub async fn configure_is_idempotent(
connection_string: &str,
chinook_deployment_path: impl AsRef<Path>,
) {
let expected_value = read_configuration(chinook_deployment_path);

let mut args: configuration::RawConfiguration = serde_json::from_value(expected_value.clone())
.expect("Unable to deserialize as RawConfiguration");

args.mssql_connection_string = connection_string.to_string();

let actual = configuration::configure(&args)
.await
.expect("configuration::configure");

let actual_value = serde_json::to_value(actual).expect("serde_json::to_value");

assert_eq!(expected_value, actual_value);
}

pub async fn configure_initial_configuration_is_unchanged(
connection_string: &str,
) -> ndc_sqlserver::configuration::RawConfiguration {
let args = configuration::RawConfiguration {
mssql_connection_string: connection_string.to_string(),

..configuration::RawConfiguration::empty()
};

configuration::configure(&args)
.await
.expect("configuration::configure")
}

fn read_configuration(chinook_deployment_path: impl AsRef<Path>) -> serde_json::Value {
let file = fs::File::open(get_path_from_project_root(chinook_deployment_path))
.expect("fs::File::open");
serde_json::from_reader(file).expect("serde_json::from_reader")
}

/// Find the project root via the crate root provided by `cargo test`,
/// and get our single static configuration file.
/// This depends on the convention that all our crates live in `/crates/<name>`
/// and will break in the unlikely case that we change this
pub fn get_path_from_project_root(deployment_path: impl AsRef<Path>) -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.push("../../");
d.push(deployment_path);
d
}
Loading

0 comments on commit ba3c1ef

Please sign in to comment.