Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,25 @@ type Account {
}
```

#### Column's Not Null

Use the `"not_null"` JSON key to mark a column as non-nullable. This is particularly useful for [views](views.md#not-null-columns) where PostgreSQL does not preserve `NOT NULL` constraints from underlying tables.

```sql
create view "Person" as
select id, name from "Account";

comment on column "Person".id is
e'@graphql({"not_null": true})';
```

results in:
```graphql
type Person {
id: Int! # would be "Int" without the directive
}
```

#### Computed Field

Use the `"name"` JSON key to override a [computed field's](computed_fields.md) name.
Expand Down
55 changes: 53 additions & 2 deletions docs/views.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ tells pg_graphql to treat `"Person".id` as the primary key for the `Person` enti
```graphql
type Person {
nodeId: ID!
id: Int!
name: String!
id: Int
name: String
}
```

!!! note
View columns are nullable by default because PostgreSQL does not preserve `NOT NULL` constraints from underlying tables. See [Not Null Columns](#not-null-columns) to learn how to mark view columns as non-nullable.

!!! warning
Values of the primary key column/s must be unique within the table. If they are not unique, you will experience inconsistent behavior with `ID!` types, sorting, and pagination.

Expand Down Expand Up @@ -124,3 +127,51 @@ type EmailAddress {
account: Account!
}
```

## Not Null Columns

PostgreSQL views do not preserve `NOT NULL` constraints from their underlying tables. This means view columns appear as nullable in the GraphQL schema even when the source table columns are `NOT NULL`. To mark a view column as non-nullable, use the `not_null` [comment directive](configuration.md#comment-directives) on the column:

```json
{"not_null": true}
```

For example:

```sql
create table "Account"(
id serial primary key,
name text not null
);

create view "Person" as
select
id,
name
from
"Account";

comment on view "Person" is e'@graphql({"primary_key_columns": ["id"]})';
comment on column "Person".id is e'@graphql({"not_null": true})';
comment on column "Person".name is e'@graphql({"not_null": true})';
```

Without the `not_null` directives, the GraphQL type would have nullable fields:

```graphql
type Person {
nodeId: ID!
id: Int
name: String
}
```

With the `not_null` directives applied, the fields become non-nullable:

```graphql
type Person {
nodeId: ID!
id: Int!
name: String!
}
```
3 changes: 2 additions & 1 deletion sql/load_sql_context.sql
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ select
select
jsonb_build_object(
'name', d.directive ->> 'name',
'description', d.directive -> 'description'
'description', d.directive -> 'description',
'not_null', (d.directive ->> 'not_null')::boolean
)
from
directives d
Expand Down
4 changes: 2 additions & 2 deletions src/graphql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1976,7 +1976,7 @@ pub fn sql_column_to_graphql_type(col: &Column, schema: &Arc<__Schema>) -> Optio
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 {
Some(type_with_list_mod) => match col.is_not_null() {
true => Some(__Type::NonNull(NonNullType {
type_: Box::new(type_with_list_mod),
})),
Expand All @@ -1996,7 +1996,7 @@ impl NodeType {
self.table
.columns
.iter()
.any(|c| &c.name == colname && c.is_not_null)
.any(|c| &c.name == colname && c.is_not_null())
&& !fkey.referenced_table_meta.is_rls_enabled
&& !is_reverse_reference
}) {
Expand Down
9 changes: 9 additions & 0 deletions src/sql_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub struct ColumnPermissions {
pub struct ColumnDirectives {
pub name: Option<String>,
pub description: Option<String>,
pub not_null: Option<bool>,
}

#[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
Expand All @@ -46,6 +47,14 @@ pub struct Column {
pub directives: ColumnDirectives,
}

impl Column {
/// Returns true if the column is not null, considering both the SQL constraint
/// and any directive override (useful for views where SQL doesn't preserve NOT NULL)
pub fn is_not_null(&self) -> bool {
self.directives.not_null.unwrap_or(self.is_not_null)
}
}

#[derive(Deserialize, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FunctionDirectives {
pub name: Option<String>,
Expand Down
141 changes: 141 additions & 0 deletions test/expected/not_null_directive.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
begin;
-- Create a table with NOT NULL columns
create table account(
id serial primary key,
name text not null
);
-- Create a view from the table
create view person as
select id, name from account;
-- Add primary key directive so the view is exposed
comment on view person is e'@graphql({"primary_key_columns": ["id"]})';
-- Check that view columns are nullable by default (no NOT NULL constraint preserved)
-- The "kind" should be a scalar type directly, not NON_NULL
select jsonb_pretty(
graphql.resolve($$
{
__type(name: "Person") {
fields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}
$$)
);
jsonb_pretty
-----------------------------------------------
{ +
"data": { +
"__type": { +
"fields": [ +
{ +
"name": "nodeId", +
"type": { +
"kind": "NON_NULL", +
"name": null, +
"ofType": { +
"kind": "SCALAR",+
"name": "ID" +
} +
} +
}, +
{ +
"name": "id", +
"type": { +
"kind": "SCALAR", +
"name": "Int", +
"ofType": null +
} +
}, +
{ +
"name": "name", +
"type": { +
"kind": "SCALAR", +
"name": "String", +
"ofType": null +
} +
} +
] +
} +
} +
}
(1 row)

-- Apply not_null directive to view columns
comment on column person.id is e'@graphql({"not_null": true})';
comment on column person.name is e'@graphql({"not_null": true})';
-- Check that view columns are now non-nullable
-- The "kind" should be NON_NULL with ofType containing the scalar type
select jsonb_pretty(
graphql.resolve($$
{
__type(name: "Person") {
fields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}
$$)
);
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" +
} +
} +
} +
] +
} +
} +
}
(1 row)

rollback;
63 changes: 63 additions & 0 deletions test/sql/not_null_directive.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
begin;
-- Create a table with NOT NULL columns
create table account(
id serial primary key,
name text not null
);

-- Create a view from the table
create view person as
select id, name from account;

-- Add primary key directive so the view is exposed
comment on view person is e'@graphql({"primary_key_columns": ["id"]})';

-- Check that view columns are nullable by default (no NOT NULL constraint preserved)
-- The "kind" should be a scalar type directly, not NON_NULL
select jsonb_pretty(
graphql.resolve($$
{
__type(name: "Person") {
fields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}
$$)
);

-- Apply not_null directive to view columns
comment on column person.id is e'@graphql({"not_null": true})';
comment on column person.name is e'@graphql({"not_null": true})';

-- Check that view columns are now non-nullable
-- The "kind" should be NON_NULL with ofType containing the scalar type
select jsonb_pretty(
graphql.resolve($$
{
__type(name: "Person") {
fields {
name
type {
kind
name
ofType {
kind
name
}
}
}
}
}
$$)
);

rollback;