Validate and format schema files
Two native verbs check schema files before anything connects to a database.
ptah schema validate reports structural problems in a desired schema, once per
target dialect. ptah schema fmt rewrites HCL schema files into HashiCorp HCL’s
canonical layout, or reports the ones that are not in it.
Use them as a pre-commit hook and as the first job of a pipeline. Neither one
takes a --db-url, so both run on a machine with no database and finish in the
time a linter takes.
| Command | Reads | Answers |
|---|---|---|
ptah schema validate |
the supported sources listed below | Is this schema structurally sound for the dialects I target? |
ptah schema fmt |
.hcl files on disk |
Are these files in canonical HCL layout? |
| Source | Selector | Source-specific limitation |
|---|---|---|
| SQL file | --schema-file schema.sql |
Validates the Ptah DDL parser subset for the selected dialect. |
| YAML file | --schema-file schema.yaml |
Validates Ptah YAML schema objects and documented dialect overrides. |
| HCL file | --schema-file schema.hcl |
Validates the Atlas-compatible HCL subset plus Ptah extensions. |
| DBML file | --schema-file schema.dbml |
DBML cannot express every Ptah object. |
| Go annotations | --root-dir ./models |
Reads the native Go annotation model from a Go source tree. |
| OCI artifact | --schema-file oci://registry.example/app:v1 |
Validates the canonical HCL stored in the artifact. |
| Composite source | repeat --schema-file with compatible inputs |
Uses the union of the selected formats; conflicting definitions fail. |
The source-support manifest records focused command evidence separately from accepted transport. A supported input that lacks a focused command test is not a claim that its format can express every Ptah object.
Prerequisites: an installed ptah binary (Install Ptah)
and a desired schema as local files.
Starting state
Section titled “Starting state”Save this as broken.yaml. It declares one table with two problems: an index
over a column the table does not have, and a foreign key to a table nothing
declares.
tables: orders: columns: id: type: SERIAL primary: true customer_id: type: INTEGER not_null: true foreign: customers(id) indexes: idx_orders_status: fields: [status]Save this beside it as schema.hcl. It declares a sound schema in a layout
that is not canonical:
schema "public" {}table "customers" {schema = schema.publiccolumn "id" {type = int}column "email" { type = varchar(255) null = false}}Validate against one dialect
Section titled “Validate against one dialect”--dialect is required, because a declaration valid for one target can be
invalid for another:
ptah schema validate --schema-file broken.yaml --dialect postgresExpected output includes:
postgres: index "idx_orders_status": names column "status", which table "orders" does not declarepostgres: schema: invalid foreign key: field "customer_id" references unknown table "customers"2 structural problemsThe run exits 1. Every line names the dialect it was found under, then the
object, then what is wrong with it. A schema with nothing wrong prints nothing
and exits 0, so a hook can read the status alone.
Validate against every dialect you ship to
Section titled “Validate against every dialect you ship to”--dialect is repeatable, and each value is checked separately with its own
lines:
ptah schema validate --schema-file broken.yaml --dialect postgres --dialect mysqlExpected output includes:
postgres: index "idx_orders_status": names column "status", which table "orders" does not declarepostgres: schema: invalid foreign key: field "customer_id" references unknown table "customers"mysql: index "idx_orders_status": names column "status", which table "orders" does not declaremysql: schema: invalid foreign key: field "customer_id" references unknown table "customers"4 structural problemsA problem that only one target has is reported under that target alone. A
schema declaring a foreign key validates on postgres and fails on
clickhouse with clickhouse: schema: clickhouse does not support foreign keys, because ClickHouse models none.
--root-dir reads Go annotations instead of a file, and
--schema-file is repeatable. Naming both merges them into one
composite desired schema. --server-version refines the
capability set a dialect stands for, for example --dialect postgres --server-version 17.
Format HCL schema files
Section titled “Format HCL schema files”ptah schema fmt walks the paths it is given, or the current directory when it
is given none, and rewrites every .hcl file whose layout is not canonical. It
prints the files it changed:
ptah schema fmt .Expected output includes:
schema.hclOnly files whose content changed are printed, so a run that changes nothing
prints nothing. The rewrite is HashiCorp HCL’s own canonical layout —
indentation, alignment and spacing — and it changes no value the file declares.
schema.hcl becomes:
schema "public" {}table "customers" { schema = schema.public column "id" { type = int } column "email" { type = varchar(255) null = false }}Gate a pipeline on formatting
Section titled “Gate a pipeline on formatting”--check rewrites nothing. It prints the files that are not canonically
formatted and refuses:
ptah schema fmt --check .Expected output includes:
schema.hclerror: 1 file(s) are not canonically formatted; run `ptah schema fmt` to rewrite themThat run exits 2, not 1: an unformatted file is reported as a command
failure rather than as an expected negative result. A pipeline step that treats
any non-zero status as a failure needs no special handling; one that
distinguishes 1 from 2 has to know this.
Failure modes
Section titled “Failure modes”| Message on stderr | Cause | Exit |
|---|---|---|
error: --dialect is required: validation is per target, and a declaration valid for one dialect can be invalid for another |
schema validate with no --dialect. |
2 |
error: schema fmt nosuch.hcl: stat nosuch.hcl: no such file or directory |
schema fmt given a path that does not exist. |
2 |
error: 1 file(s) are not canonically formatted; run \ptah schema fmt` to rewrite them` |
schema fmt --check found files to rewrite. |
2 |
See Exit codes for the contract these follow.
Limitations
Section titled “Limitations”ptah schema validatechecks structure, not renderability. A declaration the renderer refuses for the same dialect can validate cleanly: aSERIALcolumn validates againstclickhouseand exits0, whileptah schema render --dialect clickhouseover the same source exits2withclickhouse: SERIAL has no auto-increment equivalent. Render as well as validate before trusting a target.ptah schema fmtreads.hclfiles only. A YAML, SQL or DBML schema file in the same directory is left alone and not reported, so a formatting gate over a mixed directory covers the HCL half of it.ptah schema fmttakes no--configand no--env. It works on paths, not on the schema sources a project configuration names.
Exact reference
Section titled “Exact reference”Run ptah schema validate --help and ptah schema fmt --help for the flag sets
with their environment variables.
Native commands places both verbs in the
tree, and Exit codes carries a row for each.
Next steps
Section titled “Next steps”- Ready to see the SQL the schema renders to? Work with a schema source.
- Want the same check against a live database? Compare and drift.
- Adding these to a pipeline? Continuous integration.