Skip to content

Auto-generated insert procedures v1 #261

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 18 commits into from
Jan 19, 2024
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Added

- Support auto-generating insert procedures v1
([#261](https://github.com/hasura/ndc-postgres/pull/261))
- Return the generated SQL of an explain request with empty variables
([#241](https://github.com/hasura/ndc-postgres/pull/241))
- Emit invalid request, constraint not met, and unprocessable content errors at the relevant scenarios
Expand Down
14 changes: 8 additions & 6 deletions crates/connectors/ndc-postgres/src/configuration/version2.sql
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,14 @@ WITH
WHEN att.attidentity = 'a' THEN 'identityAlways'
ELSE 'notIdentity'
END
AS is_identity
-- skipped because yugabyte is based on pg11 that does not have this field.
-- CASE WHEN att.attgenerated = 's' THEN 'isGenerated' ELSE 'notGenerated' END
-- AS is_generated
AS is_identity,
CASE WHEN attgenerated_exists
THEN CASE WHEN attgenerated::text = 's' THEN 'stored' ELSE 'notGenerated' END
ELSE 'notGenerated'
END as is_generated
FROM
pg_catalog.pg_attribute AS att
CROSS JOIN (SELECT current_setting('server_version_num')::int >= 120000) AS attgenerated(attgenerated_exists)
WHERE
-- We only include columns that are actually part of the table currently.
NOT att.attisdropped -- This table also records historic columns.
Expand Down Expand Up @@ -905,8 +907,8 @@ FROM
c.has_default,
'isIdentity',
c.is_identity,
-- 'isGenerated',
-- c.is_generated,
'isGenerated',
c.is_generated,
'description',
comm.description
)
Expand Down
77 changes: 74 additions & 3 deletions crates/connectors/ndc-postgres/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::configuration;
use ndc_sdk::connector;
use ndc_sdk::models;
use query_engine_metadata::metadata;
use query_engine_translation::translation::mutation::{delete, generate};
use query_engine_translation::translation::mutation::{delete, generate, insert};

/// Get the connector's schema.
///
Expand Down Expand Up @@ -252,16 +252,18 @@ pub async fn get_schema(
})
.collect();

let mut more_object_types = BTreeMap::new();
let generated_procedures: Vec<models::ProcedureInfo> =
query_engine_translation::translation::mutation::generate::generate(
&metadata.tables,
&config.mutations_version,
)
.iter()
.map(|(name, mutation)| mutation_to_procedure(name, mutation))
.map(|(name, mutation)| mutation_to_procedure(name, mutation, &mut more_object_types))
.collect();

procedures.extend(generated_procedures);
object_types.extend(more_object_types);

Ok(models::SchemaResponse {
collections,
Expand Down Expand Up @@ -294,9 +296,16 @@ fn type_to_type(typ: &metadata::Type) -> models::Type {
}

/// Turn our different `Mutation` items into `ProcedureInfo`s to be output in the schema
fn mutation_to_procedure(name: &String, mutation: &generate::Mutation) -> models::ProcedureInfo {
fn mutation_to_procedure(
name: &String,
mutation: &generate::Mutation,
object_types: &mut BTreeMap<String, models::ObjectType>,
) -> models::ProcedureInfo {
match mutation {
generate::Mutation::DeleteMutation(delete) => delete_to_procedure(name, delete),
generate::Mutation::InsertMutation(insert) => {
insert_to_procedure(name, insert, object_types)
}
}
}

Expand Down Expand Up @@ -330,3 +339,65 @@ fn delete_to_procedure(name: &String, delete: &delete::DeleteMutation) -> models
}
}
}

/// Create an ObjectType out of columns metadata.
fn make_object_type(
columns: &BTreeMap<String, metadata::database::ColumnInfo>,
) -> models::ObjectType {
let mut fields = BTreeMap::new();
for (name, column) in columns {
match column {
// columns that are generated or are always identity should not be insertable.
metadata::database::ColumnInfo {
is_generated: metadata::database::IsGenerated::Stored,
..
}
| metadata::database::ColumnInfo {
is_identity: metadata::database::IsIdentity::IdentityAlways,
..
} => (),
_ => {
fields.insert(
name.clone(),
models::ObjectField {
r#type: column_to_type(column),
description: None,
},
);
}
}
}
models::ObjectType {
description: None,
fields,
}
}

/// Given an `InsertMutation`, turn it into a `ProcedureInfo` to be output in the schema.
fn insert_to_procedure(
name: &String,
insert: &insert::InsertMutation,
object_types: &mut BTreeMap<String, models::ObjectType>,
) -> models::ProcedureInfo {
let mut arguments = BTreeMap::new();
let object_type = make_object_type(&insert.columns);
let object_name = format!("{name}_object").to_string();
object_types.insert(object_name.clone(), object_type);

arguments.insert(
"_object".to_string(),
models::ArgumentInfo {
argument_type: models::Type::Named { name: object_name },
description: None,
},
);

models::ProcedureInfo {
name: name.to_string(),
description: Some(insert.description.to_string()),
arguments,
result_type: models::Type::Named {
name: insert.collection_name.to_string(),
},
}
}
2 changes: 1 addition & 1 deletion crates/query-engine/metadata/src/metadata/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ pub enum IsIdentity {
pub enum IsGenerated {
#[default]
NotGenerated,
IsGenerated,
Stored,
}

/// Information about a database column.
Expand Down
11 changes: 11 additions & 0 deletions crates/query-engine/sql/src/sql/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct CommonTableExpression {
pub enum CTExpr {
RawSql(Vec<RawSql>),
Delete(Delete),
Insert(Insert),
}

/// Raw SQL written by a user which is opaque to us
Expand All @@ -51,6 +52,16 @@ pub struct Select {
pub limit: Limit,
}

/// An INSERT clause
#[derive(Debug, Clone, PartialEq)]
pub struct Insert {
pub schema: SchemaName,
pub table: TableName,
pub columns: Vec<ColumnName>,
pub values: Vec<Expression>,
pub returning: Returning,
}

/// A DELETE clause
#[derive(Debug, Clone, PartialEq)]
pub struct Delete {
Expand Down
33 changes: 33 additions & 0 deletions crates/query-engine/sql/src/sql/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ impl CTExpr {
}
}
CTExpr::Delete(delete) => delete.to_sql(sql),
CTExpr::Insert(insert) => insert.to_sql(sql),
}
}
}
Expand Down Expand Up @@ -122,6 +123,38 @@ impl Select {
}
}

impl Insert {
pub fn to_sql(&self, sql: &mut SQL) {
sql.append_syntax("INSERT INTO ");

sql.append_identifier(&self.schema.0);
sql.append_syntax(".");
sql.append_identifier(&self.table.0);
sql.append_syntax("(");
for (index, column_name) in self.columns.iter().enumerate() {
sql.append_identifier(&column_name.0.to_string());
if index < (self.columns.len() - 1) {
sql.append_syntax(", ")
}
}
sql.append_syntax(")");

sql.append_syntax(" VALUES ");
sql.append_syntax("(");
for (index, value) in self.values.iter().enumerate() {
value.to_sql(sql);
if index < (self.values.len() - 1) {
sql.append_syntax(", ")
}
}
sql.append_syntax(")");

sql.append_syntax(" ");

self.returning.to_sql(sql);
}
}

impl Delete {
pub fn to_sql(&self, sql: &mut SQL) {
let Delete {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub fn normalize_cte(mut cte: CommonTableExpression) -> CommonTableExpression {
.collect(),
),
CTExpr::Delete(delete) => CTExpr::Delete(normalize_delete(delete)),
CTExpr::Insert(insert) => CTExpr::Insert(normalize_insert(insert)),
};
cte
}
Expand All @@ -111,6 +112,12 @@ fn normalize_delete(mut delete: Delete) -> Delete {
delete
}

/// Normalize everything in an Insert
fn normalize_insert(mut insert: Insert) -> Insert {
insert.values = insert.values.into_iter().map(normalize_expr).collect();
insert
}

/// Constant expressions folding. Remove redundant expressions.
/// This is the main work. The other parts are just trying to apply
/// this rewrite to their Expressions.
Expand Down
20 changes: 20 additions & 0 deletions crates/query-engine/translation/src/translation/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum Error {
UnexpectedVariable,
CapabilityNotSupported(UnsupportedCapabilities),
UnableToDeserializeNumberAsF64(serde_json::Number),
ColumnIsGenerated(String),
ColumnIsIdentityAlways(String),
MissingColumnInInsert(String, String),
NotImplementedYet(String),
InternalError(String),
}
Expand Down Expand Up @@ -93,6 +96,23 @@ impl std::fmt::Display for Error {
Error::UnableToDeserializeNumberAsF64(num) => {
write!(f, "Unable to deserialize the number '{}' as f64.", num)
}
Error::ColumnIsGenerated(column) => {
write!(
f,
"Unable to insert into the generated column '{}'.",
column
)
}
Error::ColumnIsIdentityAlways(column) => {
write!(f, "Unable to insert into the identity column '{}'.", column)
}
Error::MissingColumnInInsert(column, collection) => {
write!(
f,
"Unable to insert into '{}'. Column '{}' is missing.",
collection, column
)
}
Error::CapabilityNotSupported(thing) => {
write!(f, "Queries containing {} are not supported.", thing)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Given introspection data, generate a set of standard mutation procedures

use super::delete::{generate_delete_by_unique, DeleteMutation};
use super::insert;
use super::insert::InsertMutation;
use query_engine_metadata::metadata::{database, mutations};
use std::collections::BTreeMap;

#[derive(Debug, Clone)]
pub enum Mutation {
DeleteMutation(DeleteMutation),
InsertMutation(InsertMutation),
}

/// Given our introspection data, work out all the mutations we can generate
Expand All @@ -24,6 +27,8 @@ pub fn generate(
for (name, delete_mutation) in delete_mutations {
mutations.insert(name, Mutation::DeleteMutation(delete_mutation));
}
let (name, insert_mutation) = insert::generate(collection_name, table_info);
mutations.insert(name, Mutation::InsertMutation(insert_mutation));
}
}
None => {}
Expand Down
Loading