Skip to content

Latest commit

 

History

History
218 lines (174 loc) · 10.2 KB

File metadata and controls

218 lines (174 loc) · 10.2 KB

Smart comments

The built-in smart-comments plugin (enabled by default) reads PostGraphile-style tags out of ordinary PostgreSQL COMMENTs and customizes the generated GraphQL schema — rename, hide, deprecate, and extend objects with plain DDL, no pdbq configuration, no redeploy (watch mode picks comment changes up like any other DDL).

Syntax

Lines at the start of a comment beginning with @ are tags; everything after the first non-tag line is the description (tag lines never leak into GraphQL descriptions):

COMMENT ON TABLE app.users IS E'@name people\n@omit delete\nA person with an account.';

Tags may repeat where noted. Disable everything with plugins.disabled: [smart-comments].

Tag reference

Tables and views

Tag Effect
@omit Hide the table entirely (type, queries, mutations, relations).
@omit read No root query fields (allUsers, userById, …); the type stays reachable through relations — useful for detail/join tables.
@omit all No connection query (allUsers); by-key lookups stay.
@omit many No one-to-many relation fields listing this table's rows on other types.
@omit create / update / delete Suppress the corresponding mutation (same effect as missing the SQL privilege).
@omit filter / @omit order Drop the table's <Type>Filter input / <Types>OrderBy enum and every argument referencing them.
@name <new_name> Rename the table for naming purposes. Give a name shaped like a table name (e.g. snake-case plural); every derived name follows: @name customers on users yields Customer, allCustomers, createCustomer, CustomerFilter, … Composes with simple-names.
@primaryKey col[, col] Declare a logical primary key on a view (or PK-less table): enables nodeId, the by-key lookup, and keyset pagination. Mutations remain gated by real privileges, so views stay read-only.
@unique col[, col] Declare a logical unique constraint: generates the single-row lookup field (repeatable).
@foreignKey (col, …) references [schema.]table (col, …) Declare a logical foreign key (typically on views): generates relation fields in both directions and relation filters (repeatable). Append |@fieldName x|@foreignFieldName y to name the generated fields.
@enum Treat the table as a GraphQL enum: its rows become the enum values. See Enum tables.

@omit actions combine: @omit create, delete or repeated @omit lines.

Columns

Tag Effect
@omit Hide the column everywhere (output, inputs, filter, orderBy). The column may still back PKs/FKs.
@omit read / create / update / filter / order Hide it from just that surface.
@name <new_name> Rename the field (and its filter field, orderBy values, input fields, lookup arguments).
@deprecated [reason] Emit @deprecated(reason: …) on the output field.
@notNull / @nullable Override introspected nullability — views lose NOT NULL, @notNull restores the non-null GraphQL type.
@filterable (alias @sortable) Admit the column to filtering and ordering despite filters.indexed_only — the per-column form of filters.allow_columns. Combine with @omit filter or @omit order to expose only one side.

Constraints (COMMENT ON CONSTRAINT … ON <table>)

Tag Effect
@omit on a foreign key Remove both relation fields and the relation filters.
@omit many on a foreign key Remove only the one-to-many side.
@omit filter on a foreign key Keep the relation fields, drop the relation filter (both directions).
@fieldName <name> / @foreignFieldName <name> Exact GraphQL names for the many-to-one / one-to-many relation fields. Applied through the shared naming pipeline, so advanced-filters filter fields and nested-mutations input fields align automatically.
@filterable on a foreign key Opt the relation into filtering when advanced-filters runs in opt-in mode (below).
@costMultiplier <n> on a foreign key Expected rows per parent on the one-to-many side, used by the cost estimator instead of the requested page size. See Cost estimation for recursive relations.
@omit on a unique constraint Remove the generated by-unique lookup (userByEmail); filtering is unaffected (the backing index remains).

Functions

Tag Effect
@omit Hide the function (root field or computed column).
@name <new_name> Rename the generated field.
@deprecated [reason] Deprecate the generated field(s).
@omit filter / @omit order on a computed column Keep the computed field, drop its advanced-filters filter field / orderBy values.
@filterable (alias @sortable) Opt the computed column in when advanced-filters runs in opt-in mode.

Enums

Tag Effect
@name <new_name> Rename the GraphQL enum type.

Enum tables

@enum on a table turns its rows into a GraphQL enum — the PostGraphile pattern for lookup tables that are really a fixed vocabulary with referential integrity:

CREATE TABLE event_type (
    code        text PRIMARY KEY,
    description text
);
COMMENT ON TABLE event_type IS E'@enum\nKind of event.';

INSERT INTO event_type (code, description) VALUES
    ('conference', 'A large formal gathering'),
    ('meetup',     'Casual get-together');

CREATE TABLE events (
    id         integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name       text NOT NULL,
    type_code  text NOT NULL REFERENCES event_type (code)
);

generates

enum EventType {
  """A large formal gathering"""
  CONFERENCE
  """Casual get-together"""
  MEETUP
}

type Event implements Node {
  id: Int!
  name: String!
  typeCode: EventType!
}
  • The value column is the table's single-column primary key, or the column of a single-column unique constraint commented @enum. It must be a text-family column — the row values become the enum value names (meetupMEETUP), and clients speak names in both directions while the database stores the raw values.
  • Descriptions come from a column commented @enumDescription, falling back to a column named description.
  • The enum table itself disappears from the API (no type, queries, or mutations), and every single-column foreign key referencing the value column types its local column as the enum; those FKs generate no relation fields.
  • @name on the table renames the enum (@name event_kindEventKind).
  • Tables that do not qualify (multi-column or non-text key, or no rows — an empty GraphQL enum is invalid) are left as ordinary tables.

Rows are read at introspection time and values are ordered by the value column; adding a row is DDL-adjacent — watch mode (or a restart) picks it up like any other schema change. Like @costMultiplier, the tag is applied during introspection and therefore works even with the smart-comments plugin disabled.

Tuning advanced-filters per object

The relation/computed filter tags above are parsed by advanced-filters directly from the catalog, so they work even if smart-comments is disabled. Beyond the default opt-out (@omit filter/@omit order), the whole surface can be flipped to explicit opt-in:

plugins:
  settings:
    advanced-filters:
      relations_opt_in: true   # only FKs tagged @filterable get relation filters
      computed_opt_in: true    # only functions tagged @filterable get filter/order

Cost estimation for recursive relations

The executor rejects an operation whose estimated cost exceeds server.max_cost (default 10000). The estimate multiplies each subtree by the page size requested at that level, so a nested query costs roughly page^depth. That is right for an unrelated fan-out, but badly pessimistic for self-referential trees — a Reddit-style comment thread — where the requested page is an upper bound that almost no row reaches.

A four-level thread at first: 20 per level estimates ~345,000, which no reasonable max_cost can accommodate, even though the query returns a handful of rows in practice.

@costMultiplier <n> on the recursive foreign key declares the expected rows per parent, and the estimator uses it in place of the requested page size:

CREATE TABLE comments (
    id         integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    post_id    integer NOT NULL REFERENCES posts (id),
    parent_id  integer REFERENCES comments (id),
    body       text NOT NULL
);

COMMENT ON CONSTRAINT comments_parent_id_fkey ON comments IS '@costMultiplier 3';

The same query now estimates 1,861 — comfortably inside the default limit.

Details worth knowing:

  • It only ever lowers an estimate. A query requesting fewer rows than the declared multiplier is charged the smaller requested page, so the tag can never make a cheap query look expensive.
  • It applies to the one-to-many side, the direction that multiplies.
  • Values below 1 and unparseable values are ignored, since a zero or negative multiplier would let a subtree escape the cost limit entirely.
  • It is parsed straight from the catalog, so it works with the smart-comments plugin disabled.
  • It changes the estimate, not the query. Rows actually returned are still bounded by first/last and server.max_page_size. Setting the multiplier far below real fan-out weakens the cost limit as a guard — pick a value near the true average, not the smallest one that makes a query pass.

Examples

-- A view made first-class: identified, relatable, read-only.
COMMENT ON VIEW app.order_totals IS E'@primaryKey order_id\n@foreignKey (order_id) references app.orders (id)';

-- Hide credentials, rename a legacy column, phase out another.
COMMENT ON COLUMN app.users.hashed_password IS '@omit';
COMMENT ON COLUMN app.users.fullname IS '@name display_name';
COMMENT ON COLUMN app.users.balance IS '@deprecated use credits';

-- Sharpen relation naming.
COMMENT ON CONSTRAINT posts_author_id_fkey ON app.posts IS E'@fieldName author\n@foreignFieldName posts';

-- A search function that should not become a computed filter.
COMMENT ON FUNCTION app.users_score(app.users) IS '@omit filter,order';