# Go annotations

Use annotated Go structs as the desired database schema.

Source: https://docs.ptah.run/v0.8.1/schema/go-annotations/

When annotated structs own the schema, use this Go-specific frontend. For a
source-neutral starting point, see [work with a schema
source](../work-with-a-source/).

Use Go annotations when your Go application owns the schema and the database
should follow annotated model types. Ptah reads comments, not runtime Go tags,
so the model remains ordinary Go code.

## When to use them

| Use Go annotations when | Use another source when |
| --- | --- |
| The application structs already describe the domain. | A database team owns SQL or HCL directly. |
| You want code review to cover schema changes next to model changes. | You need an HCL schema construct Ptah has not implemented yet. |
| You want generated migrations from desired/live differences. | You only need to apply an existing migration directory. |

## Model the schema

The smallest annotation source that is still useful in a real project is a
table, a primary key, and a unique constraint:

```text
models/
  account.go
migrations/
```

Create `models/account.go`:

```go
package models

//ptah:schema:table name="accounts"
type Account struct {
	//ptah:schema:field name="id" type="SERIAL" primary="true"
	ID int

	//ptah:schema:field name="email" type="TEXT" unique="true" not_null="true"
	Email string
}
```

Ptah recursively reads regular `*.go` files under each `--root-dir`. It skips
`*_test.go`, hidden directories, and directories named exactly `vendor`.
Names that merely contain that word, such as `myvendor`, remain part of the
source tree.

Render the desired SQL before connecting to a database:

```bash
ptah schema render --root-dir ./models --dialect postgres
```

Expected output includes:

```sql
CREATE TABLE "accounts" (
  "id" SERIAL PRIMARY KEY NOT NULL,
  "email" TEXT UNIQUE NOT NULL
);
```

The exact type rendering depends on the selected dialect and field tags. To
smoke-check without any daemon, render the SQLite dialect to a file:

```bash
ptah schema render --root-dir ./models --dialect sqlite >/tmp/ptah-schema.sql
sed -n '1,80p' /tmp/ptah-schema.sql
```

Standard output contains SQL only. Source-loading progress, schema counts, and
the dependency summary go to standard error, so the redirected file can be
executed unchanged. For PostgreSQL-family, MySQL-family, SQL Server, and
Spanner targets, Ptah creates all tables before adding foreign keys. SQLite
keeps foreign keys inline because it cannot add them after table creation.

Malformed foreign keys and constraints unsupported by the selected dialect
fail before Ptah emits any SQL. The output never silently omits a declared
foreign key. Ptah also checks referenced-key policy, compatible column types,
constraint-name scope, and dialect-specific index or storage restrictions.

Always pass `--dialect` when redirecting executable SQL. Without it, Ptah
attempts the built-in review targets and emits separate labeled sections only
if every target can render the schema. Any unsupported feature fails atomically
with empty standard output.

### Add an INCLUDE covering index

Use `include` to keep payload columns in a covering index without making them
search keys. Ptah preserves the comma-separated order after trimming whitespace:

```go
type AccountIndexes struct {
	//ptah:schema:index name="idx_accounts_email" fields="email" include="display_name,created_at" table="accounts"
	_ int
}
```

For PostgreSQL, YugabyteDB, CockroachDB, and the Spanner PostgreSQL dialect, the
annotation renders `INCLUDE ("display_name", "created_at")`. PostgreSQL accepts
the default, `BTREE`, and `GIST` access methods, plus `SPGIST` on PostgreSQL 14
and newer. YugabyteDB accepts the default and `LSM`; `BTREE` is its documented
alias for the default LSM and renders identically to the default. CockroachDB
accepts the default and `BTREE`, which is also its default, and refuses `GIN`
and `GIST` because both name an inverted index there and an inverted index
stores no payload. The Spanner PostgreSQL dialect accepts only the default.
Every other dialect rejects `include` before emitting SQL. Omit `include` when
there are no payload columns; a present list with an empty element is a parse
error.

CockroachDB spells the payload `STORING` in its own output, so an index written
with `include` on a table named `accounts` is reported by `SHOW CREATE TABLE` as
`INDEX idx_accounts_email (email ASC) STORING (display_name)`. It is the same
index, and `ptah db read` describes it with `INCLUDE` again.

### Add an INCLUDE covering constraint

A UNIQUE or PRIMARY KEY constraint takes the same `include` payload, on a
constraint annotation rather than an index one:

```go
type Account struct {
	//ptah:schema:field name="email" type="VARCHAR(255)" not_null="true"
	Email string

	//ptah:schema:field name="display_name" type="VARCHAR(255)"
	//ptah:schema:constraint name="uq_accounts_email" type="UNIQUE" table="accounts" columns="email" include="display_name"
	DisplayName string
}
```

PostgreSQL, YugabyteDB, and CockroachDB render
`CONSTRAINT "uq_accounts_email" UNIQUE ("email") INCLUDE ("display_name")`.
CockroachDB stores it as a unique index with a `STORING` clause, which is its
spelling of the same payload.

A covering `PRIMARY KEY` is narrower: PostgreSQL and YugabyteDB take it, and
CockroachDB does not.

Every other target refuses the render, naming the constraint and the targets
that accept it:

```text
error: error rendering mysql schema: mysql does not support INCLUDE columns on UNIQUE constraint "uq_accounts_email"; target postgres, yugabytedb, or cockroachdb
```

The targets for a constraint are not the targets for an index. The Spanner
PostgreSQL dialect takes `include` on an index and refuses it on a constraint,
and CockroachDB takes it on an index and on a UNIQUE constraint but not on a
primary key. Pick the object first, then read its list.

### Install a PostgreSQL extension in a schema

An extension annotation belongs on a type declaration. Set `schema` when the
extension must live outside PostgreSQL's default namespace:

```go
//ptah:schema:extension name="pgcrypto" schema="extensions" if_not_exists="true"
type PostgreSQLExtensions struct{}
```

Ptah creates `extensions` first and renders `CREATE EXTENSION ... WITH SCHEMA
extensions`. The same installation schema survives parsing from HCL or YAML,
Go-to-HCL export, live inspection, comparison, and a later apply.

## Render and generate migrations from Go structs

Everything from here is the same for every source, and lives once on
[Work with a desired schema](../work-with-a-source/): rendering the SQL a
source produces, comparing it with a live database, gating a pipeline on
drift, composing several sources, and validating across dialects. The Go form
of the flag is `--root-dir`, which is repeatable and mixes freely with
`--schema-file`:

```bash
ptah schema compare --root-dir ./models --db-url "$DATABASE_URL"
ptah migrations generate --root-dir ./models --db-url "$DATABASE_URL" --migrations-dir ./migrations
ptah schema apply --root-dir ./models --db-url "$DATABASE_URL"
```

One thing is worth rendering more than once when the schema is Go annotations,
because the annotations are meant to be portable and a mapping surprise is
easier to see than to reason about:

```bash
ptah schema render --root-dir ./models --dialect postgres >/tmp/schema.pg.sql
ptah schema render --root-dir ./models --dialect mysql >/tmp/schema.mysql.sql
```

Dialect differences are expected — enum storage, serial columns, generated
columns. What the two renders check is that each target produces valid SQL for
the capabilities it has.

## Move the schema to HCL

Start with a non-destructive export:

```bash
ptah schema export \
  --from go \
  --to hcl \
  --root-dir ./models \
  --out schema.hcl
```

Ptah parses the generated HCL and verifies that its canonical re-render is
stable before it writes `schema.hcl`. Every valid Go annotation semantic has an
HCL representation. Function, view, materialized-view, and trigger bodies are
emitted as opaque HCL strings, and Ptah reports a warning for each because it
does not structurally parse those dialect-specific SQL sub-languages. Review
every warning before treating the export as semantically complete. A separate
diagnostic reports any source string whose bytes change during Unicode NFC
normalization.

One export captures the complete selected Go source set and uses that immutable
view for both HCL parsing and cleanup planning. Ptah rechecks source membership,
file identity, permissions, and contents before publishing the HCL; a concurrent
source change aborts the export. The output directory is bound before staging,
and Ptah also rechecks an existing output's identity, permissions, and contents.
An output creation, edit, or replacement detected at this commit barrier is
left untouched and aborts publication. Successful HCL replacement is flushed
to durable storage before Go annotation cleanup starts.

Preview annotation removal only after the export has no diagnostics:

```bash
ptah schema export \
  --from go \
  --to hcl \
  --root-dir ./models \
  --out schema.hcl \
  --cleanup-go-annotations \
  --cleanup-diff
```

The diff mode writes the validated HCL file but does not modify Go source. Run
the same command without `--cleanup-diff` to apply the prevalidated cleanup
plan.

Before publishing HCL, cleanup accounts for every recognized standalone Ptah
directive in the captured Go AST. Each directive must use a placement listed in
the [Go annotation reference](../../reference/go-annotations/) and must produce
the corresponding parsed schema object. A misplaced role/function directive or
a file-scoped RLS directive that resolves to no RLS object stops the operation
with its source file and line; neither the HCL output nor Go sources change.
Comments that only share a prefix with a directive, such as
`//ptah:schema:tableau`, are ordinary comments and remain byte-for-byte intact.

:::caution[Cleanup is a one-time migration]
Cleanup fails before HCL publication when the export reports any diagnostic,
including the expected warning for each opaque SQL body. It also fails when the
output uses a `.go` path, aliases a protected source, a removable directive was
not represented in the parsed schema, or no removable annotations remain.

Export without cleanup, review the emitted bodies in
`schema.hcl`, then remove all Ptah schema annotations manually in one reviewed
change and switch the project to the HCL source. Do not rerun export after
manual removal starts. An export with no annotations fails and preserves the
existing HCL file. Annotations that produce no exportable HCL object fail with
the same preservation guarantee.

Ptah revalidates the Go source set before HCL publication and again before
cleanup. If a source changes between those steps, the HCL file remains published
but cleanup leaves every Go file unchanged and returns an error.

If a later source fails during a multi-file cleanup, Ptah rolls back earlier
replacements only while they still match the exact cleaned file it committed.
If that check detects a concurrent edit, Ptah leaves the edit in place,
preserves the original source in a `.ptah-backup-*` file, and reports its
location. These checks are optimistic; exclude uncooperative writers that can
mutate the same paths after the final commit barrier. Do not repeat cleanup
after a successful migration; use `schema.hcl` as the new source.
:::

## Next steps

- Looking up a directive or attribute? [Go annotation reference](../../reference/go-annotations/).
- Modeling in files instead of Go? [YAML schema](../yaml/), [HCL schema](../hcl/), or [SQL schema](../sql/).
- Embedding the parser in your own tool? [Public API](../../extend/public-api/).
