Skip to Content
We are live but in Staging 🎉
Data EnginesSQLSchema changes

Schema changes

ALTER TABLE

ChangeStatusNotes
ADD COLUMN c TYPE [DEFAULT expr] [NOT NULL], one or manySupported since 0.1.42Online. Existing rows read NULL, or the DEFAULT, immediately; a write that sets the new column is accepted immediately.
DROP COLUMN cSupportedLogical: the column disappears from every read and from DESCRIBE; data is not rewritten.
ALTER COLUMN c SET DATA TYPE TPartialWidening 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 statementSupportedApplied as one change.
ADD CONSTRAINT … CHECK (…)Enforced23514Live from the next statement. Predicate grammar on Constraints.
ADD IMMUTABLE name CHECK (…)Enforced23P90Seals matching rows against any further write.
ADD CONSTRAINT … FOREIGN KEY (c) REFERENCES p (id)Enforced23503 since 0.1.44Insert-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 … UNIQUEEnforced23505Enforced through a companion unique index; existing duplicates fail the ALTER. Applies on every node from the next statement since 0.1.45.
DROP CONSTRAINT nameSupportedRemoves a stored CHECK or FOREIGN KEY. An unknown name is a no-op, so migration downgrades proceed.
RENAME COLUMN / RENAME TORefused42703ALTER TABLE form not supported …. See below.
ALTER COLUMN other than SET DATA TYPE (SET DEFAULT, SET NOT NULL, …)Refused42703Recreate 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

FormStatusNotes
CREATE INDEX ON t (col)SupportedNon-unique, exactly one column. Speeds up equality lookups on non-key columns.
CREATE UNIQUE INDEX ON t (a, b)Enforced23505Composite 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)SupportedVector 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)SupportedVector ANN, IVF.
Index on a populated tableSupported since 0.1.41Rows 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-uniqueRefused42601One index per column, or a unique index.
CREATE INDEX ON t (lower(email))Refused42601Materialise the expression into a column.
DROP INDEX nameRefused42601An 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 required

What 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 OF requires 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 emitsWhat happens
CREATE TABLE … id SERIAL PRIMARY KEY / GENERATED … AS IDENTITYColumn becomes BIGINT with a generated 64-bit id. Read it back with RETURNING.
INTEGER columnsStored and introspected as BIGINT. A migration autogenerate may propose a retype; it is a no-op, ignore or accept it.
Bare NUMERIC / DECIMALBecomes 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 DEFERREDFails 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 downgradeRemoves the FK if it exists; an unknown name is a no-op.
CREATE INDEX … ON t (a, b) non-uniqueFails. Split into one index per column or make it unique.
ALTER TABLE … RENAMEFails. Write the copy migration by hand.
CREATE SCHEMA public, SET search_path, public. qualifiersAccepted and ignored. Other schema names are not rewritten and fail.
CREATE TRIGGER, CREATE FUNCTION … RETURNS trigger0A000 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 SEQUENCE42601 statement is not part of the tables SQL surface. Materialise with CREATE TABLE … AS SELECT; use SERIAL for ids.
DEFAULT 0 on a NUMERIC column42703 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.