Skip to content
PtahPtah

Extension environment variables

You want a capability Ptah models and the community CLI does not, on the ptah-compat surface rather than on a native ptah verb. Each one is reached through a PTAH_* boolean environment variable, and this page is the inventory: what each variable’s default is, what setting it turns on, and why the default is what it is.

They are environment variables rather than flags on purpose, and Capabilities retained deliberately carries that argument. Native ptah verbs always emit everything Ptah models, with no switch to set.

Every variable below is a boolean, and they all read the same way: leaving it unset selects the default described here, a valid boolean is honored, and anything else — including an exported empty value — fails the command before it does any work, naming the variable and the value you typed. The accepted spellings and the error shape are documented once, in Boolean environment variables.

The value is read on every run of the command that owns it, not only on the runs that would have used the enabled behavior, so PTAH_ATLAS_LINT_ALL_VERSIONS=yes in a CI environment file fails the next run rather than the next run that happens to omit --latest.

Each entry below carries the default, the reason for it, and what the pinned community CLI does on the same input.

Variable Set to 1
PTAH_ATLAS_INSPECT_ALL_BLOCKS Emit every PostgreSQL block, unreferenced ones included
PTAH_POSTGRES_INSPECT_ALL_ROLES Describe every role on the server
PTAH_ALLOW_RESERVED_ROLE_NAMES Plan a reserved role name
PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP Plan a virtual-table drop
PTAH_SQLITE_ALLOW_UNREGISTERED_VIRTUAL_MODULE Compare an unregistered module’s storage
PTAH_ALLOW_EXTERNAL_SCHEMA Evaluate data "external_schema"
PTAH_ATLAS_LINT_WITHOUT_DEV_URL Lint with no dev database
PTAH_STRICT_DIR_QUERY Refuse an unknown --dir query key
PTAH_ATLAS_LINT_ALL_VERSIONS Lint the whole directory
PTAH_ATLAS_PLAN_LINT_FAIL_ON_ERROR Let a plan-lint error decide the exit code
PTAH_SCHEMA_DIFF_TEMPLATE_HELPERS Register the shared --format helpers
PTAH_HCL_MERGE_REDECLARATIONS Merge repeated blocks again
PTAH_HCL_STRICT_REDECLARATIONS Refuse four more repeated block kinds
PTAH_HCL_SCHEMA_SCOPED_ENUMS Key enum blocks by qualified name

By default, ptah-compat schema inspect leaves an extension, sequence or policy block out of PostgreSQL HCL output when nothing else in the document depends on it, and reports each omission on standard error. For an extension, “depends on” is measured against what the catalog says the extension supplies — isn supplies the type isbn — rather than against its name, and against what the catalog resolved for the document’s indexes, since a GIN index over an integer column needs btree_gin and prints no word of it. Set it to 1 and every block Ptah models is emitted: the output describes the database in full, and the community CLI refuses it.

By default, a PostgreSQL read describes only the roles the inspected schemas use, because roles are cluster-wide and a description of one database is not a place to list another tenant’s roles. Each read reports on standard error how many managed roles it left out. Set it to 1 and every role Ptah manages on the server is described again, which is what you need to reproduce one cluster’s roles in another. It widens the description only: comparison already treats undescribed roles as present, so the planned statements are identical either way. Reserved pg_ names and the bootstrap postgres superuser are outside it in both directions.

By default, a desired schema that declares a reserved PostgreSQL role is refused before anything is compared or planned, naming the role and the rule, because Ptah manages neither the pg_ roles nor the bootstrap postgres superuser in either direction and the declaration would otherwise become a CREATE ROLE the server rejects at SQLSTATE 42939 or 42710. Set it to 1 and the declaration is planned instead, as it was before the refusal existed. That is worth having on a cluster bootstrapped under a name other than postgres, where CREATE ROLE "postgres" succeeds.

By default, a comparison whose database side holds a SQLite virtual table is refused before anything is compared, naming the table and its module. No desired-state format can declare a virtual table, so its absence is not a request to drop it, and planning the removal deletes the index and everything in it. Measured on the pinned community binary v1.3.0, schema diff plans that drop plus one per shadow table and exits 0.

  • Set it to 1 and the removal is planned as before.
  • --exclude <table> is the other direction: the table is kept and the rest of the schema converges.
  • A malformed value refuses every SQLite comparison command and public migration-generator call before filesystem path resolution, schema source loading, database connection, or SQL. Non-SQLite operations ignore it.
  • The opt-in covers only the removal. A desired ordinary table colliding with a live virtual one stays refused however it is set, because the planner cannot convert one kind into the other.
  • schema inspect compares nothing and is unaffected.

See SQLite for the whole picture.

PTAH_SQLITE_ALLOW_UNREGISTERED_VIRTUAL_MODULE

Section titled “PTAH_SQLITE_ALLOW_UNREGISTERED_VIRTUAL_MODULE”

By default, a comparison whose database side holds a virtual table using a module this build does not register is refused before anything is compared, naming the table, the module, and the modules this build does register. SQLite marks a module’s shadow tables as shadow only while the module is loaded, so without it that module’s private storage is described as ordinary user tables — and a desired state that does not name them reads as a request to drop them. Measured on fts3 and fts4: excluding the virtual table left the storage in the comparison and ptah schema apply dropped all of it at exit 0, after which MATCH answered SQL logic error. The fts5 control, whose module this build does register, reported a synced schema and changed nothing.

  • Set it to 1 and the comparison proceeds against the module’s storage as the ordinary tables it appears to be, accepting those drops. This is what Ptah did before the refusal existed.
  • Excluding the virtual table is not an escape here and is not suggested: the tables at risk are the module’s own storage, not the table an operator would name, and Ptah cannot list them without the module.
  • It is separate from PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP, which permits dropping a virtual table Ptah can see. Neither implies the other.
  • Adding such a table has no opt-in. A plan carrying CREATE VIRTUAL TABLE ... USING fts4 fails on this build with no such module: fts4, and no value of a variable makes a module exist. This fires only where that statement would actually be planned — virtual on the desired side, absent from the database — so two databases that both already hold the same fts4 index compare normally under the opt-in.
  • A read is never refused. ptah db read and schema inspect print a note naming the table and module, and leave standard output and the exit code alone.
  • A project that skips table drops is not asked for it. With diff { skip { drop_table = true } } in the project file — or diff.skip: [drop_table] in ptah.yaml — every table drop and the dependent removals it carries are deleted from the diff before any SQL is rendered, so the refusal, which is a claim about a DROP TABLE, does not fire. What still fires is a rebuild: a desired state that NAMES one of the module’s storage tables and describes it differently is refused under the policy too, because skip drop_table filters removals rather than modifications and SQLite converges a modification by dropping and recreating the table.
  • A change SQLite can make in place is not asked for it either. A table whose only change is a column the desired state adds is planned as ALTER TABLE ... ADD COLUMN, which drops and rebuilds nothing, so a narrowed comparison such as --include users against a database holding an fts4 index runs at exit 0 and prints that one statement. Remove or change a column on the same table, or change a constraint, and it is a rebuild again and refused again.

See SQLite for the whole picture.

By default, atlas.hcl data "external_schema" is not evaluated, because it runs a repository-controlled program. Set it to 1 and the data source is evaluated, matching the native --allow-external-schema flag.

By default, ptah-compat migrate lint requires --dev-url, because the community CLI marks it required and exits 1 without it. Ptah’s analyzers read the migration files and need no database, so set it to 1 and the run proceeds with no dev database and reports what the static analysis finds. Native ptah migrations lint needs no opt-in.

By default, a --dir URL query key other than format is ignored, exactly as the community CLI ignores it, and named on standard error so a misspelled ?fromat=goose does not quietly read the directory in the wrong layout. Set it to 1 and such a key is a refusal instead, for a pipeline that wants a typo to stop the run. The value is read on every run of the eight verbs that accept a --dir query — apply, diff, hash, lint, new, set, status and validate — whether or not the URL carries a query at all, so PTAH_STRICT_DIR_QUERY=nope in a CI environment file fails the next run rather than the next typo. migrate checkpoint, down, edit, rebase, rm and test refuse a --dir query outright, so neither the note nor this variable applies there.

By default, ptah-compat migrate lint refuses a run that names no scope, because the community CLI refuses it: --latest, --git-base or an atlas.hcl lint block supplying one is required, and without it the answer is Error: --latest or --git-base is required at exit 1, before the migration directory is read and before --dev-url is contacted. Set it to 1 and the whole directory is linted instead, which is what Ptah’s own linter does. Native ptah migrations lint needs no scope and ignores the variable.

By default, ptah-compat schema plan lint reports and does not gate: a plan carrying a destructive change is described on stdout and the command exits 0. A plan is a document an operator reviews and approves, and a report that refuses on their behalf is one they cannot approve anything with. Set it to 1 when a pipeline needs the report to gate, and an error-severity finding exits 1 with the report still on stdout.

Either way the command states on stderr what the report covers: Ptah’s rule set is its own and does not name every hazard a schema change can carry, so a report without findings describes the rules rather than the plan. It is an environment variable rather than a flag for the same reason the two above are — the conformance cli-surface tier asserts flag parity with the pinned binary, and an environment variable is invisible to the help surface.

By default, ptah-compat schema diff --format registers one helper, sql, which is what the pinned community binary offers there; {{ json . }} exits 1 with function "json" not defined. Set the variable to 1 and the shared helper set schema apply already registers becomes available on this verb too, so {{ json . }} renders a document carrying From, To and Changes. The default stays narrow because registering more would let ptah-compat accept a template the community binary refuses. Native ptah schema diff needs no variable: --format json emits a machine-readable diff there already.

By default, an HCL schema document that declares one object twice is refused, naming the kind and the object. Before the refusal existed the second declaration was folded into the first and the run reported success, so a file declaring table "users" twice was read as one table while the community CLI exits 1 on it with pq: relation "users" already exists. Set the variable to 1 and the merge comes back, on both the compatibility surface and native ptah verbs.

Which kinds refuse is measured rather than chosen: a repeat is refused where the community CLI refuses the same document.

  • Refused: table, column, index, a named check or constraint, foreign_key, enum, sequence, domain, composite, range, extension, trigger, policy.
  • Exempt under every setting: schema, because a directory of HCL files is one document and its files each open with the same block; function, because two blocks sharing a name can be two legal overloads; permission, which renders a GRANT the engine accepts twice; and data, which declares no database object.

By default, a repeated view, materialized, role or unique block is read at exit 0, because the community CLI reads it at exit 0: it drops the first three unread and merges two unique blocks sharing a label into one. Refusing them is above the drop-in floor rather than on it, so it is opt-in. Set the variable to 1 and each of the four is refused within one document, which is the rule an HCL schema directory already applies across its files. The kinds refused by default are still refused with it set; the four exceptions above are still exempt.

By default, two enum blocks sharing a bare name are one object however they are spelled, because the community CLI keys enums by their bare name and answers duplicate enum "mood" at exit 1 for enum "mood" in two schemas and for the two-label enum "public" "mood" / enum "other" "mood" alike. Set the variable to 1 and they are keyed by their qualified name, which is what public.mood and other.mood are. This is the setting under which ptah-compat schema inspect of a database holding one enum name in two schemas can be applied again: the document names both, and reading it back re-renders it byte for byte. The community CLI has no such setting and refuses the document its own inspect writes for that database.

One shape has no Atlas-readable form at all

Section titled “One shape has no Atlas-readable form at all”

Suppression can only leave out a block nothing else names. A sequence behind a column default is named, so the block stays and the document is not readable by the community CLI:

CREATE SEQUENCE order_seq;
CREATE TABLE orders (id integer NOT NULL DEFAULT nextval('order_seq'::regclass));

This is not a gap Ptah can close. Measured on PostgreSQL 17, the community CLI’s own inspect of that database emits default = sql("nextval('order_seq'::regclass)") with no sequence block, and then cannot read its own output back: pq: relation "order_seq" does not exist. There is no faithful description of that database the CLI can read — not Ptah’s and not its own. Ptah keeps the sequence, so the document is at least readable by Ptah and true about the database, and says so on standard error. Dropping the column’s default to make the file readable would describe a database you do not have, which is the one outcome worse than a refusal.

So ptah-compat schema inspect is not a promise that every PostgreSQL database produces community-CLI-readable HCL. It is a promise that the output is always self-consistent, that nothing disappears without being reported, and that the full description is one environment variable away.