Skip to content

Graphic preview

100%
Page type: reference

HCL schema reference

Atlas-compatible HCL schema subset and Ptah parity extensions.

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.

Terminal window
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.

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.

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, and MySQL and MariaDB support type = HASH.
index columns, on { column = ... }, on { expr = ... }, desc, on { nulls_first = ... } or on { nulls_last = ... }, unique, type, where, comment, PostgreSQL include/storage options, and Ptah platform blocks for provider-owned source properties, such as a ClickHouse data-skipping index’s type and granularity.
constraint Ptah block used when annotation metadata cannot fit the Atlas-native constraint blocks, and for EXCLUDE constraints. using is refused on a UNIQUE, CHECK or FOREIGN KEY constraint.
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, sequence, function and procedure permissions. A routine is for = function.<name> or procedure.<name>, with its argument types in the Ptah attribute args, because PostgreSQL overloads a name by them. The Ptah columns attribute limits a privilege to columns of a table.
revoke Ptah block: privileges a role must not hold, with from, for, args, privileges, columns and comment. The Atlas community CLI has no such block and ignores it.
default_privilege Ptah block: a PostgreSQL default privilege, with for_role, schema, object_type, to, privileges, grantable, revoked (privileges the grantee must not hold by default) and comment. Without schema it is the global default, which applies in every schema.
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. A materialized block accepts a ClickHouse refresh clause.
trigger Trigger timing and every event its timing block sets (insert, update, delete, truncate), target, execution mode, function body, and comments.
policy PostgreSQL RLS policy fields.
row_policy ClickHouse on, using, to, and as. See row policies.
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. The table may sit in another file of the schema directory; without a schema, the label names the one table of that name.
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.
coordination_node Ptah block: a YDB coordination node, with schema and the settings of the //ptah:schema:coordinationnode annotation as strings.
data Ptah managed-data declaration with a table reference, key columns, and a file path relative to the HCL file.

For YDB, an index block also accepts the index partitioning attributes. Use quoted ENABLED or DISABLED for the splitting switches, numbers for partition counts and size, and a quoted value such as ANY_AZ:2 for read_replicas_settings. These are Ptah extensions; Atlas has no YDB driver. An omitted setting leaves the existing index’s value unchanged.

Omitted YDB coordination nodes remain unmanaged unless the document explicitly claims a complete namespace. Ptah exports that claim in a versioned // ptah:feature-coverage header. Keep the header when editing the export; incompatible model definitions and duplicate accounts are refused. See coordination nodes for declaration and limit semantics.

The header belongs to HCL sources. Renaming the document to .sql does not convert it; SQL source loading refuses this metadata instead of discarding it.

A default_privilege block takes no labels. for_role names the role that creates objects, and to names the role that receives their default privileges. Each accepts a role reference or a string. A dot inside a quoted role name stays part of that name.

privileges, grantable, and revoked accept lists of strings or bare privilege names. A scalar names one privilege. Variables, locals, and functions can supply strings or lists of strings. Null values and non-string list elements are refused. Every grantable entry must appear in privileges.

Blocks with the same grantor, schema, object type, and grantee combine their privileges. A privilege with a grant option keeps that option when another block grants it without one. Conflicting comments and declarations that both grant and revoke a privilege are refused. An omitted schema means the global default, even when the export names a default schema.

An export preserves an unread default ACL with a ptah:notdescribed header. An unnamed limit covers the whole default privilege namespace. A named limit uses a JSON tuple of grantor, schema, object type, and grantee. A tuple without the grantee covers one ACL class; an empty grantor in that tuple means CockroachDB’s FOR ALL ROLES. An empty schema means global defaults.

// ptah:notdescribed default_privilege "[\"owner\",\"app\",\"TABLES\",\"reader\"]"

Reading this source leaves that ACL unmanaged. The limit does not declare an empty privilege list or authorize a revoke.

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 a ClickHouse refresh string:

materialized "event_stats" {
as = "SELECT count() AS c FROM events"
refresh = "EVERY 1 DAY OFFSET 2 HOUR RANDOMIZE FOR 30 MINUTE APPEND"
}

The clause supports EVERY, AFTER, OFFSET, RANDOMIZE FOR, DEPENDS ON, and APPEND. Omitting refresh or writing refresh = "" declares no schedule. Removing a schedule replaces the view and discards its stored rows. HCL export preserves supported schedules and refuses schedules the catalog cannot capture. The schedule applies only to ClickHouse; see its refresh rules.

The shared refresh_strategy attribute is refused on presence, before its expression is read. For PostgreSQL materialized views, run refreshes from your own scheduler.

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.

Spanner refuses the schema names its server refuses to create: spanner_sys, spanner, default, every name that starts with spanner_ in lower case, and any spelling in another case of public, information_schema, pg_catalog, spanner_sys, spanner or default, such as schema "Public" {}. It accepts schema "public" {}, and schema "SPANNER_APP" {} and schema "PG_TOAST" {}, which the server creates.

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.

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:

Terminal window
ptah-compat schema diff --dev-url "sqlite://file?mode=memory" \
--from file://empty.hcl --to file://schema.hcl --var status=live

One --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.

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.

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.

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 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.

Table features in HCL export stay attached by database schema and table name. Changing the Go struct name does not change their placement.

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 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.