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.
At a glance
Section titled “At a glance”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 |
PTAH_ATLAS_INSPECT_ALL_BLOCKS
Section titled “PTAH_ATLAS_INSPECT_ALL_BLOCKS”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.
PTAH_POSTGRES_INSPECT_ALL_ROLES
Section titled “PTAH_POSTGRES_INSPECT_ALL_ROLES”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.
PTAH_ALLOW_RESERVED_ROLE_NAMES
Section titled “PTAH_ALLOW_RESERVED_ROLE_NAMES”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.
PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP
Section titled “PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP”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
1and 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 inspectcompares 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
1and 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 fts4fails on this build withno 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 samefts4index compare normally under the opt-in. - A read is never refused.
ptah db readandschema inspectprint 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 — ordiff.skip: [drop_table]inptah.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 aDROP 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, becauseskip drop_tablefilters 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 usersagainst a database holding anfts4index 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.
PTAH_ALLOW_EXTERNAL_SCHEMA
Section titled “PTAH_ALLOW_EXTERNAL_SCHEMA”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.
PTAH_ATLAS_LINT_WITHOUT_DEV_URL
Section titled “PTAH_ATLAS_LINT_WITHOUT_DEV_URL”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.
PTAH_STRICT_DIR_QUERY
Section titled “PTAH_STRICT_DIR_QUERY”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.
PTAH_ATLAS_LINT_ALL_VERSIONS
Section titled “PTAH_ATLAS_LINT_ALL_VERSIONS”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.
PTAH_ATLAS_PLAN_LINT_FAIL_ON_ERROR
Section titled “PTAH_ATLAS_PLAN_LINT_FAIL_ON_ERROR”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.
PTAH_SCHEMA_DIFF_TEMPLATE_HELPERS
Section titled “PTAH_SCHEMA_DIFF_TEMPLATE_HELPERS”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.
PTAH_HCL_MERGE_REDECLARATIONS
Section titled “PTAH_HCL_MERGE_REDECLARATIONS”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 namedcheckorconstraint,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; anddata, which declares no database object.
PTAH_HCL_STRICT_REDECLARATIONS
Section titled “PTAH_HCL_STRICT_REDECLARATIONS”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.
PTAH_HCL_SCHEMA_SCOPED_ENUMS
Section titled “PTAH_HCL_SCHEMA_SCOPED_ENUMS”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.
Next steps
Section titled “Next steps”- The argument these variables implement: Capabilities retained deliberately.
- The boolean grammar and the strict-compatibility selector: Configuration.
- The per-verb flag surface that reads them: Atlas-compatible commands.