diff --git a/docs/configuration.md b/docs/configuration.md index f4022c8e..a9786d6d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -101,7 +101,7 @@ When no exposed schema has opted in, `__schema` and `__type` selections return a { "errors": [{ "message": "Unknown field \"__schema\" on type Query" }] } ``` -#### Partial introspection accross multiple schemas +#### Partial introspection across multiple schemas !!!note "disable introspection for all schemas" diff --git a/src/graphql.rs b/src/graphql.rs index 7628b74b..fb7b4339 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -1314,11 +1314,15 @@ impl ___Type for QueryType { // Use graphql_column_field_name to convert snake_case to camelCase if needed let arg_name = self.schema.graphql_column_field_name(col); + // sql_column_to_graphql_type already wraps NOT NULL columns in NonNull. + // For view columns (always nullable in PG), we must add it ourselves. + let non_null_col_type = match col_type { + __Type::NonNull(_) => col_type, + t => __Type::NonNull(NonNullType { type_: Box::new(t) }), + }; pk_args.push(__InputValue { name_: arg_name, - type_: __Type::NonNull(NonNullType { - type_: Box::new(col_type), - }), + type_: non_null_col_type, description: Some(format!("The record's `{}` value", col_name)), default_value: None, sql_type: Some(NodeSQLType::Column(Arc::clone(col))), diff --git a/test/expected/function_calls.out b/test/expected/function_calls.out index d61ca197..eec6bb24 100644 --- a/test/expected/function_calls.out +++ b/test/expected/function_calls.out @@ -2058,8 +2058,8 @@ begin; "kind": "NON_NULL", + "name": null, + "ofType": { + - "kind": "NON_NULL", + - "name": null + + "kind": "SCALAR", + + "name": "Int" + } + } + } + diff --git a/test/expected/primary_key_queries.out b/test/expected/primary_key_queries.out index 51f0b58c..d796f192 100644 --- a/test/expected/primary_key_queries.out +++ b/test/expected/primary_key_queries.out @@ -1187,4 +1187,1006 @@ begin; } (1 row) + -- Test 31: ByPk argument types must be NON_NULL (regression: double NON_NULL wrapping) + -- Covers int (smallint), bigint, and each supported scalar PK type. + -- Expected: each arg has kind=NON_NULL wrapping a SCALAR directly (no double NON_NULL). + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + $$) + ); + jsonb_pretty +-------------------------------------------------------------------------- + { + + "data": { + + "__type": { + + "fields": [ + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "BigInt", + + "ofType": null + + } + + } + + } + + ], + + "name": "bigintPkTableByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BigintPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "INPUT_OBJECT", + + "name": "BigintPkTableOrderBy" + + } + + } + + } + + } + + ], + + "name": "bigintPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BoolPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "INPUT_OBJECT", + + "name": "BoolPkTableOrderBy" + + } + + } + + } + + } + + ], + + "name": "boolPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "ByteaPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "INPUT_OBJECT", + + "name": "ByteaPkTableOrderBy" + + } + + } + + } + + } + + ], + + "name": "byteaPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "FloatPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "INPUT_OBJECT", + + "name": "FloatPkTableOrderBy" + + } + + } + + } + + } + + ], + + "name": "floatPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "nodeId", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "ID", + + "ofType": null + + } + + } + + } + + ], + + "name": "node" + + }, + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + } + + } + + ], + + "name": "smallintPkTableByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "SmallintPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "INPUT_OBJECT", + + "name": "SmallintPkTableOrderBy"+ + } + + } + + } + + } + + ], + + "name": "smallintPkTableCollection" + + } + + ] + + } + + } + + } +(1 row) + + -- Test 32: View-backed byPk args must also be NON_NULL + -- PostgreSQL treats view columns as nullable even when the underlying table column is NOT NULL. + -- sql_column_to_graphql_type returns a plain Scalar for view columns, so the byPk arg + -- construction must explicitly wrap it in NonNull. + -- Expected: viewByPk has id arg with kind=NON_NULL wrapping SCALAR(Int), not just SCALAR(Int). + create table base_table(id int primary key, name text); + create view base_view as select * from base_table; + comment on view base_view is e'@graphql({"primary_key_columns": ["id"]})'; + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + ofType { + kind + name + } + } + } + } + } + } + $$) + ); + jsonb_pretty +------------------------------------------------------------------ + { + + "data": { + + "__type": { + + "fields": [ + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "Int" + + } + + } + + } + + ], + + "name": "baseTableByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BaseTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "baseTableCollection" + + }, + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "Int" + + } + + } + + } + + ], + + "name": "baseViewByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BaseViewFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "baseViewCollection" + + }, + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "BigInt" + + } + + } + + } + + ], + + "name": "bigintPkTableByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BigintPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "bigintPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "BoolPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "boolPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "ByteaPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "byteaPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "FloatPkTableFilter", + + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "floatPkTableCollection" + + }, + + { + + "args": [ + + { + + "name": "nodeId", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "ID" + + } + + } + + } + + ], + + "name": "node" + + }, + + { + + "args": [ + + { + + "name": "id", + + "type": { + + "kind": "NON_NULL", + + "name": null, + + "ofType": { + + "kind": "SCALAR", + + "name": "Int" + + } + + } + + } + + ], + + "name": "smallintPkTableByPk" + + }, + + { + + "args": [ + + { + + "name": "first", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "last", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "before", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "after", + + "type": { + + "kind": "SCALAR", + + "name": "Cursor", + + "ofType": null + + } + + }, + + { + + "name": "offset", + + "type": { + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + + } + + }, + + { + + "name": "filter", + + "type": { + + "kind": "INPUT_OBJECT", + + "name": "SmallintPkTableFilter",+ + "ofType": null + + } + + }, + + { + + "name": "orderBy", + + "type": { + + "kind": "LIST", + + "name": null, + + "ofType": { + + "kind": "NON_NULL", + + "name": null + + } + + } + + } + + ], + + "name": "smallintPkTableCollection" + + } + + ] + + } + + } + + } +(1 row) + rollback; diff --git a/test/expected/resolve_graphiql_schema.out b/test/expected/resolve_graphiql_schema.out index 6b64823c..3ddfd1d5 100644 --- a/test/expected/resolve_graphiql_schema.out +++ b/test/expected/resolve_graphiql_schema.out @@ -5754,13 +5754,9 @@ begin; "kind": "NON_NULL", + "name": null, + "ofType": { + - "kind": "NON_NULL", + - "name": null, + - "ofType": { + - "kind": "SCALAR", + - "name": "Int", + - "ofType": null + - } + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + } + }, + "description": "The record's `id` value", + @@ -5880,13 +5876,9 @@ begin; "kind": "NON_NULL", + "name": null, + "ofType": { + - "kind": "NON_NULL", + - "name": null, + - "ofType": { + - "kind": "SCALAR", + - "name": "Int", + - "ofType": null + - } + + "kind": "SCALAR", + + "name": "Int", + + "ofType": null + } + }, + "description": "The record's `id` value", + @@ -6006,13 +5998,9 @@ begin; "kind": "NON_NULL", + "name": null, + "ofType": { + - "kind": "NON_NULL", + - "name": null, + - "ofType": { + - "kind": "SCALAR", + - "name": "UUID", + - "ofType": null + - } + + "kind": "SCALAR", + + "name": "UUID", + + "ofType": null + } + }, + "description": "The record's `id` value", + diff --git a/test/sql/primary_key_queries.sql b/test/sql/primary_key_queries.sql index e444c58e..d9b28ea9 100644 --- a/test/sql/primary_key_queries.sql +++ b/test/sql/primary_key_queries.sql @@ -646,4 +646,66 @@ begin; $$) ); + -- Test 31: ByPk argument types must be NON_NULL (regression: double NON_NULL wrapping) + -- Covers int (smallint), bigint, and each supported scalar PK type. + -- Expected: each arg has kind=NON_NULL wrapping a SCALAR directly (no double NON_NULL). + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + $$) + ); + + -- Test 32: View-backed byPk args must also be NON_NULL + -- PostgreSQL treats view columns as nullable even when the underlying table column is NOT NULL. + -- sql_column_to_graphql_type returns a plain Scalar for view columns, so the byPk arg + -- construction must explicitly wrap it in NonNull. + -- Expected: viewByPk has id arg with kind=NON_NULL wrapping SCALAR(Int), not just SCALAR(Int). + create table base_table(id int primary key, name text); + create view base_view as select * from base_table; + comment on view base_view is e'@graphql({"primary_key_columns": ["id"]})'; + + select jsonb_pretty( + graphql.resolve($$ + { + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + ofType { + kind + name + } + } + } + } + } + } + $$) + ); + rollback;