Bring a database full of existing tables under atlantis: generate declarations from what is live, review and commit them, and baseline — recording the schema without running any DDL against your tables.
How the pieces fit
atlantis creates its own entity tables at atlantis.<namespace>_<entity>
by default, with the entity name snake-cased — entity VendorImport in vendor is atlantis.vendor_vendor_import. Existing tables live elsewhere, so each adopted entity carries
the table "<schema.table>" modifier pointing at its physical table:
entity Account in consumer {
table "consumer.accounts"
id varchar(8) primary
email varchar(255) not null unique
created_at timestamptz not null default now()
deleted_at timestamptz
soft_delete by deleted_at
}Field names match the live column names byte for byte, and each
table "..." value is unique across every declared entity — two entities
claiming one physical table is rejected at tide plan.
1. Generate declarations
Two routes to the same files:
From the console. The Schema page’s onboarding offers Import a database: the console reads the database read-only and opens a review page — every discovered table as a generated entity, suggested tightenings (missing primary keys, unindexed foreign keys, tenant-isolation candidates), and notes on what introspection could not verify.
From the terminal. In a repository with no .atl files yet,
tide inspect --generate writes one .atl file per undeclared table,
reading columns, types, keys, defaults, and foreign keys from the
catalogue:
tide inspect --generate=schema/ --schemas=consumer,vendorEntity names are derived from table names, so user_accounts becomes
UserAccounts. Renaming one after adoption is a breaking change, so
edit the files before committing. Skipped tables are reported with a
reason, existing files are never overwritten, and warnings about what
introspection could not verify print with the run; inside a generated
file, a comment block lists any column whose Postgres type has no .atl
spelling.
2. Review and commit
The generated declarations understate a schema: introspection does not
read check predicates or every index shape back. Read each file, tighten
what you know — then commit them to the repository at the paths
tide.yaml lists.
3. Baseline
The baseline happens in the console. On the import’s review page, Commit computes the plan, and Apply, behind a re-authentication, records the declarations as the baseline. A baseline records the checkpoint; it runs no DDL against your tables. It refuses when a declaration and the database disagree about something that exists on both sides, naming each mismatch; tables declared but absent, or present but undeclared, are reported and do not block.
Then register each caller against the baseline:
tide plan
tide applyWith the checkpoint recorded and the declarations matching it, the plan is
zero changes and the apply is a metadata write: the caller’s files are
recorded and the checkpoint moves. No DDL runs. Do not run tide apply
before the console baseline — with no checkpoint recorded, the plan is a
full set of CREATE statements for tables that already exist.
Legacy unique indexes can block apply
A zero-change plan does not guarantee a clean apply. atlantis declares
uniqueness as a Postgres UNIQUE constraint; a legacy database often
carries the same uniqueness as a bare CREATE UNIQUE INDEX with no
backing constraint, which never appears in the DDL diff but rejects
writes the declared schema allows. tide apply refuses to proceed over
one, printing the live index name and the remediations:
- Declare the uniqueness in the
.atlso atlantis owns it —uniqueon the field orunique by a, bfor a non-partial index (classified backfill-required), orunique index partial by <cols> where <pred>for a partial one (predicates are normalised through Postgres before comparing, so casts and operand order don’t matter). - Drop the index from the database, if it is unwanted.
tide plan only warns about index drift, and only in --format=json
(index_drift, index_drift_notes, index_drift_error).
4. Verify
Run tide plan again — it reports zero changes — and issue a read through
the generated typed client, confirming it returns the rows direct SQL
sees.
5. Cut over
Move your application package by package: flag-gate the atlantis client beside the existing database code and flip per package — both paths read and write the same physical tables, so the flip is a routing change, not a data change.
Common errors
<entity>: invalid table name "<value>"— thetable "..."value has an invalid identifier (embedded space, leading digit, multiple dots). Use a simple[schema.]tablename.table "<name>" is claimed by both <A> and <B>— two entities mapped to one physical table; each value must be unique across the merged schema.- A
cross_caller_breakingplan withtable override changed: "<old>" -> "<new>"— the modifier value moved relative to the applied schema. atlantis does not auto-rename; rename the physical table first.
Related
- DSL grammar reference — the
tablemodifier alongside other entity-body clauses. tide inspect— the drift report and--generate.- The apply path — the checkpoint a baseline records.