Skip to content

Commit

Permalink
rename "native query" to "native procedure"; remove read-only native …
Browse files Browse the repository at this point in the history
…procedures (#29)

Going forward "native query" will refer to a thing defined by an aggregation pipeline (not an arbitrary MongoDB command) that will appear in the data graph as a function. (Maybe we will add support for native queries that appear as collections at some point, but we've been asked to specifically implement the function case.)

What was previously called a "native query" is now called a "native procedure". It maps exclusively to a procedure in the data graph. The read-write/read-only switch is gone - procedures will only be used for mutations.

https://hasurahq.atlassian.net/browse/MDB-102
  • Loading branch information
hallettj authored Apr 4, 2024
1 parent a9886fe commit 42dddff
Show file tree
Hide file tree
Showing 17 changed files with 89 additions and 278 deletions.
27 changes: 13 additions & 14 deletions crates/configuration/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@ use itertools::Itertools;
use schemars::JsonSchema;
use serde::Deserialize;

use crate::{native_queries::NativeQuery, read_directory, schema::ObjectType, Schema};
use crate::{native_procedure::NativeProcedure, read_directory, schema::ObjectType, Schema};

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Configuration {
/// Descriptions of collections and types used in the database
pub schema: Schema,

/// Native queries allow arbitrary MongoDB aggregation pipelines where types of results are
/// Native procedures allow arbitrary MongoDB commands where types of results are
/// specified via user configuration.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub native_queries: BTreeMap<String, NativeQuery>,
pub native_procedures: BTreeMap<String, NativeProcedure>,
}

impl Configuration {
pub fn validate(
schema: Schema,
native_queries: BTreeMap<String, NativeQuery>,
native_procedures: BTreeMap<String, NativeProcedure>,
) -> anyhow::Result<Self> {
let config = Configuration {
schema,
native_queries,
native_procedures,
};

{
Expand Down Expand Up @@ -55,14 +55,14 @@ impl Configuration {
read_directory(configuration_dir).await
}

/// Returns object types collected from schema and native queries
/// Returns object types collected from schema and native procedures
pub fn object_types(&self) -> impl Iterator<Item = (&String, &ObjectType)> {
let object_types_from_schema = self.schema.object_types.iter();
let object_types_from_native_queries = self
.native_queries
let object_types_from_native_procedures = self
.native_procedures
.values()
.flat_map(|native_query| &native_query.object_types);
object_types_from_schema.chain(object_types_from_native_queries)
.flat_map(|native_procedure| &native_procedure.object_types);
object_types_from_schema.chain(object_types_from_native_procedures)
}
}

Expand All @@ -87,9 +87,9 @@ mod tests {
.into_iter()
.collect(),
};
let native_queries = [(
let native_procedures = [(
"hello".to_owned(),
NativeQuery {
NativeProcedure {
object_types: [(
"Album".to_owned(),
ObjectType {
Expand All @@ -104,12 +104,11 @@ mod tests {
arguments: Default::default(),
selection_criteria: Default::default(),
description: Default::default(),
mode: Default::default(),
},
)]
.into_iter()
.collect();
let result = Configuration::validate(schema, native_queries);
let result = Configuration::validate(schema, native_procedures);
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("multiple definitions"));
assert!(error_msg.contains("Album"));
Expand Down
10 changes: 5 additions & 5 deletions crates/configuration/src/directory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tokio_stream::wrappers::ReadDirStream;
use crate::{with_name::WithName, Configuration, Schema};

pub const SCHEMA_DIRNAME: &str = "schema";
pub const NATIVE_QUERIES_DIRNAME: &str = "native_queries";
pub const NATIVE_PROCEDURES_DIRNAME: &str = "native_procedures";

pub const CONFIGURATION_EXTENSIONS: [(&str, FileFormat); 3] =
[("json", JSON), ("yaml", YAML), ("yml", YAML)];
Expand All @@ -38,16 +38,16 @@ pub async fn read_directory(
.unwrap_or_default();
let schema = schemas.into_values().fold(Schema::default(), Schema::merge);

let native_queries = read_subdir_configs(&dir.join(NATIVE_QUERIES_DIRNAME))
let native_procedures = read_subdir_configs(&dir.join(NATIVE_PROCEDURES_DIRNAME))
.await?
.unwrap_or_default();

Configuration::validate(schema, native_queries)
Configuration::validate(schema, native_procedures)
}

/// Parse all files in a directory with one of the allowed configuration extensions according to
/// the given type argument. For example if `T` is `NativeQuery` this function assumes that all
/// json and yaml files in the given directory should be parsed as native query configurations.
/// the given type argument. For example if `T` is `NativeProcedure` this function assumes that all
/// json and yaml files in the given directory should be parsed as native procedure configurations.
///
/// Assumes that every configuration file has a `name` field.
async fn read_subdir_configs<T>(subdir: &Path) -> anyhow::Result<Option<BTreeMap<String, T>>>
Expand Down
2 changes: 1 addition & 1 deletion crates/configuration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod configuration;
mod directory;
pub mod native_queries;
pub mod native_procedure;
pub mod schema;
mod with_name;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ use crate::schema::{ObjectField, ObjectType, Type};

/// An arbitrary database command using MongoDB's runCommand API.
/// See https://www.mongodb.com/docs/manual/reference/method/db.runCommand/
///
/// Native Procedures appear as "procedures" in your data graph.
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct NativeQuery {
pub struct NativeProcedure {
/// You may define object types here to reference in `result_type`. Any types defined here will
/// be merged with the definitions in `schema.json`. This allows you to maintain hand-written
/// types for native queries without having to edit a generated `schema.json` file.
/// types for native procedures without having to edit a generated `schema.json` file.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub object_types: BTreeMap<String, ObjectType>,

/// Type of data returned by the query. You may reference object types defined in the
/// Type of data returned by the procedure. You may reference object types defined in the
/// `object_types` list in this definition, or you may reference object types from
/// `schema.json`.
pub result_type: Type,

/// Arguments to be supplied for each query invocation. These will be substituted into the
/// Arguments to be supplied for each procedure invocation. These will be substituted into the
/// given `command`.
///
/// Argument values are standard JSON mapped from GraphQL input types, not Extended JSON.
Expand All @@ -38,7 +40,7 @@ pub struct NativeQuery {
/// See https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/
///
/// Keys and values in the command may contain placeholders of the form `{{variableName}}`
/// which will be substituted when the native query is executed according to the given
/// which will be substituted when the native procedure is executed according to the given
/// arguments.
///
/// Placeholders must be inside quotes so that the command can be stored in JSON format. If the
Expand Down Expand Up @@ -75,22 +77,6 @@ pub struct NativeQuery {

#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,

/// Set to `readWrite` if this native query might modify data in the database. When refreshing
/// a dataconnector native queries will appear in the corresponding `DataConnectorLink`
/// definition as `functions` if they are read-only, or as `procedures` if they are read-write.
/// Functions are intended to map to GraphQL Query fields, while procedures map to Mutation
/// fields.
#[serde(default)]
pub mode: Mode,
}

#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Mode {
#[default]
ReadOnly,
ReadWrite,
}

type Object = serde_json::Map<String, serde_json::Value>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::BTreeMap;

use configuration::{native_queries::NativeQuery, schema::ObjectType};
use configuration::{native_procedure::NativeProcedure, schema::ObjectType};
use mongodb::Client;

#[derive(Clone, Debug)]
Expand All @@ -10,6 +10,6 @@ pub struct MongoConfig {
/// Name of the database to connect to
pub database: String,

pub native_queries: BTreeMap<String, NativeQuery>,
pub native_procedures: BTreeMap<String, NativeProcedure>,
pub object_types: BTreeMap<String, ObjectType>,
}
62 changes: 31 additions & 31 deletions crates/mongodb-agent-common/src/procedure/interpolated_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::ProcedureError;

type Result<T> = std::result::Result<T, ProcedureError>;

/// Parse native query commands, and interpolate arguments.
/// Parse native procedure commands, and interpolate arguments.
pub fn interpolated_command(
command: &bson::Document,
arguments: &BTreeMap<String, Bson>,
Expand Down Expand Up @@ -69,19 +69,19 @@ fn interpolate_document(
///
/// if the type of the variable `recordId` is `int`.
fn interpolate_string(string: &str, arguments: &BTreeMap<String, Bson>) -> Result<Bson> {
let parts = parse_native_query(string);
let parts = parse_native_procedure(string);
if parts.len() == 1 {
let mut parts = parts;
match parts.remove(0) {
NativeQueryPart::Text(string) => Ok(Bson::String(string)),
NativeQueryPart::Parameter(param) => resolve_argument(&param, arguments),
NativeProcedurePart::Text(string) => Ok(Bson::String(string)),
NativeProcedurePart::Parameter(param) => resolve_argument(&param, arguments),
}
} else {
let interpolated_parts: Vec<String> = parts
.into_iter()
.map(|part| match part {
NativeQueryPart::Text(string) => Ok(string),
NativeQueryPart::Parameter(param) => {
NativeProcedurePart::Text(string) => Ok(string),
NativeProcedurePart::Parameter(param) => {
let argument_value = resolve_argument(&param, arguments)?;
match argument_value {
Bson::String(string) => Ok(string),
Expand All @@ -101,30 +101,30 @@ fn resolve_argument(argument_name: &str, arguments: &BTreeMap<String, Bson>) ->
Ok(argument.clone())
}

/// A part of a Native Query text, either raw text or a parameter.
/// A part of a Native Procedure command text, either raw text or a parameter.
#[derive(Debug, Clone, PartialEq, Eq)]
enum NativeQueryPart {
enum NativeProcedurePart {
/// A raw text part
Text(String),
/// A parameter
Parameter(String),
}

/// Parse a string or key in a native query into parts where variables have the syntax
/// Parse a string or key in a native procedure into parts where variables have the syntax
/// `{{<variable>}}`.
fn parse_native_query(string: &str) -> Vec<NativeQueryPart> {
let vec: Vec<Vec<NativeQueryPart>> = string
fn parse_native_procedure(string: &str) -> Vec<NativeProcedurePart> {
let vec: Vec<Vec<NativeProcedurePart>> = string
.split("{{")
.filter(|part| !part.is_empty())
.map(|part| match part.split_once("}}") {
None => vec![NativeQueryPart::Text(part.to_string())],
None => vec![NativeProcedurePart::Text(part.to_string())],
Some((var, text)) => {
if text.is_empty() {
vec![NativeQueryPart::Parameter(var.trim().to_owned())]
vec![NativeProcedurePart::Parameter(var.trim().to_owned())]
} else {
vec![
NativeQueryPart::Parameter(var.trim().to_owned()),
NativeQueryPart::Text(text.to_string()),
NativeProcedurePart::Parameter(var.trim().to_owned()),
NativeProcedurePart::Text(text.to_string()),
]
}
}
Expand All @@ -135,7 +135,7 @@ fn parse_native_query(string: &str) -> Vec<NativeQueryPart> {

#[cfg(test)]
mod tests {
use configuration::native_queries::NativeQuery;
use configuration::native_procedure::NativeProcedure;
use pretty_assertions::assert_eq;
use serde_json::json;

Expand All @@ -148,7 +148,7 @@ mod tests {

#[test]
fn interpolates_non_string_type() -> anyhow::Result<()> {
let native_query_input = json!({
let native_procedure_input = json!({
"resultType": { "object": "InsertArtist" },
"arguments": {
"id": { "type": { "scalar": "int" } },
Expand All @@ -169,13 +169,13 @@ mod tests {
.into_iter()
.collect();

let native_query: NativeQuery = serde_json::from_value(native_query_input)?;
let native_procedure: NativeProcedure = serde_json::from_value(native_procedure_input)?;
let arguments = resolve_arguments(
&native_query.object_types,
&native_query.arguments,
&native_procedure.object_types,
&native_procedure.arguments,
input_arguments,
)?;
let command = interpolated_command(&native_query.command, &arguments)?;
let command = interpolated_command(&native_procedure.command, &arguments)?;

assert_eq!(
command,
Expand All @@ -192,7 +192,7 @@ mod tests {

#[test]
fn interpolates_array_argument() -> anyhow::Result<()> {
let native_query_input = json!({
let native_procedure_input = json!({
"name": "insertArtist",
"resultType": { "object": "InsertArtist" },
"objectTypes": {
Expand Down Expand Up @@ -221,13 +221,13 @@ mod tests {
.into_iter()
.collect();

let native_query: NativeQuery = serde_json::from_value(native_query_input)?;
let native_procedure: NativeProcedure = serde_json::from_value(native_procedure_input)?;
let arguments = resolve_arguments(
&native_query.object_types,
&native_query.arguments,
&native_procedure.object_types,
&native_procedure.arguments,
input_arguments,
)?;
let command = interpolated_command(&native_query.command, &arguments)?;
let command = interpolated_command(&native_procedure.command, &arguments)?;

assert_eq!(
command,
Expand All @@ -250,7 +250,7 @@ mod tests {

#[test]
fn interpolates_arguments_within_string() -> anyhow::Result<()> {
let native_query_input = json!({
let native_procedure_input = json!({
"name": "insert",
"resultType": { "object": "Insert" },
"arguments": {
Expand All @@ -269,13 +269,13 @@ mod tests {
.into_iter()
.collect();

let native_query: NativeQuery = serde_json::from_value(native_query_input)?;
let native_procedure: NativeProcedure = serde_json::from_value(native_procedure_input)?;
let arguments = resolve_arguments(
&native_query.object_types,
&native_query.arguments,
&native_procedure.object_types,
&native_procedure.arguments,
input_arguments,
)?;
let command = interpolated_command(&native_query.command, &arguments)?;
let command = interpolated_command(&native_procedure.command, &arguments)?;

assert_eq!(
command,
Expand Down
14 changes: 7 additions & 7 deletions crates/mongodb-agent-common/src/procedure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod interpolated_command;
use std::borrow::Cow;
use std::collections::BTreeMap;

use configuration::native_queries::NativeQuery;
use configuration::native_procedure::NativeProcedure;
use configuration::schema::{ObjectField, ObjectType, Type};
use mongodb::options::SelectionCriteria;
use mongodb::{bson, Database};
Expand All @@ -25,16 +25,16 @@ pub struct Procedure<'a> {
}

impl<'a> Procedure<'a> {
pub fn from_native_query(
native_query: &'a NativeQuery,
pub fn from_native_procedure(
native_procedure: &'a NativeProcedure,
arguments: BTreeMap<String, serde_json::Value>,
) -> Self {
Procedure {
arguments,
command: Cow::Borrowed(&native_query.command),
parameters: Cow::Borrowed(&native_query.arguments),
result_type: native_query.result_type.clone(),
selection_criteria: native_query.selection_criteria.as_ref().map(Cow::Borrowed),
command: Cow::Borrowed(&native_procedure.command),
parameters: Cow::Borrowed(&native_procedure.arguments),
result_type: native_procedure.result_type.clone(),
selection_criteria: native_procedure.selection_criteria.as_ref().map(Cow::Borrowed),
}
}

Expand Down
Loading

0 comments on commit 42dddff

Please sign in to comment.