Skip to content

Adopt an existing database

Bring a database under atlantis by generating declarations from live tables, reviewing and committing them, and baselining without running DDL.

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,vendor

Entity 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 apply

With 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 .atl so atlantis owns it — unique on the field or unique by a, b for a non-partial index (classified backfill-required), or unique 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>" — the table "..." value has an invalid identifier (embedded space, leading digit, multiple dots). Use a simple [schema.]table name.
  • 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_breaking plan with table override changed: "<old>" -> "<new>" — the modifier value moved relative to the applied schema. atlantis does not auto-rename; rename the physical table first.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close