# Public Go API

Stable embedder packages and API compatibility guardrails.

Source: https://docs.ptah.run/v0.8.0/extend/public-api/

Ptah is pre-GA, but embedders need a documented import surface. The packages on
this page are the stable embedder API, and the table below is enforced: a
ledger in the repository is the source of truth, and
`scripts/check-public-api-docs-sync.sh` keeps this page's table equal to it.

The ledger classifies every importable library package into one of two
categories, and only the first is on this page. Anything it does not classify is
a program, a directory holding only tests, or behind a Go `internal/` boundary.

## Stable packages

| Package | Purpose |
| --- | --- |
| `atlascompat` | Stable wrappers for Atlas-compatible schema, SQL, and migration-sum behavior. |
| `config` | Project-level config loading helpers. |
| `config/projectconfig` | Typed Ptah/Atlas project config IR, including validated online-DDL policy. |
| `core/ast` | Typed schema DDL AST nodes. |
| `core/astbuilder` | Fluent builders that construct `core/ast` DDL nodes without hand-written struct literals. |
| `core/coverage` | Schema description scope facts, so an absent object is not read as a removed one. |
| `core/goschema` | Go annotation parser. Produces a `schemamodel.Database`. |
| `core/schemamodel` | The desired-schema model every authoring source produces: Go annotations, HCL, YAML, SQL, DBML and live-catalog conversion. |
| `core/platform` | Dialect and platform constants. |
| `core/platform/capability` | Capability flags for dialect/version behavior. |
| `core/platform/identifier` | Catalog identifier comparison and namespace semantics. |
| `core/ptaherr` | Typed public errors and sentinel errors. |
| `core/query` | Fluent builder for parameterized, dialect-aware SELECT statements. |
| `core/renderer` | Dialect-aware SQL rendering from AST/schema IR, including fail-closed two-phase foreign key ordering. |
| `core/schemasource` | Runs an external desired-schema program and parses its output into schema IR. |
| `core/sqlutil` | SQL utility helpers used by public paths. |
| `core/yamlschema` | Reads Ptah's YAML authoring format into the schema IR, strictly. |
| `dbschema` | Live database schema introspection connection layer. |
| `catalog` | Shared database schema types. |
| `docs` | Ptah's own documentation embedded in the binary as an `embed.FS`. |
| `migration/datadiff` | Row-level diffing between declared managed data and live table rows. |
| `migration/dbtest` | Declarative migration/schema test cases, runners, and reports. |
| `migration/diffpolicy` | Declarative policy for which destructive changes a planner may emit. |
| `migration/generator` | Migration file generation. |
| `migration/importer` | Converts migration directories from other versioned-migration tools into Ptah's native layout. |
| `migration/lint` | Migration SQL linting rules, immutable analysis snapshots, and findings. |
| `migration/migrationfile` | Migration file names, directory formats, directives, and per-file transaction modes. |
| `migration/migrator` | Migration providers, revision metadata, dry-run plans, and execution. |
| `migration/planner` | Schema change planning. |
| `migration/risk` | Migration risk classification. |
| `migration/safety` | Destructive-change assessment and safety reports. |
| `migration/schemadiff` | Desired/live schema diffing. |
| `migration/schemadiff/difftypes` | Shared schema-diff types. |
| `migration/seeder` | Seed discovery and execution. |
| `migration/shadow` | Migration verification against a live disposable database. |

Import paths use the module prefix:

```go
import "ptah.run/core/renderer"
```

The `migration/dbtest` package is the embeddable engine behind the native test
commands, including regular-expression case selection through `FilterCases`.
See [Test migrations and schemas](../../testing/migrations-and-schema/) for
its case model and [Database test commands](../../reference/test-cases/) for CLI behavior.

`migration/lint.ValidateOptions` checks rule definitions, configured selectors,
severity and path overrides, compatibility mode, and migration-directory format
without reading migration files. Host applications that can skip analysis on a
no-work or explicit-override path should validate first so policy errors cannot
bypass the gate.

`projectconfig.ParseAtlasFSWithOptions` evaluates `atlas.hcl` against a
caller-provided `fs.FS`. Use it when project config and its `file()` or
`fileset()` inputs must come from one anchored or immutable filesystem view.
Use the collection-valued parse or load functions for an env `for_each` that
selects several configs; singular functions reject that cardinality.

Set `projectconfig.AtlasLoadOptions.Context` or
`projectconfig.LoadOptions.Context` to govern project data-source connections,
runtime-variable reads, and subprocesses. A nil context uses a background
context. `projectconfig.Config.MigrationDirectoryFS` returns the immutable
filesystem behind a resolved `data.template_dir` URL; a host that consumes the
configured migration directory should check it before opening the URL as an
ordinary directory. `projectconfig.Config.MigrationDirectorySource` also
returns the sandbox-relative source path for hosts that synchronize newly
written migration files.

`projectconfig.Config.IgnoredConstructs` carries every Atlas CE-compatible
no-op name with its kind and source location, and `projectconfig.Merge`
preserves the collection. Ptah's CLI reports each entry; embedders decide how
to expose the same metadata.

`renderer.ValidateSchema` and `renderer.ValidateSchemaWithCapabilities` check
a complete `schemamodel.Database` without rendering SQL. They use the same
foreign-key and capability validation as ordered schema rendering and migration
planning.

`core/astbuilder` writes `core/ast` DDL nodes as method chains: `NewTable` and
`NewIndex` build one statement, `NewSchema` builds an `*ast.StatementList` in
declaration order. The builders return AST types and nothing of their own, so a
chain and a hand-written literal mix freely. They validate nothing — an unknown
type or an unresolved foreign key reaches the AST and is reported by
`core/renderer` or by the database.

`core/yamlschema` reads Ptah's YAML authoring format: `Parse` from bytes,
`ParseFile` from a path, both returning the `*schemamodel.Database` that Go
annotations, HCL, SQL, and DBML also produce. Parsing refuses an unknown key
and refuses a second YAML document in the same stream, so a misspelled
attribute cannot pass as an intentional setting. Use `core/schemasource` when
the YAML is written by an external program rather than held in a file.

`schemamodel.Extension.Schema` is the PostgreSQL installation namespace.
`ast.ExtensionNode.Schema` and `SetSchema` preserve it through SQL rendering;
the renderer emits `CREATE EXTENSION ... WITH SCHEMA ...` in PostgreSQL's
required clause order. Empty means the target's default schema. Preserve the
field in custom schema codecs so an extension is not relocated silently.

`schemamodel.Finalize` can be called again after mutating schema input. It
rebuilds materialized embedded fields and marks them with
`Field.GeneratedFromEmbedded`; source declarations should leave that derived
metadata false.

`Database.EmbeddedSources` preserves source-only field and embedding
declarations needed to rebuild nested embedded fields after `Finalize` or
`Merge`. Embedders normally should not modify this bookkeeping directly. Keep
it when copying a finalized `Database` that will be finalized or merged again;
discarding it can also discard the source declarations behind materialized
`GeneratedFromEmbedded` fields.

MySQL-family readers populate the JSON-hidden
`catalog.Function.Definer` and `CurrentAccount` execution facts.
Database-aware `schemadiff.CompareWithDatabase` entry points use them to refuse
a modified `SQL SECURITY DEFINER` routine when recreating it would change the
executing account. Custom readers that supply a modified definer routine must
preserve both fields; missing facts fail closed with
`ptaherr.ErrInvalidSchemaDiff`. Offline comparison has no live ownership facts
and is not the safety boundary for applying such a replacement.

The separate [`testkit`](https://github.com/stokaro/ptah-testkit) module
(`ptah.run/testkit`) is an opt-in helper for tests that need real
databases. It keeps `testcontainers-go` out of Ptah's main module graph, lives
in its own repository, and versions independently. It depends on Ptah one way
and consumes a published release, so nothing here builds against it.

## Migration statement observation

`migration/migrator.WithStatementObserver` attaches a read-only callback to a
filesystem migration provider. The observer runs after every successfully
executed statement and receives its source path, one-based statement ordinal,
total statement count, SQL text, and an event-local copy of file directives.

Use `migrator.StatementObserverFunc` for a closure or implement
`migrator.StatementObserver` for a stateful collector. The callback receives
no database connection and cannot alter the migrator execution path. A
database-aware collector may capture a consumer-owned connection when that
consumer controls transaction visibility. Returning an error stops the
migration and returns a `migrator.StatementObservationError` with source and
statement context; dirty progress includes the statement that completed before
the callback failed.

For SQL-backed `no_transaction` migrations, Ptah writes a durable progress
checkpoint before invoking the observer. Before each statement, it first marks
that statement's outcome as unknown; after success, it advances the completed
count and clears the marker. Process exit, context cancellation, or deadline
while execution is in flight preserves the unknown-outcome marker. This
includes Atlas-format down execution. A custom `MigrationFunc` is opaque to the
migrator and does not receive statement-level checkpointing.

Dirty SQL-backed resumes verify the already committed source prefix before
skipping it. Native rows use the `partial:h1:` value in `Checksum`; Atlas rows
use cumulative `partial_hashes`. A failure after changing transaction mode
cannot reduce the recorded applied count below that verified prefix.

Negative `applied` or `total` values and `applied > total` are rejected whenever
a revision is read, including through `GetRevisions`,
`GetAppliedMigrations`, `GetCurrentVersion`, and `GetMigrationStatus`. Native
rows accept only `applied`, `pending`, `failed`, `pending:down`, and
`failed:down`. An applied row cannot claim that state until `applied == total`;
other spellings, explicit `:up` suffixes, and direction-suffixed applied states
are invalid because a completed rollback deletes its revision row.

`RepairMigration` holds the session advisory lock across revision inspection,
resumed SQL, safety checks, and the final metadata write.

SQL-backed `MigrationTxModeNone` attempts pin their migration SQL to one
physical database session. Server-database revision metadata remains on the
original connection; SQLite uses the pinned session with a `main`-qualified
table to support its single-connection in-memory mode. Resume replays recognized
session controls from the verified committed prefix on a fresh session and
refuses prefixes whose session-local state cannot be reconstructed safely.
Top-level transaction-control statements are rejected before session pinning or
revision mutation because their commit boundary would conflict with Ptah's
durable per-statement checkpoints.

On PostgreSQL, an up migration may clean invalid index residue with a matching
`DROP INDEX` that executes before the create in the current attempt. The
migrator resolves unqualified drops and target tables through `search_path`,
rejects any other relation that owns the schema-level index name, and rechecks
transaction-local catalog state before writing a clean revision. It records the
resolved schema and target at each conditional create, rather than resolving a
deduplicated raw name under the final `search_path`. Repair without an explicit
replayable path checks every same-named target in PostgreSQL user schemas. A drop skipped
by resume does not satisfy the preflight. `RepairMigration` performs the same
positive index-state check, including when `Force` is set.

The observer composes with `StatementInterceptor`: a statement handled by an
external executor is observed once after that executor reports success.

Programmatic migrations set `Migration.UpTxMode` and `Migration.DownTxMode`
with `MigrationFileTxModeUnspecified`, `MigrationFileTxModeFile`, or
`MigrationFileTxModeNone`. Use `ParseMigrationUp` when a tool needs the
executable up-direction SQL, explicit mode, and source-line offset from plain
SQL or Atlas txtar content. Up and down values remain independent.

Atlas transaction-mode directive validation errors expose
`migrator.AtlasTxModeDirectiveError` through `errors.As`; the leaf error keeps
the source file and transaction-mode details in its message.

This pre-GA API replaces the former Boolean transaction fields. Use
`NewFSMigrationProvider` (with `WithStatementInterceptor` when an external
executor takes over statements) to load up/down pairs: loaded migrations carry
transaction modes, timeouts, source paths, and functions attached, so
execution policy cannot be discarded while assembling a provider.

`MigrateUpOptions.PlanObserver` receives the plan recalculated under the
migration lock before transaction-mode validation, including an empty plan. It
captures metadata but cannot abort execution. Use the abort-capable `Preflight`
hook for work that must run after static validation and before any schema or
revision change.

`MigrateUpOptions.DiscardRolledBackFailure` applies only to the Atlas
revision-table format; it has no effect with native Ptah metadata. It removes
only the failed revision written by the current invocation, and only after
transaction rollback succeeds. Existing dirty revisions and commit, rollback,
partial-progress, and unknown-outcome failures remain recorded and block
automatic retry.

## Pinned database sessions

`dbschema.DatabaseConnection.WithSession` pins one physical database session
for the duration of a callback and rebinds the dialect reader, writer, and SQL
runner to that session. Use it for cleanup, replay, and inspection workflows
that depend on session-local state or SQLite attached-database visibility.

Root MySQL capability metadata remains conservative. On MySQL 8.4+ the scoped
connection refines its referenced-key policy from
`restrict_fk_on_non_standard_key` on the pinned physical session before the
callback, so planning and execution use the same effective policy.

The scoped connection must not escape the callback. Ptah discards the physical
connection afterward so session-local state cannot leak to a later pool user.
Use `dbschema.DatabaseConnection.WithSessionOrCurrent` when the same operation
can be called either from a pool-backed connection or from an existing pinned
session; it pins only when needed and otherwise reuses the caller's current
session lifecycle.

`dbschema.DatabaseConnection.WithIsolatedQuerySession` exposes a query-only
`dbschema.IsolatedQueryer` on one physical session. Transaction-capable drivers
always roll the transaction back; ClickHouse runs directly on the disposable
session because its driver does not implement transactions. Ptah discards the
physical session afterward, except for in-memory SQLite, whose only connection
owns the database lifetime and is returned to the pool after rollback. The
callback cannot control transactions or reach Ptah schema writers. Callers
remain responsible for restricting SQL to read-only queries.

`migration/migrator.CheckFailedError` identifies one failed or invalid
pre-migration assertion. `CheckGroupFailedError` identifies an Atlas `oneof`
check file in which no assertion returned a truthy result, including an empty
group. Callers can distinguish a group-level precondition failure from an
assertion execution or result-shape failure with `errors.As`.

`migration/migrator.ParseChecks` requires the target dialect together with the
SQL source. This intentional pre-v1 signature change prevents fail-open parsing
when PostgreSQL escape strings or MySQL/MariaDB comment rules determine whether
a later check directive is SQL code or literal/comment content.

## Migration statement validation

`migration/migrator.WithStatementValidator` attaches a pre-execution SQL
safety gate to a filesystem provider. Ptah splits and validates every
statement in one migration before executing its first statement. Rejecting a
later statement cannot leave an earlier statement applied.

Implement `migrator.StatementValidator` when an embedder must confine replay to
a disposable database or reject unsupported statement forms. Validators
inspect SQL but do not replace execution. Combine a validator with
`StatementInterceptor` only when an external tool must execute accepted
statements.

## Schema diff and planning contracts

`migration/schemadiff/difftypes.SchemaDiff` stores index additions and removals as
canonical `[]IndexRef` fields. Every index reference includes its owning
table. Live comparisons snapshot catalog identifier semantics into the diff so
comparison, policy, forward planning, and reverse planning share one source of
truth.

`SchemaDiff.ExtensionsModified` contains `ExtensionDiff` values with the name
and `FromSchema`/`ToSchema` placement. Default-schema normalization follows the
diff's identifier semantics. PostgreSQL extension moves currently return
`ptaherr.ErrInvalidSchemaDiff` before any AST is emitted; creates and drops are
still planned.

Row-level security policies use the same shape. `RLSPoliciesAdded` and
`RLSPoliciesRemoved` are `[]RLSPolicyRef`, `RLSPoliciesModified` is
`[]RLSPolicyDiff`, and every entry names the owning table next to the policy
name. That pair is the policy's identity: a PostgreSQL policy name is scoped to
its table, so two tables in one schema may each carry `tenant_isolation`. The
table half is compared under the diff's identifier semantics, not as a raw
string, so the desired spelling `public.orders` and the introspected spelling
`orders` resolve to one table in both the forward and the reverse direction.
A reference the target schema cannot resolve is rejected with
`ptaherr.ErrInvalidSchemaDiff`; it is never dropped from the plan.

Use `migration/generator.GenerateCheckpointFromShadow` for a SQL Server
schema whose live catalog semantics must survive checkpoint planning: the
shadow replay reads identifier semantics from the live catalog, where the
dialect-only offline rules stay conservative.

`migration/generator.PlanMigration` returns an unpublished plan bound to the
migration-directory snapshot used during planning. `MigrationPlan.WriteFiles`
rejects changed history with `generator.ErrMigrationDirectoryChanged` under
the shared cross-process publication lock.

A plan holds the migration directory open until it is published, so an
embedder that may not publish should `defer plan.Close()`. `Close` is a no-op
on a published plan and a no-op called twice; skipping it leaves the directory
held until the plan is garbage collected, which on Windows blocks removing or
renaming it in the meantime.

When its URL or connection selects SQLite, `PlanMigration` validates
`PTAH_SQLITE_ALLOW_VIRTUAL_TABLE_DROP` before resolving `OutputDir`. A malformed
value therefore fails before filesystem work; non-SQLite plans do not consult
the variable.

`generator.GenerateCheckpointFromShadow` and `shadow.VerifyBaseline` apply the
same SQLite-only validation before connecting to or mutating a shadow database.
The checkpoint path therefore cannot drop and replay a shadow database before
reporting a malformed value.

Embedders that need cancellation while waiting for that lock use
`WriteFilesContext`; concurrent use of one plan fails with
`generator.ErrMigrationPlanInUse`. `migration/planner.Planner` exposes only
checked planning; malformed references, unresolved additions, and target
index-namespace conflicts fail before SQL is returned. The returned
`generator.MigrationFiles.Files` slice is the authoritative list of generated
pairs and published paths, in apply order.

## Safety reports and shadow errors

`migration/safety.RenderJSON` writes a `safety.Report` with the highest risk,
the destructive verdict, and every rendered statement assessment. Use this
API when an embedder needs the same machine-readable contract as
`ptah migrations plan --report json`. Setting
`generator.GenerateMigrationOptions.ReportFormat` to `json` instead publishes
one `.safety.json` artifact beside each generated migration pair.

`migration/shadow` owns verification against a live disposable database:
`VerifyMigration` measures a candidate migration, `VerifyBaseline` measures a
replayed history against the target, `VerifyRollback` rehearses a rollback plan,
and `PlanDynamicRollback` derives rollback statements from the schema a version
defines rather than from a down body. `migration/generator` calls the first of
these when `ShadowDatabaseURL` is set.

When candidate or baseline shadow verification fails,
`generator.PlanMigration`, `generator.GenerateMigration`, and
`shadow.VerifyBaseline` preserve a typed
`*shadow.VerificationError`. Inspect it with `errors.As` instead of
parsing `Error()`:

```go
var shadowErr *shadow.VerificationError
if errors.As(err, &shadowErr) {
	stage := shadowErr.Result.Stage
	mismatches := shadowErr.Result.Mismatches
	// Report stage and mismatches to the caller.
}
```

`Result.Stage` identifies the failed boundary, such as `connect`, `replay`, or
`schema-match`. Candidate and baseline verification use the same names at
shared boundaries. Baseline verification can additionally report
`target-introspect`, `reset-schemas`, and `drop-metadata`; candidate-only
`round-trip-down` and `round-trip-up` stages do not occur during baseline
verification.

Baseline display text keeps the
`baseline shadow check failed:` prefix expected by CLI users. A schema-match
result contains every mismatch in deterministic category and object order, not
only the first.
Each `shadow.Mismatch` has a stable `Kind`, a human-readable `Message`, and the
available object, table, column, constraint, or changed-property fields.
Operational failures also preserve their underlying error through `Unwrap`;
a structural schema mismatch has no wrapped error. A successful verification
returns `nil` and continues planning; `shadow.VerificationResult` is only the
structured failure payload carried by `shadow.VerificationError`.

## Error contracts

Public failures should use `core/ptaherr` when callers can reasonably branch on
the error:

- annotation and parser failures should support `errors.As` with
  `*ptaherr.ParseError`;
- unsupported dialect failures should support `errors.Is` with
  `ptaherr.ErrUnsupportedDialect`;
- invalid schema diffs rejected during planning should support `errors.Is` with
  `ptaherr.ErrInvalidSchemaDiff`;
- shadow candidate and baseline verification should support `errors.As` with
  `*shadow.VerificationError`;
- command wrappers should preserve typed errors instead of replacing them with
  string-only errors.

## API guardrails

CI protects the public API without duplicating its declarations in a committed
snapshot:

| Check | Purpose |
| --- | --- |
| `scripts/check-public-api.sh` | Fails if an importable library package is classified by neither ledger category. |
| `scripts/check-public-api-released.sh` | Compares stable packages against the latest `v0.x` release tag with `apidiff`. |
| `scripts/check-exported-docs.sh` | Requires documentation on exported functions and types. |
| `scripts/check-public-api-docs-sync.sh` | Keeps this page's package table aligned with the canonical ledger. |

Additive API changes receive normal code review. An intentional incompatible
change must update the relevant documentation and include an explicit approval
entry for the current release baseline in the same PR.

## Documentation-only packages

The ledger's second category is a short list of sample packages that stay
importable because published documentation reaches them. The migrator's godoc
examples import the sample migration directory they run against, and an example
nobody outside this module can import is not documentation.

They carry no compatibility guarantee of any kind. Their contents change with
the documentation they serve, `apidiff` does not compare them against a release
baseline, and they are absent from the table above for that reason. Read them;
do not build against them.

## Embedding guidance

Use [Reusable components](../components/) for task-oriented examples.
Use this page to decide whether a package is supported for embedding. Do not
import `internal/...` packages from another module, and do not import a
documentation-only package from production code.
