How do you design a CREATE TABLE statement — what actually belongs in it?
A CREATE TABLE is four decisions in one statement. The columns and their types, the primary key, the constraints that protect the data, and the defaults that fill in what the caller did not supply. The order you make them in matters, because a column's type constrains what constraints can say about it.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL,
email VARCHAR(200) NOT NULL UNIQUE,
country VARCHAR(60) NOT NULL DEFAULT 'GB',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
CONSTRAINT chk_email_has_at
CHECK (instr(email, '@') > 0)
);Six decisions in five lines. The id is the primary key, so it is NOT NULL and unique by definition. The name is required. The email is required and unique, because one account per address is a business rule rather than an application concern. The country has a default so that callers do not have to supply it. The timestamp has a default expression that is evaluated per row. And the CHECK constraint enforces a rule that a type declaration cannot express.
The design principle underneath all of it is that the database should own the invariants. An application-level check can be bypassed by a migration script, a debugging session or a second service; a constraint cannot. If the rule is that an order always belongs to a customer, that belongs in a FOREIGN KEY, not in a controller.
Two things to avoid. Do not add a constraint for a rule you do not actually mean — a UNIQUE column that the application treats as optional causes real grief when two rows legitimately share the value. And do not skip the NOT NULL declarations because the column "is always supplied": the day somebody inserts a row without it, you want the database to stop them rather than to store a NULL that breaks every downstream query.