Skip to main content
Dropping a column, changing a type, or renaming a table are changes that can permanently destroy data if applied carelessly. supaschema blocks these operations by default and requires you to explicitly acknowledge them with a hint before it will render the corresponding SQL. This two-step pattern — declare intent, then generate — prevents accidental data loss from a typo in a schema file and creates a reviewable paper trail for every destructive decision.

What counts as destructive

supaschema classifies the following as destructive operations that require an explicit hint:
  • Column drops — removing a column from a CREATE TABLE statement
  • Column type changes — changing the data type of an existing column
  • Table drops — removing a table entirely from your schema
  • View drops — removing a view, or redefining a view in a way that changes its column list
  • Function signature changes — altering parameter types or return types of an existing function
  • Enum value removal or reorder — removing a label from an existing CREATE TYPE ... AS ENUM or changing the order of labels
Operations that are not destructive and require no hint include adding columns, adding tables, adding enum values, creating new indexes, and updating DEFAULT expressions.

Default behavior

When supaschema detects a destructive change without a corresponding hint, the diff command exits non-zero and prints one of these diagnostics:
Diagnostic codeTrigger
SUPA_PLAN_DESTRUCTIVE_HINT_REQUIREDColumn drop, table drop, view drop
SUPA_PLAN_COLUMN_ALTER_HINT_REQUIREDColumn type change
SUPA_PLAN_RENAME_VERIFY_REQUIREDTable or column rename
SUPA_CHECK_ALTER_COLUMN_TYPE_REWRITEType change that requires a full table rewrite
No migration file is written until all required hints are present.

Dropping a column

Before

-- supabase/schemas/001_app.sql
CREATE TABLE app.accounts (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name        text NOT NULL,
  legacy_ref  text,           -- <-- you want to drop this
  status      text NOT NULL DEFAULT 'active'
);

After

-- supabase/schemas/001_app.sql
CREATE TABLE app.accounts (
  id      uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name    text NOT NULL,
  status  text NOT NULL DEFAULT 'active'
);
1

Remove the column from your schema file

Delete the legacy_ref text, line from supabase/schemas/001_app.sql and save.
2

Run diff with --write-hints to discover the hint key

npx supaschema diff \
  --from git:HEAD \
  --to dir:supabase/schemas \
  --write-hints
supaschema prints the SUPA_PLAN_DESTRUCTIVE_HINT_REQUIRED diagnostic and writes the required hint key to stdout. It also appends a suggested entry to supaschema.config.json under hints.destructive.
3

Confirm the hint in your config

Open supaschema.config.json and verify the entry was added:
{
  "hints": {
    "destructive": [
      "column:app.accounts.legacy_ref"
    ]
  }
}
4

Run diff again to generate the migration

npx supaschema diff \
  --from git:HEAD \
  --to dir:supabase/schemas
supaschema now renders the migration and writes it to supabase/migrations/:
-- supabase/migrations/20240601120000_drop_legacy_ref.sql
ALTER TABLE app.accounts
  DROP COLUMN IF EXISTS legacy_ref;
5

Verify the migration

npx supaschema verify \
  --from git:HEAD \
  --to dir:supabase/schemas \
  --migration supabase/migrations/20240601120000_drop_legacy_ref.sql \
  --database-url postgresql://postgres:postgres@localhost:54322/postgres
The hint key "column:app.accounts.legacy_ref" is a permanent acknowledgement that this column may be dropped. Remove the entry from hints.destructive after the migration has been applied to production so a future accidental deletion is caught again.

Changing a column type

Before → After

-- Before
created_at  bigint NOT NULL

-- After
created_at  timestamptz NOT NULL
1

Update the type in your schema file

Change bigint to timestamptz for the created_at column.
2

Get the hint key

npx supaschema diff --from git:HEAD --to dir:supabase/schemas --write-hints
The diagnostic SUPA_PLAN_COLUMN_ALTER_HINT_REQUIRED is raised. The suggested key is written to your config:
{
  "hints": {
    "destructive": [
      "column:app.accounts.created_at"
    ]
  }
}
3

Run diff to generate the migration

npx supaschema diff --from git:HEAD --to dir:supabase/schemas
The generated SQL uses a USING cast to convert existing values:
ALTER TABLE app.accounts
  ALTER COLUMN created_at TYPE timestamptz
  USING to_timestamp(created_at);
4

Review the USING expression

supaschema generates a best-effort USING clause, but you should verify it produces correct results for your data. Edit the migration file directly if the default conversion is not appropriate — for example, if your bigint stores milliseconds rather than seconds.
The SUPA_CHECK_ALTER_COLUMN_TYPE_REWRITE diagnostic is raised when the type change requires PostgreSQL to rewrite the entire table. This takes an ACCESS EXCLUSIVE lock and blocks reads and writes for the duration of the rewrite. For large tables, schedule the migration during a maintenance window or use a shadow-column approach.

Renaming a table or column

Supaschema cannot infer a rename from a schema diff alone — a renamed table looks identical to a drop followed by a create. You must supply a rename hint that explicitly maps the old name to the new name.

Renaming a table

{
  "hints": {
    "renames": [
      { "from": "table:app.users", "to": "table:app.accounts" }
    ]
  }
}
1

Update the table name in your schema file

Change every reference from app.users to app.accounts in your schema SQL files.
2

Add the rename hint to your config

Add the from/to entry shown above under hints.renames.
3

Run diff

npx supaschema diff --from git:HEAD --to dir:supabase/schemas
supaschema renders:
ALTER TABLE app.users RENAME TO accounts;
4

Run verify — strongly recommended

The SUPA_PLAN_RENAME_VERIFY_REQUIRED diagnostic flags renames as operations that should always be verified before applying:
npx supaschema verify \
  --from git:HEAD \
  --to dir:supabase/schemas \
  --migration supabase/migrations/20240601130000_rename_users.sql \
  --database-url postgresql://postgres:postgres@localhost:54322/postgres \
  --ensure-roles

Renaming a column

{
  "hints": {
    "renames": [
      { "from": "column:app.accounts.user_name", "to": "column:app.accounts.display_name" }
    ]
  }
}
The generated SQL will be:
ALTER TABLE app.accounts RENAME COLUMN user_name TO display_name;

Changing a view’s column list

Adding, removing, or reordering columns in a view cannot be done with CREATE OR REPLACE VIEW alone when the column list changes. supaschema drops and recreates the view.
1

Update the view in your schema file

-- Before
CREATE VIEW app.account_summary AS
  SELECT id, name, legacy_ref, status FROM app.accounts;

-- After
CREATE VIEW app.account_summary AS
  SELECT id, name, status FROM app.accounts;
2

Run diff with --write-hints

npx supaschema diff --from git:HEAD --to dir:supabase/schemas --write-hints
The hint key "view:app.account_summary" is suggested.
3

Confirm the hint

{
  "hints": {
    "destructive": [
      "view:app.account_summary"
    ]
  }
}
4

Run diff to generate the migration

npx supaschema diff --from git:HEAD --to dir:supabase/schemas
Generated SQL:
DROP VIEW IF EXISTS app.account_summary;

CREATE VIEW app.account_summary AS
  SELECT id, name, status FROM app.accounts;
Dependent objects (materialized views, functions that reference the view) will also be dropped by the DROP VIEW cascade. Check your schema for dependents before applying.

Enum changes

Adding values — no hint needed

Adding labels to an existing enum is non-destructive. supaschema renders this automatically:
-- Before
CREATE TYPE app.account_status AS ENUM ('active', 'suspended');

-- After
CREATE TYPE app.account_status AS ENUM ('active', 'suspended', 'deleted');
Generated migration:
ALTER TYPE app.account_status ADD VALUE IF NOT EXISTS 'deleted';

Removing or reordering enum values — must hand-author

PostgreSQL does not support ALTER TYPE ... DROP VALUE or reordering of enum labels. supaschema will raise SUPA_PLAN_DESTRUCTIVE_HINT_REQUIRED and will not attempt to automate this change because there is no safe, reversible SQL expression for it. You must hand-author a migration using the temporary-type pattern:
-- Step 1: Create a new enum with the desired values
CREATE TYPE app.account_status_new AS ENUM ('active', 'deleted');

-- Step 2: Update all columns that use the old type
ALTER TABLE app.accounts
  ALTER COLUMN status TYPE app.account_status_new
  USING status::text::app.account_status_new;

-- Step 3: Drop the old type and rename the new one
DROP TYPE app.account_status;
ALTER TYPE app.account_status_new RENAME TO account_status;
The USING cast in step 2 will fail at runtime if any row contains a value that does not exist in the new enum. Run a data audit — SELECT DISTINCT status FROM app.accounts — before applying.
After hand-authoring the migration, update your schema file to match the new enum definition and run npx supaschema diff to confirm zero drift.

Complete before/after example

The following shows a real schema change that combines a column drop and a type change, the resulting hint config, and the generated migration.

Schema before

-- supabase/schemas/001_app.sql
CREATE TABLE app.orders (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id  uuid NOT NULL REFERENCES app.accounts(id),
  amount      bigint NOT NULL,        -- stored as cents, bigint
  note        text,                   -- to be dropped
  created_at  timestamptz NOT NULL DEFAULT now()
);

Schema after

-- supabase/schemas/001_app.sql
CREATE TABLE app.orders (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id  uuid NOT NULL REFERENCES app.accounts(id),
  amount      numeric(12, 2) NOT NULL, -- changed to decimal
  created_at  timestamptz NOT NULL DEFAULT now()
);

supaschema.config.json

{
  "$schema": "./node_modules/supaschema/config-schema.json",
  "hints": {
    "destructive": [
      "column:app.orders.note",
      "column:app.orders.amount"
    ]
  }
}

Generated migration

-- supabase/migrations/20240601140000_orders_cleanup.sql
ALTER TABLE app.orders
  DROP COLUMN IF EXISTS note;

ALTER TABLE app.orders
  ALTER COLUMN amount TYPE numeric(12, 2)
  USING (amount / 100.0);

Verify

npx supaschema verify \
  --from git:HEAD \
  --to dir:supabase/schemas \
  --migration supabase/migrations/20240601140000_orders_cleanup.sql \
  --database-url postgresql://postgres:postgres@localhost:54322/postgres
A passing verify confirms the migration applies cleanly, is idempotent, and leaves the database in a state that matches your declared schema.