Foreign keys
A foreign key links one or more columns to another table. The common case — a single column referencing another table’s id — you author inline with s.references(table), which emits a text column plus a FOREIGN KEY constraint; composite and non-id keys use the table-level .foreignKey(...). This guide covers both.
A single foreign key
s.references(table) declares a column that references another table’s primary key:
import { defineTable, s } from "@schemic/postgres";
export const usr = defineTable("usr", { name: s.text() });
export const post = defineTable("post", {
author: s.references("usr"),
});CREATE TABLE "post" (
"id" text PRIMARY KEY,
"author" text NOT NULL
);
CREATE TABLE "usr" (
"id" text PRIMARY KEY,
"name" text NOT NULL
);
ALTER TABLE "post" ADD CONSTRAINT "post_author_fkey" FOREIGN KEY ("author") REFERENCES "usr" ("id");The foreign key is emitted as its own ALTER TABLE ... ADD CONSTRAINT after both tables, so the order of references between tables never matters — even a mutual reference resolves.
Referential actions
Pass onDelete and onUpdate to control what happens when the referenced row changes. Actions are emitted uppercased; the default NO ACTION is omitted:
export const post = defineTable("post", {
author: s.references("usr", { onDelete: "cascade", onUpdate: "restrict" }),
});ALTER TABLE "post" ADD CONSTRAINT "post_author_fkey" FOREIGN KEY ("author") REFERENCES "usr" ("id") ON DELETE CASCADE ON UPDATE RESTRICT;Referencing a table object
When you already have the target table in scope, table.record(opts?) is a typed alternative to passing the name as a string — it returns the same s.references field:
const usr = defineTable("usr", { name: s.text() });
const post = defineTable("post", {
author: usr.record({ onDelete: "cascade" }),
});ALTER TABLE "post" ADD CONSTRAINT "post_author_fkey" FOREIGN KEY ("author") REFERENCES "usr" ("id") ON DELETE CASCADE;Composite and non-id foreign keys
s.references is the sugar for the common case — a single column pointing at another table’s id. For a multi-column key, or one that targets a non-id column, use the table-level .foreignKey(...):
const team = defineTable("team", { org_id: s.text(), code: s.text() })
.primaryKey("org_id", "code");
const member = defineTable("member", { org_id: s.text(), team_code: s.text() })
.foreignKey({
columns: ["org_id", "team_code"],
refTable: "team",
refColumns: ["org_id", "code"],
onDelete: "cascade",
});ALTER TABLE "member" ADD CONSTRAINT "member_org_id_team_code_fkey" FOREIGN KEY ("org_id", "team_code") REFERENCES "team" ("org_id", "code") ON DELETE CASCADE;To reference a non-id column — here a UNIQUE one — point refColumns at it:
const u = defineTable("u", { email: s.text().$unique() });
const prof = defineTable("prof", { email: s.text() })
.foreignKey({ columns: ["email"], refTable: "u", refColumns: ["email"] });ALTER TABLE "prof" ADD CONSTRAINT "prof_email_fkey" FOREIGN KEY ("email") REFERENCES "u" ("email");.foreignKey({ columns, refTable, refColumns?, onDelete?, onUpdate?, name? }) is the general form: refColumns defaults to ["id"], and the constraint name defaults to <table>_<columns>_fkey.
Putting it together
Foreign keys compose with checks, defaults, generated columns, and indexes in one domain:
import { defineTable, s, sqlExpr } from "@schemic/postgres";
const customer = defineTable("customer", {
email: s.text().$unique().$check(sqlExpr("email ~* '^[^@]+@[^@]+$'")),
name: s.text(),
});
const order = defineTable("order", {
customer: customer.record({ onDelete: "cascade" }),
quantity: s.integer().$check(sqlExpr("quantity > 0")),
unitPrice: s.numeric(10, 2),
total: s.numeric(12, 2).$generated('quantity * "unitPrice"'),
createdAt: s.timestamptz().$default(sqlExpr("now()")),
});CREATE TABLE "customer" (
"id" text PRIMARY KEY,
"email" text NOT NULL CHECK (email ~* '^[^@]+@[^@]+$'),
"name" text NOT NULL
);
CREATE TABLE "order" (
"id" text PRIMARY KEY,
"createdAt" timestamp with time zone NOT NULL DEFAULT now(),
"customer" text NOT NULL,
"quantity" integer NOT NULL CHECK (quantity > 0),
"total" numeric(12, 2) NOT NULL GENERATED ALWAYS AS (quantity * "unitPrice") STORED,
"unitPrice" numeric(10, 2) NOT NULL
);
CREATE UNIQUE INDEX "customer_email_key" ON "customer" ("email");
ALTER TABLE "order" ADD CONSTRAINT "order_customer_fkey" FOREIGN KEY ("customer") REFERENCES "customer" ("id") ON DELETE CASCADE;Where to go next
- Indexes —
$uniqueand secondary indexes. - Constraints & defaults — the clauses used above.
- Generate & run migrations — apply the schema to the database.