Subchapter 63.9
references/ddl-migrations/overview.mdMarkdown10 KBView on GitHub
This guide provides the Table Recreation Pattern for schema modifications that require rebuilding tables.
For column-level operations, see column-operations.md. For constraint and structural operations, see . For batched migration patterns, see .
The Table Recreation Pattern involves DESTRUCTIVE operations that can result in DATA LOSS.
Table recreation requires dropping the original table, which is irreversible. If any step fails after the original table is dropped, data may be permanently lost.
Agents MUST obtain explicit user approval before executing migrations on live tables:
Before proceeding, the user MUST confirm:
The following ALTER TABLE operations MUST use the Table Recreation Pattern:
| Operation | Key Approach |
|---|---|
| DROP COLUMN | Exclude column from new table |
| ALTER COLUMN TYPE | Cast data type in SELECT |
| ALTER COLUMN SET/DROP NOT NULL | Change constraint in new table definition |
| ALTER COLUMN SET/DROP DEFAULT | Define default in new table definition |
| ADD CONSTRAINT | Include constraint in new table definition |
| DROP CONSTRAINT | Remove constraint from new table definition |
| MODIFY PRIMARY KEY | Define new PK, validate uniqueness first |
| Split/Merge Columns | Use SPLIT_PART, SUBSTRING, or CONCAT in SELECT |
Note: The following operations ARE supported directly. Each is still subject to the
one-DDL-per-transaction rule — issue each as its own psql -c invocation (or its own
BEGIN/COMMIT block when scripted):
ALTER TABLE ... RENAME COLUMN - Rename a columnALTER TABLE ... RENAME TO - Rename a tableALTER TABLE ... ADD COLUMN - Add a new columnDSQL accepts a narrow subset of standard PostgreSQL CREATE INDEX syntax. The skill enforces
CREATE INDEX ASYNC everywhere; additional clauses behave as follows (validated against a live
DSQL cluster):
| Clause | DSQL behavior |
|---|---|
IF NOT EXISTS | Accepted |
INCLUDE (<columns>) | Accepted (covering indexes) |
UNIQUE (CREATE UNIQUE INDEX ASYNC) | Accepted |
USING <method> (btree/hash/gin/…) | Rejected: ERROR: USING not supported for CREATE INDEX — DSQL is btree-only |
WHERE <predicate> | Rejected: ERROR: WHERE not supported for CREATE INDEX — partial indexes are unavailable |
CONCURRENTLY | Rejected: ERROR: CONCURRENTLY not supported for CREATE INDEX — use ASYNC instead (non-blocking by design) |
Without ASYNC, DSQL rejects with ERROR: unsupported mode. please use CREATE INDEX ASYNC. —
useful to grep for in failure logs.
If the migration source (e.g., a vanilla PostgreSQL dump) relies on partial indexes or non-btree access methods, the pattern MUST be rewritten — denormalize via a filter column, or add a CHECK constraint and a covering composite index. Document the workaround in the migration plan.
MUST follow this sequence with user verification at each step:
Defaults below; verify against the live limits via the AWS MCP Server’s aws___search_documentation if available (aurora dsql transaction limits), or read the DSQL documentation (opens in a new tab) directly:
All migrations end with this pattern (referenced in column-operations.md and constraint-operations.md).
CRITICAL: MUST obtain explicit user confirmation before DROP TABLE step.
-- MUST verify counts match
SELECT COUNT(*) FROM target_table;
SELECT COUNT(*) FROM target_table_new;
-- CHECKPOINT: MUST present count comparison to user and obtain confirmation
-- Agent MUST display: "Original table has X rows, new table has Y rows.
-- Proceeding will DROP the original table. This action is IRREVERSIBLE.
-- Do you want to proceed? (yes/no)"
-- MUST NOT proceed without explicit "yes" confirmation
-- MUST swap tables (DESTRUCTIVE - requires user confirmation above).
-- Each DDL below MUST run in its own transaction (DSQL: one DDL per
-- transaction). Run as separate `psql-connect.sh --command` calls,
-- or as separate transactions in your driver:
DROP TABLE target_table;
ALTER TABLE target_table_new RENAME TO target_table;
-- MUST recreate indexes (each in its own transaction; CREATE INDEX ASYNC
-- is non-blocking and required by DSQL):
CREATE INDEX ASYNC idx_target_tenant ON target_table(tenant_id);When target_table_new has fewer rows than target_table, treat the migration as incomplete.
The original table still holds the authoritative data, so recovery is always possible — MUST NOT
proceed with DROP TABLE until the counts agree.
Diagnose — find the missing rows by comparing ranges (for cursor-based migrations, query
target_table for IDs greater than MAX(id) in target_table_new; for OFFSET-based, check
which batch dropped rows by re-running the SELECT portion of each batch and comparing counts).
Retry the missing batches — insert only the gap rows into target_table_new. Filter out
already-migrated rows to avoid PK collisions (which would roll back the entire batch):
-- Cursor-based: only insert rows beyond what was already migrated
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
WHERE id > (SELECT COALESCE(MAX(id), 0) FROM target_table_new)
ORDER BY id LIMIT 1000;
-- For non-sequential gaps, use NOT EXISTS:
INSERT INTO target_table_new (id, col1, col2)
SELECT id, col1, col2 FROM target_table
WHERE NOT EXISTS (
SELECT 1 FROM target_table_new WHERE target_table_new.id = target_table.id
)
ORDER BY id LIMIT 1000;If a type cast or constraint rejected rows — migration cannot complete until the data is
reconciled. Fix the source data in target_table (or adjust the new table’s constraint),
then re-run the missing batches.
Escape hatch — if diagnosis stalls, drop target_table_new and restart the migration
from a clean slate. The original table is untouched, so no data is at risk.
Re-run the count comparison after each retry. Only proceed to DROP TABLE once
COUNT(*) matches exactly.