Constraints
Constraints in PizzaSQL fall into two camps: the ones that are actually implemented, and the ones that merely parse. This page is deliberately blunt about the difference, because silently assuming a UNIQUE or FOREIGN KEY constraint is enforced will corrupt data.
The enforced constraints
Section titled “The enforced constraints”PRIMARY KEY
Section titled “PRIMARY KEY”- A single column can be
PRIMARY KEY, or a table-levelPRIMARY KEY (col, ...). - A table-level composite key uses only the first named column as the effective primary key; true composite keys are not supported.
- An
INTEGER PRIMARY KEYcolumn becomes an alias for the implicit rowid. Inserting without a value auto-assigns the next rowid; inserting with a value sets the rowid and advances the counter past it. - A non-integer primary key (e.g.
TEXT PRIMARY KEY) is a normal column with a separate hidden rowid counter. - The primary key drives duplicate detection on
INSERT, and thereforeINSERT OR REPLACE/OR IGNOREandON CONFLICT DO NOTHING/DO UPDATE— all of which are keyed on the primary key only. - Without any declared primary key, the engine assigns a synthetic
_rowid_primary key, so duplicate and NULL values are allowed in user columns.
NOT NULL
Section titled “NOT NULL”NOT NULLis enforced onINSERT: inserting a row that omits aNOT NULLcolumn with no default is rejected withmissing required column.- It does not enforce on
UPDATEtoNULLin all paths, and does not validate type.
DEFAULT
Section titled “DEFAULT”DEFAULT expris evaluated when the table is created, then stored and applied to inserted rows that omit the column.- Because it’s evaluated at DDL time, only constant expressions are useful. There is no
DEFAULT (expr)re-evaluation per row.
AUTOINCREMENT
Section titled “AUTOINCREMENT”AUTOINCREMENTparses but has no behaviour. It does not prevent rowid reuse, does not maintain a separate sequence, and behaves exactly like a plainINTEGER PRIMARY KEY.- It is retained in the schema JSON for compatibility but is never read.
The non-enforced constraints
Section titled “The non-enforced constraints”These are accepted by the parser and then silently discarded — the schema does not store them and no code checks them:
| Constraint | Parsed? | Stored? | Enforced? |
|---|---|---|---|
UNIQUE (col) |
yes | no | no |
UNIQUE column constraint |
yes | no | no |
CHECK (expr) |
yes | no | no |
FOREIGN KEY ... REFERENCES ... |
yes | no | no |
CREATE UNIQUE INDEX |
yes | definition stored | no |
Consequences to internalize:
- Duplicate values are allowed in any column except the primary key. If you need uniqueness, enforce it in your application.
- Foreign-key relationships are not validated; deleting a parent row does not cascade, restrict, or set null on children.
CHECKconstraints never run.CREATE TABLE t (age INTEGER CHECK (age > 0))happily accepts-5.CREATE UNIQUE INDEXrecords an index markeduniquein the catalog (it shows up inpg_indexes), but the uniqueness flag is cosmetic — the index still only accelerates lookups and does not reject duplicates.
Why this matters for migrations
Section titled “Why this matters for migrations”SQLite-flavoured schemas often lean on UNIQUE and FOREIGN KEY for integrity. When importing such a schema (SQL dump or SQLite file), those constraints are dropped silently rather than erroring. Inspect the imported schema afterwards and add application-level checks for anything that must stay unique or referentially consistent.
See Compatibility for the wider gap list, and Indexes for how CREATE INDEX/UNIQUE INDEX actually behave at the storage layer.