From 8489d41c53b6cfd622def738cba41aa84ee122fd Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Wed, 22 Mar 2023 12:25:18 -0500 Subject: [PATCH 01/12] recursive sql_to_graphql type resolution --- docs/computed_fields.md | 1 - sql/load_sql_context.sql | 11 +- src/graphql.rs | 231 ++++++++++++++------------------------- src/sql_types.rs | 184 ++++++++++++++++++++++++++++++- 4 files changed, 274 insertions(+), 153 deletions(-) diff --git a/docs/computed_fields.md b/docs/computed_fields.md index f3ccbae7..b88d167e 100644 --- a/docs/computed_fields.md +++ b/docs/computed_fields.md @@ -16,7 +16,6 @@ For example: For arbitrary computations that do not meet the requirements for [generated columns](https://www.postgresql.org/docs/14/ddl-generated-columns.html), a table's reflected GraphQL type can be extended by creating a function that: - accepts a single parameter of the table's tuple type -- has a name starting with an underscore ```sql --8<-- "test/expected/extend_type_with_function.out" diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql index 02e2861e..03ee0544 100644 --- a/sql/load_sql_context.sql +++ b/sql/load_sql_context.sql @@ -73,15 +73,19 @@ select jsonb_build_object( 'oid', pt.oid::int, 'schema_oid', pt.typnamespace::int, + -- 'name', pt.typname::regtype::text, 'name', pt.typname, - -- if type is an array, points at the underlying element type 'category', case when pt.typcategory = 'A' then 'Array' when pt.typcategory = 'E' then 'Enum' + when pt.typcategory = 'C' and tabs.oid is not null then 'Table' when pt.typcategory = 'C' then 'Composite' else 'Other' end, + -- if category is 'Array', points at the underlying element type 'array_element_type_oid', nullif(pt.typelem::int, 0), + -- if category is 'Table' points to the table oid + 'table_oid', tabs.oid::int, 'comment', pg_catalog.obj_description(pt.oid, 'pg_type'), 'directives', jsonb_build_object( 'name', graphql.comment_directive(pg_catalog.obj_description(pt.oid, 'pg_type')) ->> 'name' @@ -95,6 +99,8 @@ select pg_type pt join schemas_ spo on pt.typnamespace = spo.oid + left join pg_class tabs + on pt.typrelid = tabs.oid ), jsonb_build_object() ), @@ -244,6 +250,7 @@ select 'type_name', pp.prorettype::regtype::text, 'schema_oid', pronamespace::int, 'schema_name', pronamespace::regnamespace::text, + 'is_set_of', pp.proretset::bool, 'comment', pg_catalog.obj_description(pp.oid, 'pg_proc'), 'directives', ( with directives(directive) as ( @@ -271,8 +278,6 @@ select where pp.pronargs = 1 -- one argument and pp.proargtypes[0] = pc.reltype -- first argument is table type - and pp.proname like '\_%' -- starts with underscore - and not pp.proretset -- disallow set returning functions (for now) ), jsonb_build_array() ), diff --git a/src/graphql.rs b/src/graphql.rs index 79643d49..27c48f4e 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -825,23 +825,6 @@ pub struct __DirectiveLocationType; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct __DirectiveType; -/* - * TODO(or): - * - Revert schema from Arc to Rc - * - Update the __Type enum to be - * __Type(Rc, SomeInnerType> - * for all implementations - * SCRATCH THAT: Arc is needed so __Schema can be cached. - * - * - Update __Type::field() to call into a cached function - * - * - Add a pub fn cache_key(&self) to __Type - * so that it can be reused for all field_maps - * - * fn field_map(type_: __Type). - * since the schema will be availble, at __ - */ - #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct ListType { pub type_: Box<__Type>, @@ -1540,131 +1523,78 @@ impl ___Type for EdgeType { } } -pub fn sql_type_to_graphql_type( - type_oid: u32, - type_name: &str, - max_characters: Option, - schema: &Arc<__Schema>, -) -> __Type { - let mut type_w_list_mod = match type_oid { - 20 => __Type::Scalar(Scalar::BigInt), // bigint - 16 => __Type::Scalar(Scalar::Boolean), // boolean - 1082 => __Type::Scalar(Scalar::Date), // date - 1184 => __Type::Scalar(Scalar::Datetime), // timestamp with time zone - 1114 => __Type::Scalar(Scalar::Datetime), // timestamp without time zone - 701 => __Type::Scalar(Scalar::Float), // double precision - 23 => __Type::Scalar(Scalar::Int), // integer - 21 => __Type::Scalar(Scalar::Int), // smallint - 700 => __Type::Scalar(Scalar::Float), // real - 3802 => __Type::Scalar(Scalar::JSON), // jsonb - 114 => __Type::Scalar(Scalar::JSON), // json - 1083 => __Type::Scalar(Scalar::Time), // time without time zone - 2950 => __Type::Scalar(Scalar::UUID), // uuid - 1700 => __Type::Scalar(Scalar::BigFloat), // numeric - 25 => __Type::Scalar(Scalar::String(None)), // text - // char, bpchar, varchar - 18 | 1042 | 1043 => __Type::Scalar(Scalar::String(max_characters)), - 1009 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::String(None))), - }), // text[] - 1016 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::BigInt)), - }), // bigint[] - 1000 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Boolean)), - }), // boolean[] - 1182 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Date)), - }), // date[] - 1115 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Datetime)), - }), // timestamp without time zone[] - 1185 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Datetime)), - }), // timestamp with time zone[] - 1022 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Float)), - }), // double precision[] - 1021 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Float)), - }), // real[] - 1005 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Int)), - }), // smallint[] - 1007 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Int)), - }), // integer[] - 199 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::JSON)), - }), // json[] - 3807 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::JSON)), - }), // jsonb[] - 1183 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Time)), - }), // time without time zone[] - 2951 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::UUID)), - }), // uuid[] - 1231 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::BigFloat)), - }), // numeric[] - // char[], bpchar[], varchar[] - 1002 | 1014 | 1015 => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::String(max_characters))), - }), // char[] or char(n)[] - _ => match type_name.ends_with("[]") { - true => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Opaque)), - }), - false => __Type::Scalar(Scalar::Opaque), - }, - }; - - if let Some(exact_type) = schema.context.types.get(&type_oid) { - if exact_type.permissions.is_usable { - match exact_type.category { - TypeCategory::Enum => match schema.context.enums.get(&exact_type.oid) { - Some(enum_) => { - type_w_list_mod = __Type::Enum(EnumType { - enum_: EnumSource::Enum(Arc::clone(enum_)), - schema: schema.clone(), - }) - } - None => {} - }, - TypeCategory::Array => { - match schema - .context - .enums - .get(&exact_type.array_element_type_oid.unwrap()) - { - Some(base_enum) => { - type_w_list_mod = __Type::List(ListType { - type_: Box::new(__Type::Enum(EnumType { - enum_: EnumSource::Enum(Arc::clone(base_enum)), - schema: Arc::clone(schema), - })), - }) - } - None => {} - } +impl Type { + fn to_graphql_type( + &self, + max_characters: Option, + is_set_of: bool, + schema: &Arc<__Schema>, + ) -> __Type { + match self.category { + TypeCategory::Other => { + match self.oid { + 20 => __Type::Scalar(Scalar::BigInt), // bigint + 16 => __Type::Scalar(Scalar::Boolean), // boolean + 1082 => __Type::Scalar(Scalar::Date), // date + 1184 => __Type::Scalar(Scalar::Datetime), // timestamp with time zone + 1114 => __Type::Scalar(Scalar::Datetime), // timestamp without time zone + 701 => __Type::Scalar(Scalar::Float), // double precision + 23 => __Type::Scalar(Scalar::Int), // integer + 21 => __Type::Scalar(Scalar::Int), // smallint + 700 => __Type::Scalar(Scalar::Float), // real + 3802 => __Type::Scalar(Scalar::JSON), // jsonb + 114 => __Type::Scalar(Scalar::JSON), // json + 1083 => __Type::Scalar(Scalar::Time), // time without time zone + 2950 => __Type::Scalar(Scalar::UUID), // uuid + 1700 => __Type::Scalar(Scalar::BigFloat), // numeric + 25 => __Type::Scalar(Scalar::String(None)), // text + // char, bpchar, varchar + 18 | 1042 | 1043 => __Type::Scalar(Scalar::String(max_characters)), + _ => __Type::Scalar(Scalar::Opaque), } - _ => {} } + TypeCategory::Array => match self.array_element_type_oid { + Some(array_element_type_oid) => { + let sql_types = schema.context.types(); + let element_sql_type: Option<&Arc> = + sql_types.get(&array_element_type_oid); + + let inner_graphql_type: __Type = match element_sql_type { + Some(sql_type) => match sql_type.permissions.is_usable { + true => sql_type.to_graphql_type(None, false, schema), + false => __Type::Scalar(Scalar::Opaque), + }, + None => __Type::Scalar(Scalar::Opaque), + }; + __Type::List(ListType { + type_: Box::new(inner_graphql_type), + }) + } + // Should not be possible + None => __Type::Scalar(Scalar::Opaque), + }, + TypeCategory::Enum => match schema.context.enums.get(&self.oid) { + Some(enum_) => __Type::Enum(EnumType { + enum_: EnumSource::Enum(Arc::clone(enum_)), + schema: schema.clone(), + }), + None => __Type::Scalar(Scalar::Opaque), + }, + // TODO + TypeCategory::Table => __Type::Scalar(Scalar::Opaque), + TypeCategory::Composite => __Type::Scalar(Scalar::Opaque), } - }; - type_w_list_mod + } } pub fn sql_column_to_graphql_type(col: &Column, schema: &Arc<__Schema>) -> __Type { - let type_w_list_mod = sql_type_to_graphql_type( - col.type_oid, - col.type_name.as_str(), - col.max_characters, - schema, - ); - + let sql_types = schema.context.types(); + let sql_type = sql_types.get(&col.type_oid); + if sql_type.is_none() { + return __Type::Scalar(Scalar::Opaque); + } + let sql_type = sql_type.unwrap(); + let type_w_list_mod = sql_type.to_graphql_type(col.max_characters, false, schema); match col.is_not_null { true => __Type::NonNull(NonNullType { type_: Box::new(type_w_list_mod), @@ -1751,18 +1681,23 @@ impl ___Type for NodeType { .functions .iter() .filter(|x| x.permissions.is_executable) - .map(|func| __Field { - name_: self.schema.graphql_function_field_name(&func), - type_: sql_type_to_graphql_type( - func.type_oid, - func.type_name.as_str(), - None, - &self.schema, - ), - args: vec![], - description: func.directives.description.clone(), - deprecation_reason: None, - sql_type: Some(NodeSQLType::Function(Arc::clone(func))), + .map(|func| { + // unwrap because no sql types are filtered + let sql_types = self.schema.context.types(); + let sql_ret_type = sql_types.get(&func.type_oid); + __Field { + name_: self.schema.graphql_function_field_name(&func), + type_: match sql_ret_type { + None => __Type::Scalar(Scalar::Opaque), + Some(sql_type) => { + sql_type.to_graphql_type(None, func.is_set_of, &self.schema) + } + }, + args: vec![], + description: func.directives.description.clone(), + deprecation_reason: None, + sql_type: Some(NodeSQLType::Function(Arc::clone(func))), + } }) .filter(|x| is_valid_graphql_name(&x.name_)) .collect(); diff --git a/src/sql_types.rs b/src/sql_types.rs index 44e8ce50..b37adac5 100644 --- a/src/sql_types.rs +++ b/src/sql_types.rs @@ -58,6 +58,7 @@ pub struct Function { pub schema_name: String, pub type_oid: u32, pub type_name: String, + pub is_set_of: bool, pub comment: Option, pub directives: FunctionDirectives, pub permissions: FunctionPermissions, @@ -80,6 +81,7 @@ pub struct TypePermissions { pub enum TypeCategory { Enum, Composite, + Table, Array, Other, } @@ -91,6 +93,7 @@ pub struct Type { pub name: String, pub category: TypeCategory, pub array_element_type_oid: Option, + pub table_oid: Option, pub comment: Option, pub permissions: TypePermissions, pub directives: EnumDirectives, @@ -317,7 +320,7 @@ pub struct Context { pub schemas: HashMap, pub tables: HashMap>, foreign_keys: Vec>, - pub types: HashMap>, + types: HashMap>, pub enums: HashMap>, pub composites: Vec>, } @@ -482,6 +485,185 @@ impl Context { .iter() .all(|col| referenced_columns_selectable.contains(col)) } + + pub fn types(&self) -> HashMap> { + let mut types = self.types.clone(); + + for (oid, name, category, array_elem_oid) in vec![ + (16, "bool", TypeCategory::Other, None), + (17, "bytea", TypeCategory::Other, None), + (19, "name", TypeCategory::Other, Some(18)), + (20, "int8", TypeCategory::Other, None), + (21, "int2", TypeCategory::Other, None), + (22, "int2vector", TypeCategory::Array, Some(21)), + (23, "int4", TypeCategory::Other, None), + (24, "regproc", TypeCategory::Other, None), + (25, "text", TypeCategory::Other, None), + (26, "oid", TypeCategory::Other, None), + (27, "tid", TypeCategory::Other, None), + (28, "xid", TypeCategory::Other, None), + (29, "cid", TypeCategory::Other, None), + (30, "oidvector", TypeCategory::Array, Some(26)), + (114, "json", TypeCategory::Other, None), + (142, "xml", TypeCategory::Other, None), + (143, "_xml", TypeCategory::Array, Some(142)), + (199, "_json", TypeCategory::Array, Some(114)), + (210, "_pg_type", TypeCategory::Array, Some(71)), + (270, "_pg_attribute", TypeCategory::Array, Some(75)), + (271, "_xid8", TypeCategory::Array, Some(5069)), + (272, "_pg_proc", TypeCategory::Array, Some(81)), + (273, "_pg_class", TypeCategory::Array, Some(83)), + (600, "point", TypeCategory::Other, Some(701)), + (601, "lseg", TypeCategory::Other, Some(600)), + (602, "path", TypeCategory::Other, None), + (603, "box", TypeCategory::Other, Some(600)), + (604, "polygon", TypeCategory::Other, None), + (628, "line", TypeCategory::Other, Some(701)), + (629, "_line", TypeCategory::Array, Some(628)), + (650, "cidr", TypeCategory::Other, None), + (651, "_cidr", TypeCategory::Array, Some(650)), + (700, "float4", TypeCategory::Other, None), + (701, "float8", TypeCategory::Other, None), + (718, "circle", TypeCategory::Other, None), + (719, "_circle", TypeCategory::Array, Some(718)), + (774, "macaddr8", TypeCategory::Other, None), + (775, "_macaddr8", TypeCategory::Array, Some(774)), + (790, "money", TypeCategory::Other, None), + (791, "_money", TypeCategory::Array, Some(790)), + (829, "macaddr", TypeCategory::Other, None), + (869, "inet", TypeCategory::Other, None), + (1000, "_bool", TypeCategory::Array, Some(16)), + (1001, "_bytea", TypeCategory::Array, Some(17)), + (1002, "_char", TypeCategory::Array, Some(18)), + (1003, "_name", TypeCategory::Array, Some(19)), + (1005, "_int2", TypeCategory::Array, Some(21)), + (1006, "_int2vector", TypeCategory::Array, Some(22)), + (1007, "_int4", TypeCategory::Array, Some(23)), + (1008, "_regproc", TypeCategory::Array, Some(24)), + (1009, "_text", TypeCategory::Array, Some(25)), + (1010, "_tid", TypeCategory::Array, Some(27)), + (1011, "_xid", TypeCategory::Array, Some(28)), + (1012, "_cid", TypeCategory::Array, Some(29)), + (1013, "_oidvector", TypeCategory::Array, Some(30)), + (1014, "_bpchar", TypeCategory::Array, Some(1042)), + (1015, "_varchar", TypeCategory::Array, Some(1043)), + (1016, "_int8", TypeCategory::Array, Some(20)), + (1017, "_point", TypeCategory::Array, Some(600)), + (1018, "_lseg", TypeCategory::Array, Some(601)), + (1019, "_path", TypeCategory::Array, Some(602)), + (1020, "_box", TypeCategory::Array, Some(603)), + (1021, "_float4", TypeCategory::Array, Some(700)), + (1022, "_float8", TypeCategory::Array, Some(701)), + (1027, "_polygon", TypeCategory::Array, Some(604)), + (1028, "_oid", TypeCategory::Array, Some(26)), + (1033, "aclitem", TypeCategory::Other, None), + (1034, "_aclitem", TypeCategory::Array, Some(1033)), + (1040, "_macaddr", TypeCategory::Array, Some(829)), + (1041, "_inet", TypeCategory::Array, Some(869)), + (1042, "bpchar", TypeCategory::Other, None), + (1043, "varchar", TypeCategory::Other, None), + (1082, "date", TypeCategory::Other, None), + (1083, "time", TypeCategory::Other, None), + (1114, "timestamp", TypeCategory::Other, None), + (1115, "_timestamp", TypeCategory::Array, Some(1114)), + (1182, "_date", TypeCategory::Array, Some(1082)), + (1183, "_time", TypeCategory::Array, Some(1083)), + (1184, "timestamptz", TypeCategory::Other, None), + (1185, "_timestamptz", TypeCategory::Array, Some(1184)), + (1186, "interval", TypeCategory::Other, None), + (1187, "_interval", TypeCategory::Array, Some(1186)), + (1231, "_numeric", TypeCategory::Array, Some(1700)), + (1263, "_cstring", TypeCategory::Array, Some(2275)), + (1266, "timetz", TypeCategory::Other, None), + (1270, "_timetz", TypeCategory::Array, Some(1266)), + (1560, "bit", TypeCategory::Other, None), + (1561, "_bit", TypeCategory::Array, Some(1560)), + (1562, "varbit", TypeCategory::Other, None), + (1563, "_varbit", TypeCategory::Array, Some(1562)), + (1700, "numeric", TypeCategory::Other, None), + (1790, "refcursor", TypeCategory::Other, None), + (2201, "_refcursor", TypeCategory::Array, Some(1790)), + (2202, "regprocedure", TypeCategory::Other, None), + (2203, "regoper", TypeCategory::Other, None), + (2204, "regoperator", TypeCategory::Other, None), + (2205, "regclass", TypeCategory::Other, None), + (2206, "regtype", TypeCategory::Other, None), + (2207, "_regprocedure", TypeCategory::Array, Some(2202)), + (2208, "_regoper", TypeCategory::Array, Some(2203)), + (2209, "_regoperator", TypeCategory::Array, Some(2204)), + (2210, "_regclass", TypeCategory::Array, Some(2205)), + (2211, "_regtype", TypeCategory::Array, Some(2206)), + (2949, "_txid_snapshot", TypeCategory::Array, Some(2970)), + (2950, "uuid", TypeCategory::Other, None), + (2951, "_uuid", TypeCategory::Array, Some(2950)), + (2970, "txid_snapshot", TypeCategory::Other, None), + (3220, "pg_lsn", TypeCategory::Other, None), + (3221, "_pg_lsn", TypeCategory::Array, Some(3220)), + (3614, "tsvector", TypeCategory::Other, None), + (3615, "tsquery", TypeCategory::Other, None), + (3642, "gtsvector", TypeCategory::Other, None), + (3643, "_tsvector", TypeCategory::Array, Some(3614)), + (3644, "_gtsvector", TypeCategory::Array, Some(3642)), + (3645, "_tsquery", TypeCategory::Array, Some(3615)), + (3734, "regconfig", TypeCategory::Other, None), + (3735, "_regconfig", TypeCategory::Array, Some(3734)), + (3769, "regdictionary", TypeCategory::Other, None), + (3770, "_regdictionary", TypeCategory::Array, Some(3769)), + (3802, "jsonb", TypeCategory::Other, None), + (3807, "_jsonb", TypeCategory::Array, Some(3802)), + (3904, "int4range", TypeCategory::Other, None), + (3905, "_int4range", TypeCategory::Array, Some(3904)), + (3906, "numrange", TypeCategory::Other, None), + (3907, "_numrange", TypeCategory::Array, Some(3906)), + (3908, "tsrange", TypeCategory::Other, None), + (3909, "_tsrange", TypeCategory::Array, Some(3908)), + (3910, "tstzrange", TypeCategory::Other, None), + (3911, "_tstzrange", TypeCategory::Array, Some(3910)), + (3912, "daterange", TypeCategory::Other, None), + (3913, "_daterange", TypeCategory::Array, Some(3912)), + (3926, "int8range", TypeCategory::Other, None), + (3927, "_int8range", TypeCategory::Array, Some(3926)), + (4072, "jsonpath", TypeCategory::Other, None), + (4073, "_jsonpath", TypeCategory::Array, Some(4072)), + (4089, "regnamespace", TypeCategory::Other, None), + (4090, "_regnamespace", TypeCategory::Array, Some(4089)), + (4096, "regrole", TypeCategory::Other, None), + (4097, "_regrole", TypeCategory::Array, Some(4096)), + (4191, "regcollation", TypeCategory::Other, None), + (4192, "_regcollation", TypeCategory::Array, Some(4191)), + (4451, "int4multirange", TypeCategory::Other, None), + (4532, "nummultirange", TypeCategory::Other, None), + (4533, "tsmultirange", TypeCategory::Other, None), + (4534, "tstzmultirange", TypeCategory::Other, None), + (4535, "datemultirange", TypeCategory::Other, None), + (4536, "int8multirange", TypeCategory::Other, None), + (5038, "pg_snapshot", TypeCategory::Other, None), + (5039, "_pg_snapshot", TypeCategory::Array, Some(5038)), + (5069, "xid8", TypeCategory::Other, None), + (6150, "_int4multirange", TypeCategory::Array, Some(4451)), + (6151, "_nummultirange", TypeCategory::Array, Some(4532)), + (6152, "_tsmultirange", TypeCategory::Array, Some(4533)), + (6153, "_tstzmultirange", TypeCategory::Array, Some(4534)), + (6155, "_datemultirange", TypeCategory::Array, Some(4535)), + (6157, "_int8multirange", TypeCategory::Array, Some(4536)), + ] { + types.insert( + oid, + Arc::new(Type { + oid, + schema_oid: 11, + name: name.to_string(), + category, + table_oid: None, + comment: None, + permissions: TypePermissions { is_usable: true }, + directives: EnumDirectives { name: None }, + array_element_type_oid: array_elem_oid, + }), + ); + } + types + } } pub fn load_sql_config() -> Config { From 4b6de5ab822bea8159b0003d29af722c7f6dcfc6 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 23 Mar 2023 15:35:33 -0500 Subject: [PATCH 02/12] add table types to converter --- src/graphql.rs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/graphql.rs b/src/graphql.rs index 27c48f4e..d89e4be5 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1570,8 +1570,9 @@ impl Type { type_: Box::new(inner_graphql_type), }) } - // Should not be possible - None => __Type::Scalar(Scalar::Opaque), + None => __Type::List(ListType { + type_: Box::new(__Type::Scalar(Scalar::Opaque)), + }), }, TypeCategory::Enum => match schema.context.enums.get(&self.oid) { Some(enum_) => __Type::Enum(EnumType { @@ -1581,7 +1582,30 @@ impl Type { None => __Type::Scalar(Scalar::Opaque), }, // TODO - TypeCategory::Table => __Type::Scalar(Scalar::Opaque), + TypeCategory::Table => { + match self.table_oid { + // no guarentees of whats going on here. + None => __Type::Scalar(Scalar::Opaque), + Some(table_oid) => match schema.context.tables.get(&table_oid) { + None => __Type::Scalar(Scalar::Opaque), + Some(table) => match is_set_of { + true => __Type::Connection(ConnectionType { + table: Arc::clone(table), + fkey: None, + reverse_reference: None, + schema: Arc::clone(schema), + }), + false => __Type::Node(NodeType { + table: Arc::clone(table), + fkey: None, + reverse_reference: None, + schema: Arc::clone(schema), + }), + }, + }, + } + } + // Composites not yet supported TypeCategory::Composite => __Type::Scalar(Scalar::Opaque), } } From d676023c0edf9b8e3dafc1417398fd9b4600a479 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Wed, 19 Apr 2023 14:31:03 -0500 Subject: [PATCH 03/12] functions have selection --- src/builder.rs | 37 +++++++ src/graphql.rs | 244 ++++++++++++++++++----------------------- src/transpile.rs | 21 +++- test/sql/issue_347.sql | 102 +++++++++++++++++ 4 files changed, 261 insertions(+), 143 deletions(-) create mode 100644 test/sql/issue_347.sql diff --git a/src/builder.rs b/src/builder.rs index 1e6769dd..1a7d066e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -794,6 +794,14 @@ pub struct FunctionBuilder { pub alias: String, pub function: Arc, pub table: Arc, + pub selection: FunctionSelection, +} + +#[derive(Clone, Debug)] +pub enum FunctionSelection { + ScalarSelf, + Connection(ConnectionBuilder), + Node(NodeBuilder), } fn restrict_allowed_arguments<'a, T>( @@ -1382,10 +1390,39 @@ where column: Arc::clone(col), }), NodeSQLType::Function(func) => { + let function_selection = match &f.type_() { + __Type::Scalar(_) => FunctionSelection::ScalarSelf, + __Type::Node(_) => { + let node_builder = to_node_builder( + f, + selection_field, + fragment_definitions, + variables, + // TODO need ref to fkey here + )?; + FunctionSelection::Node(node_builder) + } + __Type::Connection(_) => { + let connection_builder = to_connection_builder( + f, + selection_field, + fragment_definitions, + variables, + // TODO need ref to fkey here + )?; + FunctionSelection::Connection(connection_builder) + } + _ => { + return Err(format!( + "invalid return type from function" + )) + } + }; NodeSelection::Function(FunctionBuilder { alias, function: Arc::clone(func), table: Arc::clone(&xtype.table), + selection: function_selection, }) } NodeSQLType::NodeId(pkey_columns) => { diff --git a/src/graphql.rs b/src/graphql.rs index 929a85b5..17673857 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -892,6 +892,73 @@ pub struct ConnectionType { pub schema: Arc<__Schema>, } +impl ConnectionType { + // default arguments for all connections + fn get_connection_input_args(&self) -> Vec<__InputValue> { + vec![ + __InputValue { + name_: "first".to_string(), + type_: __Type::Scalar(Scalar::Int), + description: Some("Query the first `n` records in the collection".to_string()), + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "last".to_string(), + type_: __Type::Scalar(Scalar::Int), + description: Some("Query the last `n` records in the collection".to_string()), + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "before".to_string(), + type_: __Type::Scalar(Scalar::Cursor), + description: Some( + "Query values in the collection before the provided cursor".to_string(), + ), + default_value: None, + sql_type: None, + }, + __InputValue { + name_: "after".to_string(), + type_: __Type::Scalar(Scalar::Cursor), + description: Some( + "Query values in the collection after the provided cursor".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, + }, + __InputValue { + name_: "orderBy".to_string(), + type_: __Type::List(ListType { + type_: Box::new(__Type::NonNull(NonNullType { + type_: Box::new(__Type::OrderByEntity(OrderByEntityType { + table: Arc::clone(&self.table), + schema: self.schema.clone(), + })), + })), + }), + description: Some("Sort order to apply to the collection".to_string()), + default_value: None, + sql_type: None, + }, + ] + } +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum EnumSource { Enum(Arc), @@ -1003,73 +1070,19 @@ impl ___Type for QueryType { { let table_base_type_name = &self.schema.graphql_table_base_type_name(&table); + let connection_type = ConnectionType { + table: Arc::clone(table), + fkey: None, + reverse_reference: None, + schema: Arc::clone(&self.schema), + }; + + let connection_args = connection_type.get_connection_input_args(); + let collection_entrypoint = __Field { - name_: format!( - "{}Collection", - lowercase_first_letter(table_base_type_name) - ), - type_: __Type::Connection(ConnectionType { - table: Arc::clone(table), - fkey: None, - reverse_reference: None, - schema: Arc::clone(&self.schema), - }), - args: vec![ - __InputValue { - name_: "first".to_string(), - type_: __Type::Scalar(Scalar::Int), - description: Some("Query the first `n` records in the collection".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "last".to_string(), - type_: __Type::Scalar(Scalar::Int), - description: Some("Query the last `n` records in the collection".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "before".to_string(), - type_: __Type::Scalar(Scalar::Cursor), - description: Some("Query values in the collection before the provided cursor".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "after".to_string(), - type_: __Type::Scalar(Scalar::Cursor), - description: Some("Query values in the collection after the provided cursor".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "filter".to_string(), - type_: __Type::FilterEntity(FilterEntityType { - table: Arc::clone(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, - }, - __InputValue { - name_: "orderBy".to_string(), - type_: __Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::OrderByEntity( - OrderByEntityType { - table: Arc::clone(table), - schema: self.schema.clone(), - }, - )), - })), - }), - description: Some("Sort order to apply to the collection".to_string()), - default_value: None, - sql_type: None, - }, - ], + name_: format!("{}Collection", lowercase_first_letter(table_base_type_name)), + type_: __Type::Connection(connection_type), + args: connection_args, description: Some(format!( "A pagable collection of type `{}`", table_base_type_name @@ -1706,18 +1719,26 @@ impl ___Type for NodeType { .iter() .filter(|x| x.permissions.is_executable) .map(|func| { - // unwrap because no sql types are filtered let sql_types = self.schema.context.types(); let sql_ret_type = sql_types.get(&func.type_oid); + let gql_ret_type = match sql_ret_type { + None => __Type::Scalar(Scalar::Opaque), + Some(sql_type) => { + sql_type.to_graphql_type(None, func.is_set_of, &self.schema) + } + }; + + let gql_args = match &gql_ret_type { + __Type::Connection(connection_type) => { + connection_type.get_connection_input_args() + } + _ => vec![], + }; + __Field { name_: self.schema.graphql_function_field_name(&func), - type_: match sql_ret_type { - None => __Type::Scalar(Scalar::Opaque), - Some(sql_type) => { - sql_type.to_graphql_type(None, func.is_set_of, &self.schema) - } - }, - args: vec![], + type_: gql_ret_type, + args: gql_args, description: func.directives.description.clone(), deprecation_reason: None, sql_type: Some(NodeSQLType::Function(Arc::clone(func))), @@ -1802,71 +1823,20 @@ impl ___Type for NodeType { let relation_field = match self.schema.context.fkey_is_locally_unique(fkey) { false => { + let connection_type = ConnectionType { + table: Arc::clone(foreign_table), + fkey: Some(Arc::clone(fkey)), + reverse_reference: Some(reverse_reference), + schema: Arc::clone(&self.schema), + }; + let connection_args = connection_type.get_connection_input_args(); __Field { - name_: self.schema.graphql_foreign_key_field_name(fkey, reverse_reference), + name_: self + .schema + .graphql_foreign_key_field_name(fkey, reverse_reference), // XXX: column nullability ignored for NonNull type to match pg_graphql - type_: __Type::Connection(ConnectionType { - table: Arc::clone(foreign_table), - fkey: Some(Arc::clone(fkey)), - reverse_reference: Some(reverse_reference), - schema: Arc::clone(&self.schema), - }), - args: vec![ - __InputValue { - name_: "first".to_string(), - type_: __Type::Scalar(Scalar::Int), - description: Some("Query the first `n` records in the collection".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "last".to_string(), - type_: __Type::Scalar(Scalar::Int), - description: Some("Query the last `n` records in the collection".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "before".to_string(), - type_: __Type::Scalar(Scalar::Cursor), - description: Some("Query values in the collection before the provided cursor".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "after".to_string(), - type_: __Type::Scalar(Scalar::Cursor), - description: Some("Query values in the collection after the provided cursor".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "filter".to_string(), - type_: __Type::FilterEntity(FilterEntityType { - table: Arc::clone(foreign_table), - schema: Arc::clone(&self.schema), - }), - description: Some("Filters to apply to the results set when querying from the collection".to_string()), - default_value: None, - sql_type: None, - }, - __InputValue { - name_: "orderBy".to_string(), - type_: __Type::List(ListType { - type_: Box::new(__Type::NonNull(NonNullType { - type_: Box::new(__Type::OrderByEntity( - OrderByEntityType { - table: Arc::clone(foreign_table), - schema: Arc::clone(&self.schema), - }, - )), - })), - }), - description: Some("Sort order to apply to the collection".to_string()), - default_value: None, - sql_type: None, - }, - ], + type_: __Type::Connection(connection_type), + args: connection_args, description: None, deprecation_reason: None, sql_type: None, diff --git a/src/transpile.rs b/src/transpile.rs index be36292b..6bd6e264 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -114,7 +114,8 @@ pub trait QueryEntrypoint { match spi_result { Ok(Some(jsonb)) => Ok(jsonb.0), Ok(None) => Ok(serde_json::Value::Null), - _ => Err("Internal Error: Failed to execute transpiled query".to_string()), + Err(x) => Err(format!("Err {:?}", x)), + _ => Err("Internal Error: Failed to executeee transpiled query".to_string()), } } } @@ -1302,11 +1303,19 @@ impl FunctionBuilder { pub fn to_sql(&self, block_name: &str) -> Result { let schema_name = &self.function.schema_name; let function_name = &self.function.name; - Ok(format!( - "{schema_name}.{function_name}({block_name}::{}.{})", - quote_ident(&self.table.schema), - quote_ident(&self.table.name) - )) + + let sql_frag = match &self.selection { + FunctionSelection::ScalarSelf => format!( + "{schema_name}.{function_name}({block_name}::{}.{})", + quote_ident(&self.table.schema), + quote_ident(&self.table.name) + ), + FunctionSelection::Node(node_builder) => return Err("node builder".to_string()), + FunctionSelection::Connection(connection_builder) => { + return Err("connection builder".to_string()); + } + }; + Ok(sql_frag) } } diff --git a/test/sql/issue_347.sql b/test/sql/issue_347.sql new file mode 100644 index 00000000..de290a38 --- /dev/null +++ b/test/sql/issue_347.sql @@ -0,0 +1,102 @@ +begin; + + create table public.ingredient ( + id bigint generated by default as identity not null, + created_at timestamp with time zone null default now(), + name text, + constraint ingredient_pkey primary key (id), + constraint ingredient_name_key unique (name) + ); + + create table public.recipe ( + id bigint generated by default as identity not null, + created_at timestamp with time zone default now(), + name text not null, + constraint recipe_pkey primary key (id), + constraint recipe_name_key unique (name) + ); + + create table public.recipe_ingredient ( + id bigint generated by default as identity not null, + created_at timestamp with time zone null default now(), + recipe_id bigint references recipe (id), + ingredient_id bigint references ingredient (id), + recipe_ingredient_id bigint, + constraint recipe_ingredient_pkey primary key (id), + constraint recipe_ingredient_recipe_ingredient_id_fkey foreign key (recipe_ingredient_id) references public.recipe (id) + ); + + comment on constraint recipe_ingredient_recipe_ingredient_id_fkey + on public.recipe_ingredient + is E'@graphql({"foreign_name": "recipeIngredient", "local_name": "someOtherName"})'; + + insert into public.recipe(name) + values ('BBQ Dry Rub'), ('Carolina BBQ Sauce'); + + insert into public.ingredient(id, name) + values (2, 'Smoked Paprika'); + + insert into recipe_ingredient(recipe_id, ingredient_id, recipe_ingredient_id) + values (2, 2, null), (2, null, 1); + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "RecipeIngredient") { + kind + fields { + name + } + } + } + $$) + ); + + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Recipe") { + kind + fields { + name + } + } + } + $$) + ); + + select jsonb_pretty( + graphql.resolve($$ + query GetRecipe { + recipeCollection(filter: {id: {eq: 2}}) { + edges { + node { + id + name + recipeIngredientCollection { + edges { + node { + id + recipe { + name + } + ingredient { + name + } + recipeIngredient { + name + } + } + } + } + } + } + } + } + $$) + ); + + + +rollback; From 64897f10133c25d284a1021594ad696cf92690bc Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Wed, 19 Apr 2023 15:18:39 -0500 Subject: [PATCH 04/12] extend with function to one --- src/builder.rs | 168 ++++++++++++++++++++++++----------------------- src/transpile.rs | 25 ++++++- 2 files changed, 107 insertions(+), 86 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 1a7d066e..4cf402fc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1360,96 +1360,98 @@ where )) } Some(f) => { - match f.type_().unmodified_type() { - __Type::Connection(_) => { - let con_builder = to_connection_builder( - f, - selection_field, - fragment_definitions, - variables, - // TODO need ref to fkey here - ); - builder_fields.push(NodeSelection::Connection(con_builder?)); - } - __Type::Node(_) => { - let node_builder = to_node_builder( - f, - selection_field, - fragment_definitions, - variables, - // TODO need ref to fkey here - ); - builder_fields.push(NodeSelection::Node(node_builder?)); - } - _ => { - let alias = alias_or_name(selection_field); - let node_selection = match &f.sql_type { - Some(node_sql_type) => match node_sql_type { - NodeSQLType::Column(col) => NodeSelection::Column(ColumnBuilder { - alias, - column: Arc::clone(col), - }), - NodeSQLType::Function(func) => { - let function_selection = match &f.type_() { - __Type::Scalar(_) => FunctionSelection::ScalarSelf, - __Type::Node(_) => { - let node_builder = to_node_builder( - f, - selection_field, - fragment_definitions, - variables, - // TODO need ref to fkey here - )?; - FunctionSelection::Node(node_builder) - } - __Type::Connection(_) => { - let connection_builder = to_connection_builder( - f, - selection_field, - fragment_definitions, - variables, - // TODO need ref to fkey here - )?; - FunctionSelection::Connection(connection_builder) - } - _ => { - return Err(format!( - "invalid return type from function" - )) - } - }; - NodeSelection::Function(FunctionBuilder { - alias, - function: Arc::clone(func), - table: Arc::clone(&xtype.table), - selection: function_selection, - }) + let alias = alias_or_name(selection_field); + + let node_selection = match &f.sql_type { + Some(node_sql_type) => match node_sql_type { + NodeSQLType::Column(col) => NodeSelection::Column(ColumnBuilder { + alias, + column: Arc::clone(col), + }), + NodeSQLType::Function(func) => { + let function_selection = match &f.type_() { + __Type::Scalar(_) => FunctionSelection::ScalarSelf, + __Type::Node(_) => { + let node_builder = to_node_builder( + f, + selection_field, + fragment_definitions, + variables, + // TODO need ref to fkey here + )?; + FunctionSelection::Node(node_builder) } - NodeSQLType::NodeId(pkey_columns) => { - NodeSelection::NodeId(NodeIdBuilder { - alias, - columns: pkey_columns.clone(), // interior is arc - table_name: xtype.table.name.clone(), - schema_name: xtype.table.schema.clone(), - }) + __Type::Connection(_) => { + let connection_builder = to_connection_builder( + f, + selection_field, + fragment_definitions, + variables, + // TODO need ref to fkey here + )?; + FunctionSelection::Connection(connection_builder) + } + _ => { + return Err(format!( + "invalid return type from function" + )) + } + }; + NodeSelection::Function(FunctionBuilder { + alias, + function: Arc::clone(func), + table: Arc::clone(&xtype.table), + selection: function_selection, + }) + } + NodeSQLType::NodeId(pkey_columns) => { + NodeSelection::NodeId(NodeIdBuilder { + alias, + columns: pkey_columns.clone(), // interior is arc + table_name: xtype.table.name.clone(), + schema_name: xtype.table.schema.clone(), + }) + } + }, + _ => match f.name().as_ref() { + "__typename" => NodeSelection::Typename { + alias: alias_or_name(selection_field), + typename: xtype.name().unwrap(), + }, + _ => { + match f.type_().unmodified_type() { + __Type::Connection(_) => { + let con_builder = to_connection_builder( + f, + selection_field, + fragment_definitions, + variables, + ); + NodeSelection::Connection(con_builder?) + } + __Type::Node(_) => { + let node_builder = to_node_builder( + f, + selection_field, + fragment_definitions, + variables, + ); + NodeSelection::Node(node_builder?) } - }, - _ => match f.name().as_ref() { - "__typename" => NodeSelection::Typename { - alias: alias_or_name(selection_field), - typename: xtype.name().unwrap(), - }, _ => { return Err(format!( "unexpected field type on node {}", f.name() - )) + )); } - }, - }; - builder_fields.push(node_selection); - } - } + } + + } + }, + }; + builder_fields.push(node_selection); + + } } } diff --git a/src/transpile.rs b/src/transpile.rs index 6bd6e264..5afd5da0 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1257,7 +1257,7 @@ impl NodeSelection { format!( "{}, {}{}", quote_literal(&builder.alias), - builder.to_sql(block_name)?, + builder.to_sql(block_name, param_context)?, type_adjustment_clause ) } @@ -1300,7 +1300,7 @@ impl NodeIdBuilder { } impl FunctionBuilder { - pub fn to_sql(&self, block_name: &str) -> Result { + pub fn to_sql(&self, block_name: &str, param_context: &mut ParamContext,) -> Result { let schema_name = &self.function.schema_name; let function_name = &self.function.name; @@ -1310,7 +1310,26 @@ impl FunctionBuilder { quote_ident(&self.table.schema), quote_ident(&self.table.name) ), - FunctionSelection::Node(node_builder) => return Err("node builder".to_string()), + FunctionSelection::Node(node_builder) => { + let func_block_name = rand_block_name(); + let object_clause = node_builder.to_sql(&func_block_name, param_context)?; + + let from_clause = format!( + "{schema_name}.{function_name}({block_name}::{}.{})", + quote_ident(&self.table.schema), + quote_ident(&self.table.name) + ); + format!( + " + ( + select + {object_clause} + from + {from_clause} as {func_block_name} + ) + " + ) + } FunctionSelection::Connection(connection_builder) => { return Err("connection builder".to_string()); } From a2a7348348c4442bea5eab372b29707dcd047dec Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Wed, 19 Apr 2023 16:01:57 -0500 Subject: [PATCH 05/12] computed to-many POC w lots of redundency --- src/transpile.rs | 188 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 182 insertions(+), 6 deletions(-) diff --git a/src/transpile.rs b/src/transpile.rs index 5afd5da0..f558a1f6 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -114,8 +114,7 @@ pub trait QueryEntrypoint { match spi_result { Ok(Some(jsonb)) => Ok(jsonb.0), Ok(None) => Ok(serde_json::Value::Null), - Err(x) => Err(format!("Err {:?}", x)), - _ => Err("Internal Error: Failed to executeee transpiled query".to_string()), + _ => Err("Internal Error: Failed to execute transpiled query".to_string()), } } } @@ -942,6 +941,175 @@ impl ConnectionBuilder { )" )) } + + pub fn to_sql_from_func( + &self, + input_block_name: &str, + function_schema: &str, + function_name: &str, + param_context: &mut ParamContext, + ) -> Result { + let quoted_block_name = rand_block_name(); + let quoted_schema = quote_ident(function_schema); + let quoted_func = quote_ident(function_name); + + let where_clause = + self.filter + .to_where_clause("ed_block_name, &self.table, param_context)?; + + let order_by_clause = self.order_by.to_order_by_clause("ed_block_name); + let order_by_clause_reversed = self + .order_by + .reverse() + .to_order_by_clause("ed_block_name); + + let is_reverse_pagination = self.last.is_some() || self.before.is_some(); + + let order_by_clause_records = match is_reverse_pagination { + true => &order_by_clause_reversed, + false => &order_by_clause, + }; + + let requested_total = self.requested_total(); + let requested_next_page = self.requested_next_page(); + let requested_previous_page = self.requested_previous_page(); + + let frags: Vec = self + .selections + .iter() + .map(|x| { + x.to_sql( + "ed_block_name, + &self.order_by, + &self.table, + param_context, + ) + }) + .collect::, _>>()?; + + let limit: u64 = cmp::min( + self.first + .unwrap_or_else(|| self.last.unwrap_or(self.max_rows)), + self.max_rows, + ); + + let object_clause = frags.join(", "); + + let cursor = &self.before.clone().or_else(|| self.after.clone()); + + let selectable_columns_clause = self.table.to_selectable_columns_clause(); + + let pkey_tuple_clause_from_block = + self.table.to_primary_key_tuple_clause("ed_block_name); + let pkey_tuple_clause_from_records = self.table.to_primary_key_tuple_clause("__records"); + + let pagination_clause = { + let order_by = match is_reverse_pagination { + true => self.order_by.reverse(), + false => self.order_by.clone(), + }; + match cursor { + Some(cursor) => self.table.to_pagination_clause( + "ed_block_name, + &order_by, + cursor, + param_context, + false, + )?, + None => "true".to_string(), + } + }; + + // initialized assuming forwards pagination + let mut has_next_page_query = format!( + " + with page_plus_1 as ( + select + 1 + from + {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + where + {where_clause} + and {pagination_clause} + order by + {order_by_clause} + limit ({limit} + 1) + ) + select count(*) > {limit} from page_plus_1 + " + ); + + let mut has_prev_page_query = format!(" + with page_minus_1 as ( + select + not ({pkey_tuple_clause_from_block} = any( __records.seen )) is_pkey_in_records + from + {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + left join (select array_agg({pkey_tuple_clause_from_records}) from __records ) __records(seen) + on true + where + {where_clause} + order by + {order_by_clause_records} + limit 1 + ) + select coalesce(bool_and(is_pkey_in_records), false) from page_minus_1 + "); + + if is_reverse_pagination { + // Reverse has_next_page and has_previous_page + std::mem::swap(&mut has_next_page_query, &mut has_prev_page_query); + } + if !requested_next_page { + has_next_page_query = "select null".to_string() + } + if !requested_previous_page { + has_prev_page_query = "select null".to_string() + } + + Ok(format!( + " + ( + with __records as ( + select + {selectable_columns_clause} + from + {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + where + true + and {where_clause} + and {pagination_clause} + order by + {order_by_clause_records} + limit + {limit} + ), + __total_count(___total_count) as ( + select + count(*) + from + {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + where + {requested_total} -- skips total when not requested + and {where_clause} + ), + __has_next_page(___has_next_page) as ( + {has_next_page_query} + + ), + __has_previous_page(___has_previous_page) as ( + {has_prev_page_query} + ) + select + jsonb_build_object({object_clause}) -- sorted within edge + from + __records {quoted_block_name}, + __total_count, + __has_next_page, + __has_previous_page + )" + )) + } } impl QueryEntrypoint for ConnectionBuilder { @@ -1300,7 +1468,11 @@ impl NodeIdBuilder { } impl FunctionBuilder { - pub fn to_sql(&self, block_name: &str, param_context: &mut ParamContext,) -> Result { + pub fn to_sql( + &self, + block_name: &str, + param_context: &mut ParamContext, + ) -> Result { let schema_name = &self.function.schema_name; let function_name = &self.function.name; @@ -1330,9 +1502,13 @@ impl FunctionBuilder { " ) } - FunctionSelection::Connection(connection_builder) => { - return Err("connection builder".to_string()); - } + FunctionSelection::Connection(connection_builder) => connection_builder + .to_sql_from_func( + block_name, + &self.function.schema_name, + &self.function.name, + param_context, + )?, }; Ok(sql_frag) } From d03ec8aee6180cc5e3420ef0e8ab25ce0ab0dfa2 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 20 Apr 2023 12:35:54 -0500 Subject: [PATCH 06/12] computed connection types --- src/builder.rs | 21 +++- src/graphql.rs | 20 ++-- src/transpile.rs | 285 +++++++++++++---------------------------------- 3 files changed, 105 insertions(+), 221 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 4cf402fc..82f9ef7a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -546,6 +546,16 @@ where } } + + + +#[derive(Clone, Debug)] +pub struct ConnectionBuilderSource { + pub table: Arc
, + pub fkey: Option +} + + #[derive(Clone, Debug)] pub struct ConnectionBuilder { pub alias: String, @@ -559,9 +569,7 @@ pub struct ConnectionBuilder { pub order_by: OrderByBuilder, // metadata - pub table: Arc
, - pub fkey: Option>, - pub reverse_reference: Option, + pub source: ConnectionBuilderSource, //fields pub selections: Vec, @@ -1128,9 +1136,10 @@ where } Ok(ConnectionBuilder { alias, - table: Arc::clone(&xtype.table), - fkey: xtype.fkey.clone(), - reverse_reference: xtype.reverse_reference, + source: ConnectionBuilderSource { + table: Arc::clone(&xtype.table), + fkey: xtype.fkey.clone() + }, first, last, before, diff --git a/src/graphql.rs b/src/graphql.rs index 17673857..1a1c0f41 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -880,14 +880,16 @@ pub struct DeleteResponseType { pub schema: Arc<__Schema>, } +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct ForeignKeyReversible { + pub fkey: Arc, + pub reverse_reference: bool, +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct ConnectionType { pub table: Arc
, - - // If one is present, both should be present - // could be improved - pub fkey: Option>, - pub reverse_reference: Option, + pub fkey: Option, pub schema: Arc<__Schema>, } @@ -1073,7 +1075,6 @@ impl ___Type for QueryType { let connection_type = ConnectionType { table: Arc::clone(table), fkey: None, - reverse_reference: None, schema: Arc::clone(&self.schema), }; @@ -1605,7 +1606,6 @@ impl Type { true => __Type::Connection(ConnectionType { table: Arc::clone(table), fkey: None, - reverse_reference: None, schema: Arc::clone(schema), }), false => __Type::Node(NodeType { @@ -1825,8 +1825,9 @@ impl ___Type for NodeType { false => { let connection_type = ConnectionType { table: Arc::clone(foreign_table), - fkey: Some(Arc::clone(fkey)), - reverse_reference: Some(reverse_reference), + fkey: Some( + ForeignKeyReversible { fkey: Arc::clone(fkey), reverse_reference: reverse_reference } + ), schema: Arc::clone(&self.schema), }; let connection_args = connection_type.get_connection_input_args(); @@ -3535,7 +3536,6 @@ impl __Schema { types_.push(__Type::Connection(ConnectionType { table: Arc::clone(table), fkey: None, - reverse_reference: None, schema: Arc::clone(&schema_rc), })); diff --git a/src/transpile.rs b/src/transpile.rs index f558a1f6..28533e10 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1,6 +1,6 @@ use crate::builder::*; use crate::graphql::*; -use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Table}; +use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Table, Function}; use pgx::pg_sys::submodules::panic::CaughtError; use pgx::pg_sys::PgBuiltInOids; use pgx::prelude::*; @@ -726,6 +726,12 @@ impl FilterBuilder { } } +pub struct FromFunction { + function: Arc, + // The block name for the functions argument + input_block_name: String +} + impl ConnectionBuilder { fn requested_total(&self) -> bool { self.selections @@ -757,205 +763,84 @@ impl ConnectionBuilder { .any(|x| matches!(&x, PageInfoSelection::HasPreviousPage { alias: _ })) } - pub fn to_sql( - &self, - quoted_parent_block_name: Option<&str>, - param_context: &mut ParamContext, - ) -> Result { - let quoted_block_name = rand_block_name(); - let quoted_schema = quote_ident(&self.table.schema); - let quoted_table = quote_ident(&self.table.name); - - let where_clause = - self.filter - .to_where_clause("ed_block_name, &self.table, param_context)?; - - let order_by_clause = self.order_by.to_order_by_clause("ed_block_name); - let order_by_clause_reversed = self - .order_by - .reverse() - .to_order_by_clause("ed_block_name); - - let is_reverse_pagination = self.last.is_some() || self.before.is_some(); - - let order_by_clause_records = match is_reverse_pagination { - true => &order_by_clause_reversed, - false => &order_by_clause, - }; - - let requested_total = self.requested_total(); - let requested_next_page = self.requested_next_page(); - let requested_previous_page = self.requested_previous_page(); + fn is_reverse_pagination(&self) -> bool { + self.last.is_some() || self.before.is_some() + } - let join_clause = match &self.fkey { + fn to_join_clause(&self, quoted_block_name: &str, quoted_parent_block_name: &Option<&str>) -> Result { + match &self.source.fkey { Some(fkey) => { let quoted_parent_block_name = quoted_parent_block_name .ok_or("Internal Error: Parent block name is required when fkey_ix is set")?; - self.table.to_join_clause( - fkey, - self.reverse_reference.unwrap(), + self.source.table.to_join_clause( + &fkey.fkey, + fkey.reverse_reference, "ed_block_name, quoted_parent_block_name, - )? + ) } - None => "true".to_string(), - }; + None => Ok("true".to_string()), + } + } + + fn object_clause(&self, quoted_block_name: &str, param_context: &mut ParamContext) -> Result { let frags: Vec = self .selections .iter() .map(|x| { x.to_sql( - "ed_block_name, + quoted_block_name, &self.order_by, - &self.table, + &self.source.table, param_context, ) }) .collect::, _>>()?; - let limit: u64 = cmp::min( + Ok(frags.join(", ")) + } + + fn limit_clause(&self) -> u64 { + cmp::min( self.first .unwrap_or_else(|| self.last.unwrap_or(self.max_rows)), self.max_rows, - ); - - let object_clause = frags.join(", "); - - let cursor = &self.before.clone().or_else(|| self.after.clone()); + ) + } - let selectable_columns_clause = self.table.to_selectable_columns_clause(); + fn from_clause(&self, quoted_block_name: &str, function: &Option) -> String{ - let pkey_tuple_clause_from_block = - self.table.to_primary_key_tuple_clause("ed_block_name); - let pkey_tuple_clause_from_records = self.table.to_primary_key_tuple_clause("__records"); + let quoted_schema = quote_ident(&self.source.table.schema); + let quoted_table = quote_ident(&self.source.table.name); - let pagination_clause = { - let order_by = match is_reverse_pagination { - true => self.order_by.reverse(), - false => self.order_by.clone(), - }; - match cursor { - Some(cursor) => self.table.to_pagination_clause( - "ed_block_name, - &order_by, - cursor, - param_context, - false, - )?, - None => "true".to_string(), + match function { + Some(from_function) => { + let quoted_func_schema = quote_ident(&from_function.function.schema_name); + let quoted_func = quote_ident(&from_function.function.name); + let input_block_name = &from_function.input_block_name; + format!("{quoted_func_schema}.{quoted_func}({input_block_name}::{quoted_schema}.{quoted_table}) {quoted_block_name}") + } + None => { + format!("{quoted_schema}.{quoted_table} {quoted_block_name}") } - }; - - // initialized assuming forwards pagination - let mut has_next_page_query = format!( - " - with page_plus_1 as ( - select - 1 - from - {quoted_schema}.{quoted_table} {quoted_block_name} - where - {join_clause} - and {where_clause} - and {pagination_clause} - order by - {order_by_clause} - limit ({limit} + 1) - ) - select count(*) > {limit} from page_plus_1 - " - ); - - let mut has_prev_page_query = format!(" - with page_minus_1 as ( - select - not ({pkey_tuple_clause_from_block} = any( __records.seen )) is_pkey_in_records - from - {quoted_schema}.{quoted_table} {quoted_block_name} - left join (select array_agg({pkey_tuple_clause_from_records}) from __records ) __records(seen) - on true - where - {join_clause} - and {where_clause} - order by - {order_by_clause_records} - limit 1 - ) - select coalesce(bool_and(is_pkey_in_records), false) from page_minus_1 - "); - - if is_reverse_pagination { - // Reverse has_next_page and has_previous_page - std::mem::swap(&mut has_next_page_query, &mut has_prev_page_query); - } - if !requested_next_page { - has_next_page_query = "select null".to_string() - } - if !requested_previous_page { - has_prev_page_query = "select null".to_string() } - - Ok(format!( - " - ( - with __records as ( - select - {selectable_columns_clause} - from - {quoted_schema}.{quoted_table} {quoted_block_name} - where - true - and {join_clause} - and {where_clause} - and {pagination_clause} - order by - {order_by_clause_records} - limit - {limit} - ), - __total_count(___total_count) as ( - select - count(*) - from - {quoted_schema}.{quoted_table} {quoted_block_name} - where - {requested_total} -- skips total when not requested - and {join_clause} - and {where_clause} - ), - __has_next_page(___has_next_page) as ( - {has_next_page_query} - - ), - __has_previous_page(___has_previous_page) as ( - {has_prev_page_query} - ) - select - jsonb_build_object({object_clause}) -- sorted within edge - from - __records {quoted_block_name}, - __total_count, - __has_next_page, - __has_previous_page - )" - )) } - pub fn to_sql_from_func( + + pub fn to_sql( &self, - input_block_name: &str, - function_schema: &str, - function_name: &str, + quoted_parent_block_name: Option<&str>, param_context: &mut ParamContext, + from_func: Option ) -> Result { let quoted_block_name = rand_block_name(); - let quoted_schema = quote_ident(function_schema); - let quoted_func = quote_ident(function_name); + + let from_clause = self.from_clause("ed_block_name, &from_func); let where_clause = self.filter - .to_where_clause("ed_block_name, &self.table, param_context)?; + .to_where_clause("ed_block_name, &self.source.table, param_context)?; let order_by_clause = self.order_by.to_order_by_clause("ed_block_name); let order_by_clause_reversed = self @@ -963,9 +848,7 @@ impl ConnectionBuilder { .reverse() .to_order_by_clause("ed_block_name); - let is_reverse_pagination = self.last.is_some() || self.before.is_some(); - - let order_by_clause_records = match is_reverse_pagination { + let order_by_clause_records = match self.is_reverse_pagination() { true => &order_by_clause_reversed, false => &order_by_clause, }; @@ -974,42 +857,25 @@ impl ConnectionBuilder { let requested_next_page = self.requested_next_page(); let requested_previous_page = self.requested_previous_page(); - let frags: Vec = self - .selections - .iter() - .map(|x| { - x.to_sql( - "ed_block_name, - &self.order_by, - &self.table, - param_context, - ) - }) - .collect::, _>>()?; - - let limit: u64 = cmp::min( - self.first - .unwrap_or_else(|| self.last.unwrap_or(self.max_rows)), - self.max_rows, - ); - - let object_clause = frags.join(", "); + let join_clause = self.to_join_clause("ed_block_name, "ed_parent_block_name)?; let cursor = &self.before.clone().or_else(|| self.after.clone()); - let selectable_columns_clause = self.table.to_selectable_columns_clause(); + let object_clause = self.object_clause("ed_block_name, param_context)?; + + let selectable_columns_clause = self.source.table.to_selectable_columns_clause(); let pkey_tuple_clause_from_block = - self.table.to_primary_key_tuple_clause("ed_block_name); - let pkey_tuple_clause_from_records = self.table.to_primary_key_tuple_clause("__records"); + self.source.table.to_primary_key_tuple_clause("ed_block_name); + let pkey_tuple_clause_from_records = self.source.table.to_primary_key_tuple_clause("__records"); let pagination_clause = { - let order_by = match is_reverse_pagination { + let order_by = match self.is_reverse_pagination() { true => self.order_by.reverse(), false => self.order_by.clone(), }; match cursor { - Some(cursor) => self.table.to_pagination_clause( + Some(cursor) => self.source.table.to_pagination_clause( "ed_block_name, &order_by, cursor, @@ -1020,6 +886,8 @@ impl ConnectionBuilder { } }; + let limit = self.limit_clause(); + // initialized assuming forwards pagination let mut has_next_page_query = format!( " @@ -1027,9 +895,10 @@ impl ConnectionBuilder { select 1 from - {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + {from_clause} where - {where_clause} + {join_clause} + and {where_clause} and {pagination_clause} order by {order_by_clause} @@ -1044,11 +913,12 @@ impl ConnectionBuilder { select not ({pkey_tuple_clause_from_block} = any( __records.seen )) is_pkey_in_records from - {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + {from_clause} left join (select array_agg({pkey_tuple_clause_from_records}) from __records ) __records(seen) on true where - {where_clause} + {join_clause} + and {where_clause} order by {order_by_clause_records} limit 1 @@ -1056,7 +926,7 @@ impl ConnectionBuilder { select coalesce(bool_and(is_pkey_in_records), false) from page_minus_1 "); - if is_reverse_pagination { + if self.is_reverse_pagination() { // Reverse has_next_page and has_previous_page std::mem::swap(&mut has_next_page_query, &mut has_prev_page_query); } @@ -1074,9 +944,10 @@ impl ConnectionBuilder { select {selectable_columns_clause} from - {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + {from_clause} where true + and {join_clause} and {where_clause} and {pagination_clause} order by @@ -1088,9 +959,10 @@ impl ConnectionBuilder { select count(*) from - {quoted_schema}.{quoted_func}({input_block_name}) {quoted_block_name} + {from_clause} where {requested_total} -- skips total when not requested + and {join_clause} and {where_clause} ), __has_next_page(___has_next_page) as ( @@ -1110,11 +982,12 @@ impl ConnectionBuilder { )" )) } + } impl QueryEntrypoint for ConnectionBuilder { fn to_sql_entrypoint(&self, param_context: &mut ParamContext) -> Result { - self.to_sql(None, param_context) + self.to_sql(None, param_context, None) } } @@ -1403,7 +1276,7 @@ impl NodeSelection { Self::Connection(builder) => format!( "{}, {}", quote_literal(&builder.alias), - builder.to_sql(Some(block_name), param_context)? + builder.to_sql(Some(block_name), param_context, None)? ), Self::Node(builder) => format!( "{}, {}", @@ -1503,11 +1376,13 @@ impl FunctionBuilder { ) } FunctionSelection::Connection(connection_builder) => connection_builder - .to_sql_from_func( - block_name, - &self.function.schema_name, - &self.function.name, + .to_sql( + None, param_context, + Some(FromFunction { + function: Arc::clone(&self.function), + input_block_name: block_name.to_string() + }) )?, }; Ok(sql_frag) From d7104d576b5df09074ae4013a74e5e3867accdf6 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 20 Apr 2023 14:20:55 -0500 Subject: [PATCH 07/12] bugfix: input table type on computed relationshps --- src/transpile.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/transpile.rs b/src/transpile.rs index 28533e10..3a886b6d 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -114,7 +114,8 @@ pub trait QueryEntrypoint { match spi_result { Ok(Some(jsonb)) => Ok(jsonb.0), Ok(None) => Ok(serde_json::Value::Null), - _ => Err("Internal Error: Failed to execute transpiled query".to_string()), + Err(e) => Err(format!("{sql}")), + //_ => Err("Internal Error: Failed to execute transpiled query".to_string()), } } } @@ -728,8 +729,9 @@ impl FilterBuilder { pub struct FromFunction { function: Arc, + input_table: Arc
, // The block name for the functions argument - input_block_name: String + input_block_name: String, } impl ConnectionBuilder { @@ -819,7 +821,9 @@ impl ConnectionBuilder { let quoted_func_schema = quote_ident(&from_function.function.schema_name); let quoted_func = quote_ident(&from_function.function.name); let input_block_name = &from_function.input_block_name; - format!("{quoted_func_schema}.{quoted_func}({input_block_name}::{quoted_schema}.{quoted_table}) {quoted_block_name}") + let quoted_input_schema = quote_ident(&from_function.input_table.schema); + let quoted_input_table = quote_ident(&from_function.input_table.name); + format!("{quoted_func_schema}.{quoted_func}({input_block_name}::{quoted_input_schema}.{quoted_input_table}) {quoted_block_name}") } None => { format!("{quoted_schema}.{quoted_table} {quoted_block_name}") @@ -1375,15 +1379,18 @@ impl FunctionBuilder { " ) } - FunctionSelection::Connection(connection_builder) => connection_builder + FunctionSelection::Connection(connection_builder) => { + connection_builder .to_sql( None, param_context, Some(FromFunction { function: Arc::clone(&self.function), + input_table: Arc::clone(&self.table), input_block_name: block_name.to_string() }) - )?, + )? + } }; Ok(sql_frag) } From 6b41e643fa525fe41ba7c8e799e36e4939eb86e4 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 27 Apr 2023 11:47:32 -0500 Subject: [PATCH 08/12] tests passing --- sql/load_sql_context.sql | 17 +- src/graphql.rs | 265 ++++++++----- src/sql_types.rs | 187 +-------- src/transpile.rs | 61 +-- .../extend_type_with_function_relation.out | 369 ++++++++++++++++++ .../issue_339_function_return_table.out | 37 +- .../extend_type_with_function_relation.sql | 151 +++++++ test/sql/issue_339_function_return_table.sql | 27 +- test/sql/issue_347.sql | 102 ----- 9 files changed, 748 insertions(+), 468 deletions(-) create mode 100644 test/expected/extend_type_with_function_relation.out create mode 100644 test/sql/extend_type_with_function_relation.sql delete mode 100644 test/sql/issue_347.sql diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql index 03ee0544..19e7b398 100644 --- a/sql/load_sql_context.sql +++ b/sql/load_sql_context.sql @@ -53,8 +53,6 @@ select pg_enum pe join pg_type pt on pt.oid = pe.enumtypid - join schemas_ spo - on pt.typnamespace = spo.oid group by pt.oid ) @@ -78,8 +76,9 @@ select 'category', case when pt.typcategory = 'A' then 'Array' when pt.typcategory = 'E' then 'Enum' - when pt.typcategory = 'C' and tabs.oid is not null then 'Table' - when pt.typcategory = 'C' then 'Composite' + when pt.typcategory = 'C' + and tabs.relkind in ('r', 't', 'v', 'm', 'f', 'p') then 'Table' + when pt.typcategory = 'C' and tabs.relkind = 'c' then 'Composite' else 'Other' end, -- if category is 'Array', points at the underlying element type @@ -97,8 +96,6 @@ select ) from pg_type pt - join schemas_ spo - on pt.typnamespace = spo.oid left join pg_class tabs on pt.typrelid = tabs.oid ), @@ -115,10 +112,12 @@ select ) from pg_type pt - join schemas_ spo - on pt.typnamespace = spo.oid + join pg_class tabs + on pt.typrelid = tabs.oid where - pt.typtype = 'c' + pt.typcategory = 'C' + and tabs.relkind = 'c' + ), jsonb_build_array() ), diff --git a/src/graphql.rs b/src/graphql.rs index 1a1c0f41..e62470c0 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1543,10 +1543,19 @@ impl Type { max_characters: Option, is_set_of: bool, schema: &Arc<__Schema>, - ) -> __Type { + ) -> Option<__Type> { + if is_set_of && !(self.category == TypeCategory::Table) { + // If a function returns a pseudotype with a single column + // e.g. table( id int ) + // postgres records that in pg_catalog as returning a setof int + // we don't support pseudo type returns, but this was sneaking through + // because it looks like a concrete type + return None; + } + match self.category { TypeCategory::Other => { - match self.oid { + Some(match self.oid { 20 => __Type::Scalar(Scalar::BigInt), // bigint 16 => __Type::Scalar(Scalar::Boolean), // boolean 1082 => __Type::Scalar(Scalar::Date), // date @@ -1565,78 +1574,89 @@ impl Type { // char, bpchar, varchar 18 | 1042 | 1043 => __Type::Scalar(Scalar::String(max_characters)), _ => __Type::Scalar(Scalar::Opaque), - } + }) } TypeCategory::Array => match self.array_element_type_oid { Some(array_element_type_oid) => { - let sql_types = schema.context.types(); + let sql_types = &schema.context.types; let element_sql_type: Option<&Arc> = sql_types.get(&array_element_type_oid); let inner_graphql_type: __Type = match element_sql_type { Some(sql_type) => match sql_type.permissions.is_usable { - true => sql_type.to_graphql_type(None, false, schema), - false => __Type::Scalar(Scalar::Opaque), + true => match sql_type.to_graphql_type(None, false, schema) { + None => { + return None; + } + Some(inner_type) => inner_type, + }, + false => { + return None; + } }, None => __Type::Scalar(Scalar::Opaque), }; - __Type::List(ListType { + Some(__Type::List(ListType { type_: Box::new(inner_graphql_type), - }) + })) } - None => __Type::List(ListType { - type_: Box::new(__Type::Scalar(Scalar::Opaque)), - }), + // should not hpapen + None => None, }, TypeCategory::Enum => match schema.context.enums.get(&self.oid) { - Some(enum_) => __Type::Enum(EnumType { + Some(enum_) => Some(__Type::Enum(EnumType { enum_: EnumSource::Enum(Arc::clone(enum_)), schema: schema.clone(), - }), - None => __Type::Scalar(Scalar::Opaque), + })), + None => Some(__Type::Scalar(Scalar::Opaque)), }, - // TODO TypeCategory::Table => { match self.table_oid { - // no guarentees of whats going on here. - None => __Type::Scalar(Scalar::Opaque), + // Shouldn't happen + None => None, Some(table_oid) => match schema.context.tables.get(&table_oid) { - None => __Type::Scalar(Scalar::Opaque), + // Can happen if search path doesn't include referenced table + None => None, Some(table) => match is_set_of { - true => __Type::Connection(ConnectionType { + true => Some(__Type::Connection(ConnectionType { table: Arc::clone(table), fkey: None, schema: Arc::clone(schema), - }), - false => __Type::Node(NodeType { + })), + false => Some(__Type::Node(NodeType { table: Arc::clone(table), fkey: None, reverse_reference: None, schema: Arc::clone(schema), - }), + })), }, }, } } // Composites not yet supported - TypeCategory::Composite => __Type::Scalar(Scalar::Opaque), + TypeCategory::Composite => None, + // Psudotypes like "record" are not supported + TypeCategory::Pseudo => None, } } } -pub fn sql_column_to_graphql_type(col: &Column, schema: &Arc<__Schema>) -> __Type { - let sql_types = schema.context.types(); - let sql_type = sql_types.get(&col.type_oid); +pub fn sql_column_to_graphql_type(col: &Column, schema: &Arc<__Schema>) -> Option<__Type> { + let sql_type = schema.context.types.get(&col.type_oid); if sql_type.is_none() { - return __Type::Scalar(Scalar::Opaque); + // Should never happen + return None; } let sql_type = sql_type.unwrap(); - let type_w_list_mod = sql_type.to_graphql_type(col.max_characters, false, schema); - match col.is_not_null { - true => __Type::NonNull(NonNullType { - type_: Box::new(type_w_list_mod), - }), - _ => type_w_list_mod, + let maybe_type_w_list_mod = sql_type.to_graphql_type(col.max_characters, false, schema); + match maybe_type_w_list_mod { + None => None, + Some(type_with_list_mod) => match col.is_not_null { + true => Some(__Type::NonNull(NonNullType { + type_: Box::new(type_with_list_mod), + })), + _ => Some(type_with_list_mod), + }, } } @@ -1675,13 +1695,19 @@ impl ___Type for NodeType { .iter() .filter(|x| x.permissions.is_selectable) .filter(|x| !self.schema.context.is_composite(x.type_oid)) - .map(|col| __Field { - name_: self.schema.graphql_column_field_name(&col), - type_: sql_column_to_graphql_type(col, &self.schema), - args: vec![], - description: col.directives.description.clone(), - deprecation_reason: None, - sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + .filter_map(|col| { + if let Some(utype) = sql_column_to_graphql_type(col, &self.schema) { + Some(__Field { + name_: self.schema.graphql_column_field_name(&col), + type_: utype, + args: vec![], + description: col.directives.description.clone(), + deprecation_reason: None, + sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + }) + } else { + None + } }) .filter(|x| is_valid_graphql_name(&x.name_)) .collect(); @@ -1709,6 +1735,7 @@ impl ___Type for NodeType { node_id_field.push(node_id); }; + let sql_types = &self.schema.context.types; // Functions require selecting an entire row. the whole table must be selectable // for functions to work let mut function_fields: Vec<__Field> = vec![]; @@ -1718,30 +1745,44 @@ impl ___Type for NodeType { .functions .iter() .filter(|x| x.permissions.is_executable) - .map(|func| { - let sql_types = self.schema.context.types(); - let sql_ret_type = sql_types.get(&func.type_oid); - let gql_ret_type = match sql_ret_type { - None => __Type::Scalar(Scalar::Opaque), + .filter(|func| { + // TODO: remove in favor of making `to_sql_type` return an Option + // so we can optionally remove inappropriate types + match sql_types.get(&func.type_oid) { + None => true, Some(sql_type) => { - sql_type.to_graphql_type(None, func.is_set_of, &self.schema) + // disallow pseudo types + match &sql_type.category { + TypeCategory::Pseudo => false, + _ => true, + } } - }; - - let gql_args = match &gql_ret_type { - __Type::Connection(connection_type) => { - connection_type.get_connection_input_args() + } + }) + .filter_map(|func| match sql_types.get(&func.type_oid) { + None => None, + Some(sql_type) => { + if let Some(gql_ret_type) = + sql_type.to_graphql_type(None, func.is_set_of, &self.schema) + { + let gql_args = match &gql_ret_type { + __Type::Connection(connection_type) => { + connection_type.get_connection_input_args() + } + _ => vec![], + }; + + Some(__Field { + name_: self.schema.graphql_function_field_name(&func), + type_: gql_ret_type, + args: gql_args, + description: func.directives.description.clone(), + deprecation_reason: None, + sql_type: Some(NodeSQLType::Function(Arc::clone(func))), + }) + } else { + None } - _ => vec![], - }; - - __Field { - name_: self.schema.graphql_function_field_name(&func), - type_: gql_ret_type, - args: gql_args, - description: func.directives.description.clone(), - deprecation_reason: None, - sql_type: Some(NodeSQLType::Function(Arc::clone(func))), } }) .filter(|x| is_valid_graphql_name(&x.name_)) @@ -1825,9 +1866,10 @@ impl ___Type for NodeType { false => { let connection_type = ConnectionType { table: Arc::clone(foreign_table), - fkey: Some( - ForeignKeyReversible { fkey: Arc::clone(fkey), reverse_reference: reverse_reference } - ), + fkey: Some(ForeignKeyReversible { + fkey: Arc::clone(fkey), + reverse_reference: reverse_reference, + }), schema: Arc::clone(&self.schema), }; let connection_args = connection_type.get_connection_input_args(); @@ -2730,15 +2772,20 @@ impl ___Type for InsertInputType { .filter(|x| !x.is_generated) .filter(|x| !x.is_serial) .filter(|x| !self.schema.context.is_composite(x.type_oid)) - // TODO: not composite - .map(|col| __InputValue { - name_: self.schema.graphql_column_field_name(&col), - // 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_: sql_column_to_graphql_type(col, &self.schema).nullable_type(), - description: None, - default_value: None, - sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + .filter_map(|col| { + if let Some(utype) = sql_column_to_graphql_type(col, &self.schema) { + Some(__InputValue { + name_: self.schema.graphql_column_field_name(&col), + // 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_: utype.nullable_type(), + description: None, + default_value: None, + sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + }) + } else { + None + } }) .collect(), ) @@ -2817,13 +2864,19 @@ impl ___Type for UpdateInputType { .filter(|x| !x.is_generated) .filter(|x| !x.is_serial) .filter(|x| !self.schema.context.is_composite(x.type_oid)) - .map(|col| __InputValue { - name_: self.schema.graphql_column_field_name(&col), - // TODO: handle possible array inputs - type_: sql_column_to_graphql_type(col, &self.schema).nullable_type(), - description: None, - default_value: None, - sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + .filter_map(|col| { + if let Some(utype) = sql_column_to_graphql_type(col, &self.schema) { + Some(__InputValue { + name_: self.schema.graphql_column_field_name(&col), + // TODO: handle possible array inputs + type_: utype.nullable_type(), + description: None, + default_value: None, + sql_type: Some(NodeSQLType::Column(Arc::clone(col))), + }) + } else { + None + } }) .collect(), ) @@ -3242,33 +3295,35 @@ impl ___Type for FilterEntityType { .filter(|x| !vec!["json", "jsonb"].contains(&x.type_name.as_ref())) .filter_map(|col| { // Should be a scalar - let utype = sql_column_to_graphql_type(col, &self.schema).unmodified_type(); - - let column_graphql_name = self.schema.graphql_column_field_name(col); - - match utype { - __Type::Scalar(s) => Some(__InputValue { - name_: column_graphql_name, - type_: __Type::FilterType(FilterTypeType { - entity: FilterableType::Scalar(s), - schema: Arc::clone(&self.schema), + if let Some(utype) = sql_column_to_graphql_type(col, &self.schema) { + let column_graphql_name = self.schema.graphql_column_field_name(col); + + match utype.unmodified_type() { + __Type::Scalar(s) => Some(__InputValue { + name_: column_graphql_name, + type_: __Type::FilterType(FilterTypeType { + entity: FilterableType::Scalar(s), + schema: Arc::clone(&self.schema), + }), + description: None, + default_value: None, + sql_type: Some(NodeSQLType::Column(Arc::clone(col))), }), - description: None, - default_value: None, - sql_type: Some(NodeSQLType::Column(Arc::clone(col))), - }), - // ERROR HERE - __Type::Enum(s) => Some(__InputValue { - name_: column_graphql_name, - type_: __Type::FilterType(FilterTypeType { - entity: FilterableType::Enum(s), - schema: Arc::clone(&self.schema), + // ERROR HERE + __Type::Enum(s) => Some(__InputValue { + name_: column_graphql_name, + type_: __Type::FilterType(FilterTypeType { + entity: FilterableType::Enum(s), + schema: Arc::clone(&self.schema), + }), + description: None, + default_value: None, + sql_type: Some(NodeSQLType::Column(Arc::clone(col))), }), - description: None, - default_value: None, - sql_type: Some(NodeSQLType::Column(Arc::clone(col))), - }), - _ => None, + _ => None, + } + } else { + None } }) .filter(|x| is_valid_graphql_name(&x.name_)) diff --git a/src/sql_types.rs b/src/sql_types.rs index b37adac5..96209b67 100644 --- a/src/sql_types.rs +++ b/src/sql_types.rs @@ -2,7 +2,9 @@ use cached::proc_macro::cached; use cached::SizedCache; use pgx::*; use serde::{Deserialize, Serialize}; +use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use std::*; @@ -83,6 +85,7 @@ pub enum TypeCategory { Composite, Table, Array, + Pseudo, Other, } @@ -320,7 +323,7 @@ pub struct Context { pub schemas: HashMap, pub tables: HashMap>, foreign_keys: Vec>, - types: HashMap>, + pub types: HashMap>, pub enums: HashMap>, pub composites: Vec>, } @@ -485,185 +488,6 @@ impl Context { .iter() .all(|col| referenced_columns_selectable.contains(col)) } - - pub fn types(&self) -> HashMap> { - let mut types = self.types.clone(); - - for (oid, name, category, array_elem_oid) in vec![ - (16, "bool", TypeCategory::Other, None), - (17, "bytea", TypeCategory::Other, None), - (19, "name", TypeCategory::Other, Some(18)), - (20, "int8", TypeCategory::Other, None), - (21, "int2", TypeCategory::Other, None), - (22, "int2vector", TypeCategory::Array, Some(21)), - (23, "int4", TypeCategory::Other, None), - (24, "regproc", TypeCategory::Other, None), - (25, "text", TypeCategory::Other, None), - (26, "oid", TypeCategory::Other, None), - (27, "tid", TypeCategory::Other, None), - (28, "xid", TypeCategory::Other, None), - (29, "cid", TypeCategory::Other, None), - (30, "oidvector", TypeCategory::Array, Some(26)), - (114, "json", TypeCategory::Other, None), - (142, "xml", TypeCategory::Other, None), - (143, "_xml", TypeCategory::Array, Some(142)), - (199, "_json", TypeCategory::Array, Some(114)), - (210, "_pg_type", TypeCategory::Array, Some(71)), - (270, "_pg_attribute", TypeCategory::Array, Some(75)), - (271, "_xid8", TypeCategory::Array, Some(5069)), - (272, "_pg_proc", TypeCategory::Array, Some(81)), - (273, "_pg_class", TypeCategory::Array, Some(83)), - (600, "point", TypeCategory::Other, Some(701)), - (601, "lseg", TypeCategory::Other, Some(600)), - (602, "path", TypeCategory::Other, None), - (603, "box", TypeCategory::Other, Some(600)), - (604, "polygon", TypeCategory::Other, None), - (628, "line", TypeCategory::Other, Some(701)), - (629, "_line", TypeCategory::Array, Some(628)), - (650, "cidr", TypeCategory::Other, None), - (651, "_cidr", TypeCategory::Array, Some(650)), - (700, "float4", TypeCategory::Other, None), - (701, "float8", TypeCategory::Other, None), - (718, "circle", TypeCategory::Other, None), - (719, "_circle", TypeCategory::Array, Some(718)), - (774, "macaddr8", TypeCategory::Other, None), - (775, "_macaddr8", TypeCategory::Array, Some(774)), - (790, "money", TypeCategory::Other, None), - (791, "_money", TypeCategory::Array, Some(790)), - (829, "macaddr", TypeCategory::Other, None), - (869, "inet", TypeCategory::Other, None), - (1000, "_bool", TypeCategory::Array, Some(16)), - (1001, "_bytea", TypeCategory::Array, Some(17)), - (1002, "_char", TypeCategory::Array, Some(18)), - (1003, "_name", TypeCategory::Array, Some(19)), - (1005, "_int2", TypeCategory::Array, Some(21)), - (1006, "_int2vector", TypeCategory::Array, Some(22)), - (1007, "_int4", TypeCategory::Array, Some(23)), - (1008, "_regproc", TypeCategory::Array, Some(24)), - (1009, "_text", TypeCategory::Array, Some(25)), - (1010, "_tid", TypeCategory::Array, Some(27)), - (1011, "_xid", TypeCategory::Array, Some(28)), - (1012, "_cid", TypeCategory::Array, Some(29)), - (1013, "_oidvector", TypeCategory::Array, Some(30)), - (1014, "_bpchar", TypeCategory::Array, Some(1042)), - (1015, "_varchar", TypeCategory::Array, Some(1043)), - (1016, "_int8", TypeCategory::Array, Some(20)), - (1017, "_point", TypeCategory::Array, Some(600)), - (1018, "_lseg", TypeCategory::Array, Some(601)), - (1019, "_path", TypeCategory::Array, Some(602)), - (1020, "_box", TypeCategory::Array, Some(603)), - (1021, "_float4", TypeCategory::Array, Some(700)), - (1022, "_float8", TypeCategory::Array, Some(701)), - (1027, "_polygon", TypeCategory::Array, Some(604)), - (1028, "_oid", TypeCategory::Array, Some(26)), - (1033, "aclitem", TypeCategory::Other, None), - (1034, "_aclitem", TypeCategory::Array, Some(1033)), - (1040, "_macaddr", TypeCategory::Array, Some(829)), - (1041, "_inet", TypeCategory::Array, Some(869)), - (1042, "bpchar", TypeCategory::Other, None), - (1043, "varchar", TypeCategory::Other, None), - (1082, "date", TypeCategory::Other, None), - (1083, "time", TypeCategory::Other, None), - (1114, "timestamp", TypeCategory::Other, None), - (1115, "_timestamp", TypeCategory::Array, Some(1114)), - (1182, "_date", TypeCategory::Array, Some(1082)), - (1183, "_time", TypeCategory::Array, Some(1083)), - (1184, "timestamptz", TypeCategory::Other, None), - (1185, "_timestamptz", TypeCategory::Array, Some(1184)), - (1186, "interval", TypeCategory::Other, None), - (1187, "_interval", TypeCategory::Array, Some(1186)), - (1231, "_numeric", TypeCategory::Array, Some(1700)), - (1263, "_cstring", TypeCategory::Array, Some(2275)), - (1266, "timetz", TypeCategory::Other, None), - (1270, "_timetz", TypeCategory::Array, Some(1266)), - (1560, "bit", TypeCategory::Other, None), - (1561, "_bit", TypeCategory::Array, Some(1560)), - (1562, "varbit", TypeCategory::Other, None), - (1563, "_varbit", TypeCategory::Array, Some(1562)), - (1700, "numeric", TypeCategory::Other, None), - (1790, "refcursor", TypeCategory::Other, None), - (2201, "_refcursor", TypeCategory::Array, Some(1790)), - (2202, "regprocedure", TypeCategory::Other, None), - (2203, "regoper", TypeCategory::Other, None), - (2204, "regoperator", TypeCategory::Other, None), - (2205, "regclass", TypeCategory::Other, None), - (2206, "regtype", TypeCategory::Other, None), - (2207, "_regprocedure", TypeCategory::Array, Some(2202)), - (2208, "_regoper", TypeCategory::Array, Some(2203)), - (2209, "_regoperator", TypeCategory::Array, Some(2204)), - (2210, "_regclass", TypeCategory::Array, Some(2205)), - (2211, "_regtype", TypeCategory::Array, Some(2206)), - (2949, "_txid_snapshot", TypeCategory::Array, Some(2970)), - (2950, "uuid", TypeCategory::Other, None), - (2951, "_uuid", TypeCategory::Array, Some(2950)), - (2970, "txid_snapshot", TypeCategory::Other, None), - (3220, "pg_lsn", TypeCategory::Other, None), - (3221, "_pg_lsn", TypeCategory::Array, Some(3220)), - (3614, "tsvector", TypeCategory::Other, None), - (3615, "tsquery", TypeCategory::Other, None), - (3642, "gtsvector", TypeCategory::Other, None), - (3643, "_tsvector", TypeCategory::Array, Some(3614)), - (3644, "_gtsvector", TypeCategory::Array, Some(3642)), - (3645, "_tsquery", TypeCategory::Array, Some(3615)), - (3734, "regconfig", TypeCategory::Other, None), - (3735, "_regconfig", TypeCategory::Array, Some(3734)), - (3769, "regdictionary", TypeCategory::Other, None), - (3770, "_regdictionary", TypeCategory::Array, Some(3769)), - (3802, "jsonb", TypeCategory::Other, None), - (3807, "_jsonb", TypeCategory::Array, Some(3802)), - (3904, "int4range", TypeCategory::Other, None), - (3905, "_int4range", TypeCategory::Array, Some(3904)), - (3906, "numrange", TypeCategory::Other, None), - (3907, "_numrange", TypeCategory::Array, Some(3906)), - (3908, "tsrange", TypeCategory::Other, None), - (3909, "_tsrange", TypeCategory::Array, Some(3908)), - (3910, "tstzrange", TypeCategory::Other, None), - (3911, "_tstzrange", TypeCategory::Array, Some(3910)), - (3912, "daterange", TypeCategory::Other, None), - (3913, "_daterange", TypeCategory::Array, Some(3912)), - (3926, "int8range", TypeCategory::Other, None), - (3927, "_int8range", TypeCategory::Array, Some(3926)), - (4072, "jsonpath", TypeCategory::Other, None), - (4073, "_jsonpath", TypeCategory::Array, Some(4072)), - (4089, "regnamespace", TypeCategory::Other, None), - (4090, "_regnamespace", TypeCategory::Array, Some(4089)), - (4096, "regrole", TypeCategory::Other, None), - (4097, "_regrole", TypeCategory::Array, Some(4096)), - (4191, "regcollation", TypeCategory::Other, None), - (4192, "_regcollation", TypeCategory::Array, Some(4191)), - (4451, "int4multirange", TypeCategory::Other, None), - (4532, "nummultirange", TypeCategory::Other, None), - (4533, "tsmultirange", TypeCategory::Other, None), - (4534, "tstzmultirange", TypeCategory::Other, None), - (4535, "datemultirange", TypeCategory::Other, None), - (4536, "int8multirange", TypeCategory::Other, None), - (5038, "pg_snapshot", TypeCategory::Other, None), - (5039, "_pg_snapshot", TypeCategory::Array, Some(5038)), - (5069, "xid8", TypeCategory::Other, None), - (6150, "_int4multirange", TypeCategory::Array, Some(4451)), - (6151, "_nummultirange", TypeCategory::Array, Some(4532)), - (6152, "_tsmultirange", TypeCategory::Array, Some(4533)), - (6153, "_tstzmultirange", TypeCategory::Array, Some(4534)), - (6155, "_datemultirange", TypeCategory::Array, Some(4535)), - (6157, "_int8multirange", TypeCategory::Array, Some(4536)), - ] { - types.insert( - oid, - Arc::new(Type { - oid, - schema_oid: 11, - name: name.to_string(), - category, - table_oid: None, - comment: None, - permissions: TypePermissions { is_usable: true }, - directives: EnumDirectives { name: None }, - array_element_type_oid: array_elem_oid, - }), - ); - } - types - } } pub fn load_sql_config() -> Config { @@ -673,9 +497,6 @@ pub fn load_sql_config() -> Config { config } -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; - pub fn calculate_hash(t: &T) -> u64 { let mut s = DefaultHasher::new(); t.hash(&mut s); diff --git a/src/transpile.rs b/src/transpile.rs index 3a886b6d..ad446789 100644 --- a/src/transpile.rs +++ b/src/transpile.rs @@ -1,6 +1,6 @@ use crate::builder::*; use crate::graphql::*; -use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Table, Function}; +use crate::sql_types::{Column, ForeignKey, ForeignKeyTableInfo, Function, Table}; use pgx::pg_sys::submodules::panic::CaughtError; use pgx::pg_sys::PgBuiltInOids; use pgx::prelude::*; @@ -114,8 +114,7 @@ pub trait QueryEntrypoint { match spi_result { Ok(Some(jsonb)) => Ok(jsonb.0), Ok(None) => Ok(serde_json::Value::Null), - Err(e) => Err(format!("{sql}")), - //_ => Err("Internal Error: Failed to execute transpiled query".to_string()), + _ => Err("Internal Error: Failed to execute transpiled query".to_string()), } } } @@ -769,7 +768,11 @@ impl ConnectionBuilder { self.last.is_some() || self.before.is_some() } - fn to_join_clause(&self, quoted_block_name: &str, quoted_parent_block_name: &Option<&str>) -> Result { + fn to_join_clause( + &self, + quoted_block_name: &str, + quoted_parent_block_name: &Option<&str>, + ) -> Result { match &self.source.fkey { Some(fkey) => { let quoted_parent_block_name = quoted_parent_block_name @@ -785,8 +788,11 @@ impl ConnectionBuilder { } } - - fn object_clause(&self, quoted_block_name: &str, param_context: &mut ParamContext) -> Result { + fn object_clause( + &self, + quoted_block_name: &str, + param_context: &mut ParamContext, + ) -> Result { let frags: Vec = self .selections .iter() @@ -811,8 +817,7 @@ impl ConnectionBuilder { ) } - fn from_clause(&self, quoted_block_name: &str, function: &Option) -> String{ - + fn from_clause(&self, quoted_block_name: &str, function: &Option) -> String { let quoted_schema = quote_ident(&self.source.table.schema); let quoted_table = quote_ident(&self.source.table.name); @@ -831,12 +836,11 @@ impl ConnectionBuilder { } } - pub fn to_sql( &self, quoted_parent_block_name: Option<&str>, param_context: &mut ParamContext, - from_func: Option + from_func: Option, ) -> Result { let quoted_block_name = rand_block_name(); @@ -869,9 +873,12 @@ impl ConnectionBuilder { let selectable_columns_clause = self.source.table.to_selectable_columns_clause(); - let pkey_tuple_clause_from_block = - self.source.table.to_primary_key_tuple_clause("ed_block_name); - let pkey_tuple_clause_from_records = self.source.table.to_primary_key_tuple_clause("__records"); + let pkey_tuple_clause_from_block = self + .source + .table + .to_primary_key_tuple_clause("ed_block_name); + let pkey_tuple_clause_from_records = + self.source.table.to_primary_key_tuple_clause("__records"); let pagination_clause = { let order_by = match self.is_reverse_pagination() { @@ -986,7 +993,6 @@ impl ConnectionBuilder { )" )) } - } impl QueryEntrypoint for ConnectionBuilder { @@ -1350,8 +1356,8 @@ impl FunctionBuilder { block_name: &str, param_context: &mut ParamContext, ) -> Result { - let schema_name = &self.function.schema_name; - let function_name = &self.function.name; + let schema_name = quote_ident(&self.function.schema_name); + let function_name = quote_ident(&self.function.name); let sql_frag = match &self.selection { FunctionSelection::ScalarSelf => format!( @@ -1375,22 +1381,21 @@ impl FunctionBuilder { {object_clause} from {from_clause} as {func_block_name} + where + {func_block_name} is not null ) " ) } - FunctionSelection::Connection(connection_builder) => { - connection_builder - .to_sql( - None, - param_context, - Some(FromFunction { - function: Arc::clone(&self.function), - input_table: Arc::clone(&self.table), - input_block_name: block_name.to_string() - }) - )? - } + FunctionSelection::Connection(connection_builder) => connection_builder.to_sql( + None, + param_context, + Some(FromFunction { + function: Arc::clone(&self.function), + input_table: Arc::clone(&self.table), + input_block_name: block_name.to_string(), + }), + )?, }; Ok(sql_frag) } diff --git a/test/expected/extend_type_with_function_relation.out b/test/expected/extend_type_with_function_relation.out new file mode 100644 index 00000000..8c27f47a --- /dev/null +++ b/test/expected/extend_type_with_function_relation.out @@ -0,0 +1,369 @@ +begin; + create table account( + id serial primary key, + email varchar(255) not null + ); + insert into account(email) values ('foo'), ('bar'), ('baz'); + create table blog( + id serial primary key, + name varchar(255) not null + ); + insert into blog(name) + select + 'blog ' || x + from + generate_series(1, 5) y(x); + create function public.many_blogs(public.account) + returns setof public.blog + language sql + as + $$ + select * from public.blog where id between $1.id * 4 - 4 and $1.id * 4; + $$; + -- To Many + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Account") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + jsonb_pretty +--------------------------------------------------- + { + + "data": { + + "__type": { + + "fields": [ + + { + + "name": "nodeId", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "ID" + + } + + } + + }, + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "Int" + + } + + } + + }, + + { + + "name": "email", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "String" + + } + + } + + }, + + { + + "name": "manyBlogs", + + "type": { + + "kind": "OBJECT", + + "name": "BlogConnection",+ + "ofType": null + + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty( + graphql.resolve($$ + { + accountCollection { + edges { + node { + id + manyBlogs(first: 2) { + pageInfo { + hasNextPage + } + edges { + node { + id + name + } + } + } + } + } + } + } + $$) + ); + jsonb_pretty +---------------------------------------------------------- + { + + "data": { + + "accountCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "manyBlogs": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "name": "blog 1"+ + } + + }, + + { + + "node": { + + "id": 2, + + "name": "blog 2"+ + } + + } + + ], + + "pageInfo": { + + "hasNextPage": true + + } + + } + + } + + }, + + { + + "node": { + + "id": 2, + + "manyBlogs": { + + "edges": [ + + { + + "node": { + + "id": 4, + + "name": "blog 4"+ + } + + }, + + { + + "node": { + + "id": 5, + + "name": "blog 5"+ + } + + } + + ], + + "pageInfo": { + + "hasNextPage": false + + } + + } + + } + + }, + + { + + "node": { + + "id": 3, + + "manyBlogs": { + + "edges": [ + + ], + + "pageInfo": { + + "hasNextPage": false + + } + + } + + } + + } + + ] + + } + + } + + } +(1 row) + + -- To One + create function public.one_account(public.blog) + returns public.account + language sql + as + $$ + select * from public.account where id = $1.id - 2; + $$; + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Blog") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + jsonb_pretty +----------------------------------------------- + { + + "data": { + + "__type": { + + "fields": [ + + { + + "name": "nodeId", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "ID" + + } + + } + + }, + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "Int" + + } + + } + + }, + + { + + "name": "name", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "String" + + } + + } + + }, + + { + + "name": "oneAccount", + + "type": { + + "kind": "OBJECT", + + "name": "Account", + + "ofType": null + + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty( + graphql.resolve($$ + { + blogCollection(first: 3) { + edges { + node { + id + oneAccount { + id + email + } + } + } + } + } + $$) + ); + jsonb_pretty +-------------------------------------------- + { + + "data": { + + "blogCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "oneAccount": null+ + } + + }, + + { + + "node": { + + "id": 2, + + "oneAccount": null+ + } + + }, + + { + + "node": { + + "id": 3, + + "oneAccount": { + + "id": 1, + + "email": "foo"+ + } + + } + + } + + ] + + } + + } + + } +(1 row) + + -- Confirm name overrides work + comment on function public.one_account(public.blog) is E'@graphql({"name": "acctOverride"})'; + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Blog") { + fields { + name + } + } + } + $$) + ); + jsonb_pretty +-------------------------------------------- + { + + "data": { + + "__type": { + + "fields": [ + + { + + "name": "nodeId" + + }, + + { + + "name": "id" + + }, + + { + + "name": "name" + + }, + + { + + "name": "acctOverride"+ + } + + ] + + } + + } + + } +(1 row) + +rollback; diff --git a/test/expected/issue_339_function_return_table.out b/test/expected/issue_339_function_return_table.out index ae499ce3..3f6716ae 100644 --- a/test/expected/issue_339_function_return_table.out +++ b/test/expected/issue_339_function_return_table.out @@ -2,6 +2,7 @@ begin; create table public.account( id int primary key ); + -- appears in pg_catalog as returning a set of int create function public._computed(rec public.account) returns table ( id int ) immutable @@ -10,7 +11,17 @@ begin; as $$ select 2 as id; $$; + -- appears in pg_catalog as returning a set of pseudotype "record" + create function public._computed2(rec public.account) + returns table ( id int, name text ) + immutable + strict + language sql + as $$ + select 2 as id, 'abc' as name; + $$; insert into account(id) values (1); + -- neither computed nor computed2 should be present select jsonb_pretty( graphql.resolve($$ { @@ -42,30 +53,4 @@ begin; } (1 row) - select jsonb_pretty( - graphql.resolve($$ - { - accountCollection { - edges { - node { - id - computed - } - } - } - } - $$) - ); - jsonb_pretty ---------------------------------------------------------------------- - { + - "data": null, + - "errors": [ + - { + - "message": "Unknown field 'computed' on type 'Account'"+ - } + - ] + - } -(1 row) - rollback; diff --git a/test/sql/extend_type_with_function_relation.sql b/test/sql/extend_type_with_function_relation.sql new file mode 100644 index 00000000..0bb6d7fb --- /dev/null +++ b/test/sql/extend_type_with_function_relation.sql @@ -0,0 +1,151 @@ +begin; + create table account( + id serial primary key, + email varchar(255) not null + ); + + insert into account(email) values ('foo'), ('bar'), ('baz'); + + create table blog( + id serial primary key, + name varchar(255) not null + ); + + insert into blog(name) + select + 'blog ' || x + from + generate_series(1, 5) y(x); + + + create function public.many_blogs(public.account) + returns setof public.blog + language sql + as + $$ + select * from public.blog where id between $1.id * 4 - 4 and $1.id * 4; + $$; + + -- To Many + + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Account") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + + select jsonb_pretty( + graphql.resolve($$ + { + accountCollection { + edges { + node { + id + manyBlogs(first: 2) { + pageInfo { + hasNextPage + } + edges { + node { + id + name + } + } + } + } + } + } + } + $$) + ); + + -- To One + + create function public.one_account(public.blog) + returns public.account + language sql + as + $$ + select * from public.account where id = $1.id - 2; + $$; + + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Blog") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + + + select jsonb_pretty( + graphql.resolve($$ + { + blogCollection(first: 3) { + edges { + node { + id + oneAccount { + id + email + } + } + } + } + } + $$) + ); + + -- Confirm name overrides work + comment on function public.one_account(public.blog) is E'@graphql({"name": "acctOverride"})'; + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Blog") { + fields { + name + } + } + } + $$) + ); + + +rollback; diff --git a/test/sql/issue_339_function_return_table.sql b/test/sql/issue_339_function_return_table.sql index 175c0f5c..c4db26ae 100644 --- a/test/sql/issue_339_function_return_table.sql +++ b/test/sql/issue_339_function_return_table.sql @@ -3,6 +3,7 @@ begin; id int primary key ); + -- appears in pg_catalog as returning a set of int create function public._computed(rec public.account) returns table ( id int ) immutable @@ -12,8 +13,19 @@ begin; select 2 as id; $$; + -- appears in pg_catalog as returning a set of pseudotype "record" + create function public._computed2(rec public.account) + returns table ( id int, name text ) + immutable + strict + language sql + as $$ + select 2 as id, 'abc' as name; + $$; + insert into account(id) values (1); + -- neither computed nor computed2 should be present select jsonb_pretty( graphql.resolve($$ { @@ -27,19 +39,4 @@ begin; $$) ); - select jsonb_pretty( - graphql.resolve($$ - { - accountCollection { - edges { - node { - id - computed - } - } - } - } - $$) - ); - rollback; diff --git a/test/sql/issue_347.sql b/test/sql/issue_347.sql deleted file mode 100644 index de290a38..00000000 --- a/test/sql/issue_347.sql +++ /dev/null @@ -1,102 +0,0 @@ -begin; - - create table public.ingredient ( - id bigint generated by default as identity not null, - created_at timestamp with time zone null default now(), - name text, - constraint ingredient_pkey primary key (id), - constraint ingredient_name_key unique (name) - ); - - create table public.recipe ( - id bigint generated by default as identity not null, - created_at timestamp with time zone default now(), - name text not null, - constraint recipe_pkey primary key (id), - constraint recipe_name_key unique (name) - ); - - create table public.recipe_ingredient ( - id bigint generated by default as identity not null, - created_at timestamp with time zone null default now(), - recipe_id bigint references recipe (id), - ingredient_id bigint references ingredient (id), - recipe_ingredient_id bigint, - constraint recipe_ingredient_pkey primary key (id), - constraint recipe_ingredient_recipe_ingredient_id_fkey foreign key (recipe_ingredient_id) references public.recipe (id) - ); - - comment on constraint recipe_ingredient_recipe_ingredient_id_fkey - on public.recipe_ingredient - is E'@graphql({"foreign_name": "recipeIngredient", "local_name": "someOtherName"})'; - - insert into public.recipe(name) - values ('BBQ Dry Rub'), ('Carolina BBQ Sauce'); - - insert into public.ingredient(id, name) - values (2, 'Smoked Paprika'); - - insert into recipe_ingredient(recipe_id, ingredient_id, recipe_ingredient_id) - values (2, 2, null), (2, null, 1); - - select jsonb_pretty( - graphql.resolve($$ - { - __type(name: "RecipeIngredient") { - kind - fields { - name - } - } - } - $$) - ); - - - select jsonb_pretty( - graphql.resolve($$ - { - __type(name: "Recipe") { - kind - fields { - name - } - } - } - $$) - ); - - select jsonb_pretty( - graphql.resolve($$ - query GetRecipe { - recipeCollection(filter: {id: {eq: 2}}) { - edges { - node { - id - name - recipeIngredientCollection { - edges { - node { - id - recipe { - name - } - ingredient { - name - } - recipeIngredient { - name - } - } - } - } - } - } - } - } - $$) - ); - - - -rollback; From 7e4b7d9f95ebf8f2e579fe5508ae8b04e341c3e5 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Mon, 1 May 2023 16:17:36 -0500 Subject: [PATCH 09/12] handle returns "setof ... rows 1" as returning 1 record --- sql/load_sql_context.sql | 7 +- .../extend_type_with_function_relation.out | 139 +++++++++++++++++- .../extend_type_with_function_relation.sql | 58 +++++++- 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql index 19e7b398..0da07090 100644 --- a/sql/load_sql_context.sql +++ b/sql/load_sql_context.sql @@ -249,7 +249,12 @@ select 'type_name', pp.prorettype::regtype::text, 'schema_oid', pronamespace::int, 'schema_name', pronamespace::regnamespace::text, - 'is_set_of', pp.proretset::bool, + -- Functions may be defined as "returns sefof rows 1" + -- those should return a single record, not a connection + -- this is important because set returning functions are inlined + -- and returning a single record isn't. + 'is_set_of', pp.proretset::bool and pp.prorows <> 1, + 'n_rows', pp.prorows::int, 'comment', pg_catalog.obj_description(pp.oid, 'pg_proc'), 'directives', ( with directives(directive) as ( diff --git a/test/expected/extend_type_with_function_relation.out b/test/expected/extend_type_with_function_relation.out index 8c27f47a..8298a62f 100644 --- a/test/expected/extend_type_with_function_relation.out +++ b/test/expected/extend_type_with_function_relation.out @@ -194,7 +194,8 @@ begin; } (1 row) - -- To One + -- To One (function returns single value) + savepoint a; create function public.one_account(public.blog) returns public.account language sql @@ -329,6 +330,142 @@ begin; } (1 row) + rollback to savepoint a; + -- To One (function returns set of <> rows 1) + create or replace function public.one_account(public.blog) + returns setof public.account rows 1 + language sql + as + $$ + select * from public.account where id = $1.id - 2; + $$; + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Blog") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + jsonb_pretty +----------------------------------------------- + { + + "data": { + + "__type": { + + "fields": [ + + { + + "name": "nodeId", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "ID" + + } + + } + + }, + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "Int" + + } + + } + + }, + + { + + "name": "name", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR",+ + "name": "String" + + } + + } + + }, + + { + + "name": "oneAccount", + + "type": { + + "kind": "OBJECT", + + "name": "Account", + + "ofType": null + + } + + } + + ] + + } + + } + + } +(1 row) + + select jsonb_pretty( + graphql.resolve($$ + { + blogCollection(first: 3) { + edges { + node { + id + oneAccount { + id + email + } + } + } + } + } + $$) + ); + jsonb_pretty +-------------------------------------------- + { + + "data": { + + "blogCollection": { + + "edges": [ + + { + + "node": { + + "id": 1, + + "oneAccount": null+ + } + + }, + + { + + "node": { + + "id": 2, + + "oneAccount": null+ + } + + }, + + { + + "node": { + + "id": 3, + + "oneAccount": { + + "id": 1, + + "email": "foo"+ + } + + } + + } + + ] + + } + + } + + } +(1 row) + -- Confirm name overrides work comment on function public.one_account(public.blog) is E'@graphql({"name": "acctOverride"})'; select jsonb_pretty( diff --git a/test/sql/extend_type_with_function_relation.sql b/test/sql/extend_type_with_function_relation.sql index 0bb6d7fb..2f05de0c 100644 --- a/test/sql/extend_type_with_function_relation.sql +++ b/test/sql/extend_type_with_function_relation.sql @@ -78,7 +78,8 @@ begin; $$) ); - -- To One + -- To One (function returns single value) + savepoint a; create function public.one_account(public.blog) returns public.account @@ -114,6 +115,61 @@ begin; ); + select jsonb_pretty( + graphql.resolve($$ + { + blogCollection(first: 3) { + edges { + node { + id + oneAccount { + id + email + } + } + } + } + } + $$) + ); + + rollback to savepoint a; + + -- To One (function returns set of <> rows 1) + create or replace function public.one_account(public.blog) + returns setof public.account rows 1 + language sql + as + $$ + select * from public.account where id = $1.id - 2; + $$; + + select jsonb_pretty( + graphql.resolve($$ + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + } + } + + { + __type(name: "Blog") { + fields { + name + type { + ...TypeRef + } + } + } + } + $$) + ); + + select jsonb_pretty( graphql.resolve($$ { From 8783e4ba9a4513f6ef887090dab10fa84f435f27 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Tue, 2 May 2023 13:25:35 -0500 Subject: [PATCH 10/12] docs for computed relation --- docs/computed_fields.md | 237 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 235 insertions(+), 2 deletions(-) diff --git a/docs/computed_fields.md b/docs/computed_fields.md index b88d167e..e835e199 100644 --- a/docs/computed_fields.md +++ b/docs/computed_fields.md @@ -1,4 +1,6 @@ -## PostgreSQL Builtin (Preferred) +## Computed Values + +### PostgreSQL Builtin (Preferred) PostgreSQL has a builtin method for adding [generated columns](https://www.postgresql.org/docs/14/ddl-generated-columns.html) to tables. Generated columns are reflected identically to non-generated columns. This is the recommended approach to adding computed fields when your computation meets the restrictions. Namely: @@ -11,7 +13,7 @@ For example: ``` -## Extending Types with Functions +### Extending Types with Functions For arbitrary computations that do not meet the requirements for [generated columns](https://www.postgresql.org/docs/14/ddl-generated-columns.html), a table's reflected GraphQL type can be extended by creating a function that: @@ -20,3 +22,234 @@ For arbitrary computations that do not meet the requirements for [generated colu ```sql --8<-- "test/expected/extend_type_with_function.out" ``` + + +## Computed Relationships + +Computed relations can be helpful to express relationships: + +- between entities that don't support foreign keys +- too complex to be expressed via a foreign key + +If the relationship is simple, but involves an entity that does not support foreign keys e.g. Foreign Data Wrappers / Views, defining a comment directive is the easiest solution. See the [view doc](/pg_graphql/views) for a complete example. Note that for entities that do not support a primary key, like views, you must define one using a [comment directive](/pg_graphql/configuration/#comment-directives) to use them in a computed relationship. + +Alternatively, if the relationship is complex, or you need compatibility with PostgREST, you can define a relationship using set returning functions. + + +### To-One + +To One relationships can be defined using a function that returns `setof rows 1` + +For example +```sql +create table "Person" ( + id int primary key, + name text +); + +create table "Address"( + id int primary key, + "isPrimary" bool not null default false, + "personId" int references "Person"(id), + address text +); + +-- Example computed relation +create function "primaryAddress"("Person") + returns setof "Address" rows 1 + language sql + as +$$ + select addr + from "Address" addr + where $1.id = addr."personId" + and addr."isPrimary" + limit 1 +$$; + +insert into "Person"(id, name) +values (1, 'Foo Barington'); + +insert into "Address"(id, "isPrimary", "personId", address) +values (4, true, 1, '1 Main St.'); +``` + +results in the GraphQL type + +=== "Person" + ```graphql + type Person implements Node { + """Globally Unique Record Identifier""" + nodeId: ID! + ... + primaryAddress: Address + } + ``` + +and can be queried like a natively enforced relationship + +=== "Query" + + ```graphql + { + personCollection { + edges { + node { + id + name + primaryAddress { + address + } + } + } + + } + } + ``` + +=== "Response" + + ```json + { + "data": { + "personCollection": { + "edges": [ + { + "node": { + "id": 1, + "name": "Foo Barington", + "primaryAddress": { + "address": "1 Main St." + } + } + } + ] + } + } + } + ``` + + + +### To-Many + +To-many relationships can be defined using a function that returns a `setof ` + + +For example: +```sql +create table "Person" ( + id int primary key, + name text +); + +create table "Address"( + id int primary key, + address text +); + +create table "PersonAtAddress"( + id int primary key, + "personId" int not null, + "addressId" int not null +); + + +-- Computed relation to bypass "PersonAtAddress" table for cleaner API +create function "addresses"("Person") + returns setof "Address" + language sql + as +$$ + select + addr + from + "PersonAtAddress" pa + join "Address" addr + on pa."addressId" = "addr".id + where + pa."personId" = $1.id +$$; + +insert into "Person"(id, name) +values (1, 'Foo Barington'); + +insert into "Address"(id, address) +values (4, '1 Main St.'); + +insert into "PersonAtAddress"(id, "personId", "addressId") +values (2, 1, 4); +``` + +results in the GraphQL type + +=== "Person" + ```graphql + type Person implements Node { + """Globally Unique Record Identifier""" + nodeId: ID! + ... + addresses( + first: Int + last: Int + before: Cursor + after: Cursor + filter: AddressFilter + orderBy: [AddressOrderBy!] + ): AddressConnection + } + ``` + +and can be queried like a natively enforced relationship + +=== "Query" + + ```graphql + { + personCollection { + edges { + node { + id + name + addresses { + edges { + node { + id + address + } + } + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "data": { + "personCollection": { + "edges": [ + { + "node": { + "id": 1, + "name": "Foo Barington", + "addresses": { + "edges": [ + { + "node": { + "id": 4, + "address": "1 Main St." + } + } + ] + } + } + } + ] + } + } + } + ``` From 2b451c0f82769a5269d7e0fb51225a72956122f1 Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 4 May 2023 12:55:12 -0500 Subject: [PATCH 11/12] doc tweaks --- docs/computed_fields.md | 2 +- sql/load_sql_context.sql | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/computed_fields.md b/docs/computed_fields.md index e835e199..98e24220 100644 --- a/docs/computed_fields.md +++ b/docs/computed_fields.md @@ -17,7 +17,7 @@ For example: For arbitrary computations that do not meet the requirements for [generated columns](https://www.postgresql.org/docs/14/ddl-generated-columns.html), a table's reflected GraphQL type can be extended by creating a function that: -- accepts a single parameter of the table's tuple type +- accepts a single argument of the table's tuple type ```sql --8<-- "test/expected/extend_type_with_function.out" diff --git a/sql/load_sql_context.sql b/sql/load_sql_context.sql index 0da07090..86fbe41e 100644 --- a/sql/load_sql_context.sql +++ b/sql/load_sql_context.sql @@ -71,7 +71,6 @@ select jsonb_build_object( 'oid', pt.oid::int, 'schema_oid', pt.typnamespace::int, - -- 'name', pt.typname::regtype::text, 'name', pt.typname, 'category', case when pt.typcategory = 'A' then 'Array' From de9992c5c0c0bbf3cb8b25b5b944eb22ee58a11f Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Thu, 4 May 2023 12:59:29 -0500 Subject: [PATCH 12/12] add to changelog --- docs/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.md b/docs/changelog.md index 638eee89..0e8f908b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -24,3 +24,4 @@ ## master - feature: `String` type filters support `regex`, `iregex` +- feature: computed relationships via functions returning setof