Skip to content

Implement native query builder in the CLI plugin #511

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 31 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! then done, making it easier to test this crate deterministically.

mod metadata;
mod native_operations;

use std::path::PathBuf;

Expand Down Expand Up @@ -40,6 +41,8 @@ pub enum Command {
#[arg(long)]
dir_to: PathBuf,
},
#[command(subcommand)]
NativeOperation(native_operations::Command),
}

/// The set of errors that can go wrong _in addition to_ generic I/O or parsing errors.
Expand All @@ -55,6 +58,7 @@ pub async fn run(command: Command, context: Context<impl Environment>) -> anyhow
Command::Initialize { with_metadata } => initialize(with_metadata, context).await?,
Command::Update => update(context).await?,
Command::Upgrade { dir_from, dir_to } => upgrade(dir_from, dir_to).await?,
Command::NativeOperation(cmd) => native_operations::run(cmd, context).await?,
};
Ok(())
}
Expand Down
229 changes: 229 additions & 0 deletions crates/cli/src/native_operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! Handle the creation of Native Operations.

use std::path::PathBuf;

use super::{update, Context};
use ndc_postgres_configuration as configuration;
use ndc_postgres_configuration::environment::Environment;

pub use configuration::version4::native_operations::Kind;

/// Commands on Native Operations.
#[derive(Debug, Clone, clap::Subcommand)]
pub enum Command {
/// List the existing Native Operations.
List,
/// Create a new Native Operation from a SQL file.
Create {
/// Relative path to the SQL file inside the connector configuration directory.
#[arg(long)]
operation_path: PathBuf,

/// Operation kind.
#[arg(long)]
kind: Kind,

/// Override the Native Operation definition if it exists.
#[arg(long)]
r#override: bool,
},
/// Delete an existing Native Operation from the configuration.
Delete {
/// The name of the Native Operation.
#[arg(long)]
name: String,

/// Operation kind.
#[arg(long)]
kind: Kind,
},
}

/// Run a command in a given directory.
pub async fn run(command: Command, context: Context<impl Environment>) -> anyhow::Result<()> {
match command {
Command::List => list(context).await?,
Command::Create {
operation_path,
kind,
r#override,
} => {
create(
context,
operation_path,
kind,
if r#override {
Override::Yes
} else {
Override::No
},
)
.await?;
}
Command::Delete { name, kind } => {
delete(context, name, kind).await?;
}
};
Ok(())
}

/// List all native operations.
async fn list(context: Context<impl Environment>) -> anyhow::Result<()> {
// Read the configuration.
let mut configuration =
configuration::parse_configuration(context.context_path.clone()).await?;

match configuration {
configuration::ParsedConfiguration::Version3(_) => Err(anyhow::anyhow!(
"To use the native operations commands, please upgrade to the latest version."
))?,
configuration::ParsedConfiguration::Version4(ref mut configuration) => {
let operations = &configuration.metadata.native_queries.0;
println!("Native Queries:");
for native_operation in operations.iter().filter(|op| !op.1.is_procedure) {
println!("- {}", native_operation.0);
}
println!("Native Mutations:");
for native_operation in operations.iter().filter(|op| op.1.is_procedure) {
println!("- {}", native_operation.0);
}
}
};
Ok(())
}

/// Override Native Operation definition if exists?
#[derive(Debug, Clone, clap::ValueEnum)]
enum Override {
Yes,
No,
}

/// Take a SQL file containing a Native Operation, check against the database that it is valid,
/// and add it to the configuration if it is.
async fn create(
context: Context<impl Environment>,
operation_path: PathBuf,
kind: Kind,
override_entry: Override,
) -> anyhow::Result<()> {
// Read the configuration.
let mut configuration =
configuration::parse_configuration(context.context_path.clone()).await?;

// Prepare the Native Operation SQL so it can be checked against the db.
let name = operation_path
.file_stem()
.ok_or(anyhow::anyhow!("SQL file not found"))?
.to_str()
.ok_or(anyhow::anyhow!("Could not convert SQL file name to string"))?
.to_string();

// Read the SQL file.
let file_contents = match std::fs::read_to_string(context.context_path.join(&operation_path)) {
Ok(ok) => ok,
Err(err) => anyhow::bail!("{}: {}", operation_path.display(), err),
};

match configuration {
configuration::ParsedConfiguration::Version3(_) => Err(anyhow::anyhow!(
"To use the native operations commands, please upgrade to the latest version."
))?,
configuration::ParsedConfiguration::Version4(ref mut configuration) => {
let connection_string = configuration.get_connection_uri()?;

let new_native_operation = configuration::version4::native_operations::create(
configuration,
&connection_string,
&operation_path,
&file_contents,
kind,
)
.await?;

// Add the new native operation to the configuration.
match override_entry {
Override::Yes => {
configuration
.metadata
.native_queries
.0
.insert(name, new_native_operation);
}
Override::No => {
// Only insert if vacant.
if let std::collections::btree_map::Entry::Vacant(entry) =
configuration.metadata.native_queries.0.entry(name.clone())
{
entry.insert(new_native_operation);
} else {
anyhow::bail!("A Native Operation with the name '{}' already exists. To override, use the --override flag.", name);
}
}
}
}
};

// We write the configuration including the new Native Operation to file.
configuration::write_parsed_configuration(configuration, context.context_path.clone()).await?;

// We update the configuration as well so that the introspection will add missing scalar type entries if necessary.
update(context).await
}

/// Delete a Native Operation by name.
async fn delete(
context: Context<impl Environment>,
name: String,
kind: Kind,
) -> anyhow::Result<()> {
// Read the configuration.
let mut configuration =
configuration::parse_configuration(context.context_path.clone()).await?;

let error_message_not_exist = format!(
"A Native {} with the name '{}' does not exists.",
match kind {
Kind::Mutation => "Mutation",
Kind::Query => "Query",
},
name
);

match configuration {
configuration::ParsedConfiguration::Version3(_) => Err(anyhow::anyhow!(
"To use the native operations commands, please upgrade to the latest version."
))?,
configuration::ParsedConfiguration::Version4(ref mut configuration) => {
// Delete if exists and is of the same type, error if not.
match configuration.metadata.native_queries.0.entry(name.clone()) {
std::collections::btree_map::Entry::Occupied(entry) => {
let value = entry.get();
if value.is_procedure {
match kind {
Kind::Mutation => {
entry.remove_entry();
}
Kind::Query => {
anyhow::bail!(format!("{error_message_not_exist}\n Did you mean the Native Mutation with the same name?"));
}
}
} else {
match kind {
Kind::Mutation => {
anyhow::bail!(format!("{error_message_not_exist}\n Did you mean the Native Query with the same name?"));
}
Kind::Query => {
entry.remove_entry();
}
}
}
}
std::collections::btree_map::Entry::Vacant(_) => {
anyhow::bail!(error_message_not_exist);
}
}
}
}
Ok(())
}
3 changes: 3 additions & 0 deletions crates/configuration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ workspace = true

[dependencies]
query-engine-metadata = { path = "../query-engine/metadata" }
query-engine-sql = { path = "../query-engine/sql" }

anyhow = { workspace = true }
# We only use clap for the derive.
clap = { workspace = true, features = ["derive", "env"] }
prometheus = {workspace = true }
schemars = { workspace = true, features = ["smol_str", "preserve_order"] }
serde = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/configuration/src/version3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pub(crate) mod comparison;
pub mod connection_settings;
pub(crate) mod metadata;
pub mod metadata;
pub(crate) mod options;

use std::borrow::Cow;
Expand Down
21 changes: 20 additions & 1 deletion crates/configuration/src/version4/metadata/native_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#![allow(clippy::wrong_self_convention)]
use super::database::*;

use query_engine_sql::sql;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
Expand Down Expand Up @@ -227,6 +229,23 @@ impl From<NativeQueryParts> for String {
}
}

impl NativeQueryParts {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We introduce a method to prettyprint a native query.

pub fn to_sql(&self) -> sql::string::SQL {
let mut sql = sql::string::SQL::new();

for part in &self.0 {
match part {
NativeQueryPart::Text(text) => sql.append_syntax(text),
NativeQueryPart::Parameter(param) => {
sql.append_param(sql::string::Param::Variable(param.to_string()));
}
}
}

sql
}
}

impl JsonSchema for NativeQueryParts {
fn schema_name() -> String {
"InlineNativeQuerySql".to_string()
Expand Down Expand Up @@ -256,7 +275,7 @@ pub fn parse_native_query_from_file(
}

/// Parse a native query into parts where variables have the syntax `{{<variable>}}`.
fn parse_native_query(string: &str) -> NativeQueryParts {
pub fn parse_native_query(string: &str) -> NativeQueryParts {
let vec: Vec<Vec<NativeQueryPart>> = string
.split("{{")
.map(|part| match part.split_once("}}") {
Expand Down
15 changes: 14 additions & 1 deletion crates/configuration/src/version4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

mod comparison;
pub mod connection_settings;
mod metadata;
pub mod metadata;
pub mod native_operations;
mod options;
mod to_runtime_configuration;
mod upgrade_from_v3;
Expand Down Expand Up @@ -76,6 +77,18 @@ impl ParsedConfiguration {
mutations_version: None,
}
}

/// Extract the connection uri from the configuration + ENV if needed.
pub fn get_connection_uri(&self) -> Result<String, anyhow::Error> {
let connection_uri = self.connection_settings.connection_uri.clone();

match connection_uri.0 {
super::values::Secret::Plain(connection_string) => Ok(connection_string),
super::values::Secret::FromEnvironment { variable } => {
Ok(std::env::var(variable.to_string())?)
}
}
}
}

fn get_type_ndc_name(r#type: &metadata::Type) -> &str {
Expand Down
Loading