From 4b046c85697df51bb379255925bf3b3a22acd2f6 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Fri, 29 Mar 2024 12:04:03 -0500 Subject: [PATCH 01/10] Column typess - towards onConflict clause --- src/graphql.rs | 65 ++++++++++++++++++++++++++++++++++++---------- src/parser_util.rs | 2 ++ 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/graphql.rs b/src/graphql.rs index d91fea97..2b382018 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1095,6 +1095,7 @@ impl ConnectionType { pub enum EnumSource { Enum(Arc), FilterIs, + TableColumns(Arc
), } #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -1426,22 +1427,40 @@ impl ___Type for MutationType { table: Arc::clone(table), schema: Arc::clone(&self.schema), }), - args: vec![__InputValue { - name_: "objects".to_string(), - type_: __Type::NonNull(NonNullType { - type_: Box::new(__Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::InsertInput(InsertInputType { - table: Arc::clone(table), - schema: Arc::clone(&self.schema), + args: vec![ + __InputValue { + name_: "objects".to_string(), + type_: __Type::NonNull(NonNullType { + type_: Box::new(__Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::InsertInput(InsertInputType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), + })), })), })), - })), - }), - description: None, - default_value: None, - sql_type: None, - }], + }), + description: None, + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "update".to_string(), + type_: __Type::NonNull(NonNullType { + type_: Box::new(__Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::InsertInput(InsertInputType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), + })), + })), + })), + }), + description: None, + default_value: None, + sql_type: None, + }, + ], description: Some(format!( "Adds one or more `{}` records to the collection", table_base_type_name @@ -1629,6 +1648,10 @@ impl ___Type for EnumType { ) } EnumSource::FilterIs => Some("FilterIs".to_string()), + EnumSource::TableColumns(table) => Some(format!( + "{}Field", + self.schema.graphql_table_base_type_name(&table) + )), } } @@ -1667,6 +1690,15 @@ impl ___Type for EnumType { }, ] } + EnumSource::TableColumns(table) => table + .columns + .iter() + .map(|col| __EnumValue { + name: self.schema.graphql_column_field_name(col), + description: None, + deprecation_reason: None, + }) + .collect(), }) } } @@ -4160,6 +4192,11 @@ impl __Schema { table: Arc::clone(table), schema: Arc::clone(&schema_rc), })); + // Used by on conflict + types_.push(__Type::Enum(EnumType { + enum_: EnumSource::TableColumns(Arc::clone(table)), + schema: Arc::clone(&schema_rc), + })); } if self.graphql_table_update_types_are_valid(table) { diff --git a/src/parser_util.rs b/src/parser_util.rs index 35a9914a..49362ed5 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -412,6 +412,8 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result value.clone(), + // TODO(or): Do I need to check directives here? + EnumSource::TableColumns(_e) => value.clone(), } } None => return Err(format!("Invalid input for {} type", enum_name)), From 01492e133e8242b853fd716932c0fc1994aa8afa Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 18 Apr 2024 12:49:40 -0500 Subject: [PATCH 02/10] onConflict input types --- src/graphql.rs | 106 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 5 deletions(-) diff --git a/src/graphql.rs b/src/graphql.rs index 2b382018..9ca5f784 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -515,6 +515,7 @@ pub enum __Type { // Mutation Mutation(MutationType), InsertInput(InsertInputType), + InsertOnConflictInput(InsertOnConflictType), InsertResponse(InsertResponseType), UpdateInput(UpdateInputType), UpdateResponse(UpdateResponseType), @@ -593,6 +594,7 @@ impl ___Type for __Type { Self::Node(x) => x.kind(), Self::NodeInterface(x) => x.kind(), Self::InsertInput(x) => x.kind(), + Self::InsertOnConflictInput(x) => x.kind(), Self::InsertResponse(x) => x.kind(), Self::UpdateInput(x) => x.kind(), Self::UpdateResponse(x) => x.kind(), @@ -628,6 +630,7 @@ impl ___Type for __Type { Self::Node(x) => x.name(), Self::NodeInterface(x) => x.name(), Self::InsertInput(x) => x.name(), + Self::InsertOnConflictInput(x) => x.name(), Self::InsertResponse(x) => x.name(), Self::UpdateInput(x) => x.name(), Self::UpdateResponse(x) => x.name(), @@ -663,6 +666,7 @@ impl ___Type for __Type { Self::Node(x) => x.description(), Self::NodeInterface(x) => x.description(), Self::InsertInput(x) => x.description(), + Self::InsertOnConflictInput(x) => x.description(), Self::InsertResponse(x) => x.description(), Self::UpdateInput(x) => x.description(), Self::UpdateResponse(x) => x.description(), @@ -699,6 +703,7 @@ impl ___Type for __Type { Self::Node(x) => x.fields(_include_deprecated), Self::NodeInterface(x) => x.fields(_include_deprecated), Self::InsertInput(x) => x.fields(_include_deprecated), + Self::InsertOnConflictInput(x) => x.fields(_include_deprecated), Self::InsertResponse(x) => x.fields(_include_deprecated), Self::UpdateInput(x) => x.fields(_include_deprecated), Self::UpdateResponse(x) => x.fields(_include_deprecated), @@ -735,6 +740,7 @@ impl ___Type for __Type { Self::Node(x) => x.interfaces(), Self::NodeInterface(x) => x.interfaces(), Self::InsertInput(x) => x.interfaces(), + Self::InsertOnConflictInput(x) => x.interfaces(), Self::InsertResponse(x) => x.interfaces(), Self::UpdateInput(x) => x.interfaces(), Self::UpdateResponse(x) => x.interfaces(), @@ -780,6 +786,7 @@ impl ___Type for __Type { Self::Node(x) => x.enum_values(_include_deprecated), Self::NodeInterface(x) => x.enum_values(_include_deprecated), Self::InsertInput(x) => x.enum_values(_include_deprecated), + Self::InsertOnConflictInput(x) => x.enum_values(), Self::InsertResponse(x) => x.enum_values(_include_deprecated), Self::UpdateInput(x) => x.enum_values(_include_deprecated), Self::UpdateResponse(x) => x.enum_values(_include_deprecated), @@ -816,6 +823,7 @@ impl ___Type for __Type { Self::Node(x) => x.input_fields(), Self::NodeInterface(x) => x.input_fields(), Self::InsertInput(x) => x.input_fields(), + Self::InsertOnConflictInput(x) => x.input_fields(), Self::InsertResponse(x) => x.input_fields(), Self::UpdateInput(x) => x.input_fields(), Self::UpdateResponse(x) => x.input_fields(), @@ -962,6 +970,12 @@ pub struct InsertResponseType { pub schema: Arc<__Schema>, } +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct InsertOnConflictType { + pub table: Arc
, + pub schema: Arc<__Schema>, +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct UpdateResponseType { pub table: Arc
, @@ -1445,14 +1459,16 @@ impl ___Type for MutationType { sql_type: None, }, __InputValue { - name_: "update".to_string(), + name_: "onConflict".to_string(), type_: __Type::NonNull(NonNullType { type_: Box::new(__Type::List(ListType { type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::InsertInput(InsertInputType { - table: Arc::clone(table), - schema: Arc::clone(&self.schema), - })), + type_: Box::new(__Type::InsertOnConflictInput( + InsertOnConflictType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), + }, + )), })), })), }), @@ -1693,6 +1709,8 @@ impl ___Type for EnumType { EnumSource::TableColumns(table) => table .columns .iter() + // TODO: is this the right thing to filter on? + .filter(|x| x.permissions.is_selectable) .map(|col| __EnumValue { name: self.schema.graphql_column_field_name(col), description: None, @@ -3132,6 +3150,80 @@ impl ___Type for InsertInputType { } } +impl ___Type for InsertOnConflictType { + fn kind(&self) -> __TypeKind { + __TypeKind::INPUT_OBJECT + } + + fn name(&self) -> Option { + Some(format!( + "{}OnConflictInput", + self.schema.graphql_table_base_type_name(&self.table) + )) + } + + fn fields(&self, _include_deprecated: bool) -> Option> { + None + } + + fn input_fields(&self) -> Option> { + Some(vec![ + __InputValue { + // TODO: Create a custom type for available constraints + name_: "constraint".to_string(), + // If triggers are involved, we can't detect if a field is non-null. Default + // all fields to non-null and let postgres errors handle it. + type_: __Type::NonNull(NonNullType { + type_: Box::new(__Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::Enum(EnumType { + enum_: EnumSource::TableColumns(Arc::clone(&self.table)), + schema: Arc::clone(&self.schema), + })), + })), + })), + }), + description: Some( + "A unique constraint that may conflict with the inserted records".to_string(), + ), + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "updateFields".to_string(), + // If triggers are involved, we can't detect if a field is non-null. Default + // all fields to non-null and let postgres errors handle it. + type_: __Type::NonNull(NonNullType { + type_: Box::new(__Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::Enum(EnumType { + enum_: EnumSource::TableColumns(Arc::clone(&self.table)), + schema: Arc::clone(&self.schema), + })), + })), + })), + }), + description: Some("Fields to be updated if conflict occurs".to_string()), + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "filter".to_string(), + type_: __Type::FilterEntity(FilterEntityType { + table: Arc::clone(&self.table), + schema: self.schema.clone(), + }), + description: Some( + "Filters to apply to the results set when querying from the collection" + .to_string(), + ), + default_value: None, + sql_type: None, + }, + ]) + } +} + impl ___Type for InsertResponseType { fn kind(&self) -> __TypeKind { __TypeKind::OBJECT @@ -4197,6 +4289,10 @@ impl __Schema { enum_: EnumSource::TableColumns(Arc::clone(table)), schema: Arc::clone(&schema_rc), })); + types_.push(__Type::InsertOnConflictType(InsertOnConflictType { + table: Arc::clone(table), + schema: Arc::clone(&schema_rc), + })); } if self.graphql_table_update_types_are_valid(table) { From 758ca22fc23ae159eadaaf9b497fa5fcc7f48802 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 9 May 2024 14:38:07 -0500 Subject: [PATCH 03/10] load index names for onConflict clause --- sql/load_sql_context.sql | 5 ++++- src/graphql.rs | 19 +++++-------------- src/sql_types.rs | 2 ++ src/transpile.rs | 1 - 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql index 15899200..140df83a 100644 --- a/sql/load_sql_context.sql +++ b/sql/load_sql_context.sql @@ -281,11 +281,14 @@ select array[]::text[] ), 'is_unique', pi.indisunique and pi.indpred is null, - 'is_primary_key', pi.indisprimary + 'is_primary_key', pi.indisprimary, + 'name', pc_ix.relname ) ) from pg_catalog.pg_index pi + join pg_catalog.pg_class pc_ix + on pi.indexrelid = pc_ix.oid where pi.indrelid = pc.oid ), diff --git a/src/graphql.rs b/src/graphql.rs index 9ca5f784..c4ef286a 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -786,7 +786,7 @@ impl ___Type for __Type { Self::Node(x) => x.enum_values(_include_deprecated), Self::NodeInterface(x) => x.enum_values(_include_deprecated), Self::InsertInput(x) => x.enum_values(_include_deprecated), - Self::InsertOnConflictInput(x) => x.enum_values(), + Self::InsertOnConflictInput(x) => x.enum_values(_include_deprecated), Self::InsertResponse(x) => x.enum_values(_include_deprecated), Self::UpdateInput(x) => x.enum_values(_include_deprecated), Self::UpdateResponse(x) => x.enum_values(_include_deprecated), @@ -1460,17 +1460,9 @@ impl ___Type for MutationType { }, __InputValue { name_: "onConflict".to_string(), - type_: __Type::NonNull(NonNullType { - type_: Box::new(__Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::InsertOnConflictInput( - InsertOnConflictType { - table: Arc::clone(table), - schema: Arc::clone(&self.schema), - }, - )), - })), - })), + type_: __Type::InsertOnConflictInput(InsertOnConflictType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), }), description: None, default_value: None, @@ -3444,7 +3436,6 @@ impl ___Type for FuncCallResponseType { } use std::str::FromStr; -use std::string::ToString; #[derive(Clone, Copy, Debug)] pub enum FilterOp { @@ -4289,7 +4280,7 @@ impl __Schema { enum_: EnumSource::TableColumns(Arc::clone(table)), schema: Arc::clone(&schema_rc), })); - types_.push(__Type::InsertOnConflictType(InsertOnConflictType { + types_.push(__Type::InsertOnConflictInput(InsertOnConflictType { table: Arc::clone(table), schema: Arc::clone(&schema_rc), })); diff --git a/src/sql_types.rs b/src/sql_types.rs index 1645feec..059d52c8 100644 --- a/src/sql_types.rs +++ b/src/sql_types.rs @@ -417,6 +417,7 @@ pub struct Index { pub column_names: Vec, pub is_unique: bool, pub is_primary_key: bool, + pub name: String, } #[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash)] @@ -544,6 +545,7 @@ impl Table { column_names: column_names.clone(), is_unique: true, is_primary_key: true, + name: "dummy".to_string(), }) } } else { diff --git a/src/transpile.rs b/src/transpile.rs index 3df92baf..fc14749a 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -2,7 +2,6 @@ use crate::builder::*; use crate::graphql::*; use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Function, Table, TypeDetails}; use itertools::Itertools; -use pgrx::pg_sys::PgBuiltInOids; use pgrx::prelude::*; use pgrx::spi::SpiClient; use pgrx::{direct_function_call, JsonB}; From b2797e479520246ba2ea0decd8adb2d9e59d08c8 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Fri, 10 May 2024 14:41:08 -0500 Subject: [PATCH 04/10] onConflict intospection working --- src/graphql.rs | 111 +++++++++++++++++++++++++++------------------ src/parser_util.rs | 1 + src/sql_types.rs | 31 ++++++++++++- 3 files changed, 98 insertions(+), 45 deletions(-) diff --git a/src/graphql.rs b/src/graphql.rs index c4ef286a..82cddcc9 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1110,6 +1110,7 @@ pub enum EnumSource { Enum(Arc), FilterIs, TableColumns(Arc
), + OnConflictTarget(Arc
), } #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -1435,40 +1436,43 @@ impl ___Type for MutationType { let table_base_type_name = self.schema.graphql_table_base_type_name(table); if self.schema.graphql_table_insert_types_are_valid(table) { + let mut args = vec![__InputValue { + name_: "objects".to_string(), + type_: __Type::NonNull(NonNullType { + type_: Box::new(__Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::InsertInput(InsertInputType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), + })), + })), + })), + }), + description: None, + default_value: None, + sql_type: None, + }]; + + if table.has_upsert_support() { + args.push(__InputValue { + name_: "onConflict".to_string(), + type_: __Type::InsertOnConflictInput(InsertOnConflictType { + table: Arc::clone(table), + schema: Arc::clone(&self.schema), + }), + description: None, + default_value: None, + sql_type: None, + }); + } + f.push(__Field { name_: format!("insertInto{}Collection", table_base_type_name), type_: __Type::InsertResponse(InsertResponseType { table: Arc::clone(table), schema: Arc::clone(&self.schema), }), - args: vec![ - __InputValue { - name_: "objects".to_string(), - type_: __Type::NonNull(NonNullType { - type_: Box::new(__Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::InsertInput(InsertInputType { - table: Arc::clone(table), - schema: Arc::clone(&self.schema), - })), - })), - })), - }), - description: None, - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "onConflict".to_string(), - type_: __Type::InsertOnConflictInput(InsertOnConflictType { - table: Arc::clone(table), - schema: Arc::clone(&self.schema), - }), - description: None, - default_value: None, - sql_type: None, - }, - ], + args, description: Some(format!( "Adds one or more `{}` records to the collection", table_base_type_name @@ -1660,6 +1664,10 @@ impl ___Type for EnumType { "{}Field", self.schema.graphql_table_base_type_name(&table) )), + EnumSource::OnConflictTarget(table) => Some(format!( + "{}OnConflictConstraint", + self.schema.graphql_table_base_type_name(&table) + )), } } @@ -1709,6 +1717,18 @@ impl ___Type for EnumType { deprecation_reason: None, }) .collect(), + EnumSource::OnConflictTarget(table) => { + table + .on_conflict_indexes() + .iter() + .map(|ix| __EnumValue { + // TODO, apply name restrictions + name: ix.name.clone(), + description: None, + deprecation_reason: None, + }) + .collect() + } }) } } @@ -3166,13 +3186,9 @@ impl ___Type for InsertOnConflictType { // If triggers are involved, we can't detect if a field is non-null. Default // all fields to non-null and let postgres errors handle it. type_: __Type::NonNull(NonNullType { - type_: Box::new(__Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::Enum(EnumType { - enum_: EnumSource::TableColumns(Arc::clone(&self.table)), - schema: Arc::clone(&self.schema), - })), - })), + type_: Box::new(__Type::Enum(EnumType { + enum_: EnumSource::OnConflictTarget(Arc::clone(&self.table)), + schema: Arc::clone(&self.schema), })), }), description: Some( @@ -4275,15 +4291,22 @@ impl __Schema { table: Arc::clone(table), schema: Arc::clone(&schema_rc), })); - // Used by on conflict - types_.push(__Type::Enum(EnumType { - enum_: EnumSource::TableColumns(Arc::clone(table)), - schema: Arc::clone(&schema_rc), - })); - types_.push(__Type::InsertOnConflictInput(InsertOnConflictType { - table: Arc::clone(table), - schema: Arc::clone(&schema_rc), - })); + + // Used exclusively by onConflict + if table.has_upsert_support() { + types_.push(__Type::InsertOnConflictInput(InsertOnConflictType { + table: Arc::clone(table), + schema: Arc::clone(&schema_rc), + })); + types_.push(__Type::Enum(EnumType { + enum_: EnumSource::TableColumns(Arc::clone(table)), + schema: Arc::clone(&schema_rc), + })); + types_.push(__Type::Enum(EnumType { + enum_: EnumSource::OnConflictTarget(Arc::clone(table)), + schema: Arc::clone(&schema_rc), + })); + } } if self.graphql_table_update_types_are_valid(table) { diff --git a/src/parser_util.rs b/src/parser_util.rs index 49362ed5..7ec6fbea 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -414,6 +414,7 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result value.clone(), // TODO(or): Do I need to check directives here? EnumSource::TableColumns(_e) => value.clone(), + EnumSource::OnConflictTarget(_e) => value.clone(), } } None => return Err(format!("Invalid input for {} type", enum_name)), diff --git a/src/sql_types.rs b/src/sql_types.rs index 059d52c8..71ec7943 100644 --- a/src/sql_types.rs +++ b/src/sql_types.rs @@ -545,7 +545,7 @@ impl Table { column_names: column_names.clone(), is_unique: true, is_primary_key: true, - name: "dummy".to_string(), + name: "NOT REQUIRED".to_string(), }) } } else { @@ -553,6 +553,31 @@ impl Table { } } + pub fn on_conflict_indexes(&self) -> Vec<&Index> { + // Indexes that are valid targets for an on conflict clause + // must be unique, real (not comment directives), and must + // not contain serial or generated columns because we don't + // allow those to be set in insert statements + let unique_indexes = self.indexes.iter().filter(|x| x.is_unique); + + let allowed_column_names = self + .columns + .iter() + .filter(|x| x.permissions.is_insertable) + .filter(|x| !x.is_generated) + .filter(|x| !x.is_serial) + .map(|x| &x.name) + .collect::>(); + + unique_indexes + .filter(|uix| { + uix.column_names + .iter() + .all(|col_name| allowed_column_names.contains(col_name)) + }) + .collect::>() + } + pub fn primary_key_columns(&self) -> Vec<&Arc> { self.primary_key() .map(|x| x.column_names) @@ -567,6 +592,10 @@ impl Table { .collect::>>() } + pub fn has_upsert_support(&self) -> bool { + self.on_conflict_indexes().len() > 0 + } + pub fn is_any_column_selectable(&self) -> bool { self.columns.iter().any(|x| x.permissions.is_selectable) } From ddf28260b0b59c5b86b085011b945334ab08f855 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Tue, 13 Aug 2024 12:47:05 -0500 Subject: [PATCH 05/10] on conflict builder --- src/builder.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 00951d87..ffb47208 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -4,12 +4,19 @@ use crate::parser_util::*; use crate::sql_types::*; use graphql_parser::query::*; use serde::Serialize; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; +#[derive(Clone, Debug)] +pub struct OnConflictBuilder { + pub constraint: Index, // Could probably get away with a name ref + pub update_fields: HashSet>, // Could probably get away with a name ref + pub filter: FilterBuilder, +} + #[derive(Clone, Debug)] pub struct InsertBuilder { pub alias: String, @@ -22,6 +29,8 @@ pub struct InsertBuilder { //fields pub selections: Vec, + + pub on_conflict: Option, } #[derive(Clone, Debug)] @@ -176,6 +185,90 @@ where parse_node_id(node_id_base64_encoded_json_string) } +fn read_argument_on_conflict<'a, T>( + field: &__Field, + query_field: &graphql_parser::query::Field<'a, T>, + variables: &serde_json::Value, + variable_definitions: &Vec>, +) -> Result, String> +where + T: Text<'a> + Eq + AsRef, +{ + let validated: gson::Value = read_argument( + "onConflict", + field, + query_field, + variables, + variable_definitions, + )?; + + let insert_type: InsertOnConflictType = match field.get_arg("onConflict") { + None => return Ok(None), + Some(x) => match x.type_().unmodified_type() { + __Type::InsertOnConflictInput(insert_on_conflict) => insert_on_conflict, + _ => return Err("Could not locate Insert Entity type".to_string()), + }, + }; + + let filter: FilterBuilder = + read_argument_filter(field, query_field, variables, variable_definitions)?; + + let on_conflict_builder = match validated { + gson::Value::Absent | gson::Value::Null => None, + gson::Value::Object(contents) => { + let constraint = match contents + .get("constraint") + .expect("OnConflict revalidation error. Expected constraint") + { + gson::Value::String(ix_name) => insert_type + .table + .indexes + .iter() + .find(|ix| &ix.name == ix_name) + .expect("OnConflict revalidation error. constraint: unknown constraint name"), + _ => { + return Err( + "OnConflict revalidation error. Expected constraint as String".to_string(), + ) + } + }; + + let update_fields = match contents + .get("updateFields") + .expect("OnConflict revalidation error. Expected updateFields") + { + gson::Value::Array(col_names) => { + let mut update_columns: HashSet> = HashSet::new(); + for col_name in col_names { + match col_name { + gson::Value::String(c) => { + let col = insert_type.table.columns.iter().find(|column| &column.name == c).expect("OnConflict revalidation error. updateFields: unknown column name"); + update_columns.insert(Arc::clone(col)); + } + _ => return Err("OnConflict revalidation error. Expected updateFields to be column names".to_string()), + } + } + update_columns + } + _ => { + return Err( + "OnConflict revalidation error. Expected updateFields to be an array" + .to_string(), + ) + } + }; + + Some(OnConflictBuilder { + constraint: constraint.clone(), + update_fields, + filter, + }) + } + _ => return Err("Insert re-validation errror".to_string()), + }; + Ok(on_conflict_builder) +} + fn read_argument_objects<'a, T>( field: &__Field, query_field: &graphql_parser::query::Field<'a, T>, @@ -277,11 +370,14 @@ where match &type_ { __Type::InsertResponse(xtype) => { // Raise for disallowed arguments - restrict_allowed_arguments(&["objects"], query_field)?; + restrict_allowed_arguments(&["objects", "onConflict"], query_field)?; let objects: Vec = read_argument_objects(field, query_field, variables, variable_definitions)?; + let on_conflict: Option = + read_argument_on_conflict(field, query_field, variables, variable_definitions)?; + let mut builder_fields: Vec = vec![]; let selection_fields = normalize_selection_set( @@ -324,6 +420,7 @@ where table: Arc::clone(&xtype.table), objects, selections: builder_fields, + on_conflict, }) } _ => Err(format!( From 10ecf36efe5636d8759c538c4e409b2ffe10cbeb Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Tue, 13 Aug 2024 14:42:53 -0500 Subject: [PATCH 06/10] InsertOnConflict to OnConflict --- src/builder.rs | 10 +++++----- src/graphql.rs | 24 ++++++++++++------------ src/parser_util.rs | 11 ++++++----- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index ffb47208..688493c5 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -202,10 +202,10 @@ where variable_definitions, )?; - let insert_type: InsertOnConflictType = match field.get_arg("onConflict") { + let insert_type: OnConflictType = match field.get_arg("onConflict") { None => return Ok(None), Some(x) => match x.type_().unmodified_type() { - __Type::InsertOnConflictInput(insert_on_conflict) => insert_on_conflict, + __Type::OnConflictInput(insert_on_conflict) => insert_on_conflict, _ => return Err("Could not locate Insert Entity type".to_string()), }, }; @@ -372,12 +372,12 @@ where // Raise for disallowed arguments restrict_allowed_arguments(&["objects", "onConflict"], query_field)?; - let objects: Vec = - read_argument_objects(field, query_field, variables, variable_definitions)?; - let on_conflict: Option = read_argument_on_conflict(field, query_field, variables, variable_definitions)?; + let objects: Vec = + read_argument_objects(field, query_field, variables, variable_definitions)?; + let mut builder_fields: Vec = vec![]; let selection_fields = normalize_selection_set( diff --git a/src/graphql.rs b/src/graphql.rs index 82cddcc9..0fd0c36a 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -515,7 +515,7 @@ pub enum __Type { // Mutation Mutation(MutationType), InsertInput(InsertInputType), - InsertOnConflictInput(InsertOnConflictType), + OnConflictInput(OnConflictType), InsertResponse(InsertResponseType), UpdateInput(UpdateInputType), UpdateResponse(UpdateResponseType), @@ -594,7 +594,7 @@ impl ___Type for __Type { Self::Node(x) => x.kind(), Self::NodeInterface(x) => x.kind(), Self::InsertInput(x) => x.kind(), - Self::InsertOnConflictInput(x) => x.kind(), + Self::OnConflictInput(x) => x.kind(), Self::InsertResponse(x) => x.kind(), Self::UpdateInput(x) => x.kind(), Self::UpdateResponse(x) => x.kind(), @@ -630,7 +630,7 @@ impl ___Type for __Type { Self::Node(x) => x.name(), Self::NodeInterface(x) => x.name(), Self::InsertInput(x) => x.name(), - Self::InsertOnConflictInput(x) => x.name(), + Self::OnConflictInput(x) => x.name(), Self::InsertResponse(x) => x.name(), Self::UpdateInput(x) => x.name(), Self::UpdateResponse(x) => x.name(), @@ -666,7 +666,7 @@ impl ___Type for __Type { Self::Node(x) => x.description(), Self::NodeInterface(x) => x.description(), Self::InsertInput(x) => x.description(), - Self::InsertOnConflictInput(x) => x.description(), + Self::OnConflictInput(x) => x.description(), Self::InsertResponse(x) => x.description(), Self::UpdateInput(x) => x.description(), Self::UpdateResponse(x) => x.description(), @@ -703,7 +703,7 @@ impl ___Type for __Type { Self::Node(x) => x.fields(_include_deprecated), Self::NodeInterface(x) => x.fields(_include_deprecated), Self::InsertInput(x) => x.fields(_include_deprecated), - Self::InsertOnConflictInput(x) => x.fields(_include_deprecated), + Self::OnConflictInput(x) => x.fields(_include_deprecated), Self::InsertResponse(x) => x.fields(_include_deprecated), Self::UpdateInput(x) => x.fields(_include_deprecated), Self::UpdateResponse(x) => x.fields(_include_deprecated), @@ -740,7 +740,7 @@ impl ___Type for __Type { Self::Node(x) => x.interfaces(), Self::NodeInterface(x) => x.interfaces(), Self::InsertInput(x) => x.interfaces(), - Self::InsertOnConflictInput(x) => x.interfaces(), + Self::OnConflictInput(x) => x.interfaces(), Self::InsertResponse(x) => x.interfaces(), Self::UpdateInput(x) => x.interfaces(), Self::UpdateResponse(x) => x.interfaces(), @@ -786,7 +786,7 @@ impl ___Type for __Type { Self::Node(x) => x.enum_values(_include_deprecated), Self::NodeInterface(x) => x.enum_values(_include_deprecated), Self::InsertInput(x) => x.enum_values(_include_deprecated), - Self::InsertOnConflictInput(x) => x.enum_values(_include_deprecated), + Self::OnConflictInput(x) => x.enum_values(_include_deprecated), Self::InsertResponse(x) => x.enum_values(_include_deprecated), Self::UpdateInput(x) => x.enum_values(_include_deprecated), Self::UpdateResponse(x) => x.enum_values(_include_deprecated), @@ -823,7 +823,7 @@ impl ___Type for __Type { Self::Node(x) => x.input_fields(), Self::NodeInterface(x) => x.input_fields(), Self::InsertInput(x) => x.input_fields(), - Self::InsertOnConflictInput(x) => x.input_fields(), + Self::OnConflictInput(x) => x.input_fields(), Self::InsertResponse(x) => x.input_fields(), Self::UpdateInput(x) => x.input_fields(), Self::UpdateResponse(x) => x.input_fields(), @@ -971,7 +971,7 @@ pub struct InsertResponseType { } #[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct InsertOnConflictType { +pub struct OnConflictType { pub table: Arc
, pub schema: Arc<__Schema>, } @@ -1456,7 +1456,7 @@ impl ___Type for MutationType { if table.has_upsert_support() { args.push(__InputValue { name_: "onConflict".to_string(), - type_: __Type::InsertOnConflictInput(InsertOnConflictType { + type_: __Type::OnConflictInput(OnConflictType { table: Arc::clone(table), schema: Arc::clone(&self.schema), }), @@ -3162,7 +3162,7 @@ impl ___Type for InsertInputType { } } -impl ___Type for InsertOnConflictType { +impl ___Type for OnConflictType { fn kind(&self) -> __TypeKind { __TypeKind::INPUT_OBJECT } @@ -4294,7 +4294,7 @@ impl __Schema { // Used exclusively by onConflict if table.has_upsert_support() { - types_.push(__Type::InsertOnConflictInput(InsertOnConflictType { + types_.push(__Type::OnConflictInput(OnConflictType { table: Arc::clone(table), schema: Arc::clone(&schema_rc), })); diff --git a/src/parser_util.rs b/src/parser_util.rs index 7ec6fbea..f69bed96 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -472,11 +472,12 @@ pub fn validate_arg_from_type(type_: &__Type, value: &gson::Value) -> Result out_elem, } } - __Type::InsertInput(_) => validate_arg_from_input_object(type_, value)?, - __Type::UpdateInput(_) => validate_arg_from_input_object(type_, value)?, - __Type::OrderByEntity(_) => validate_arg_from_input_object(type_, value)?, - __Type::FilterType(_) => validate_arg_from_input_object(type_, value)?, - __Type::FilterEntity(_) => validate_arg_from_input_object(type_, value)?, + __Type::InsertInput(_) + | __Type::UpdateInput(_) + | __Type::OrderByEntity(_) + | __Type::FilterType(_) + | __Type::FilterEntity(_) + | __Type::OnConflictInput(_) => validate_arg_from_input_object(type_, value)?, _ => { return Err(format!( "Invalid Type used as input argument {}", From fe15ade0810870cbf1b07e67a4f700f470b0fbfa Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Tue, 13 Aug 2024 14:46:01 -0500 Subject: [PATCH 07/10] drop unused aliases --- src/builder.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 688493c5..149aa4ad 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -19,8 +19,6 @@ pub struct OnConflictBuilder { #[derive(Clone, Debug)] pub struct InsertBuilder { - pub alias: String, - // args pub objects: Vec, @@ -365,7 +363,6 @@ where .name() .ok_or("Encountered type without name in connection builder")?; let field_map = field_map(&type_); - let alias = alias_or_name(query_field); match &type_ { __Type::InsertResponse(xtype) => { @@ -416,7 +413,6 @@ where } } Ok(InsertBuilder { - alias, table: Arc::clone(&xtype.table), objects, selections: builder_fields, @@ -432,8 +428,6 @@ where #[derive(Clone, Debug)] pub struct UpdateBuilder { - pub alias: String, - // args pub filter: FilterBuilder, pub set: SetBuilder, @@ -535,7 +529,6 @@ where .name() .ok_or("Encountered type without name in update builder")?; let field_map = field_map(&type_); - let alias = alias_or_name(query_field); match &type_ { __Type::UpdateResponse(xtype) => { @@ -587,7 +580,6 @@ where } } Ok(UpdateBuilder { - alias, filter, set, at_most, @@ -604,8 +596,6 @@ where #[derive(Clone, Debug)] pub struct DeleteBuilder { - pub alias: String, - // args pub filter: FilterBuilder, pub at_most: i64, @@ -641,7 +631,6 @@ where .name() .ok_or("Encountered type without name in delete builder")?; let field_map = field_map(&type_); - let alias = alias_or_name(query_field); match &type_ { __Type::DeleteResponse(xtype) => { @@ -691,7 +680,6 @@ where } } Ok(DeleteBuilder { - alias, filter, at_most, table: Arc::clone(&xtype.table), From 8da22dc8b1849ab9b2947c39a5f96d321281ce99 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 26 Aug 2024 11:49:00 -0500 Subject: [PATCH 08/10] on conflict functional --- src/builder.rs | 42 ++++++++++++++++++++++++++++++++++++------ src/graphql.rs | 1 - src/parser_util.rs | 5 ++++- src/transpile.rs | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 149aa4ad..6e07f7af 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -200,7 +200,7 @@ where variable_definitions, )?; - let insert_type: OnConflictType = match field.get_arg("onConflict") { + let conflict_type: OnConflictType = match field.get_arg("onConflict") { None => return Ok(None), Some(x) => match x.type_().unmodified_type() { __Type::OnConflictInput(insert_on_conflict) => insert_on_conflict, @@ -208,9 +208,6 @@ where }, }; - let filter: FilterBuilder = - read_argument_filter(field, query_field, variables, variable_definitions)?; - let on_conflict_builder = match validated { gson::Value::Absent | gson::Value::Null => None, gson::Value::Object(contents) => { @@ -218,7 +215,7 @@ where .get("constraint") .expect("OnConflict revalidation error. Expected constraint") { - gson::Value::String(ix_name) => insert_type + gson::Value::String(ix_name) => conflict_type .table .indexes .iter() @@ -231,6 +228,36 @@ where } }; + // TODO: Filter reading logic is partially duplicated from read_argument_filter + // ideally this should be refactored + let filter_gson = contents + .get("filter") + .expect("onConflict revalidation error"); + + let filter = match filter_gson { + gson::Value::Null | gson::Value::Absent => FilterBuilder { elems: vec![] }, + gson::Value::Object(_) => { + let filter_type = conflict_type + .input_fields() + .expect("Failed to unwrap input fields on OnConflict type") + .iter() + .find(|in_f| in_f.name() == "filter") + .expect("Failed to get filter input_field on onConflict type") + .type_() + .unmodified_type(); + + if !matches!(filter_type, __Type::FilterEntity(_)) { + return Err("Could not locate Filter Entity type".to_string()); + } + let filter_field_map = input_field_map(&filter_type); + let filter_elems = create_filters(&filter_gson, &filter_field_map)?; + FilterBuilder { + elems: filter_elems, + } + } + _ => return Err("OnConflict revalidation error. invalid filter object".to_string()), + }; + let update_fields = match contents .get("updateFields") .expect("OnConflict revalidation error. Expected updateFields") @@ -240,7 +267,7 @@ where for col_name in col_names { match col_name { gson::Value::String(c) => { - let col = insert_type.table.columns.iter().find(|column| &column.name == c).expect("OnConflict revalidation error. updateFields: unknown column name"); + let col = conflict_type.table.columns.iter().find(|column| &column.name == c).expect("OnConflict revalidation error. updateFields: unknown column name"); update_columns.insert(Arc::clone(col)); } _ => return Err("OnConflict revalidation error. Expected updateFields to be column names".to_string()), @@ -1145,11 +1172,14 @@ where variable_definitions, )?; + //return Err(format!("Err {:?}", validated)); + let filter_type = field .get_arg("filter") .expect("failed to get filter argument") .type_() .unmodified_type(); + if !matches!(filter_type, __Type::FilterEntity(_)) { return Err("Could not locate Filter Entity type".to_string()); } diff --git a/src/graphql.rs b/src/graphql.rs index 0fd0c36a..5422f4bb 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -3181,7 +3181,6 @@ impl ___Type for OnConflictType { fn input_fields(&self) -> Option> { Some(vec![ __InputValue { - // TODO: Create a custom type for available constraints name_: "constraint".to_string(), // If triggers are involved, we can't detect if a field is non-null. Default // all fields to non-null and let postgres errors handle it. diff --git a/src/parser_util.rs b/src/parser_util.rs index f69bed96..2fac7fe7 100644 --- a/src/parser_util.rs +++ b/src/parser_util.rs @@ -529,7 +529,10 @@ pub fn validate_arg_from_input_object( match input_obj.get(&obj_field_key) { None => { - validate_arg_from_type(&obj_field_type, &GsonValue::Null)?; + // If there was no provided key, use "Absent" so all arguments + // always exist in the validated input datat + validate_arg_from_type(&obj_field_type, &GsonValue::Absent)?; + out_map.insert(obj_field_key, GsonValue::Absent); } Some(x) => { let out_val = validate_arg_from_type(&obj_field_type, x)?; diff --git a/src/transpile.rs b/src/transpile.rs index fc14749a..3694f377 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -309,11 +309,45 @@ impl MutationEntrypoint<'_> for InsertBuilder { let values_clause = values_rows_clause.join(", "); + let insert_quoted_block_name = rand_block_name(); + let on_conflict_clause = match &self.on_conflict { + Some(on_conflict) => { + let constraint_name = &on_conflict.constraint.name; + let do_update_set_clause = on_conflict + .update_fields + .iter() + .map(|col| { + format!( + "{} = excluded.{}", + quote_ident(&col.name), + quote_ident(&col.name), + ) + }) + .join(", "); + + let conflict_where_clause = on_conflict.filter.to_where_clause( + &insert_quoted_block_name, + &self.table, + param_context, + )?; + + format!( + " + on conflict on constraint {constraint_name} + do update set {do_update_set_clause} + where {conflict_where_clause} + ", + ) + } + None => "".to_string(), + }; + Ok(format!( " with affected as ( - insert into {quoted_schema}.{quoted_table}({referenced_columns_clause}) + insert into {quoted_schema}.{quoted_table} as {insert_quoted_block_name} ({referenced_columns_clause}) values {values_clause} + {on_conflict_clause} returning {selectable_columns_clause} ) select From ffcfe279b071bc93becebe3d621133267a1d1aa3 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 26 Aug 2024 14:01:00 -0500 Subject: [PATCH 09/10] on conflict tests --- test/expected/mutation_insert_on_conflict.out | 216 ++++++++++++++++++ test/sql/mutation_insert_on_conflict.sql | 138 +++++++++++ 2 files changed, 354 insertions(+) create mode 100644 test/expected/mutation_insert_on_conflict.out create mode 100644 test/sql/mutation_insert_on_conflict.sql diff --git a/test/expected/mutation_insert_on_conflict.out b/test/expected/mutation_insert_on_conflict.out new file mode 100644 index 00000000..c90e8298 --- /dev/null +++ b/test/expected/mutation_insert_on_conflict.out @@ -0,0 +1,216 @@ +begin; + create table account( + id int primary key, + email varchar(255) not null, + priority int, + status text default 'active' + ); + /* + Literals + */ + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "foo@barsley.com", priority: 1 }, + { id: 2, email: "bar@foosworth.com" } + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, priority, status], + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "insertIntoAccountCollection": { + + "records": [ + + { + + "id": 1, + + "email": "foo@barsley.com", + + "priority": 1 + + }, + + { + + "id": 2, + + "email": "bar@foosworth.com",+ + "priority": null + + } + + ], + + "affectedCount": 2 + + } + + } + + } +(1 row) + + -- Email should update. Priority should not + -- 1 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "new@email.com", priority: 2 }, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + ) { + affectedCount + records { + id + email + } + } + } + $$)); + jsonb_pretty +---------------------------------------------- + { + + "data": { + + "insertIntoAccountCollection": { + + "records": [ + + { + + "id": 1, + + "email": "new@email.com"+ + } + + ], + + "affectedCount": 1 + + } + + } + + } +(1 row) + + -- Email and priority should update + -- 2 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "new@email.com", priority: 2 }, + { id: 2, email: "new@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + jsonb_pretty +----------------------------------------------- + { + + "data": { + + "insertIntoAccountCollection": { + + "records": [ + + { + + "id": 1, + + "email": "new@email.com",+ + "priority": 1 + + }, + + { + + "id": 2, + + "email": "new@email.com",+ + "priority": null + + } + + ], + + "affectedCount": 2 + + } + + } + + } +(1 row) + + -- Filter prevents second row update + -- 1 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "third@email.com"}, + { id: 2, email: "new@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + filter: { + id: {id: $ifilt} + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + jsonb_pretty +----------------------------------------------------------------- + { + + "data": null, + + "errors": [ + + { + + "message": "Input contains extra keys [\"filter\"]"+ + } + + ] + + } +(1 row) + + -- Variable Filter + -- Only row id=2 updated due to where clause + select jsonb_pretty(graphql.resolve($$ + mutation AccountsFiltered($ifilt: IntFilter!) + insertIntoAccountCollection( + objects: [ + { id: 1, email: "fourth@email.com"}, + { id: 2, email: "fourth@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + filter: { + id: {id: $ifilt} + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$, + variables:= '{"ifilt": {"eq": 2}}' + )); + jsonb_pretty +-------------------------------------------------------------------------------------------------------------------------------- + { + + "errors": [ + + { + + "message": "query parse error: Parse error at 3:7\nUnexpected `insertIntoAccountCollection[Name]`\nExpected `{`\n"+ + } + + ] + + } +(1 row) + +rollback; diff --git a/test/sql/mutation_insert_on_conflict.sql b/test/sql/mutation_insert_on_conflict.sql new file mode 100644 index 00000000..7e1533c1 --- /dev/null +++ b/test/sql/mutation_insert_on_conflict.sql @@ -0,0 +1,138 @@ +begin; + + create table account( + id int primary key, + email varchar(255) not null, + priority int, + status text default 'active' + ); + + /* + Literals + */ + + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "foo@barsley.com", priority: 1 }, + { id: 2, email: "bar@foosworth.com" } + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, priority, status], + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + + -- Email should update. Priority should not + -- 1 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "new@email.com", priority: 2 }, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + ) { + affectedCount + records { + id + email + } + } + } + $$)); + + -- Email and priority should update + -- 2 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "new@email.com", priority: 2 }, + { id: 2, email: "new@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + + -- Filter prevents second row update + -- 1 row affected + select jsonb_pretty(graphql.resolve($$ + mutation { + insertIntoAccountCollection( + objects: [ + { id: 1, email: "third@email.com"}, + { id: 2, email: "new@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + filter: { + id: {id: $ifilt} + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$)); + + -- Variable Filter + -- Only row id=2 updated due to where clause + select jsonb_pretty(graphql.resolve($$ + mutation AccountsFiltered($ifilt: IntFilter!) + insertIntoAccountCollection( + objects: [ + { id: 1, email: "fourth@email.com"}, + { id: 2, email: "fourth@email.com"}, + ] + onConflict: { + constraint: account_pkey, + updateFields: [email, status], + } + filter: { + id: {id: $ifilt} + } + ) { + affectedCount + records { + id + email + priority + } + } + } + $$, + variables:= '{"ifilt": {"eq": 2}}' + )); + +rollback; From 9bf38e3cc0420b86efe4b52b8ee46b9f061cd1ff Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 26 Aug 2024 15:42:28 -0500 Subject: [PATCH 10/10] align test output --- src/builder.rs | 35 +++-- src/transpile.rs | 4 +- test/expected/inflection_types.out | 36 +++-- test/expected/mutation_insert_on_conflict.out | 40 +++-- test/expected/resolve___schema.out | 12 ++ test/expected/resolve_graphiql_schema.out | 141 ++++++++++++++++++ test/sql/mutation_insert_on_conflict.sql | 16 +- 7 files changed, 236 insertions(+), 48 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 6e07f7af..7e764051 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -192,14 +192,6 @@ fn read_argument_on_conflict<'a, T>( where T: Text<'a> + Eq + AsRef, { - let validated: gson::Value = read_argument( - "onConflict", - field, - query_field, - variables, - variable_definitions, - )?; - let conflict_type: OnConflictType = match field.get_arg("onConflict") { None => return Ok(None), Some(x) => match x.type_().unmodified_type() { @@ -208,6 +200,14 @@ where }, }; + let validated: gson::Value = read_argument( + "onConflict", + field, + query_field, + variables, + variable_definitions, + )?; + let on_conflict_builder = match validated { gson::Value::Absent | gson::Value::Null => None, gson::Value::Object(contents) => { @@ -394,10 +394,23 @@ where match &type_ { __Type::InsertResponse(xtype) => { // Raise for disallowed arguments - restrict_allowed_arguments(&["objects", "onConflict"], query_field)?; + let allowed_args = field + .args + .iter() + .map(|iv| iv.name()) + .collect::>(); + + match allowed_args.contains("onConflict") { + true => restrict_allowed_arguments(&["objects", "onConflict"], query_field)?, + false => restrict_allowed_arguments(&["objects"], query_field)?, + } - let on_conflict: Option = - read_argument_on_conflict(field, query_field, variables, variable_definitions)?; + let on_conflict: Option = match allowed_args.contains("onConflict") { + true => { + read_argument_on_conflict(field, query_field, variables, variable_definitions)? + } + false => None, + }; let objects: Vec = read_argument_objects(field, query_field, variables, variable_definitions)?; diff --git a/src/transpile.rs b/src/transpile.rs index 3694f377..d0db1f97 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -312,7 +312,7 @@ impl MutationEntrypoint<'_> for InsertBuilder { let insert_quoted_block_name = rand_block_name(); let on_conflict_clause = match &self.on_conflict { Some(on_conflict) => { - let constraint_name = &on_conflict.constraint.name; + let quoted_constraint_name = quote_ident(&on_conflict.constraint.name); let do_update_set_clause = on_conflict .update_fields .iter() @@ -333,7 +333,7 @@ impl MutationEntrypoint<'_> for InsertBuilder { format!( " - on conflict on constraint {constraint_name} + on conflict on constraint {quoted_constraint_name} do update set {do_update_set_clause} where {conflict_where_clause} ", diff --git a/test/expected/inflection_types.out b/test/expected/inflection_types.out index 33d9012e..602fb9d4 100644 --- a/test/expected/inflection_types.out +++ b/test/expected/inflection_types.out @@ -20,19 +20,22 @@ begin; '$.data.__schema.types[*].name ? (@ starts with "blog")' ) ); - jsonb_pretty ---------------------------- + jsonb_pretty +--------------------------------- "blog_post" "blog_postConnection" "blog_postDeleteResponse" "blog_postEdge" + "blog_postField" "blog_postFilter" "blog_postInsertInput" "blog_postInsertResponse" + "blog_postOnConflictConstraint" + "blog_postOnConflictInput" "blog_postOrderBy" "blog_postUpdateInput" "blog_postUpdateResponse" -(10 rows) +(13 rows) -- Inflection off, Overrides: on comment on table blog_post is e'@graphql({"name": "BlogZZZ"})'; @@ -50,19 +53,22 @@ begin; '$.data.__schema.types[*].name ? (@ starts with "Blog")' ) ); - jsonb_pretty -------------------------- + jsonb_pretty +------------------------------- "BlogZZZ" "BlogZZZConnection" "BlogZZZDeleteResponse" "BlogZZZEdge" + "BlogZZZField" "BlogZZZFilter" "BlogZZZInsertInput" "BlogZZZInsertResponse" + "BlogZZZOnConflictConstraint" + "BlogZZZOnConflictInput" "BlogZZZOrderBy" "BlogZZZUpdateInput" "BlogZZZUpdateResponse" -(10 rows) +(13 rows) rollback to savepoint a; -- Inflection on, Overrides: off @@ -81,19 +87,22 @@ begin; '$.data.__schema.types[*].name ? (@ starts with "Blog")' ) ); - jsonb_pretty --------------------------- + jsonb_pretty +-------------------------------- "BlogPost" "BlogPostConnection" "BlogPostDeleteResponse" "BlogPostEdge" + "BlogPostField" "BlogPostFilter" "BlogPostInsertInput" "BlogPostInsertResponse" + "BlogPostOnConflictConstraint" + "BlogPostOnConflictInput" "BlogPostOrderBy" "BlogPostUpdateInput" "BlogPostUpdateResponse" -(10 rows) +(13 rows) -- Inflection on, Overrides: on comment on table blog_post is e'@graphql({"name": "BlogZZZ"})'; @@ -111,18 +120,21 @@ begin; '$.data.__schema.types[*].name ? (@ starts with "Blog")' ) ); - jsonb_pretty -------------------------- + jsonb_pretty +------------------------------- "BlogZZZ" "BlogZZZConnection" "BlogZZZDeleteResponse" "BlogZZZEdge" + "BlogZZZField" "BlogZZZFilter" "BlogZZZInsertInput" "BlogZZZInsertResponse" + "BlogZZZOnConflictConstraint" + "BlogZZZOnConflictInput" "BlogZZZOrderBy" "BlogZZZUpdateInput" "BlogZZZUpdateResponse" -(10 rows) +(13 rows) rollback; diff --git a/test/expected/mutation_insert_on_conflict.out b/test/expected/mutation_insert_on_conflict.out index c90e8298..ececf656 100644 --- a/test/expected/mutation_insert_on_conflict.out +++ b/test/expected/mutation_insert_on_conflict.out @@ -148,9 +148,9 @@ begin; onConflict: { constraint: account_pkey, updateFields: [email, status], - } - filter: { - id: {id: $ifilt} + filter: { + id: $ifilt + } } ) { affectedCount @@ -161,16 +161,24 @@ begin; } } } - $$)); - jsonb_pretty ------------------------------------------------------------------ - { + - "data": null, + - "errors": [ + - { + - "message": "Input contains extra keys [\"filter\"]"+ - } + - ] + + $$, + variables:= '{"ifilt": {"eq": 2}}' + )); + jsonb_pretty +----------------------------------------------- + { + + "data": { + + "insertIntoAccountCollection": { + + "records": [ + + { + + "id": 2, + + "email": "new@email.com",+ + "priority": null + + } + + ], + + "affectedCount": 1 + + } + + } + } (1 row) @@ -186,9 +194,9 @@ begin; onConflict: { constraint: account_pkey, updateFields: [email, status], - } - filter: { - id: {id: $ifilt} + filter: { + id: $ifilt + } } ) { affectedCount diff --git a/test/expected/resolve___schema.out b/test/expected/resolve___schema.out index 5a8e4a19..d1de5f7e 100644 --- a/test/expected/resolve___schema.out +++ b/test/expected/resolve___schema.out @@ -165,6 +165,10 @@ begin; "kind": "OBJECT", + "name": "BlogPostEdge" + }, + + { + + "kind": "ENUM", + + "name": "BlogPostField" + + }, + { + "kind": "INPUT_OBJECT", + "name": "BlogPostFilter" + @@ -177,6 +181,14 @@ begin; "kind": "OBJECT", + "name": "BlogPostInsertResponse" + }, + + { + + "kind": "ENUM", + + "name": "BlogPostOnConflictConstraint" + + }, + + { + + "kind": "INPUT_OBJECT", + + "name": "BlogPostOnConflictInput" + + }, + { + "kind": "INPUT_OBJECT", + "name": "BlogPostOrderBy" + diff --git a/test/expected/resolve_graphiql_schema.out b/test/expected/resolve_graphiql_schema.out index 454f9465..ae4b500d 100644 --- a/test/expected/resolve_graphiql_schema.out +++ b/test/expected/resolve_graphiql_schema.out @@ -2463,6 +2463,60 @@ begin; "inputFields": null, + "possibleTypes": null + }, + + { + + "kind": "ENUM", + + "name": "BlogPostField", + + "fields": null, + + "enumValues": [ + + { + + "name": "id", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "blogId", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "title", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "body", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "status", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "createdAt", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + }, + + { + + "name": "updatedAt", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + } + + ], + + "interfaces": [ + + ], + + "description": null, + + "inputFields": null, + + "possibleTypes": null + + }, + { + "kind": "INPUT_OBJECT", + "name": "BlogPostFilter", + @@ -2740,6 +2794,83 @@ begin; "inputFields": null, + "possibleTypes": null + }, + + { + + "kind": "ENUM", + + "name": "BlogPostOnConflictConstraint", + + "fields": null, + + "enumValues": [ + + { + + "name": "blog_post_pkey", + + "description": null, + + "isDeprecated": false, + + "deprecationReason": null + + } + + ], + + "interfaces": [ + + ], + + "description": null, + + "inputFields": null, + + "possibleTypes": null + + }, + + { + + "kind": "INPUT_OBJECT", + + "name": "BlogPostOnConflictInput", + + "fields": null, + + "enumValues": [ + + ], + + "interfaces": [ + + ], + + "description": null, + + "inputFields": [ + + { + + "name": "constraint", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "ENUM", + + "name": "BlogPostOnConflictConstraint", + + "ofType": null + + } + + }, + + "description": "A unique constraint that may conflict with the inserted records", + + "defaultValue": null + + }, + + { + + "name": "updateFields", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "ENUM", + + "name": "BlogPostField", + + "ofType": null + + } + + } + + } + + }, + + "description": "Fields to be updated if conflict occurs", + + "defaultValue": null + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BlogPostFilter", + + "ofType": null + + }, + + "description": "Filters to apply to the results set when querying from the collection", + + "defaultValue": null + + } + + ], + + "possibleTypes": null + + }, + { + "kind": "INPUT_OBJECT", + "name": "BlogPostOrderBy", + @@ -4494,6 +4625,16 @@ begin; }, + "description": null, + "defaultValue": null + + }, + + { + + "name": "onConflict", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BlogPostOnConflictInput", + + "ofType": null + + }, + + "description": null, + + "defaultValue": null + } + ], + "name": "insertIntoBlogPostCollection", + diff --git a/test/sql/mutation_insert_on_conflict.sql b/test/sql/mutation_insert_on_conflict.sql index 7e1533c1..70fcebca 100644 --- a/test/sql/mutation_insert_on_conflict.sql +++ b/test/sql/mutation_insert_on_conflict.sql @@ -91,9 +91,9 @@ begin; onConflict: { constraint: account_pkey, updateFields: [email, status], - } - filter: { - id: {id: $ifilt} + filter: { + id: $ifilt + } } ) { affectedCount @@ -104,7 +104,9 @@ begin; } } } - $$)); + $$, + variables:= '{"ifilt": {"eq": 2}}' + )); -- Variable Filter -- Only row id=2 updated due to where clause @@ -118,9 +120,9 @@ begin; onConflict: { constraint: account_pkey, updateFields: [email, status], - } - filter: { - id: {id: $ifilt} + filter: { + id: $ifilt + } } ) { affectedCount