Seed data
Apply one-off, environment-scoped SQL seed files with ptah seed and track them in schema_seeds.
Use ptah seed to load one-off setup rows — development fixtures, demo
accounts, initial admin users — into an environment without putting them in
migration history. You need a built ptah binary, a reachable database, and a
directory of seed files.
ptah seed is the imperative data path: it runs each matching SQL file once
and records it in the schema_seeds table. For lookup tables whose exact
contents Ptah should converge on every migration, use
declarative reference data instead.
Name the seed files
Section titled “Name the seed files”Seed files follow the NNN_description.env.sql convention inside a seeds
directory (default ./seeds):
NNNis a numeric version; files apply in version order.envselects the environment:002_demo_users.dev.sqlapplies only with--env dev, and001_countries.all.sqlapplies in every environment.- A
.sqlfile in the directory that does not match the convention fails the run before anything is applied.
Starting state for the steps below:
seeds/ 001_countries.all.sql 002_demo_users.dev.sqlapp.db-- seeds/001_countries.all.sqlINSERT INTO countries (code, name) VALUES ('US', 'United States');INSERT INTO countries (code, name) VALUES ('DE', 'Germany');-- seeds/002_demo_users.dev.sqlINSERT INTO users (email, display_name) VALUES ('dev@example.com', 'Dev User');Apply seeds to an environment
Section titled “Apply seeds to an environment”ptah seed --db-url "sqlite://app.db" --env devExpected output includes:
=== SEED ===Database: sqlite://app.dbDialect: sqliteSeeds directory: seedsEnvironment: dev
Matching seeds: 2Applied seeds: 2Skipped seeds: 0Seeds completed successfully.Re-running the same command is a no-op, because both files are recorded in
schema_seeds:
Matching seeds: 2Applied seeds: 0Skipped seeds: 2Database seed data is already up to date.Add --verbose to list which files were applied or skipped, and
--seeds-dir <path> when the directory is not ./seeds.
Verify
Section titled “Verify”Query the tracker table:
sqlite3 app.db "SELECT seed_path, env FROM schema_seeds ORDER BY seed_path;"Expected output includes:
001_countries.all.sql|dev002_demo_users.dev.sql|devThe tracker table
Section titled “The tracker table”ptah seed creates schema_seeds when at least one seed file matches the
environment and the table is absent, and leaves an existing table in place. It
holds the same columns on every engine: seed_path, env, checksum and
applied_at.
SQL Server, Oracle and Spanner do not take the statement the other engines get, so each has its own:
- On SQL Server the statement is guarded with
IF OBJECT_ID(...) IS NULL, andapplied_atisDATETIME2. In T-SQL,TIMESTAMPis a row version the server fills in, not a point in time. - On Oracle the table is created from a PL/SQL block that ignores
ORA-00955(name already used). The same block runs on Oracle 21, which has noIF NOT EXISTS, and on Oracle 23. - On Spanner the statement is the portable one with two types changed:
checksumisTEXTandapplied_atisTIMESTAMPTZ. Its PostgreSQL interface has neitherCHAR(64), which arrives asbpchar, norTIMESTAMP.
Protect production-like environments
Section titled “Protect production-like environments”--env prod and --env production are refused unless --allow-prod is set:
error: refusing to seed protected environment "prod" without --allow-prodThe command exits with code 2 (see Exit codes).
Adjust the protected set with repeatable --protected-env flags, and add
repeatable --protected-table flags to require --allow-prod whenever a seed
file targets a named existing table.
Edit an applied seed
Section titled “Edit an applied seed”schema_seeds records a SHA-256 checksum of each file’s bytes alongside its
path, and the next run reads it. A seed file that changed after it was applied
is refused rather than reported as skipped:
error: error applying seeds: seed 001_countries.all.sql changed after it wasapplied: recorded checksum 85d0..., current 5bbc...; add a new seed file withthe change, or pass --force to re-apply this oneThe command prints that on one line, and the two checksums are full SHA-256 hex digests; both are wrapped and elided here.
Adding a new seed file is the normal answer, for the same reason it is with
migrations: the rows the old file wrote are already in the database, and the new
file says what changes about them. --force re-applies the edited file and
records its new checksum.
Re-run seeds
Section titled “Re-run seeds”--forcere-runs seeds that are already recorded inschema_seeds, and is what gets past the checksum refusal above. PlainINSERTstatements then hit duplicate-key errors on tables with primary or unique keys.--idempotenttreats a duplicate-key conflict as already-applied data, using a per-file savepoint, so--force --idempotentre-runs cleanly over existing rows. The savepoint is spelled for the engine: SQL Server getsSAVE TRANSACTIONandROLLBACK TRANSACTION, Oracle getsSAVEPOINTandROLLBACK TO SAVEPOINT, and neither has a statement that releases one. The flag is refused on ClickHouse, which has neither transactions nor savepoints.
Limitations
Section titled “Limitations”- Seed files are plain SQL applied once per environment; there are no down files and no rollback command.
- Seeds are outside migration history:
ptah migrations hash,validate, and the revision table do not cover them.
Next steps
Section titled “Next steps”- Ptah should own a table’s exact contents: Reference data.
- Wiring seeds into a scripted environment setup: Native commands.
- A seed run failed: Troubleshooting.