Skip to content
PtahPtah

ORM and external loaders

When the desired schema lives in an ORM or framework rather than in Go annotations or a static file, Ptah runs an external program and reads its standard output as the desired schema. Any tool that prints a schema as SQL, HCL, or YAML becomes a Ptah source — including the Atlas ecosystem’s ORM provider loaders.

This is Ptah’s open, local, MIT equivalent of Atlas’s data "external_schema" source and its ORM provider loaders. Provider compatibility depends on the emitted schema syntax; the recipes below are verified end to end with Ptah.

An ORM loader is any program that prints the complete desired schema as SQL, HCL, or YAML to stdout. Ptah runs it directly — never through a shell — parses the selected format, and uses the result wherever a desired schema is needed:

Terminal window
ptah schema render --schema-cmd "<loader>" --dialect postgres
ptah schema compare --schema-cmd "<loader>" --db-url "$DATABASE_URL"
ptah schema drift --schema-cmd "<loader>" --db-url "$DATABASE_URL"
ptah migrations plan --schema-cmd "<loader>" --db-url "$DATABASE_URL"

Primary keys, foreign keys, unique constraints, and indexes in the emitted DDL are all captured, so the loaded schema diffs and migrates faithfully.

--schema-cmd takes the program as a single string:

Terminal window
ptah schema render --schema-cmd "./scripts/export-schema" --dialect postgres
  • Format: stdout is parsed as SQL by default. Set --schema-format hcl or --schema-format yaml when the loader emits another supported source format; yml is accepted as a YAML alias. HCL and YAML payloads can carry api_name, openapi_name, graphql_name, proto_name, api_type, and api_expose. SQL payloads cannot author those export-only attributes, so public names, types, and exposure are derived from the persistence schema.
  • No shell: the program runs with an explicit argument vector split on whitespace, so pipes, globbing, and variable expansion are not available and arguments cannot contain spaces. Wrap a more complex invocation in a small script and point --schema-cmd at that.
  • Lifecycle: execution is bounded by a timeout, and Ptah owns the loader’s process tree — a process group on Unix, a kill-on-close Job Object on Windows — so descendants are cleaned up on cancellation, timeout, and after a successful parent exit.
  • Errors: if the program exits non-zero, its stderr is surfaced in the error as a bounded, secret-redacted, terminal-safe tail. Stdout is bounded, and empty or whitespace-only output is rejected so a broken provider cannot be interpreted as an intentionally empty desired schema.

Instead of the flag, declare the loader once in an external_schema block. Unlike --schema-cmd — a single string split on whitespace — the config form takes an explicit argument list, so arguments may contain spaces:

external_schema:
program: ["go", "run", "ariga.io/atlas-provider-gorm", "load", "--path", "./models"]
format: sql # optional, defaults to sql
working_dir: ./app # optional; defaults to the current directory
env: ["APP_ENV=dev"] # optional extra KEY=VALUE entries

ptah schema render, ptah schema compare, ptah schema drift, ptah migrations plan, and ptah migrations generate read this block when --schema-cmd is not passed (the flag always wins). Auto-discovered config is not permission to execute repository-controlled code, so a config-sourced loader also requires --allow-external-schema. Those commands read the other settings they consume, such as url and schemas, and honor --env to select an env block:

Terminal window
ptah schema drift \
--config ptah.yaml \
--allow-external-schema

Set --schema-cmd= explicitly to disable the configured source for one invocation.

Constraints on the block:

  • external_schema.env cannot override PATH or PWD. Put the executable path in program explicitly and use working_dir to select the command directory.
  • Relative working_dir values must remain inside the process working directory after symlink resolution. Use an explicit absolute path for a deliberately external loader.
  • The block mirrors the desired-schema role of Atlas’s data "external_schema". The same loader can also be declared directly in atlas.hcl as a data "external_schema" source and selected as an env’s desired schema; see Atlas project config. All three spellings — --schema-cmd, ptah.yaml external_schema, and the atlas.hcl data source — share one execution path and one opt-in gate.

The committed examples/orm-loaders/gorm recipe contains a small GORM model module. It invokes the official standalone atlas-provider-gorm command through load-schema.sh at the pinned version v0.6.1. The script uses an ephemeral copy of the model module, so the provider and its transitive dependencies do not become part of the application module’s dependency graph.

Download the model dependency and render the schema through the checked-in ptah.yaml:

Terminal window
cd examples/orm-loaders/gorm
go mod download
ptah schema render \
--config ptah.yaml \
--allow-external-schema \
--dialect postgres

The generated schema includes the users and pets tables with their primary keys, the unique index on email, and the pets → users foreign key.

The committed examples/orm-loaders/sqlalchemy recipe pins both the SQLAlchemy provider and SQLAlchemy itself. Its models declare users and pets, a unique user email, and a cascading pets.user_id → users.id foreign key.

Create the isolated Python environment and render it through the checked-in ptah.yaml:

Terminal window
cd examples/orm-loaders/sqlalchemy
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements.lock.txt
ptah schema render \
--config ptah.yaml \
--allow-external-schema \
--dialect postgres

The lock file records the complete environment verified with Python 3.12, atlas-provider-sqlalchemy==0.4.1, and SQLAlchemy==2.0.50. The provider emitted PostgreSQL DDL, and Ptah parsed and rendered both tables, both primary keys, the unique email constraint, and the foreign key with ON DELETE CASCADE.

The Atlas ecosystem publishes schema loaders for many ORMs (GORM, Django, SQLAlchemy, Sequelize, TypeORM, Hibernate, and more). Each one prints SQL DDL to stdout, so compatible output can plug into --schema-cmd / external_schema like the verified recipes above. Any tool that can emit a complete SQL, HCL, or YAML schema can work too, including a hand-written script that runs a framework’s schema dumper.

The Ptah side is always identical — you supply the loader command:

external_schema:
program: ["<the loader command and its arguments>"]

Consult the provider’s documentation for the exact loader command (the Atlas provider packages are named atlas-provider-<orm>), then run it once by hand to confirm it prints a complete supported schema before wiring it into Ptah. GORM and SQLAlchemy are verified against Ptah here; treat other providers as starting points and check their emitted schemas parse cleanly.

An external command composes with Go roots and schema files, so you can merge an ORM export with a vendored HCL file:

Terminal window
ptah schema render \
--schema-cmd "./scripts/export-schema" \
--schema-file ./vendor/thirdparty.hcl \
--dialect postgres

The merge and conflict rules are on Composite desired schema.