HCL schema reference
Ptah can read HCL schema files as desired schema input. The parser builds the same schema IR as Go annotations and YAML schema files, then uses Ptah’s normal rendering and planning paths.
Ptah’s HCL schema syntax includes a supported subset of Atlas HCL schema files plus Ptah extensions for Go annotation parity. Ptah is an independent implementation and is not affiliated with or endorsed by Ariga or Atlas.
Command
Section titled “Command”ptah schema render --schema-file schema.hcl --dialect postgres--schema-file accepts .hcl files for HCL schema input. Project
configuration in atlas.hcl is a different file type; see
Atlas project config.
All schema-file sources use the path boundary documented under Schema file paths.
ptah schema fmt [path ...] rewrites .hcl schema files into HashiCorp
HCL’s canonical layout, walking directory arguments recursively and printing
only the files that changed. --check reports non-canonical files without
rewriting them and exits non-zero, for CI formatting gates.
Minimal schema
Section titled “Minimal schema”schema "main" {}
table "users" { schema = schema.main
column "id" { type = int }
column "email" { type = varchar(255) null = false }
primary_key { columns = [column.id] }
index "idx_users_email" { unique = true columns = [column.email] }}How many schema blocks a document may declare
Section titled “How many schema blocks a document may declare”A document may declare as many schemas as the run can reach. A run whose URL
names one schema — any SQLite URL, a PostgreSQL-family URL carrying
search_path=<one name>, a MySQL-family URL naming a database — reaches one,
and a document declaring more than one top-level schema block is refused
there rather than narrowed. The count is of blocks: repeating
schema "main" {} in two files of a schema directory is two.
This holds on the native commands, not only the compatibility binary:
ptah schema inspect --schema-file two-schemas.hcl --dev-url sqlite://dv?mode=memory
refuses, because narrowing a desired state to the scope and reporting success
is a wrong answer wherever it happens. Give the run a realm-scoped URL to
describe every schema the document declares. A desired state with no URL to be
scoped by — Go annotation roots, ptah schema test — is unaffected.
See the Atlas-compatible schema commands for the message and the flag it names.
Supported object subset
Section titled “Supported object subset”| Object | Supported shape |
|---|---|
schema |
Labels and comments for table namespace references. |
table |
Columns, keys, indexes, constraints, checks, row security, and Ptah checks, custom, platform, api_name, openapi_name, graphql_name, and proto_name extensions. |
column |
Type, nullability, defaults, generated/identity metadata, comments, checks, and Ptah enum, platform, api_name, openapi_name, graphql_name, proto_name, api_type, and api_expose extensions. |
primary_key |
columns; PostgreSQL also supports include. |
index |
columns, on { column = ... }, on { expr = ... }, desc, on { nulls_first = ... } or on { nulls_last = ... }, unique, type, where, comment, ClickHouse granularity, and PostgreSQL include/storage options. |
constraint |
Ptah block used when annotation metadata cannot fit the Atlas-native constraint blocks, and for EXCLUDE constraints. |
unique |
columns; PostgreSQL also supports include and nulls_distinct. |
foreign_key |
One local columns entry and one table-qualified ref_columns entry, plus optional on_delete, on_update, deferrable and initially. |
check |
expr. |
enum |
values, plus the schema that owns the type. A PostgreSQL enum is created in that schema and a column declared against it is qualified with it. |
extension |
PostgreSQL installation schema, if_not_exists, version, and comments. |
role |
PostgreSQL role attributes, including password. |
permission |
PostgreSQL table, schema, and sequence permissions. |
function |
PostgreSQL metadata and raw body, with Atlas-style arg blocks or a Ptah raw params string. |
procedure |
The same shape as function without return, which a procedure does not have. Ptah block: the Atlas community CLI has no procedure block and ignores this one. |
view / materialized |
SQL body plus schema and comments. |
trigger |
Trigger timing, target, execution mode, function body, and comments. |
policy |
PostgreSQL RLS policy fields. |
sequence |
PostgreSQL type, start, increment, min_value, max_value, cache, cycle, owned_by, and if_not_exists. |
domain |
PostgreSQL type, null, default, and check. |
composite |
PostgreSQL composite type with ordered field sub-blocks. |
range |
PostgreSQL subtype, subtype_opclass, collation, canonical, and subtype_diff. Writing one of the four optional attributes as "" says the range has none; omitting it says nothing about it, so a range that already carries one keeps it. |
hypertable |
TimescaleDB partitioning of the table the label names: column, and optionally chunk_interval, if_not_exists and the schema holding the table. |
continuous_aggregate |
TimescaleDB continuous aggregate: as for the SELECT it materializes, and optionally materialized_only, a comment and the schema holding it. |
synonym |
SQL Server alias: target, plus the schema the alias lives in and a comment. |
extended_property |
SQL Server named value: value, and the object it is attached to as schema, table and column. |
data |
Ptah managed-data declaration with a table reference, key columns, and a file path relative to the HCL file. |
API export metadata attributes
Section titled “API export metadata attributes”API export metadata is a Ptah HCL extension. All values are quoted strings:
| Attribute | Accepted on | Meaning |
|---|---|---|
api_name |
table, column |
Shared OpenAPI, GraphQL, and Protobuf name fallback. |
openapi_name |
table, column |
Exact OpenAPI component key on a table or property key on a column. |
graphql_name |
table, column |
GraphQL type-name stem on a table or exact field identifier on a column. |
proto_name |
table, column |
Protobuf message-name stem on a table or exact lower-snake-case field name on a column. |
api_type |
column |
Contract-only type override shared by all three targets. It must name a type Ptah maps or a declared enum. |
api_expose |
column |
Contract exposure: read, write, read-write, or none. |
Names resolve from the target-specific attribute, then api_name, then the
database identity. Ptah singularizes and PascalCases GraphQL and Protobuf table
stems into final type/message names; it does not rewrite an invalid explicit
field identifier. Invalid target names and collisions fail before output.
These attributes affect OpenAPI, GraphQL, and Protobuf exports only; they do not
change DDL or migration planning.
Canonical HCL rendering includes every non-empty attribute above. Parsing the rendered document and rendering it again must produce the same bytes, and OCI schema artifacts use that canonical form. Native Ptah and the default compatibility profile preserve the extension. Strict Atlas CE mode rejects a schema carrying it rather than silently dropping it. See API schema export for source-neutral examples and the target-specific behavior.
An extended_property block names its owner with plain strings rather than
block references, and the level it stops at is the scope it applies to. A block
with no schema is a database-scoped property; with a schema and no table it
applies to the schema; table adds the table and column the column. SQL Server
addresses each level by name — @level1name = N'users' — so a table without a
schema, or a column without a table, names nothing and is refused when the
document is read.
synonym "current_orders" { schema = schema.app target = "sales.orders"}
extended_property "MS_Description" { schema = schema.app table = "orders" column = "total" value = "order total including tax"}A materialized block accepts no refresh_strategy. Ptah does not refresh
materialized views: one is populated when it is created, a changed as body is
reconciled as a drop and a create that populates it again, and it goes stale
only when its source data changes, which schema reconciliation cannot observe.
Refresh from your own scheduler.
The attribute is refused on presence while the document is parsed – before its value expression is read, so a bare identifier is refused the same way a string is – and the refusal names the materialized view and the reason.
Every schema "pg_catalog" {} or schema "information_schema" {} block is an
explicit schema declaration, even when an extension also refers to it, and is
refused before SQL, including when comparison would otherwise report no changes.
To preserve an extension already installed in a
server-owned namespace without requesting CREATE SCHEMA, write the placement
as a string, for example schema = "pg_catalog", and omit the schema block.
Ptah’s generated HCL uses that spelling. CockroachDB likewise refuses its exact
crdb_internal namespace. Quoted lookalikes such as
schema "PG_CATALOG" {} or schema "CRDB_INTERNAL" {} remain user schemas.
The extension.schema attribute accepts an HCL string template or an exact
one-name schema traversal such as schema.extensions or
schema["Extension Store"]. A string template may evaluate declared variables;
a direct var.* traversal, another object namespace such as table.*, and an
over-qualified traversal such as schema.extensions.extra are refused instead
of being reinterpreted as schema names.
The two-label form extension "extensions" "citext" {} uses its first label
as the installation schema. If the block also carries a schema attribute,
that value must resolve exactly to the first label. An explicit empty value is
still present, so schema = "" conflicts with a nonempty schema label; on the
one-label form it explicitly selects the target’s default schema.
Unsupported semantics fail explicitly. Ptah does not silently drop HCL objects
that it cannot represent in the schema IR. The Ptah ops index attribute
preserves a Go annotation operator class.
Variables, locals, and expressions
Section titled “Variables, locals, and expressions”A schema file may declare variable and locals blocks and read them back
through the var. and local. namespaces:
variable "status" { type = string default = "active"}
locals { state_column = "state_${var.status}"}
schema "app" {}
table "t" { schema = schema.app column "state" { type = text default = var.status comment = local.state_column }}A variable block requires type, which is one of bool, int, number,
string, or list, map or set of those. It accepts default and
description. A variable with no default needs a value from --var:
ptah-compat schema diff --dev-url "sqlite://file?mode=memory" \ --from file://empty.hcl --to file://schema.hcl --var status=liveOne --var occurrence carries comma-separated name=value assignments, and the
flag may be repeated. A variable that ends with no value fails with
missing value for required variable "status". --var does not require an
atlas.hcl; when one is present it also supplies that file’s variables.
ptah schema test --schema-file schema.hcl --var status=live applies the same
override before it provisions the throwaway database.
Attribute values are evaluated. A function call resolves against the function
set, a var. or local. reference resolves against the blocks above, and
anything that will not resolve is an error rather than the expression’s own
source text. References that name schema objects — schema.app, column.state,
enum.status — and type expressions such as text or varchar(255) are read
as written and are not evaluated, so a var. reference in a type is rejected.
Go annotation parity
Section titled “Go annotation parity”Every schema semantic accepted by Ptah’s Go annotation parser has an HCL representation. Export may use Ptah-specific blocks and attributes when the Atlas-compatible shape would lose information:
schema "app" {}
enum "enum_user_status" { values = ["active", "disabled"]}
table "users" { schema = schema.app checks = ["id > 0"] custom = "WITHOUT OIDS"
column "status" { type = enum_user_status enum = ["active", "disabled"] }
platform "mysql" { override "engine" { value = "InnoDB" } }
constraint "users_no_overlap" { type = "EXCLUDE" using = "gist" elements = "id WITH =" condition = "id > 0" }}
function "lookup_user" { params = "IN user_id BIGINT, OUT display_value DOUBLE PRECISION" return = "DOUBLE PRECISION" lang = SQL as = "SELECT user_id::double precision"}
data { table = table.users keys = ["id"] file = "users.yaml"}Embedded Go annotations export as finalized concrete columns and foreign keys.
Go struct and field names are provenance rather than schema semantics and are
intentionally not written to HCL. Role passwords are written as string
literals, and Ptah forces generated files containing them to owner-only 0600
permissions. Treat those files as sensitive.
PostgreSQL include columns
Section titled “PostgreSQL include columns”table "users" { column "id" { type = int }
column "covering" { type = int }
primary_key { columns = [column.id] include = [column.covering] }
index "idx_users_id" { columns = [column.id] include = [column.covering] }}Ptah preserves supported include columns through HCL parsing, SQL rendering, SQL parsing, schema diffing, and database introspection paths where the dialect supports the feature.
Procedures
Section titled “Procedures”A procedure is written as its own block, not as a function with no return:
procedure "reap_accounts" { schema = schema.app lang = "plpgsql" arg "before" { type = timestamptz } as = "BEGIN DELETE FROM accounts WHERE seen_at < before; END;"}The distinction is not presentational. A routine’s kind decides the verb that creates and drops it, and Ptah compares procedures against procedures and functions against functions. A procedure described as a function is therefore a different object from the one in the database: applying such a description drops the procedure and creates a function in its place.
Declaring return inside a procedure block is an error. Every other attribute
is the one function takes.
The block is not dialect-specific: every catalog reader that distinguishes a procedure describes one with it. Round-tripping a database’s own description back to it is a no-op, measured on PostgreSQL 17 and MySQL 9.7.2.
On the Atlas-compatible surface the block is emitted as well. The community CLI has no procedure block, ignores the one it finds, and leaving it out would withhold description without making any document more readable to it.
Function bodies
Section titled “Function bodies”Function bodies are stored as raw SQL text. Ptah does not parse the dialect
sub-language inside each function body. PostgreSQL,
MySQL, SQL Server, and other dialects have different procedural languages and
require dialect-specific parsers. Go-annotation export writes function, view,
materialized-view, and trigger bodies as opaque HCL strings and reports a
warning for each body. Because cleanup is destructive, any such warning prevents
--cleanup-go-annotations from publishing HCL or removing annotations.
Cleanup also refuses any recognized standalone Go directive that is attached at an unsupported scope or did not produce a parsed schema object. The diagnostic names the source file and line, and the operation leaves both the output and Go sources unchanged. Near-prefix comments are not directives and are preserved.
Unsupported constructs
Section titled “Unsupported constructs”Unsupported HCL constructs return errors rather than partial output. Treat these errors as compatibility gaps. Check Conformance and Atlas docs coverage before deciding whether the gap is already tracked.