NAME

DBIO::GraphQL - Auto-generate a GraphQL schema from a DBIO schema

VERSION

version 0.900000

SYNOPSIS

use DBIO::GraphQL;
use GraphQL::Execution qw(execute);

my $db     = My::Schema->connect(...);
my $result = DBIO::GraphQL->to_graphql($db);

# Simple plural query - always include at least one scalar field
# alongside nodes (e.g. total) - see KNOWN BEHAVIOUR below.
execute($result->{schema},
  '{ allBooks { total nodes { title } } }',
  undef, $result->{context});

# Filtered + paginated + ordered (nested-DBIO-style filter)
execute($result->{schema}, '
  query {
    allBooks(
      filter:  { title: { like: "%Perl%" } }
      orderBy: { field: "title", direction: ASC }
      page:    { skip: 0, take: 5 }
    ) {
      total
      hasNextPage
      nodes { id title }
    }
  }', undef, $result->{context});

# Cursor pagination
execute($result->{schema}, '
  query($after: String) {
    allBooks(cursor: { after: $after, first: 5 }) {
      total
      nextCursor
      hasNextPage
      nodes { id title }
    }
  }', undef, $result->{context}, { after => $cursor });

# Mutation
execute($result->{schema},
  'mutation { createBook(title: "Dune", author_id: 4) { id title } }',
  undef, $result->{context});

DESCRIPTION

Introspects every source registered with the supplied DBIO::Schema and builds a complete, executable GraphQL::Schema with:

  • One scalar field per column, typed as Int, Float, Boolean, or String based on the column's declared data_type.

  • One relationship field per DBIO relationship (has_many resolves to a List type; belongs_to / might_have resolve to a single object type). Build-time errors are emitted when the DBIO relationship contract is incomplete (see DBIO::GraphQL::Relationship).

  • A root Query type with singular lookup and plural all<Source>s queries supporting filtering, ordering, and both offset and cursor pagination. Filter arguments use a nested per-column shape that mirrors the DBIO search-condition format (see DBIO::GraphQL::Filter).

  • A root Mutation type with createX, updateX, and deleteX entry points for every source (see DBIO::GraphQL::Mutation).

Composite primary keys are fully supported throughout.

METHODS

to_graphql

my $result = DBIO::GraphQL->to_graphql($db);

Class method. Accepts a connected DBIO::Schema instance. Returns a hashref:

{
  schema  => $graphql_schema,   # GraphQL::Schema, pass to execute()
  context => $db,               # the original schema, for convenience
}

SCALAR TYPE MAPPING

SQL column types are mapped to GraphQL scalars in the following priority order:

Boolean: bool, boolean, tinyint(1)
Float  : float, double, double precision, real, money, decimal, numeric
Int    : int, integer, bigint, smallint, tinyint, mediumint, serial
String : everything else (safe fallback)

The mapping is centralised in DBIO::GraphQL::ScalarMap and reused by the filter, mutation, and relationship modules.

PLURAL QUERIES

Every source X gets an allXs query returning an XConnection:

type XConnection {
  nodes:       [X]
  total:       Int        # total rows matching filter, before pagination
  nextCursor:  String     # opaque cursor; set only during cursor pagination
  hasNextPage: Boolean    # true when more pages follow
}

Always request total or another scalar field alongside nodes in your selection set - see "KNOWN BEHAVIOUR".

Filtering

Filter arguments use a nested per-column shape that mirrors the DBIO search-condition format. Each column accepts a typed *Filter input with operators that depend on the column's scalar type (IntFilter, FloatFilter, StringFilter, BoolFilter).

allBooks(filter: {
  title:     { like:    "%Perl%" }
  author_id: { gt:      3 }
  active:    { eq:      true }
  AND: [
    { title: { contains: "Hobbit" } }
    { OR:    [ { title: { contains: "Ring" } }, { author_id: { gt: 5 } } ] }
  ]
}) { total nodes { title } }

Per-scalar operators:

IntFilter / FloatFilter
  eq, not, gt, gte, lt, lte, in, isNull

StringFilter
  eq, not, like, contains, startsWith, endsWith, in, isNull

BoolFilter
  eq, not, isNull

Logical combinators (available on every per-source filter):

AND: [ Filter, Filter, ... ]
OR:  [ Filter, Filter, ... ]

Ordering

allBooks(orderBy: { field: "title", direction: ASC }) {
  total nodes { title }
}

direction is the OrderDirection enum: ASC or DESC. When omitted, results are ordered by primary key ascending.

Offset pagination

allBooks(page: { skip: 10, take: 5 }) { total nodes { title } }

skip defaults to 0, take defaults to 10.

Cursor pagination

# First page
allBooks(cursor: { first: 5 }) {
  total nextCursor hasNextPage nodes { title }
}

# Subsequent pages - pass nextCursor from the previous response
allBooks(cursor: { after: "...", first: 5 }) {
  total nextCursor hasNextPage nodes { title }
}

first defaults to 10. Cursor pagination takes precedence over offset pagination if both are supplied in the same query. Cursors are opaque base64-encoded strings derived from the row's primary key and should be treated as implementation details subject to change.

MUTATIONS

For every source X, three mutations are generated by DBIO::GraphQL::Mutation:

createX

mutation { createBook(title: "Dune", author_id: 4) { id title } }

Accepts all column values as arguments. Columns that are non-nullable, have no declared default, and are not auto-increment are wrapped in NonNull and must be supplied. On failure, dies - the error appears in the top-level errors array of the response.

updateX

mutation { updateBook(id: 1, title: "Dune Messiah") { id title } }

Identifies the target row by its primary key or by a complete set of columns from any unique constraint declared on the source. All non-key columns must be supplied (full update). dies if the row cannot be found or the update fails.

deleteX

mutation { deleteBook(id: 1) }

Identifies the target row by primary key or unique constraint. Returns true (Boolean) on success, false if the row is not found.

ERROR HANDLING

  • createX and updateX resolver failures die. GraphQL catches the exception and surfaces it in the top-level errors array; the data field for the failed mutation will be null.

  • deleteX returns false rather than dying when a row is not found.

  • Relationship resolution dies at build time when the DBIO relationship contract is incomplete (see DBIO::GraphQL::Relationship). Set on_error => 'warn' on the resolver to downgrade to a warning + silent skip.

LIMITATIONS

  • Relationship fields are unfiltered. Relationship fields within a query (e.g. author { books { title } }) return all related rows. They do not accept filter, orderBy, or pagination arguments.

  • No nested input for mutations. createX and updateX accept only scalar column values. Related rows must be created or linked separately using their own mutations and raw foreign-key values.

  • updateX is a full update. All non-primary-key columns must be supplied; partial (sparse) updates are not supported.

  • Cursor pagination assumes primary-key order. The after cursor encodes the primary key of the last returned row and applies a pk > value condition. If you supply a custom orderBy using a non-primary-key column, the cursor will not advance correctly. Use offset pagination (page) when ordering by non-PK columns.

  • No custom scalars. Column types such as date, datetime, json, and uuid all map to String. Custom GraphQL scalars are not generated.

  • No subscriptions. Only Query and Mutation operation types are generated.

  • col_like case sensitivity is database-dependent. SQLite LIKE is case-insensitive for ASCII characters but case-sensitive for Unicode. PostgreSQL LIKE is always case-sensitive.

KNOWN BEHAVIOUR

When querying a plural allXs field, always include at least one scalar field (total, hasNextPage, or nextCursor) alongside nodes in your selection set:

# Correct
{ allBooks { total nodes { title } } }

# May silently return empty nodes in some GraphQL executor versions
{ allBooks { nodes { title } } }

This is a quirk of how GraphQL::Execution resolves connection object types when the selection set contains only a list field.

ARCHITECTURE

DBIO::GraphQL is a thin orchestrator over four focused modules:

ACKNOWLEDGEMENTS

DBIO port of DBIx::Class::Schema::GraphQL by Mohammad Sajid Anwar (MANWAR). The original DBIx::Class implementation, design, and documentation are his work; this distribution adapts them to the DBIO schema introspection API and re-architects the filter surface into a nested per-column shape that mirrors DBIO's native search-condition format.

SEE ALSO

DBIx::Class::Schema::GraphQL, GraphQL::Plugin::Convert::DBIC, DBIO::Schema, GraphQL::Schema

AUTHOR

DBIO Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.