Schema changes
ALTER TABLE
| Change | Status | Notes |
|---|---|---|
ADD COLUMN c TYPE [DEFAULT expr] [NOT NULL], one or many | Supported since 0.1.42 | Online. Existing rows read NULL, or the DEFAULT, immediately; a write that sets the new column is accepted immediately. |
DROP COLUMN c | Supported | Logical: the column disappears from every read and from DESCRIBE; data is not rewritten. |
ALTER COLUMN c SET DATA TYPE T | Partial | Widening only (for example INTEGER → BIGINT, VARCHAR(10) → VARCHAR(40), DECIMAL(10,2) → DECIMAL(18,2)). Logical cast at read. A narrowing retype is refused. |
Several ADD / DROP / ALTER COLUMN in one statement | Supported | Applied as one change. |
ADD CONSTRAINT … CHECK (…) | Enforced23514 | Live from the next statement. Predicate grammar on Constraints. |
ADD IMMUTABLE name CHECK (…) | Enforced23P90 | Seals matching rows against any further write. |
ADD CONSTRAINT … FOREIGN KEY (c) REFERENCES p (id) | Enforced23503 since 0.1.44 | Insert-side enforcement from the next statement, on every node (a DDL event evicts each coordinator’s cached constraint set). DEFERRABLE and ON DELETE/ON UPDATE actions are refused 0A000; a parent whose referenced columns are not its primary key is 42830. |
ADD CONSTRAINT … UNIQUE | Enforced23505 | Enforced through a companion unique index; existing duplicates fail the ALTER. Applies on every node from the next statement since 0.1.45. |
DROP CONSTRAINT name | Supported | Removes a stored CHECK or FOREIGN KEY. An unknown name is a no-op, so migration downgrades proceed. |
RENAME COLUMN / RENAME TO | Refused42703 | ALTER TABLE form not supported …. See below. |
ALTER COLUMN other than SET DATA TYPE (SET DEFAULT, SET NOT NULL, …) | Refused42703 | Recreate the table. |
ALTER TABLE orders
ADD COLUMN channel VARCHAR(16) DEFAULT 'web',
ADD COLUMN note VARCHAR,
DROP COLUMN legacy_flag,
ALTER COLUMN amount SET DATA TYPE DECIMAL(18,2);
SELECT id, channel, note FROM orders LIMIT 3; -- works immediately; note is NULL, channel is 'web'Renaming
Rename by copying. CREATE TABLE … AS SELECT infers the schema from the query; add the primary key with a CREATE TABLE first if you need it declared, then INSERT … SELECT.
CREATE TABLE orders_v2 (
id BIGINT PRIMARY KEY,
customer_id VARCHAR NOT NULL,
amount_gbp DECIMAL(18,2) NOT NULL -- the renamed column
);
INSERT INTO orders_v2 SELECT id, customer_id, amount FROM orders; -- columns map by position
DROP TABLE orders;Writes that arrive between the copy and the DROP are not carried over. Do the switch in a maintenance window, or dual-write during it.
Indexes
| Form | Status | Notes |
|---|---|---|
CREATE INDEX ON t (col) | Supported | Non-unique, exactly one column. Speeds up equality lookups on non-key columns. |
CREATE UNIQUE INDEX ON t (a, b) | Enforced23505 | Composite allowed. The enforced way to get uniqueness beyond the primary key. |
CREATE INDEX ON t USING hnsw (emb vector_cosine_ops) WITH (m = 16, ef_construction = 64) | Supported | Vector ANN. Operator classes vector_l2_ops (default), vector_ip_ops, vector_cosine_ops. WITH accepts m, ef_construction, lists, quantization, async. An unknown key is an error. |
CREATE INDEX ON t USING ivfflat (emb vector_l2_ops) WITH (lists = 100) | Supported | Vector ANN, IVF. |
| Index on a populated table | Supported since 0.1.41 | Rows present before the CREATE INDEX are indexed and answer WHERE col = v as soon as the statement returns. |
CREATE INDEX ON t (a, b) non-unique | Refused42601 | One index per column, or a unique index. |
CREATE INDEX ON t (lower(email)) | Refused42601 | Materialise the expression into a column. |
DROP INDEX name | Refused42601 | An index is a table named __idx_<table>_<column> (__uidx_… for unique). DROP TABLE it. |
CREATE INDEX ON customers (email);
DROP TABLE __idx_customers_email; -- the DROP INDEX spelling
CREATE UNIQUE INDEX ON memberships (org_id, user_id);
DROP TABLE __uidx_memberships_org_id_user_id;SHOW VECTOR INDEXES lists vector indexes and their build state (building for an async build).
TRUNCATE
TRUNCATE TABLE t (or TRUNCATE t) removes every row and keeps the schema, primary key, indexes, constraints and graph bindings. It is the answer to “clear the table”, since DELETE FROM t without a WHERE is refused. TRUNCATE a, b and TRUNCATE … PARTITION (…) are refused: one table per statement, all rows.
RESTORE
Every commit to a table is kept as a version, so you can read the past and go back to it.
SELECT COUNT(*) FROM orders VERSION AS OF 42; -- read a version, table unchanged
RESTORE TABLE orders TO VERSION AS OF 42; -- go back
RESTORE TABLE orders TO TIMESTAMP AS OF '2026-09-17T10:00:00Z'; -- offset requiredWhat RESTORE does, precisely:
- It rolls the table back to the target version and discards every write acknowledged after it, including writes that had not yet been folded into a version. They cannot come back.
- Indexes are not rolled back. Rebuild them (
DROP TABLE __idx_…,CREATE INDEX …) if the restored data no longer matches. TIMESTAMP AS OFrequires an explicit offset (…Z,…+02:00).- History older than 168 hours may have been removed by
vacuum; a version that no longer exists cannot be restored to. There is no way to lower that floor.
The Time travel & restore recipe walks through a safe restore.
Migration tools
The engine accepts what Alembic, Django migrations, Prisma Migrate and Prisma db push emit, with these consequences:
| Tool emits | What happens |
|---|---|
CREATE TABLE … id SERIAL PRIMARY KEY / GENERATED … AS IDENTITY | Column becomes BIGINT with a generated 64-bit id. Read it back with RETURNING. |
INTEGER columns | Stored and introspected as BIGINT. A migration autogenerate may propose a retype; it is a no-op, ignore or accept it. |
Bare NUMERIC / DECIMAL | Becomes NUMERIC(38,9) with a NOTICE. Declare (p,s) in the model. |
REFERENCES (at CREATE TABLE) | Enforced on insert (23503). Not reported back by the catalog, so a later autogenerate sees the FK as missing. Suppress FK diffs in the tool. |
ADD CONSTRAINT … FOREIGN KEY (via migration) | Enforced on insert from the next statement, on every node since 0.1.44. Not retroactive: existing orphans stay. |
… ON DELETE CASCADE / SET NULL / SET DEFAULT, DEFERRABLE, INITIALLY DEFERRED | Fails 0A000. Remove the option from the model (Django on_delete=models.CASCADE still emits it; override the migration). Cascade in the application. |
DROP CONSTRAINT <fk name> on downgrade | Removes the FK if it exists; an unknown name is a no-op. |
CREATE INDEX … ON t (a, b) non-unique | Fails. Split into one index per column or make it unique. |
ALTER TABLE … RENAME | Fails. Write the copy migration by hand. |
CREATE SCHEMA public, SET search_path, public. qualifiers | Accepted and ignored. Other schema names are not rewritten and fail. |
CREATE TRIGGER, CREATE FUNCTION … RETURNS trigger | 0A000 with the roadmap in the message since 0.1.43. Remove them; use CHECK constraints for validation and ingest rules or Actions for reactions. |
CREATE VIEW, CREATE SEQUENCE | 42601 statement is not part of the tables SQL surface. Materialise with CREATE TABLE … AS SELECT; use SERIAL for ids. |
DEFAULT 0 on a NUMERIC column | 42703 DEFAULT literal … does not match column type decimal(p,s). Drop the default; literal defaults work on BIGINT / VARCHAR / BOOLEAN. |
Prisma db push and prisma migrate dev complete, SQLAlchemy create_all and Alembic upgrade/downgrade complete, Django migrate completes. The Migrating from Postgres checklist has the per-ORM notes.
Reserved words
The dialect’s reserved words include a few common column names: at, end, order, table, to, user, offset. A bare at TIMESTAMP in CREATE TABLE, or WHERE at > …, is a syntax error. Double-quote the identifier everywhere it is referenced ("at"), or pick an unreserved name such as occurred_at.