From a73e338a1723815ed2ebb7406e0b98ad5d89fb32 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 22 Jul 2026 07:21:19 +0000 Subject: [PATCH] docs(data-modeling): document declarative triggers via ORM SDK Add SDK/ORM documentation for the declarative trigger catalog (db.trigger.create + db.triggerFunction.create): timing, events, forEach, transition tables (REFERENCING OLD/NEW TABLE AS), statement-level triggers, and the whenClause condition DSL. New references/triggers.md, a Triggers section in the constructive-data-modeling SKILL.md, a constructive-features router entry, and a features.md overview subsection. Repackaged affected skill zips. Documents the feature shipped in constructive-io/constructive-db#2414. --- .agents/skills/constructive-data-modeling.zip | Bin 8307 -> 11704 bytes .../constructive-data-modeling/SKILL.md | 42 ++++- .../references/triggers.md | 161 ++++++++++++++++++ .agents/skills/constructive-features.zip | Bin 7136 -> 7201 bytes .agents/skills/constructive-features/SKILL.md | 1 + features.md | 4 + 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/constructive-data-modeling/references/triggers.md diff --git a/.agents/skills/constructive-data-modeling.zip b/.agents/skills/constructive-data-modeling.zip index 999f498512df6d4f8775bb63a155287f86cf6f5f..2457835ee8ccca8b895f36f82e54dd9ce41c1216 100644 GIT binary patch delta 6497 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 delta 3161 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 J>Un?m{|f=x%rO7} diff --git a/.agents/skills/constructive-data-modeling/SKILL.md b/.agents/skills/constructive-data-modeling/SKILL.md index a6ca395..f922177 100644 --- a/.agents/skills/constructive-data-modeling/SKILL.md +++ b/.agents/skills/constructive-data-modeling/SKILL.md @@ -1,6 +1,6 @@ --- name: constructive-data-modeling -description: "Tables, fields, relations, constraints, indexes, enums, and database provisioning via the type-safe SDK. Use when asked to 'create a table', 'add a field', 'add a column', 'create a relation', 'add a constraint', 'add an index', 'create a foreign key', 'add a primary key', 'add a unique constraint', 'define field types', 'provision a database', 'create an enum', 'api_required', 'temporal table', 'application-time temporal', 'WITHOUT OVERLAPS', 'temporal foreign key', 'WITH PERIOD', 'period column', or when working with metaschema_public operations." +description: "Tables, fields, relations, constraints, indexes, enums, triggers, and database provisioning via the type-safe SDK. Use when asked to 'create a table', 'add a field', 'add a column', 'create a relation', 'add a constraint', 'add an index', 'create a foreign key', 'add a primary key', 'add a unique constraint', 'define field types', 'provision a database', 'create an enum', 'api_required', 'temporal table', 'application-time temporal', 'WITHOUT OVERLAPS', 'temporal foreign key', 'WITH PERIOD', 'period column', 'create a trigger', 'add a trigger', 'trigger function', 'transition tables', 'REFERENCING OLD/NEW TABLE', 'statement-level trigger', 'FOR EACH STATEMENT', 'trigger WHEN clause', or when working with metaschema_public operations." metadata: author: constructive-io version: "1.0.0" @@ -19,6 +19,7 @@ Use this skill when: - Configuring field validation (regexp, min, max) - Setting `api_required` on nullable FK columns - Adding primary key / unique / foreign key constraints +- Creating custom triggers + trigger functions, including transition tables (`REFERENCING OLD/NEW TABLE AS`), statement-level triggers, and conditional `WHEN` clauses - Declaring application-time temporal constraints (PG18): `WITHOUT OVERLAPS` keys and temporal (`WITH PERIOD`) foreign keys - Understanding the composition: table → fields → constraints → indexes → relations → security @@ -204,6 +205,44 @@ await db.index.create({ }).execute(); ``` +## Triggers + +Custom triggers are declared via `db.trigger.create`, referencing a trigger +function created with `db.triggerFunction.create`. Create the function first, +then the trigger (a trigger row without a `functionName` is registration-only and +is not provisioned into a physical trigger): + +```typescript +await db.triggerFunction.create({ + data: { databaseId, name: 'audit_fn', code: 'BEGIN RETURN NULL; END' }, + select: { id: true }, +}).execute(); + +await db.trigger.create({ + data: { + databaseId, + tableId, + name: 'audit', + functionName: 'audit_fn', + timing: 'after', // before | after | instead_of + events: ['update'], // insert | update | delete | truncate + forEach: 'statement', // row | statement + transitionOldName: 'o', // REFERENCING OLD TABLE AS o + transitionNewName: 'n', // REFERENCING NEW TABLE AS n + whenClause: { field: 'status', op: 'IS DISTINCT FROM', row: 'OLD' }, + }, + select: { id: true }, +}).execute(); +// → CREATE TRIGGER audit AFTER UPDATE ON t +// REFERENCING OLD TABLE AS o NEW TABLE AS n +// FOR EACH STATEMENT EXECUTE FUNCTION audit_fn(); +``` + +`whenClause` uses the same condition DSL as `JobTrigger`/`EventTracker`. For the +common declarative behaviors, prefer the Node Type generators (`JobTrigger`, +`EventTracker`, `DataSlug`, etc.) over hand-authored triggers. See +[triggers.md](./references/triggers.md) for the full reference. + ## `api_required` (Required API Fields) For nullable FK columns that should be required at the GraphQL API level: @@ -223,6 +262,7 @@ await db.field.update({ | [constraints.md](./references/constraints.md) | Primary key, unique, foreign key, check + application-time temporal (`WITHOUT OVERLAPS` / `WITH PERIOD`) constraints | | [field-types.md](./references/field-types.md) | Complete field type reference | | [provisioning.md](./references/provisioning.md) | Full database provisioning flow | +| [triggers.md](./references/triggers.md) | Custom triggers + trigger functions: timing, events, `forEach`, transition tables, `whenClause` DSL | ## Cross-References diff --git a/.agents/skills/constructive-data-modeling/references/triggers.md b/.agents/skills/constructive-data-modeling/references/triggers.md new file mode 100644 index 0000000..aecbcb3 --- /dev/null +++ b/.agents/skills/constructive-data-modeling/references/triggers.md @@ -0,0 +1,161 @@ +# Triggers + +Custom database triggers are declared through the SDK ORM against the metaschema +`trigger` catalog. A trigger row references a **trigger function** (created via +`db.triggerFunction.create`) and a target table; on insert it compiles to a +PostgreSQL `CREATE TRIGGER ...` that runs the function. + +| Entity | ORM entity | Generates | +|--------|-----------|-----------| +| Trigger function | `db.triggerFunction.create` | `CREATE FUNCTION .() RETURNS TRIGGER ...` | +| Trigger | `db.trigger.create` | `CREATE TRIGGER ON ...` | + +## Two-step flow + +A declarative trigger is provisioned only when its `functionName` resolves to an +existing trigger function. So always create the function first, then the trigger: + +```typescript +// 1. Trigger function — lives in the database's private (app_private) schema. +await db.triggerFunction.create({ + data: { + databaseId, + name: 'audit_fn', + code: 'BEGIN INSERT INTO app_private.audit_log(t, at) VALUES (TG_OP, now()); RETURN NULL; END', + }, + select: { id: true, name: true }, +}).execute(); + +// 2. Declarative trigger referencing that function by name. +await db.trigger.create({ + data: { + databaseId, + tableId, // target table (metaschema table id) + name: 'audit', + functionName: 'audit_fn', // resolved in the private schema + timing: 'after', // before | after | instead_of + events: ['update'], // any of insert | update | delete | truncate + forEach: 'statement', // row | statement + transitionOldName: 'o', // REFERENCING OLD TABLE AS o + transitionNewName: 'n', // REFERENCING NEW TABLE AS n + }, + select: { id: true }, +}).execute(); +``` + +This generates: + +```sql +CREATE TRIGGER audit AFTER UPDATE ON t + REFERENCING OLD TABLE AS o NEW TABLE AS n + FOR EACH STATEMENT EXECUTE FUNCTION audit_fn(); +``` + +## Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `tableId` | UUID | — | Target table (required) | +| `name` | string | — | Trigger name (required) | +| `functionName` | string | — | Trigger function name, resolved in the database's private schema. **Omit to register a catalog row without provisioning a physical trigger** | +| `timing` | string | `after` | `before`, `after`, or `instead_of` | +| `events` | string[] | — | One or more of `insert`, `update`, `delete`, `truncate` | +| `forEach` | string | `row` | `row` (FOR EACH ROW) or `statement` (FOR EACH STATEMENT) | +| `transitionOldName` | string | — | `REFERENCING OLD TABLE AS ` — the OLD transition relation | +| `transitionNewName` | string | — | `REFERENCING NEW TABLE AS ` — the NEW transition relation | +| `whenClause` | JSON | — | Optional `WHEN (...)` predicate, expressed with the condition DSL (below) | + +All of `timing` / `forEach` / `events` / `transition*` / `whenClause` are optional +and nullable. A row created with only `tableId` + `name` (no `functionName`) is a +registration-only entry — it is **not** compiled into a physical trigger, so +existing catalog rows and generators are unaffected. + +## Transition tables (statement-level, PostgreSQL) + +`transitionOldName` / `transitionNewName` expose `REFERENCING OLD/NEW TABLE AS`, +which give the trigger function set-oriented `OLD TABLE` / `NEW TABLE` relations +covering every row touched by the statement. They pair naturally with +`forEach: 'statement'` for efficient audit / sync triggers that process the whole +change set once per statement instead of once per row. + +```typescript +await db.trigger.create({ + data: { + databaseId, tableId, + name: 'audit', + functionName: 'audit_fn', + timing: 'after', + events: ['update'], + forEach: 'statement', + transitionNewName: 'n', // only NEW TABLE — OLD TABLE omitted + }, + select: { id: true }, +}).execute(); +// → CREATE TRIGGER audit AFTER UPDATE ON t +// REFERENCING NEW TABLE AS n +// FOR EACH STATEMENT EXECUTE FUNCTION audit_fn(); +``` + +Either transition name may be given independently; provide both for +`OLD TABLE AS ... NEW TABLE AS ...`. + +## `whenClause` (conditional firing) + +`whenClause` accepts the same structured condition DSL used by `JobTrigger` +`conditions` and `EventTracker` — it is compiled to the trigger's `WHEN (...)` +clause and validated against the table's columns at provisioning time. No raw SQL. + +**Leaf condition:** +```typescript +{ field: 'status', op: 'IS DISTINCT FROM', row: 'OLD' } +``` + +| Key | Required | Default | Description | +|-----|----------|---------|-------------| +| `field` | yes | — | Column name (validated against the table) | +| `op` | yes | — | `=`, `!=`, `>`, `<`, `>=`, `<=`, `LIKE`, `NOT LIKE`, `IS NULL`, `IS NOT NULL`, `IS DISTINCT FROM` | +| `value` | conditional | — | Comparison value (omit for `IS NULL`, `IS NOT NULL`, `IS DISTINCT FROM`) | +| `row` | no | `NEW` | Row reference: `NEW` or `OLD` | +| `ref` | no | — | Field-to-field comparison: `{ field, row }` | + +Arrays are an implicit AND; `{ AND: [...] }`, `{ OR: [...] }`, and `{ NOT: {...} }` +combinators nest arbitrarily. + +```typescript +await db.trigger.create({ + data: { + databaseId, tableId, + name: 'audit_status_change', + functionName: 'audit_fn', + timing: 'after', + events: ['update'], + forEach: 'row', + whenClause: { field: 'status', op: 'IS DISTINCT FROM', row: 'OLD' }, + }, + select: { id: true }, +}).execute(); +// → CREATE TRIGGER ... FOR EACH ROW +// WHEN (NEW.status IS DISTINCT FROM OLD.status) +// EXECUTE FUNCTION audit_fn(); +``` + +> `IS DISTINCT FROM` always compares the `NEW` and `OLD` value of `field` +> regardless of the `row` given, matching PostgreSQL's change-detection idiom. + +See the shared condition grammar in +[`constructive-jobs`](../../constructive-jobs/SKILL.md) and +[`constructive-events`](../../constructive-events/SKILL.md). + +## Notes & gaps + +- Trigger functions are created in the database's **private** (`app_private`) + schema; `functionName` is resolved there. +- For the common declarative behaviors (timestamps, slugs, soft-delete, job + enqueue on row change, event tracking) prefer the corresponding Node Type + generators rather than authoring a raw trigger — see + [`constructive-jobs`](../../constructive-jobs/SKILL.md) (`JobTrigger`) and + [`constructive-events`](../../constructive-events/SKILL.md) (`EventTracker`). + Use `db.trigger.create` for custom logic those generators don't cover. +- Constraint triggers (`CREATE CONSTRAINT TRIGGER`, `DEFERRABLE`) and per-trigger + `WHEN` referencing other tables have no SDK surface yet — treat these as SDK + gaps rather than dropping to raw SQL. diff --git a/.agents/skills/constructive-features.zip b/.agents/skills/constructive-features.zip index b2f2368b5818b4a906973b907ce63262fc6ee451..d5bff6e80a590e7bd7ab7b7e15a2e895f2be0e15 100644 GIT binary patch delta 6863 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