Replacing Atlas with Ptah: Preserve History and Reject Invalid Indexes
We tested Akashi's TimescaleDB migrations with Ptah Compat: the Atlas history stays readable, while an invalid concurrent index blocks migration success.
Ptah helps you plan, review, and apply database migrations. Try in your browser
Akashi records decisions made by AI agents. Its PostgreSQL database uses TimescaleDB and pgvector, and its migration history already has an Atlas runner, a checksum file, and deployed version identifiers. Trying another CLI should not require rewriting that history.
We tested Ptah Compat against the 91 migration files in our proposed integration. Ptah applied the history, Atlas read it, and each tool could continue from the other’s last migration. We also reproduced an index failure suggested by Akashi’s migration comments: a failed concurrent index can keep its name while remaining unusable. In that state, Atlas reported a successful migration; Ptah refused to record success and named the repair needed.
These measurements use Ptah Compat 0.11.4, Atlas 1.1.0 as pinned by Akashi’s CI, and PostgreSQL 18.6 with TimescaleDB 2.30.2 and pgvector 0.8.1. The supporting files contain the frozen migration history and recorded results.
The upstream proposal and draft PR remain open. The PR adds an optional CI comparison and a runbook section; it does not announce that Akashi has adopted Ptah. It still pins Ptah 0.8.1, while this article repeats the checks with the newer released binary.
Switch the executable, keep the history
Section titled “Switch the executable, keep the history”Akashi’s Makefile already accepts an ATLAS override. Its existing
atlas.hcl resolves DATABASE_URL and selects file://migrations; the
original SQL files and atlas.sum remain unchanged.
We used a fresh disposable database from the same TimescaleDB image line as Akashi’s CI and bootstrapped its extensions the same way:
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS vector;"psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS timescaledb;"We validated the original checksum file with
make migrate-validate ATLAS=ptah-compat. With ptah-compat on PATH, the
existing apply target becomes:
make migrate-apply ATLAS=ptah-compatThat target runs ptah-compat migrate apply --env local. It applied all 91
files. Their last version is 111: the version token is not the number of
files, and early versions retain their leading zeros, such as 001 and 022.
To check interoperability, we asked Atlas to read the history Ptah had written:
atlas migrate status --dir file://migrations --url "$DATABASE_URL"Migration Status: OK -- Current Version: 111 -- Next Version: Already at latest version -- Executed Files: 91 -- Pending Files: 0Both tools use the same Atlas revision table. The result shows that Atlas recognizes all the applied files, rather than treating the database as a new migration target.
We repeated the test in reverse: Atlas applied the original history to another fresh database, then Ptah inspected it:
ptah-compat migrate status --dir file://migrations --url "$DATABASE_URL"Ptah reported the same version and zero pending files. On copies of the
migration directory, we then added a small version 112 that creates a probe
table. Atlas applied it after Ptah’s history; Ptah applied it after Atlas’s.
The other tool recognized the new version in each case. The original directory
was not edited for this continuation test.
Akashi’s make verify-exit-criteria also passed after Ptah’s apply. In this
fresh-database run it checked orphan integrity and the configured dead-letter
and outbox-age thresholds. The optional strict-retention and Qdrant checks
were disabled, matching the proposed CI job.
Concurrent indexes need more than a transaction switch
Section titled “Concurrent indexes need more than a transaction switch”Migration 022_full_text_search.sql explains why it uses ordinary index
creation: the application’s migration transaction prevents
CREATE INDEX CONCURRENTLY. It suggests creating the index manually for
large production tables. Migration 037_conflict_scored_at.sql gives the
same transaction reason for its partial index.
PostgreSQL requires concurrent index creation to run outside a transaction
block. Both Atlas and Ptah recognize -- atlas:txmode none. That makes the
statement executable, but it does not make a failed build safe to ignore.
For new work, a migration can declare the transaction mode explicitly. This small fixture isolates the failure behavior; it is not an edit to Akashi’s shipped history:
-- atlas:txmode none
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS members_email_key ON members (email);The IF NOT EXISTS clause only tests whether the name is already present.
It does not establish that an existing index is valid.
A successful statement can leave the constraint unenforced
Section titled “A successful statement can leave the constraint unenforced”We created a disposable members table with duplicate email addresses, then
attempted a unique concurrent index. PostgreSQL rejected the duplicate keys
and left members_email_key in the catalog with both indisvalid and
indisready false.
After removing the duplicate row, we ran the fixture migration against the remaining invalid index. This models retrying index creation after correcting the data. The database is deliberately nonempty, so the test explicitly allows that starting state:
ptah-compat migrate apply --dir file://index-migrations --url "$DATABASE_URL" --allow-dirtyOn identical starting states, the results differed:
| Check after the migration attempt | Atlas 1.1.0 | Ptah Compat 0.11.4 |
|---|---|---|
| Command result | Success | Refused |
| Index valid and ready | No | No |
| Successful migration revision recorded | Yes | No |
| Diagnostic names the invalid index and repair | No | Yes |
Atlas saw IF NOT EXISTS, skipped the existing name, and recorded the file as
applied. A subsequent insert with the duplicate email succeeded. The migration
record said the change was complete, but the database did not enforce its
intended uniqueness.
Ptah inspected the existing index and refused to record the migration as
successful. Its error names public.members_email_key and explains that the
statement would skip the unusable index. It directs the operator to rebuild
or remove that index before retrying.
With the duplicate data already corrected, we rebuilt it:
REINDEX INDEX CONCURRENTLY members_email_key;The same Ptah apply command then succeeded. The catalog reported a valid, ready index, and a duplicate insert was rejected. Ptah does not repair the index automatically: the operator controls that database change.
Bound a blocked index build
Section titled “Bound a blocked index build”Concurrent creation avoids the ordinary index build’s write-blocking lock, but it can still wait for other sessions. Ptah can apply a PostgreSQL lock wait limit from the migration header:
-- atlas:txmode none-- +ptah lock_timeout=2s
CREATE INDEX CONCURRENTLY IF NOT EXISTS lock_probe_id_idx ON lock_probe (id);We held an ACCESS EXCLUSIVE lock on the test table in another session and
ran the migration. The command failed at the configured lock timeout:
Error: error applying migrations: failed to apply migration 001: failed to execute migration SQL: ERROR: canceling statement due to lock timeout (SQLSTATE 55P03)SQL: CREATE INDEX CONCURRENTLY IF NOT EXISTS lock_probe_id_idx ON lock_probe (id)This directive configures PostgreSQL’s statement lock wait. It is different
from the CLI’s --lock-timeout, which controls acquisition of the migration
runner’s advisory lock. It also does not cap the total duration of an index
build once the needed locks are available.
Atlas treats the Ptah directive as a comment. The file remains readable by both tools, but the PostgreSQL lock-wait setting in this header is a Ptah behavior. Teams switching executables should account for that difference.
What the proposed integration changes
Section titled “What the proposed integration changes”The Akashi PR copies the existing migration-verification job, substitutes Ptah for the apply step, then asks Atlas to check the resulting history. The runbook explains the executable override. That gives the maintainers a way to evaluate the handover before choosing whether to change their normal path.
Akashi also has an embedded migration runner. Selecting ATLAS=ptah-compat
changes the Makefile command, not the application’s startup runner. The
concurrent-index examples here exercise the CLI path; a deployment needs to
choose which runner owns migration execution.
To explore Ptah, see the documentation or use the browser playground without installing a binary.
Put Ptah to work
Example files
- AKASHI-LICENSE.txt
- akashi/atlas.hcl
- akashi/Makefile.txt
- akashi/migrations/001_initial.sql
- akashi/migrations/022_full_text_search.sql
- akashi/migrations/023_fix_outbox_index.sql
- akashi/migrations/024_composite_agent_identity.sql
- akashi/migrations/025_review_fixes.sql
- akashi/migrations/026_smarter_conflict_detection.sql
- akashi/migrations/027_semantic_conflicts.sql
- akashi/migrations/028_idempotency_keys.sql
- akashi/migrations/029_durability_audit_tables.sql
- akashi/migrations/030_agent_events_guardrails.sql
- akashi/migrations/031_mutation_audit_log.sql
- akashi/migrations/032_agent_events_archive.sql
- akashi/migrations/033_decision_claims.sql
- akashi/migrations/034_conflict_llm_validation.sql
- akashi/migrations/035_conflict_lifecycle.sql
- akashi/migrations/036_decision_immutability.sql
- akashi/migrations/037_conflict_scored_at.sql
- akashi/migrations/038_conflict_precision.sql
- akashi/migrations/039_agent_last_seen.sql
- akashi/migrations/040_decision_claims_fk.sql
- akashi/migrations/041_archive_hypertable.sql
- akashi/migrations/042_audit_immutability_and_precision.sql
- akashi/migrations/043_review_hardening.sql
- akashi/migrations/044_api_keys.sql
- akashi/migrations/045_api_key_fixes.sql
- akashi/migrations/046_winning_decision_id.sql
- akashi/migrations/047_api_keys_immutable.sql
- akashi/migrations/048_attribution_columns.sql
- akashi/migrations/049_drop_hnsw_indexes.sql
- akashi/migrations/050_completeness_score.sql
- akashi/migrations/051_decision_assessments.sql
- akashi/migrations/052_rename_repo_to_project.sql
- akashi/migrations/053_data_retention.sql
- akashi/migrations/054_conflict_groups.sql
- akashi/migrations/055_decision_erasures.sql
- akashi/migrations/056_claim_embedding_retries.sql
- akashi/migrations/057_signup.sql
- akashi/migrations/058_claim_text_on_conflicts.sql
- akashi/migrations/059_outcome_score.sql
- akashi/migrations/060_claim_categories.sql
- akashi/migrations/061_conflict_labels.sql
- akashi/migrations/062_scoring_method_external.sql
- akashi/migrations/063_project_links.sql
- akashi/migrations/064_topic_groups.sql
- akashi/migrations/065_org_settings.sql
- akashi/migrations/066_performance_indexes.sql
- akashi/migrations/067_precedent_escalation.sql
- akashi/migrations/068_cascade_delete_alternatives_evidence.sql
- akashi/migrations/069_fix_conflict_group_timestamps.sql
- akashi/migrations/070_model_server_inference.sql
- akashi/migrations/071_drop_alternatives_score_selected.sql
- akashi/migrations/072_evidence_metrics.sql
- akashi/migrations/073_precedent_reason.sql
- akashi/migrations/074_agent_context_outbox_trigger.sql
- akashi/migrations/075_simplify_conflict_statuses.sql
- akashi/migrations/076_decision_claims_org_fk.sql
- akashi/migrations/077_integrity_audit_results.sql
- akashi/migrations/078_integrity_violations.sql
- akashi/migrations/079_integrity_violations_immutability.sql
- akashi/migrations/080_integrity_audit_results_immutability.sql
- akashi/migrations/081_conflict_project_columns.sql
- akashi/migrations/082_integrity_violations_org_fk_restrict.sql
- akashi/migrations/083_decision_erasures_immutability.sql
- akashi/migrations/084_proof_leaves.sql
- akashi/migrations/085_conflict_resolutions.sql
- akashi/migrations/086_assessments_restrict_cascade.sql
- akashi/migrations/087_project_links_alias_index.sql
- akashi/migrations/088_unique_alias_per_project.sql
- akashi/migrations/089_fix_conflict_resolutions_trigger_and_proof_leaves_fk.sql
- akashi/migrations/090_decisions_org_created_index.sql
- akashi/migrations/091_decision_type_aliases.sql
- akashi/migrations/092_backfill_decision_types.sql
- akashi/migrations/093_assessment_source.sql
- akashi/migrations/094_assessment_auto_unique.sql
- akashi/migrations/095_fix_workspace_project_names.sql
- akashi/migrations/096_backfill_decision_types_with_trigger_bypass.sql
- akashi/migrations/097_project_links_org_fk.sql
- akashi/migrations/098_fix_remaining_project_assignments.sql
- akashi/migrations/099_tamper_evidence_fk_tighten.sql
- akashi/migrations/100_audit_fk_hardening.sql
- akashi/migrations/101_rename_stale_indexes.sql
- akashi/migrations/102_drop_dead_schema.sql
- akashi/migrations/103_git_branch_index.sql
- akashi/migrations/104_decision_supersedes.sql
- akashi/migrations/105_supersedes_suggestions.sql
- akashi/migrations/106_supersedes_suggestion_confirm.sql
- akashi/migrations/107_conflict_gold_labels.sql
- akashi/migrations/108_conflict_disputed_question.sql
- akashi/migrations/109_decision_bindings.sql
- akashi/migrations/110_scoring_method_binding.sql
- akashi/migrations/111_suppressed_pair_samples.sql
- akashi/migrations/atlas.sum
- akashi/migrations/embed.go
- akashi/scripts/verify_exit_criteria.py
- commands/apply.txt
- commands/bootstrap.txt
- commands/index-apply.txt
- commands/lock-apply.txt
- commands/ptah-status.txt
- commands/status.txt
- commands/validate.txt
- handover-migrations/112_handover_probe.sql
- index-migrations/001_unique_email.sql
- index-migrations/atlas.sum
- lock-migrations/001_lock_probe.sql
- lock-migrations/atlas.sum
- measured/atlas-accepts-duplicate.stderr.txt
- measured/atlas-accepts-duplicate.stdout.txt
- measured/atlas-apply.stderr.txt
- measured/atlas-apply.stdout.txt
- measured/atlas-continues-ptah.stderr.txt
- measured/atlas-continues-ptah.stdout.txt
- measured/atlas-deduplicate.stderr.txt
- measured/atlas-deduplicate.stdout.txt
- measured/atlas-index-create.stderr.txt
- measured/atlas-index-create.stdout.txt
- measured/atlas-index-fail.stderr.txt
- measured/atlas-index-fail.stdout.txt
- measured/atlas-index-setup.stderr.txt
- measured/atlas-index-setup.stdout.txt
- measured/atlas-invalid-after.stderr.txt
- measured/atlas-invalid-after.stdout.txt
- measured/atlas-invalid-apply.stderr.txt
- measured/atlas-invalid-apply.stdout.txt
- measured/atlas-invalid-before.stderr.txt
- measured/atlas-invalid-before.stdout.txt
- measured/atlas-revisions.stderr.txt
- measured/atlas-revisions.stdout.txt
- measured/atlas-sees-next.stderr.txt
- measured/atlas-sees-next.stdout.txt
- measured/atlas-sees-ptah.stderr.txt
- measured/atlas-sees-ptah.stdout.txt
- measured/atlas-version.stderr.txt
- measured/atlas-version.stdout.txt
- measured/bootstrap.stderr.txt
- measured/bootstrap.stdout.txt
- measured/copy-next-history.stderr.txt
- measured/copy-next-history.stdout.txt
- measured/exit-criteria.stderr.txt
- measured/exit-criteria.stdout.txt
- measured/extensions.stderr.txt
- measured/extensions.stdout.txt
- measured/history-commands.json
- measured/history-status.txt
- measured/index-commands.json
- measured/index-hash.stderr.txt
- measured/index-hash.stdout.txt
- measured/lock-create.stderr.txt
- measured/lock-create.stdout.txt
- measured/lock-hash.stderr.txt
- measured/lock-hash.stdout.txt
- measured/lock-holder.stderr.txt
- measured/lock-holder.stdout.txt
- measured/lock-refusal.stderr.txt
- measured/lock-refusal.stdout.txt
- measured/lock-refusal.txt
- measured/lock-release.stderr.txt
- measured/lock-release.stdout.txt
- measured/lock-setup.stderr.txt
- measured/lock-setup.stdout.txt
- measured/next-hash.stderr.txt
- measured/next-hash.stdout.txt
- measured/ptah-apply.stderr.txt
- measured/ptah-apply.stdout.txt
- measured/ptah-continues-atlas.stderr.txt
- measured/ptah-continues-atlas.stdout.txt
- measured/ptah-deduplicate.stderr.txt
- measured/ptah-deduplicate.stdout.txt
- measured/ptah-index-create.stderr.txt
- measured/ptah-index-create.stdout.txt
- measured/ptah-index-fail.stderr.txt
- measured/ptah-index-fail.stdout.txt
- measured/ptah-index-setup.stderr.txt
- measured/ptah-index-setup.stdout.txt
- measured/ptah-invalid-apply.stderr.txt
- measured/ptah-invalid-apply.stdout.txt
- measured/ptah-invalid-before.stderr.txt
- measured/ptah-invalid-before.stdout.txt
- measured/ptah-reindex.stderr.txt
- measured/ptah-reindex.stdout.txt
- measured/ptah-rejects-duplicate.stderr.txt
- measured/ptah-rejects-duplicate.stdout.txt
- measured/ptah-repaired-apply.stderr.txt
- measured/ptah-repaired-apply.stdout.txt
- measured/ptah-repeat.stderr.txt
- measured/ptah-repeat.stdout.txt
- measured/ptah-revisions.stderr.txt
- measured/ptah-revisions.stdout.txt
- measured/ptah-sees-atlas.stderr.txt
- measured/ptah-sees-atlas.stdout.txt
- measured/ptah-sees-next.stderr.txt
- measured/ptah-sees-next.stdout.txt
- measured/ptah-status.txt
- measured/ptah-valid-after.stderr.txt
- measured/ptah-valid-after.stdout.txt
- measured/ptah-version.stderr.txt
- measured/ptah-version.stdout.txt
- measured/reverse-bootstrap.stderr.txt
- measured/reverse-bootstrap.stdout.txt
- measured/reverse-create.stderr.txt
- measured/reverse-create.stdout.txt
- measured/revision-identities.stderr.txt
- measured/revision-identities.stdout.txt
- measured/server-version.stderr.txt
- measured/server-version.stdout.txt
- measured/validate.stderr.txt
- measured/validate.stdout.txt
- README.md
- sql/index-deduplicate.sql
- sql/index-fail.sql
- sql/index-repair.sql
- sql/index-setup.sql
- sql/index-state.sql
- sql/lock-setup.sql
- verified.json