Native commands
The native ptah surface uses Ptah-owned spellings; Atlas aliases are
intentionally absent from this binary. The index below contains every command
path it answers to, followed by per-verb details. The separate ptah-compat
drop-in binary has its own reference:
Atlas-compatible commands.
Desired-schema inputs are command-specific; each verb below names its own accepted selector, and work with a schema source routes source-neutral tasks.
Every native verb
Section titled “Every native verb”The index below is generated from the ptah command tree, so it names every
command path the binary answers to and no path it does not. Rows are sorted,
which keeps a namespace’s verbs together; which verb to reach for first is
Choose a workflow. Use ptah <command> --help
for the exact flag set in an installed binary.
A row marked group is a namespace rather than an operation. Running one with
no verb prints its help, with one exception: ptah assist on its own starts a
conversation, which A model of your own describes.
The tree is walked the way cobra finishes building it, so ptah help and the
four ptah completion shells have rows: they ship, and an index built from the
verbs cmd/ registers by hand would leave them out. Two spellings stay outside
it. __complete and __completeNoDesc ship and exit 0, and no walk reaches
them, because cobra registers them from an unexported function that removes them
again unless the invocation being parsed is one of the two. They answer a shell
rather than a person.
| Command | What it does | Notes |
|---|---|---|
ptah assist |
Work with Ptah through a model you supply | group |
ptah assist context |
Prints what a question would send to a model provider and sends nothing; it opens neither a database nor an endpoint, and writes no file | — |
ptah assist explain |
Opens no connection of its own; it asks a model a question and lets the model call Ptah’s own tools, each of which carries its own classification | — |
ptah assist provider |
Inspect and test the model providers this machine can reach | group |
ptah assist provider list |
Lists the provider profiles configured locally, and opens neither a database nor an endpoint | — |
ptah assist provider test |
Measures a provider profile by calling the model endpoint it names; no database is involved | — |
ptah assist sessions |
List, read and remove saved Ptah Assist conversations | group |
ptah assist sessions delete |
Removes one saved conversation file from the project; no database is opened, and the audit log is a separate file it does not touch | — |
ptah assist sessions list |
Lists the conversations saved under the project’s .ptah directory; neither a database nor a model endpoint is opened | — |
ptah assist sessions prune |
Removes saved conversation files untouched for longer than a given age; no database is opened, and the audit log is left alone | — |
ptah assist sessions show |
Prints one saved conversation from disk, including what Ptah’s tools answered during it; nothing is opened to do so | — |
ptah completion |
Generate the autocompletion script for the specified shell | group |
ptah completion bash |
Writes a bash completion script to stdout, generated from the command tree; it opens no database and writes no file | — |
ptah completion fish |
Writes a fish completion script to stdout, generated from the command tree; it opens no database and writes no file | — |
ptah completion powershell |
Writes a PowerShell completion script to stdout, generated from the command tree; it opens no database and writes no file | — |
ptah completion zsh |
Writes a zsh completion script to stdout, generated from the command tree; it opens no database and writes no file | — |
ptah db |
Work with live database schemas | group |
ptah db capabilities |
Reads the server’s version and catalogs to report the capability profile Ptah resolves | — |
ptah db drop-all |
Drops every schema object in the database it is given | — |
ptah db read |
Introspects the database and prints what it found | — |
ptah help |
Prints the help text of the verb it names, or of the root when it names none; the verb itself is not run and nothing is opened | — |
ptah inference |
Plan, run and cut over embedding-generation migrations | group |
ptah inference abandon |
Ends one run and releases its position in shared outbox history; the run-state row is written, and this command does not delete the generation or its vectors | — |
ptah inference backfill |
Reads the source, sends it to the embedding endpoint the specification names, and writes vectors and checkpoints into the target database | — |
ptah inference catchup |
Rereads the source rows recorded as changed and writes their vectors, which sends that text to the embedding endpoint, and deletes the change records every usable live feeder reading that source has processed | — |
ptah inference cutover |
Moves the pointer queries read to a different generation, and refuses when the pointer is not where the plan it was built from expects | — |
ptah inference describe |
Reads a specification file and reports what it says; it opens no database, and with --spec no connection at all, which is what makes it usable where every other verb here cannot be; --release fetches the release from a registry first |
— |
ptah inference evaluate |
Searches the generation with queries from a corpus, which sends those queries to the embedding endpoint; the database is only read | — |
ptah inference index |
Builds the generation’s vector index concurrently, which writes an index into the target database and drops an invalid leftover before rebuilding it | — |
ptah inference pause |
Stops a run at the boundary its last checkpoint reached, which writes the run’s own row: it takes the run for this process, so a worker that was running is refused at its next commit | — |
ptah inference plan |
Resolves a specification against the database and prints what would happen; nothing is created and nothing is written | — |
ptah inference prepare |
Creates the run’s own tables and, under the outbox mode, a companion table and two triggers on the source | — |
ptah inference probe |
Sends two fixed strings to the embedding endpoint the specification names and reports what came back; it opens no database, and nothing from one is sent | — |
ptah inference resume |
Returns a paused run to running, which writes the run’s own row and takes the run for this process; nothing starts working here | — |
ptah inference retire |
Drops a generation’s index and column; it is the one verb here that cannot be undone | — |
ptah inference rollback |
Moves the pointer queries read back to its recorded previous generation, when that generation is still measurably one you can go back to | — |
ptah inference status |
Prints a run’s phase, progress and watermarks from the run-state tables | — |
ptah inference verify |
Reads the source and the generation and reports what a cutover would rest on; it writes nothing | — |
ptah introspect |
Reads a live database and writes annotated Go models to disk; the database is only read | — |
ptah license |
Prints license and attribution text compiled into the binary | — |
ptah mcp |
Opens no connection of its own; it serves other operations to an MCP client, and each of those carries its own classification | — |
ptah migrations |
Manage migration plans, files, and revision state | group |
ptah migrations baseline |
Records existing migrations as applied in the target’s tracking table, and replays the directory into a disposable shadow database to verify it reproduces the schema | — |
ptah migrations checkpoint |
Squashes history into a checkpoint, replaying the directory into an ephemeral shadow database; the target is not touched | — |
ptah migrations create |
Writes an empty up and down migration file pair for someone to fill in by hand | — |
ptah migrations data |
Reads reference and seed data from the target and writes a migration file for the drift | — |
ptah migrations down |
Rolls back migrations against the target, after replaying and verifying the rollback plan in an ephemeral shadow database | — |
ptah migrations edit |
Rewrites a migration file and re-hashes the directory; the target is read to check whether the migration has been applied | — |
ptah migrations generate |
Writes migration files from schema differences; the dev database it replays into “is reset destructively” and the shadow database verifies the result | — |
ptah migrations hash |
Writes the directory’s integrity file, so a later run can tell a hand-edited migration from an intact one | — |
ptah migrations import |
Converts another tool’s migration directory into Ptah’s format on disk | — |
ptah migrations lint |
Lints migration files; the dev database it names is cleaned and replayed into | — |
ptah migrations ls |
Lists the migration files in a directory, reading nothing but the directory | — |
ptah migrations plan |
Reads the target and prints the migration SQL the difference implies, writing nothing | — |
ptah migrations pull |
Downloads a migration directory from an OCI registry and writes it to disk | — |
ptah migrations push |
Uploads a migration directory from disk to an OCI registry | — |
ptah migrations rebase |
Re-timestamps a migration to the end of history and re-hashes the directory; the target is read to check whether the migration has been applied | — |
ptah migrations repair |
Rewrites revision metadata in the target’s tracking table | — |
ptah migrations rm |
Deletes a migration’s file pair and re-hashes the directory; the target is read to check whether the migration has been applied | — |
ptah migrations set |
Sets the revision boundary in the target’s tracking table to a named version | — |
ptah migrations show |
Prints the SQL of one or more migration files, reading nothing but the files | — |
ptah migrations status |
Reads the target’s tracking table and reports which migrations are applied | — |
ptah migrations tag |
Records, lists or removes a tag in the target’s tracking table; two of the three write | — |
ptah migrations test |
Runs declarative test cases against the database named by --db-url, whose own help calls it a “Throwaway database URL”: the cases run raw SQL and apply schemas there |
— |
ptah migrations up |
Runs pending migrations against the target | — |
ptah migrations validate |
Validates the directory against its integrity file; the dev database it names is “used to clean and replay migrations for SQL validation” | — |
ptah oci |
Inspect Ptah artifacts in OCI registries | group |
ptah oci capabilities |
Asks the registry behind a reference which features it supports, and prints them | — |
ptah oci copy |
Copies an artifact between two registry repositories without rebuilding it | — |
ptah oci fetch |
Downloads the payload of metadata attached to an OCI artifact and writes it to disk | — |
ptah oci inspect |
Reports what an OCI artifact declares in its manifest, without downloading the payload | — |
ptah oci login |
Checks a registry credential and stores it; it touches no database and writes only Ptah’s own credential file | — |
ptah oci logout |
Removes the credential Ptah stored for a registry, leaving a Docker-placed one alone | — |
ptah oci referrers |
Asks a registry which metadata artifacts refer to one subject and prints them | — |
ptah oci reindex |
Republishes attachments a registry’s referrers index does not list, so a later query finds them | — |
ptah oci resolve |
Asks a registry which immutable digest a mutable tag currently names | — |
ptah oci tag |
Asks a registry to move a tag onto an artifact it already holds; nothing is uploaded | — |
ptah oci tags |
Asks a registry for the tags one repository carries and prints them | — |
ptah oci verify |
Checks an artifact against a verification policy before anything consumes it | — |
ptah project |
Read a project file and report what Ptah makes of it | group |
ptah project adopt |
Classifies every construct a project file declares as exact, compat-only or unsupported; --check reports that and writes nothing, the bare verb rewrites the compat-only spellings and refuses a project declaring anything unsupported, and --preflight also reads the revision history in the project’s database, writing nothing there |
— |
ptah project inspect |
Reads a project file and reports which of its settings Ptah acts on and which it read and ignored; it opens no database | — |
ptah schema |
Work with desired schema definitions | group |
ptah schema annotations |
Exports the Go annotation metadata compiled into the binary, as JSON or a JSON Schema | — |
ptah schema apply |
Applies a desired schema to the target; the dev database is where “the plan is rehearsed on before touching the target” | — |
ptah schema approve |
Signs a saved plan file with an SSH key and writes the signature beside it | — |
ptah schema compare |
Reads the target and reports the difference; on Oracle alone it creates a probe table in the dev database and drops it again, to learn how that engine spells a declared generated-column expression | — |
ptah schema diff |
Diffs two arbitrary schema states; a non-database source is materialized by replaying it into the dev database | — |
ptah schema drift |
Reads the target and reports how it differs from the desired schema | — |
ptah schema export |
Converts one desired-schema source format into another on disk; no database is opened | — |
ptah schema fmt |
Rewrites HCL schema files in the repository into canonical form; no database is opened | — |
ptah schema inspect |
Reads a schema source and prints it; the dev database it names “is reset destructively” | — |
ptah schema lineage |
Traces which columns each view and routine reads and writes; --db-url reads the target |
— |
ptah schema plan |
Saves a fingerprinted apply plan; the dev database is where the plan is rehearsed | — |
ptah schema pull |
Downloads a desired-schema document from an OCI registry and writes it to disk | — |
ptah schema push |
Uploads a desired-schema document from disk to an OCI registry | — |
ptah schema render |
Renders the desired schema as SQL with no connection at all; the dialect comes from a flag | — |
ptah schema security |
Reads the target’s roles, grants and policies and reports security findings | — |
ptah schema serve |
Serves a live read-only view of the schema over HTTP; it opens a listener, which is an exposure of its own even though the database is only read | — |
ptah schema stats |
Counts the objects in the target and emits them as OpenMetrics | — |
ptah schema test |
Runs declarative test cases against the database named by --db-url, whose own help calls it a “Throwaway database URL”: measured on PostgreSQL 17.11, a case with an apply_schema step created a table there and an exec step inserted into it |
— |
ptah schema validate |
Reports structural problems in a desired schema without a database | — |
ptah schema verify-approval |
Checks a saved plan’s signature against an allowed-signers file | — |
ptah seed |
Applies environment-scoped SQL seed files to the database it is given | — |
ptah sql |
Work with standalone SQL files | group |
ptah sql lint |
Lints standalone SQL files on disk and reports findings; no database is opened | — |
ptah version |
Prints the version, commit and build date compiled into the binary | — |
ptah viz |
Renders diagrams from a desired schema; no database is opened | — |
Sources and output formats
Section titled “Sources and output formats”An index cell above says what the verb does to the databases it is given; on a
namespace row it is instead the line ptah --help prints beside the group.
Neither shape holds an enumeration, so the verbs that accept several inputs or
write several formats cannot state the set there. The set is here. For the
flags that select within it, ptah <command> --help is authoritative and
Command flags is the same inventory as a page.
| Verb | Reads | Writes |
|---|---|---|
ptah schema render |
Go annotations, YAML, HCL, SQL, or an external command | SQL on stdout; progress and dependency diagnostics on stderr |
ptah schema inspect |
a live database, a local schema file, an oci:// schema artifact, or an Atlas-format migration directory |
HCL, SQL, or JSON; --out-dir and --split write files instead of stdout |
ptah schema diff |
two schema states, each a file, a database URL, or a migration directory | migration SQL or JSON |
ptah schema export |
any desired-schema source | HCL, an OpenAPI 3.0 component schema, a GraphQL SDL, a Protobuf Edition 2023 definition, or reference documentation as Markdown or a self-contained HTML page |
ptah schema test |
a desired schema from Go annotations, a SQL or HCL file, or a live database | a throwaway database the declarative seed, SQL and assert cases run against |
ptah migrations import |
a golang-migrate, Goose, Flyway, Liquibase, or dbmate directory, detected unless --from names one |
Ptah’s native migration format |
ptah viz |
a desired schema | Mermaid, DOT, or SVG; --security marks the tables the schema security rules find |
Rendering for a specific server
Section titled “Rendering for a specific server”Pass an explicit --dialect when the output must be executable by one target.
Without it, schema render attempts the built-in review targets and emits
labeled output only if every target can render the schema. An unsupported
feature fails atomically with empty standard output. The combined output is a
review artifact, not one executable SQL script.
schema render never connects, so a bare --dialect renders against that
dialect’s default capability preset — the newest release line Ptah has measured.
Add --server-version to render for the server you actually run:
ptah schema render --root-dir ./models --dialect mysql --server-version 8.0.42The flag requires --dialect, since without one the command renders every
supported target and a single server version cannot describe all of them. A
value that names no server is refused with exit code 2 rather than quietly
ignored, and a value that resolves to something other than an exact measured
release line is applied and announced on stderr as a warning: line. Accepted
shapes are a dotted version such as 17 or 8.0.42 and a server banner such as
PostgreSQL 16.3 (Debian), 10.11.6-MariaDB, or CockroachDB CCL v25.4.0.
A banner naming a different server product than --dialect is refused too:
--dialect mysql --server-version 10.11.6-MariaDB is a contradiction, not a
request, and it would otherwise render MySQL DDL against MariaDB capabilities.
Pass the banner with the dialect it names.
ptah sql lint spells the same contract --version.
Schema file paths
Section titled “Schema file paths”Native --schema-file inputs use the process working directory as their path
boundary. A relative path must resolve inside that directory after symbolic
links are followed. Ptah accepts a path that temporarily contains .. when its
resolved destination is still inside the boundary, and refuses a symbolic link
whose destination is outside it.
Pass an absolute pathname when you intentionally read a schema file outside the
working directory, such as --schema-file /srv/schemas/app.sql. Absolute
pathnames retain their existing reach and are not confined to the working
directory.
Reading a server’s capability profile
Section titled “Reading a server’s capability profile”ptah db capabilities connects, reads the server’s own version surface, and
prints what Ptah resolved from it. It executes no DDL and modifies no schema
object. Beyond the metadata queries any Ptah connection makes — the server’s
version string, and the session’s current schema or database — it adds one
statement, SERVERPROPERTY('ProductVersion'), and only on SQL Server. Opening
the connection is still opening a connection: a sqlite:// URL naming a file
that does not exist creates that file, exactly as any other command reaching the
same URL would.
ptah db capabilities --db-url "$DATABASE_URL"Expected output includes:
Dialect: postgresServer version: 18.4Server product: postgresBanner: PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bitCapability preset: Postgres17 (postgres)Preset source: version-ladderSupport level: certifiedRelease line: 18Banner is the version string the server reported verbatim; Server version
and Server product are what Ptah parsed out of it. The three differ where it
matters most: a MariaDB reached over a mysql:// URL reports dialect mysql,
product mariadb, and a preset resolved on the MariaDB ladder. Below the block the command
prints the reason for the support level, the non-boolean behavior values —
identifier limit, enum modeling, foreign key reference — and every capability
key marked supported or unsupported. A version that selected no exact
measured release line adds a Note: line naming what was planned instead.
| Flag | Value |
|---|---|
--db-url |
Database URL. Required. |
--format |
text (the default) or json. |
--connect-timeout |
Maximum time to wait for the initial connection, default 10s; 0 disables the timeout. |
The output answers four questions that a refused or unexpected operation otherwise leaves open:
-
Which capability set is in force.
Capability presetnames it, and the dialect in parentheses names whose resolution produced the set — a version ladder where the dialect has one, the dialect default or a banner match otherwise. It is not always the dialect the URL connected as. -
Why that set.
Preset sourcenames the rule that chose it:version-ladderwhen the version selected an exact measured release line,unmeasured-linewhen the dialect’s ladder answered but no measured line matched,newer-than-measuredwhen the server is newer than the newest line Ptah has measured,dialect-defaultwhen the dialect has no ladder at all, andunrecognized-bannerwhen the version string named no server. -
What Ptah promises about the release line.
Support levelandRelease line, with the reason underneath. A server on a line Ptah declares nothing about reportsbest-effortand is not refused: capabilities are resolved for it and the operations they allow are performed. -
What this server can do. Every registered capability key appears, present or absent, so an absent capability is distinguishable from one this build does not know about.
-
What this server decided rather than its release line. A version alone does not settle every key. Where the server’s own configuration answers differently, the profile says so in its own section (see below).
--format json carries everything the text form shows, plus each capability
key’s documentation string, as a stable sorted document. Two runs against an
unchanged server produce identical bytes, so a diff of them reports a change
that happened rather than a reordering.
When configuration, not the version, decides a capability
Section titled “When configuration, not the version, decides a capability”MySQL 8.4 reads its foreign-key reference policy from
restrict_fk_on_non_standard_key. Two servers on that release therefore answer
opposite ways about what a foreign key may point at, and the release line can
only be right about one of them. ptah db capabilities pins a session, reads
the set that session resolves, and names every key where the two differ:
Behavior: identifier_limit 64 characters enum_modeling inline foreign_key_reference indexed
Set by this server rather than by its release line: foreign_keys_require_indexed_reference supported (the release line answers unsupported) foreign_keys_require_unique_reference unsupported (the release line answers supported) read from this server's session settings, not from its release lineThat is the same MySQL 8.4 release as the profile above, started with
--restrict-fk-on-non-standard-key=OFF. foreign_key_reference is read from
those keys, so it moves with them — unique on a default server, indexed
here.
The section is absent when the version decided everything, which is the
ordinary case: a server whose configuration matches its release line renders
exactly what it rendered before the refinement existed. In --format json the
same information is the configuration_refinements array, omitted when empty.
The refinement reports; it does not change the server. Ptah reads settings and plans by them, and never enables a database feature or writes a server variable.
What a machine reads, and from where
Section titled “What a machine reads, and from where”Every verb that has a machine-readable format writes that document to stdout,
whatever the outcome, and keeps stderr for diagnostics. That includes the
expected-negative cases: ptah schema drift --format json writes its report to
stdout and exits 1 when there is drift, exactly as
ptah migrations status --json --exit-code writes its report to stdout and
exits 1 when migrations are pending.
The distinction a caller needs is the exit code, not the stream: 0 success,
1 an expected negative result, 2 a command or usage error. A parser reading
stdout therefore gets a document in every case except 2.
The banner, and where it is not
Section titled “The banner, and where it is not”Run ptah with no arguments in a terminal and it prints the Ptah wordmark, the
build version and https://ptah.run above the command list. ptah-compat and
ptah-ls print the same wordmark under their own names.
It appears only when the stream it would be written to is a terminal. Redirect
stdout, pipe it, capture it in CI, or read it from a program, and the output is
what it was: the entry screen alone, byte for byte. A --help, a help, and a
--version never carry it either — those are explicit requests whose output is
already parsed.
Two of the binaries take that further. ptah-ls writes its banner to stderr,
never to stdout, because stdout is the language-server protocol stream and a
client reads framed messages off it. ptah-compat under
PTAH_ATLAS_STRICT_COMPAT=1 carries no banner at all, whatever the stream is:
the strict profile is the Atlas Community Edition surface, and the community
binary prints no banner.
What an inference verb does not imply
Section titled “What an inference verb does not imply”No ptah inference verb implies another. A backfill finishing does not mean
the corpus is right; verification passing does not mean anything has cut over; and
cutting over does not make the old generation disposable. The task-oriented
walkthrough is Inference migrations.
An AI client, over MCP
Section titled “An AI client, over MCP”ptah mcp speaks the Model Context Protocol on stdin and stdout, so an AI
client or coding agent can drive Ptah’s operations directly. It is not a command
to run by hand: a client starts it and speaks the protocol to it.
Eight reading tools, each forwarding to the operation that already owns the work:
| tool | answers |
|---|---|
describe_session |
what this session may do, and what it can reach |
validate_schema |
structural problems in a declared schema, for one dialect, with no database |
render_schema |
the DDL a declared schema becomes, in dependency order |
schema_lineage |
which base columns feed each view column |
search_docs |
what Ptah’s own documentation says about a question, with the document and heading it came from |
read_database |
the schema a live database currently holds |
inference_plan |
what changing an embedding model would do, what blocks it, and what text would leave the database |
inference_status |
how far a generation run has got, and why a cutover is blocked |
Three of Ptah’s own reading verbs are absent — schema inspect, schema diff
and migrations lint — because each needs a scratch database Ptah resets
destructively. Run those from the CLI; the guide linked below states the rule.
--workspace adds three artifact tools – read_artifact, preview_patch
and apply_patch – confined to the directories --migrations-dir,
--schema-dir and --tests-dir name.
Writing stays refused until --allow-write names an artifact class, and a named
class asks for approval per patch unless --auto-approve says otherwise.
Applying anything to a database is unavailable on this surface at any setting.
AI and agents routes to MCP setup, permissions,
patch workflow, troubleshooting, and tool reference.
Credentials are the client’s to supply. The server holds none, stores none, and sends nothing anywhere: it runs locally and talks to whatever the caller names.
The transport is stdio. A remote one brings authentication, which is a security surface this release does not open.
A model of your own
Section titled “A model of your own”ptah assist talks to a model you choose, through a key you hold: a hosted API,
a gateway your organization runs, or one running on this machine. There is no
Ptah account and no Ptah-hosted model.
ptah assist holds a conversation and ptah assist explain asks one question:
either way the model calls Ptah’s tools, and every call goes through the same
surface ptah mcp serves, over an in-memory transport. The answer is the
model’s words and the tool trace is what Ptah did; --trace prints both, and a
run where no tool answered says so.
Conversations are saved to .ptah/sessions/ in the project, one JSON object per
line; --ephemeral keeps no record and --resume continues an earlier one.
ptah assist explain --format jsonl prints those same records to stdout as they
happen, so a program reading a pipe and a program reading a saved session are
reading one format.
ptah assist context prints exactly what a question would send to the provider
and sends nothing, built by the same code that builds the real request. Every
run then reports how many bytes of project content actually went.
ptah assist provider list reports
the profiles this machine can reach, including ones inferred from a key already
in your environment, and ptah assist provider test measures whether one works
– reachable, credential accepted, model served, and tool calling available,
which is the capability the workflow requires. The check sends nothing about
your project.
A profile carries a credential REFERENCE such as env:OPENAI_API_KEY; a key
written into configuration is refused, and Ptah stores none.
Use Ptah Assist is the task guide.
A live view
Section titled “A live view”ptah schema serve runs an HTTP server that renders the declared schema and how
a live database differs from it, re-reading both on every request. Every route
answers GET and HEAD and nothing else, and no code path writes to the
database. Nothing about it becomes a dependency of a migration.
| Flag | Default | Meaning |
|---|---|---|
--addr |
127.0.0.1:7070 |
Listen address. The address is read back from the listener, so 127.0.0.1:0 prints the port that was chosen. |
--refresh |
30s |
Self-reload interval as a Go duration. 0 serves a page that does not reload. |
--title |
Schema dashboard |
Heading, browser title and sidebar name. |
--root-dir |
working directory | Go annotation tree to read the declared schema from. Repeatable. |
--schemas |
connection default | Comma-separated schemas the database read is limited to. |
--db-url |
none | Database to compare against. Required. |
Serve a live schema view is the guide.
Schema documentation
Section titled “Schema documentation”ptah schema export carries two targets that write a schema as a document for a
person: --to markdown and --to html. Both read the desired schema from any
source and connect to no database.
| Target | Output | Diagram |
|---|---|---|
markdown |
One Markdown document written to stdout, or to --out. |
none |
html |
One self-contained HTML file: the styling and the diagram sit inside it, and it names no resource outside itself. | inline SVG, laid out by Ptah |
--title, --include-tables and --exclude-tables apply to both.
Generate schema documentation is the guide.
Plan approval
Section titled “Plan approval”ptah schema approve signs a saved plan file with an SSH key,
ptah schema verify-approval checks that signature against an OpenSSH
allowed_signers file, and ptah schema apply --plan <path> --require-approval
refuses a plan that does not verify. ssh-keygen performs both halves, in the
ptah-plan namespace, over the plan document byte for byte; Ptah implements no
cryptography of its own.
Three outcomes stay distinct, because they call for different actions:
| Outcome | Meaning | Exit |
|---|---|---|
| unapproved | no signature beside the plan — nobody reviewed it | 2 |
| unverifiable | the plan changed after approval, an unlisted key signed it, or ssh-keygen could not run |
2 |
| approved | verified, and the signer is named on stdout | 0 |
| Flag | Verb | Meaning |
|---|---|---|
--plan |
approve, verify-approval |
Path to the plan file. |
--key |
approve |
Private key ssh-keygen signs with. |
--allowed-signers |
verify-approval, apply |
Approver list. Default ./.ptah/allowed_signers, resolved against the working directory. |
--signer |
verify-approval |
Require the approval to belong to one named principal. |
--require-approval |
apply |
Refuse a --plan that carries no verifying approval. |
Plan and approve changes is the guide.
Column lineage
Section titled “Column lineage”ptah schema lineage derives column-to-column dependencies from the view and
materialized-view bodies a schema declares. The analysis is static and local: it
reads the schema Ptah already models and contacts nothing.
A body it cannot fully resolve is reported under undecided with a reason
rather than omitted, because “nothing reads this column” and “I could not tell”
call for different decisions.
| Flag | Default | Meaning |
|---|---|---|
--schema-file |
none | Schema file to read. Repeatable. |
--root-dir |
working directory | Go annotation tree to read instead. Repeatable. |
--dialect |
postgres |
How the source is parsed. Not a render target. |
--format |
table |
table or json. |
The run exits 0 whether or not any view landed in undecided, so a gate reads
the undecided key of the JSON output.
Trace view column lineage is the guide.
Schema object counts
Section titled “Schema object counts”ptah schema stats reads a live database and writes one OpenMetrics gauge per
object kind, ending with a literal # EOF line. The family list is fixed rather
than derived from the target, so every run emits the same names in the same
order and a family the reader found none of is reported at 0.
| Flag | Default | Meaning |
|---|---|---|
--db-url |
none | Database to read. Required; a ptah.yaml url: does not satisfy it. |
--schemas |
connection default | Comma-separated schemas. Selects what is counted on the PostgreSQL family, and adds a schemas label carrying the flag’s value as one string on every engine. |
Every gauge is named ptah_schema_<kind> and carries a dialect label. The
HELP line of each family names what it counts;
Count schema objects is the guide and shows the complete
set.
Schema security findings
Section titled “Schema security findings”ptah schema security reads a live database and reports findings over what it
declares — who holds which privilege, which tables are reachable with no
row-level policy, which routines run as their owner.
Report schema security findings is the guide; this
section is the per-code and per-engine detail behind a report.
The rules:
| Code | Finding | Severity |
|---|---|---|
PRV01 |
a table whose privileges reach a role, with row-level security not enabled | info |
PRV02 |
a SECURITY DEFINER routine, which runs with its owner’s privileges |
info |
PRV03 |
a privilege granted to PUBLIC, which every current and future role holds |
warning |
ROL01 |
a role holding privileges on an object it was not observed using | warning |
ROL03 |
two roles held by one member that grant nearly the same privileges | info |
ROL04 |
a role that cannot log in and that nobody holds | info |
OWN01 |
objects owned by a role that can log in | info |
ROL01 needs something no catalog keeps. A privilege is not use: a role holding
SELECT on a table it has never read looks identical, in every catalog Ptah
reads, to one that reads it hourly. PostgreSQL’s pg_stat_user_tables counts
scans without attributing them to a role, pg_stat_statements attributes to a
userid but is an optional extension that records statement text rather than an
object graph, MySQL and MariaDB keep no equivalent, and SQL Server’s
sys.dm_db_index_usage_stats attributes nothing to a principal.
So the observation is supplied rather than read. --role-usage <file> takes a
JSON list of what something else saw — an audit stream, pg_stat_statements, a
proxy log. Without the flag ROL01 reports itself skipped; with it, every grant
to a named role on an object absent from the list is reported.
The severity is warning rather than error because the signal is a window,
and a window is evidence of absence only for as long as it covers — a quarterly
job that did not run inside it holds a privilege the rule will name. Grants to
PUBLIC are left to PRV03, which has the better remedy for them.
ROL03 and ROL04 read the server’s role graph — who holds which role — so they
run against a live database and report themselves skipped anywhere that graph was
not read. The PostgreSQL family reads it from pg_auth_members, MySQL from
mysql.role_edges, MariaDB from mysql.roles_mapping, and SQL Server from
sys.database_role_members — where the fixed roles every database ships with,
and public, are excluded on the role side, because a membership nobody wrote
is not a finding.
OWN01 reads ownership from the PostgreSQL family and SQL Server. MySQL has no
object owner to read — a routine has a DEFINER and a table has nothing — so the
rule reports itself skipped there rather than answering from a concept the engine
does not have.
OWN01 reports one finding per owning role, not one per object: an owner is
not a grant, so whoever holds that role’s password can drop or alter everything
it owns and no revoke takes that away. It commonly fires — PostgreSQL makes the
creating role the owner — which is why it is info and why it is aggregated: a
row per table would bury every other finding.
Two details decide whether it answers at all:
- The login flag is read from the catalog beside the owner rather than looked
up in the described roles, so the rule still answers where the owner is a role
Ptah does not describe — the bootstrap superuser owns everything on a default
PostgreSQL database. On SQL Server the same question is
authentication_type_desc:dboreportsINSTANCE, whileguestand a user createdWITHOUT LOGINreportNONEand are nobody’s password. - An object with no owner of its own is owned by its schema’s owner on SQL Server, which is what most objects are, so the read resolves through the schema rather than skipping them.
An edge granted with admin option is the right to grant a role onward rather
than evidence somebody uses it, and neither rule counts it. That is measured
rather than stylistic: CREATE ROLE on MariaDB inserts an admin edge from the
creator by itself, so counting those would make ROL04 unable to fire on any
MariaDB server and would make ROL03 name the creator on every one. The cost is
stated: an explicit WITH ADMIN OPTION grant to a real user is ignored too, so
ROL04 can name a role one administrator could also use — a finding a reader
can dismiss beats one that never appears. ROL03 reports a pair when they share at least two privileges and at
least half of the smaller role’s set; one shared privilege is a coincidence on
any real schema. A login role with no members is not ROL04: it is its own
principal, and reporting every application account would bury the rule.
Severities are the ones the rest of Ptah speaks: info reports and never
blocks, warning asks for review, error blocks. --fail-on error (the
default), any, or none decides the exit code, spelled the way
ptah migrations lint spells it. No rule here is error-severity, so the default reports without
failing; --fail-on any gates on every finding.
--format json emits the same findings as a document, each with its structured
detail — the privileges, the roles, the routine’s language — so a pipeline can
group or diff them without parsing prose.
A rule that cannot run is listed under Not checked here: with its reason,
because a rule that quietly did not run is indistinguishable from one that found
nothing. Row-level security has no meaning on a target that does not model it,
so PRV01 reports itself skipped on MySQL and SQLite.
PostgreSQL’s own USAGE on schema public to PUBLIC is excluded by name,
because a finding present in every database is one a reader learns to skip.
CREATE on that schema is not excluded — PostgreSQL 15 revoked it from PUBLIC
by default, so a database that grants it is stating something.
OCI transport behavior
Section titled “OCI transport behavior”These native commands resolve an oci:// reference, each through the flag
named beside it:
| Native command | OCI source flag | Artifact |
|---|---|---|
ptah migrations up |
--migrations-dir |
migration directory |
ptah migrations status |
--migrations-dir |
migration directory |
ptah migrations ls |
--migrations-dir |
migration directory |
ptah migrations show |
--migrations-dir |
migration directory |
ptah migrations down |
--migrations-dir |
migration directory |
ptah migrations lint |
--dir |
migration directory |
ptah migrations validate |
--dir |
migration directory |
ptah schema render |
--schema-file |
desired schema |
ptah schema validate |
--schema-file |
desired schema |
ptah schema export |
--schema-file |
desired schema |
ptah schema inspect |
--schema-file |
desired schema |
ptah schema lineage |
--schema-file |
desired schema |
ptah schema compare |
--schema-file |
desired schema |
ptah schema drift |
--schema-file |
desired schema |
ptah schema plan |
--schema-file |
desired schema |
ptah schema apply |
--schema-file |
desired schema |
ptah schema push |
--schema-file |
desired schema |
ptah schema test |
--schema-file |
desired schema |
ptah migrations plan |
--schema-file |
desired schema |
ptah migrations generate |
--schema-file |
desired schema |
migrations lint can attach its canonical report with --attach, and a plan
with exactly one OCI schema source can attach its canonical safety report. Use
digest pins for reproducible runs and reserve --plain-http for an explicitly
trusted local registry. See OCI registry
artifacts.
Every command that resolves an oci:// source registers --plain-http, and
neither that pairing nor the table above is maintained by hand: a test walks the
built command tree for --schema-file, --dir and --migrations-dir, requires
every command whose value reaches the OCI loader to register the flag, drives it
at a registry to prove the value reaches the client rather than merely parsing,
and then requires the rows here to be exactly that set. A verb that starts or
stops resolving the scheme fails that test until this table says so.
migrations validate --dir oci://... answers the integrity question on the
artifact itself, with no database and nothing executed. Over a tag a successful
run adds the movable-tag qualifier on standard error, naming the digest the tag
resolved to; a digest-pinned reference prints nothing extra. migrations hash
refuses oci:// by design, because it writes the integrity file back into the
directory it hashed and a registry artifact is immutable.
ptah oci referrers <oci-reference> lists direct attachment descriptors.
--type accepts all, lint, plan, or deployment; --format accepts
text or json. Unqualified subjects resolve to :latest, tags resolve to
their current manifest, and digest subjects remain immutable. Docker
credentials and HTTPS are the defaults; --plain-http is only for an
explicitly trusted local registry. The command lists metadata; ptah oci fetch
returns the payload.
ptah oci resolve <oci-reference> prints the pinned reference a tag currently
names, so a pipeline can record the digest once and pass it to every later step
instead of resolving the tag again at each one. --format json adds the
descriptor’s media type and size.
ptah oci inspect <oci-reference> reads the manifest and stops there: artifact
type, subject, annotations, and each file layer’s name, media type, size, and
digest, without downloading the files. It also reports how each referrer was
discovered. Ptah writes both the standard referrers index and its own
content-derived durable tag, and a referrer reported as durable-tag was
returned by the second mechanism alone — Ptah finds it and another OCI client
may not. --no-referrers skips that lookup.
ptah oci fetch <oci-reference> returns the bytes of an attached report, which
is how Ptah reads back the lint, plan, and deployment reports Ptah published.
Selection never guesses: one candidate is fetched, several are refused with the
digests printed. Narrow with --type, or name one with --digest. The same
rule governs the files inside the chosen referrer — one is written, several
require --file. --output writes to a path instead of standard output.
ptah oci tags <oci-reference> lists the aliases a repository carries, which
is the view that says which of them exist before a promotion moves one.
ptah oci tag <oci-reference> <tag>... moves an alias onto an artifact that
already exists. Promotion through a push re-derives content that was already
reviewed, so what arrives in production is an artifact equal to the reviewed
one rather than the same one; moving the alias keeps the manifest digest
identical by construction, because nothing is built and nothing is uploaded.
Aliases move one at a time, and the ones already applied are named when a later
one fails.
ptah oci copy <source> <destination> copies an artifact between repositories
with its digest preserved. --recursive carries the artifact’s referrers with
it; without it the copy arrives with its lint results, plans, deployment
reports, and signatures left behind in the source repository, which is how a
promotion loses the evidence it was promoted on. A digest destination is
refused, because a digest names content that already exists.
ptah oci capabilities <oci-reference> asks the registry whether it answers
the referrers API. Ptah publishes referrers both through the standard index and
through its own content-derived tag and merges them on read, so its own
discovery works whether or not the registry answers that API — and where the
API is absent, a referrer Ptah published is one another OCI client may never
find. The question is put with the client pinned to the API so a success cannot
have come from the tag-schema fallback, and a failure to ask is reported as an
error rather than folded into a no.
This does not implement the Atlas Cloud command paths. The Atlas-compatible
migrate push and schema push remain Atlas community-edition boundary
stubs in the ptah-compat binary, and Atlas-compatible apply commands do not
expose the native OCI transport flags.
External desired-schema inputs
Section titled “External desired-schema inputs”The native desired-schema consumers — schema render, schema compare,
schema drift, migrations plan, and migrations generate — accept external
commands that emit SQL, HCL, or YAML. Each command also reads
ptah.yaml external_schema when --schema-cmd is not set and
--allow-external-schema explicitly permits config-sourced execution. See
Configuration for the external_schema block.