Foreign Keys
Foreign Keys
This document explains foreign key constraints in Stoolap, including syntax, referential actions, cascading behavior, and transaction semantics.
Overview
Stoolap supports foreign key (FK) constraints that enforce referential integrity between tables. A foreign key ensures that values in a child table column always reference an existing row in a parent table. This prevents orphaned records and maintains data consistency.
| Feature | Description |
|---|---|
| Zero cost for non-FK tables | All checks short-circuit when no FK constraints exist |
| Index-based lookups | Parent existence checks use PK or secondary indexes (O(1) or O(log N)) |
| Transaction-aware | FK checks see uncommitted rows within the same transaction |
| Automatic FK indexes | Indexes are auto-created on FK columns for efficient cascade operations |
| WAL and snapshot durable | FK metadata survives restart and WAL truncation |
Syntax
Column-Level REFERENCES
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
product_id INTEGER REFERENCES products(id) ON DELETE CASCADE,
amount FLOAT
);
When the referenced column is omitted, it defaults to the parent table’s primary key:
-- References customers(id) — the primary key
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers
);
Table-Level FOREIGN KEY
CREATE TABLE order_items (
id INTEGER PRIMARY KEY,
order_id INTEGER,
product_id INTEGER,
quantity INTEGER,
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE RESTRICT
);
Multiple FK Columns
A table can have multiple foreign key columns referencing different parent tables:
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
dept_id INTEGER REFERENCES departments(id),
mgr_id INTEGER REFERENCES managers(id),
name TEXT
);
Referential Actions
Referential actions define what happens to child rows when the referenced parent row is deleted or updated.
ON DELETE Actions
| Action | Behavior |
|---|---|
RESTRICT |
Block the delete if child rows exist (default) |
NO ACTION |
Same as RESTRICT for immediate constraint checking |
CASCADE |
Automatically delete all child rows referencing the parent |
SET NULL |
Set the FK column to NULL in all child rows referencing the parent |
ON UPDATE Actions
| Action | Behavior |
|---|---|
RESTRICT |
Block the update if child rows reference the old PK value (default) |
NO ACTION |
Same as RESTRICT for immediate constraint checking |
CASCADE |
Update the FK column in all child rows to the new PK value |
SET NULL |
Set the FK column to NULL in all child rows referencing the old PK value |
Examples
-- RESTRICT (default): block parent delete if children exist
CREATE TABLE children (
id INTEGER PRIMARY KEY,
parent_id INTEGER REFERENCES parents(id)
);
-- CASCADE: delete children when parent is deleted
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id) ON DELETE CASCADE
);
-- SET NULL: nullify FK when parent is deleted
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL
);
-- Combined actions
CREATE TABLE line_items (
id INTEGER PRIMARY KEY,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE ON UPDATE CASCADE
);
NULL FK Values
A NULL value in a FK column means “no reference” and is always allowed, regardless of the referential action. This follows the SQL standard:
-- NULL FK is valid — the row has no parent reference
INSERT INTO children (id, parent_id, name) VALUES (1, NULL, 'No Parent');
Multi-Level Cascading
CASCADE operations recurse through the FK hierarchy. If a grandparent is deleted, the cascade propagates through parent tables to child tables:
CREATE TABLE grandparents (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE parents (
id INTEGER PRIMARY KEY,
gp_id INTEGER REFERENCES grandparents(id) ON DELETE CASCADE
);
CREATE TABLE children (
id INTEGER PRIMARY KEY,
parent_id INTEGER REFERENCES parents(id) ON DELETE CASCADE
);
-- Deleting a grandparent cascades through parents to children
DELETE FROM grandparents WHERE id = 1;
-- Deletes matching parents AND their children
A maximum recursion depth of 16 levels is enforced to prevent infinite loops from circular references.
Mixed Actions in Multi-Level Hierarchies
RESTRICT at any level in the cascade chain blocks the entire operation:
CREATE TABLE grandparents (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE parents (
id INTEGER PRIMARY KEY,
gp_id INTEGER REFERENCES grandparents(id) ON DELETE CASCADE
);
-- RESTRICT here blocks the cascade from grandparents
CREATE TABLE children (
id INTEGER PRIMARY KEY,
parent_id INTEGER REFERENCES parents(id) ON DELETE RESTRICT
);
-- This fails: CASCADE would delete parents, but RESTRICT blocks because children exist
DELETE FROM grandparents WHERE id = 1;
-- Error: cannot cascade-delete row — still referenced by table 'children'
Transaction Semantics
FK Checks See Uncommitted Rows
Within a single transaction, FK checks see uncommitted inserts and deletes:
BEGIN;
-- Insert a new parent (not yet committed)
INSERT INTO parents VALUES (100, 'New Parent');
-- This succeeds: the FK check sees the uncommitted parent row
INSERT INTO children VALUES (1, 100, 'Child');
COMMIT;
BEGIN;
-- Delete a parent (not yet committed)
DELETE FROM parents WHERE id = 1;
-- This fails: the FK check sees the uncommitted delete
INSERT INTO children VALUES (2, 1, 'Child');
-- Error: referenced row does not exist
ROLLBACK;
CASCADE Atomicity
CASCADE effects participate in the caller’s transaction. A ROLLBACK undoes both the parent operation and all cascaded child changes:
BEGIN;
-- CASCADE deletes matching children
DELETE FROM parents WHERE id = 1;
-- Both the parent delete AND child cascade are undone
ROLLBACK;
-- All rows are back to their original state
DDL Interactions
DROP TABLE
DROP TABLE is blocked if any child table has rows with non-NULL FK values referencing the table, regardless of the FK action (RESTRICT, CASCADE, or SET NULL). DDL operations do not cascade to child rows:
-- Blocked: child rows reference this table
DROP TABLE parents;
-- Error: cannot drop/truncate table 'parents' — rows in 'children' still reference it
-- Solution: delete child rows first, then drop
DELETE FROM children WHERE parent_id IS NOT NULL;
DROP TABLE parents;
When a parent table is dropped (after child references are cleared), FK constraints referencing it are automatically stripped from child table schemas:
DROP TABLE parents;
-- children.parent_id is no longer a FK — inserts with any value succeed
INSERT INTO children VALUES (1, 999, 'No FK');
TRUNCATE TABLE
TRUNCATE is blocked when child tables have referencing rows, same as DROP TABLE:
-- Blocked if children reference this table
TRUNCATE TABLE parents;
-- Child table truncation is always allowed
TRUNCATE TABLE children;
CREATE TABLE Validation
FK constraints are validated at table creation time:
| Check | Error |
|---|---|
| Parent table must exist | references non-existent table |
| Referenced column must exist in parent | references non-existent column |
| Referenced column must be PRIMARY KEY or UNIQUE | neither PRIMARY KEY nor UNIQUE |
| SET NULL requires nullable FK column | SET NULL but is NOT NULL |
-- Rejected: SET NULL on NOT NULL column
CREATE TABLE bad (
id INTEGER PRIMARY KEY,
parent_id INTEGER NOT NULL REFERENCES parents(id) ON DELETE SET NULL
);
-- Error: foreign key column 'parent_id' has ON DELETE SET NULL but is NOT NULL
Automatic FK Indexes
When a FK constraint is defined, Stoolap automatically creates an index on the FK column (unless the column already has a PRIMARY KEY or UNIQUE index). This ensures efficient:
- CASCADE DELETE / SET NULL operations (find child rows by FK value)
- RESTRICT checks (quickly determine if child rows exist)
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
dept_id INTEGER REFERENCES departments(id)
);
-- An index named 'fk_employees_dept_id' is automatically created
SHOW INDEXES FROM employees;
Performance
| Operation | Complexity | Notes |
|---|---|---|
| INSERT FK check | O(1) or O(log N) | PK fast path or index lookup on parent |
| DELETE RESTRICT check | O(log N) | Index lookup on child FK column |
| DELETE CASCADE | O(K log N) | K = number of child rows to delete |
| DELETE SET NULL | O(K log N) | K = number of child rows to update |
| Non-FK table operations | Zero cost | All checks short-circuit immediately |
Persistence
FK constraints are persisted through both WAL (Write-Ahead Log) and snapshots. After a crash and recovery, or after WAL truncation:
- FK constraints are fully restored from the latest snapshot
- All referential actions (CASCADE, SET NULL, RESTRICT) work as before
- Auto-created FK indexes are also persisted and restored
Limitations
- FK constraints reference single columns (composite FK not yet supported)
- ON UPDATE CASCADE does not recursively cascade to grandchild tables when the child’s FK column is also its PK
- Self-referencing FK (a table referencing itself) follows the same rules but requires careful ordering of inserts