{"id":51481729,"url":"https://github.com/kamysh/kryzhen","last_synced_at":"2026-07-07T02:30:31.046Z","repository":{"id":364264243,"uuid":"1267048856","full_name":"kamysh/kryzhen","owner":"kamysh","description":"Forward-only, dependency-resolved SQL migrations for PostgreSQL — a Rust port of the Haskell mallard tool. Library crate + CLI.","archived":false,"fork":false,"pushed_at":"2026-06-12T09:19:47.000Z","size":70,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T11:11:09.803Z","etag":null,"topics":["cli","database-migrations","mallard","migrations","postgresql","rust","sql"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kamysh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":"CLA.md"}},"created_at":"2026-06-12T07:10:01.000Z","updated_at":"2026-06-12T09:19:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kamysh/kryzhen","commit_stats":null,"previous_names":["kamysh/kryzhen"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/kamysh/kryzhen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamysh%2Fkryzhen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamysh%2Fkryzhen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamysh%2Fkryzhen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamysh%2Fkryzhen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kamysh","download_url":"https://codeload.github.com/kamysh/kryzhen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamysh%2Fkryzhen/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35212581,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-07T02:00:07.222Z","response_time":90,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","database-migrations","mallard","migrations","postgresql","rust","sql"],"created_at":"2026-07-07T02:30:30.538Z","updated_at":"2026-07-07T02:30:31.023Z","avatar_url":"https://github.com/kamysh.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kryzhen\n\nA Rust port of the Haskell [`mallard`](https://hackage.haskell.org/package/mallard) SQL migration tool ([source](https://github.com/AndrewRademacher/mallard)). Forward-only (accretive — no down-migrations), dependency-resolved migrations for PostgreSQL. Compatible with mallard's on-disk migration file format and its `mallard.applied_migrations` tracking table.\n\nAvailable as:\n\n- **library crate** `kryzhen` — call `migrate()` programmatically.\n- **CLI binary** `kryzhen` (crate `kryzhen-cli`) — run migrations from the command line.\n\n---\n\n## Migration file format\n\nMigration files are plain `.sql` files. Each file may contain one or more `#!migration` blocks. A block begins with a header comment and is followed by the SQL body:\n\n```sql\n-- #!migration\n-- name: \"tables/phone\",\n-- description: \"Phone numbers attached to a person.\",\n-- requires: [\"tables/person\"];\nCREATE TABLE phone (id bigint);\n```\n\n### Header fields\n\n| Field | Required | Type | Notes |\n|---|---|---|---|\n| `name` | yes | string | Unique identifier for this migration. |\n| `description` | yes | string | Human-readable description. |\n| `requires` | no | string or list | A single `\"name\"` or a list `[\"a\", \"b\"]`. |\n\nHeader fields are separated by commas; the last field is terminated with `;`. The SQL body follows directly after the header.\n\n\u003e **Note:** `#!test` blocks are **not** supported — kryzhen handles migrations only, and a `#!test` block is a parse error.\n\n### Multiple blocks per file + implicit linear dependency\n\nA single `.sql` file may contain several `#!migration` blocks. Each block after the first **implicitly depends on the block immediately before it** in the same file, in addition to any explicit `requires`. This merged requires set is what gets persisted to the tracking table.\n\n---\n\n## Dependency resolution\n\nMigrations are applied in **topological order** of their `requires` graph. The following are hard errors:\n\n- Dependency cycles.\n- A `requires` reference to a migration name that does not exist.\n- Duplicate migration names (across all files).\n\n---\n\n## Tracking table\n\nkryzhen creates a `mallard` schema (if absent) with an `applied_migrations` table:\n\n| Column | Description |\n|---|---|\n| `id` | Serial primary key. |\n| `name` | Migration name. |\n| `description` | Migration description. |\n| `requires` | Dependencies recorded at apply time. |\n| `checksum` | SHA-256 of the whitespace-trimmed SQL body (`bytea`, 32 raw bytes). |\n| `script_text` | Full SQL body as applied. |\n| `applied_on` | Timestamp of application. |\n\nThis is the same schema used by mallard, so the two tools can share a database.\n\nkryzhen also keeps a companion table `mallard.applied_migration_schemas` recording, per migration, which schema(s) it has been applied to (one row per `(migration_name, schema)`), and a `mallard.migrator_version` table marking the ledger format. See [Multiple schemas](#multiple-schemas-templated-migrations).\n\n---\n\n## Multiple schemas (templated migrations)\n\nThe same migration set can be applied independently to many schemas — one schema per customer/tenant, or one per RAG corpus. Migrations act as **templates**:\n\n- **Unqualified** DDL (`CREATE TABLE widgets …`) is created in the **target schema** — kryzhen runs each migration body under `SET LOCAL search_path TO \u003cschema\u003e`.\n- **Schema-qualified** DDL (`CREATE TABLE other.thing …`) goes exactly where it names.\n\nBecause the body runs with `search_path` set to the target schema only, a migration that references an object in *another* schema (e.g. an extension function) by an unqualified name will not resolve it — schema-qualify such references.\n\nApply to a schema with `--schema` (library: the `schema` argument to `migrate`). It defaults to `public`, which reproduces single-schema behaviour. To roll the same migrations out to several schemas, run once per schema:\n\n```bash\nkryzhen --database app --schema customer_a migrations/\nkryzhen --database app --schema customer_b migrations/\n```\n\nTracking is **per schema**: the `mallard.applied_migration_schemas` table records which migrations have reached which schema. Different schemas may sit at different points in the chain — onboarding a new customer applies the whole chain into a fresh schema, while adding a new migration only applies the missing tail to each existing schema. The canonical `mallard.applied_migrations` row (body + checksum) is written once, on the first schema to apply a given migration, and tamper detection is verified against it on every run (the body is schema-independent).\n\n\u003e **Note:** the per-schema id uses `gen_random_uuid()`, which is built in on **PostgreSQL 13+**. On older servers, enable the `pgcrypto` extension.\n\n### Upgrading an existing database\n\nA database first migrated by an older kryzhen (no per-schema table) is upgraded automatically: on the next run, kryzhen creates the new tables and backfills an association for every already-applied migration against the schema the connection currently resolves to (`current_schema()` — where those objects actually live), then stamps `migrator_version`. Nothing is re-applied. This runs once and is idempotent.\n\n---\n\n## Tamper detection\n\nOn every run, kryzhen recomputes the SHA-256 checksum of each already-applied migration's SQL body and **aborts if it differs** from the stored value. Do not edit a migration file after it has been applied.\n\n---\n\n## Atomicity and idempotency\n\nEach migration runs inside its own transaction together with its bookkeeping `INSERT` into `applied_migrations`. A failure aborts the run. Already-applied migrations are skipped on re-run, so running kryzhen multiple times is safe.\n\n---\n\n## Library usage\n\nThe caller owns the database connection. Build it with `tokio_postgres` and pass it to\n`migrate`:\n\n```rust\nuse kryzhen::{file, migrate, Report};\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let (mut client, conn) = tokio_postgres::connect(\n        \"host=127.0.0.1 user=postgres dbname=mydb\",\n        tokio_postgres::NoTls,\n    ).await?;\n    tokio::spawn(async move { let _ = conn.await; });\n\n    let migrations = file::load_dir(\"migrations\")?;\n    // Apply to the `public` schema; pass a different schema per tenant to template.\n    let report: Report = migrate(\u0026mut client, \u0026migrations, \"public\", false).await?;\n\n    println!(\"Applied:         {:?}\", report.applied);\n    println!(\"Already applied: {:?}\", report.already_applied);\n    Ok(())\n}\n```\n\nPass `dry_run = true` to resolve and plan without applying anything (still connects and\nverifies checksums).\n\n### `Report` fields\n\n| Field | Type | Description |\n|---|---|---|\n| `applied` | `Vec\u003cString\u003e` | Names applied this run (or planned, in dry-run mode), in topological order. |\n| `already_applied` | `Vec\u003cString\u003e` | Names that were already present before this run. |\n\n---\n\n## CLI usage\n\n```\nkryzhen --database mydb [ROOT]\n```\n\n`ROOT` defaults to `.` (current directory).\n\n### Flags\n\n| Flag | Default | Description |\n|---|---|---|\n| `--database \u003cDATABASE\u003e` | *(required)* | Database name. |\n| `--host \u003cHOST\u003e` | `127.0.0.1` | Server host. |\n| `--port \u003cPORT\u003e` | `5432` | Server port. |\n| `--user \u003cUSER\u003e` | `postgres` | Username. |\n| `--schema \u003cSCHEMA\u003e` | `public` | Target schema for the migrations. See [Multiple schemas](#multiple-schemas-templated-migrations). |\n| `--password \u003cPASSWORD\u003e` | *(empty)* | Password. |\n| `--sslmode \u003cMODE\u003e` | `prefer` | TLS mode: `disable`, `prefer`, `require`, `verify-ca`, or `verify-full`. See [TLS](#tls). |\n| `--ssl-root-cert \u003cPATH\u003e` | *(none)* | CA certificate for `verify-ca` / `verify-full` (PEM). |\n| `--ssl-client-cert \u003cPATH\u003e` | *(none)* | Client certificate for mutual TLS (PEM). |\n| `--ssl-client-key \u003cPATH\u003e` | *(none)* | Client private key for mutual TLS (PEM). |\n| `--dry-run` | off | Print the planned migration order; apply nothing. |\n| `-v, --verbose` | off | Enable debug-level logging. |\n| `-h, --help` | | Print help. |\n| `-V, --version` | | Print version. |\n\n### Example — dry run\n\n```\nkryzhen --database mydb --dry-run migrations/\n```\n\nPrints the migrations that would be applied, in topological order, without modifying the database. (It still connects, to load the applied set and verify checksums.)\n\n---\n\n## TLS\n\nkryzhen negotiates TLS using the standard libpq `sslmode` values:\n\n| Mode | Behaviour |\n|---|---|\n| `disable` | Never use TLS; connect in plaintext. |\n| `prefer` *(default)* | Use TLS if the server offers it; fall back to plaintext otherwise. |\n| `require` | Require TLS; fail if the server does not offer it. Certificate not verified. |\n| `verify-ca` | Require TLS and verify the server certificate against `ssl_root_cert`. |\n| `verify-full` | Like `verify-ca`, and also verify the server hostname matches the certificate CN/SAN. |\n\nIn `prefer` and `require` the connection is **encrypted but the server certificate is not verified** — matching libpq's behaviour for those modes. This lets kryzhen connect to a database using a self-signed or private-CA certificate (common for internal PostgreSQL deployments).\n\nFor `verify-ca` and `verify-full`, set `ssl_root_cert` (library) or `--ssl-root-cert` (CLI) to the CA certificate PEM file.\n\n### Mutual TLS\n\nTo present a client certificate to the server (for PostgreSQL `clientcert=verify-full` authentication), set both `ssl_client_cert` and `ssl_client_key` (library) or `--ssl-client-cert` and `--ssl-client-key` (CLI). Mutual TLS can be combined with any sslmode that establishes a TLS connection (`prefer`, `require`, `verify-ca`, `verify-full`).\n\nTLS uses OpenSSL (via `native-tls`), so the build needs OpenSSL and `pkg-config` available — see [docs/development.md](docs/development.md).\n\n---\n\n## Escape hatches\n\nThe `kryzhen hack` subcommand provides low-level manipulation of `mallard.applied_migrations` for situations where normal migration flow isn't sufficient.\n\n### `hack add` — record a migration as applied without running its SQL\n\n```bash\nkryzhen hack add \u003cNAME\u003e --root migrations/ --database mydb\n```\n\nRecords `NAME` as applied (to `--schema`, default `public`) without executing the SQL body. Useful when a migration has already been applied by other means (e.g. provisioned by Terraform, applied by another tool). Fails if any of the migration's `requires` are not yet applied **to that schema**.\n\n### `hack delete` — remove a migration record without running any SQL\n\n```bash\nkryzhen hack delete \u003cNAME\u003e --database mydb\n```\n\nRemoves `NAME`'s association with `--schema` (default `public`) from `mallard.applied_migration_schemas`. Fails if any migration applied to that schema lists `NAME` in its `requires`. When that was `NAME`'s last remaining schema, the canonical `mallard.applied_migrations` row is also removed.\n\n### `hack fix-checksum` — update a stored checksum to match the current file\n\n```bash\nkryzhen hack fix-checksum \u003cNAME\u003e --root migrations/ --database mydb\n```\n\nRecomputes the checksum from the current on-disk content of migration `NAME` and overwrites the stored value in `mallard.applied_migrations`. Use this when a migration file was legitimately edited after it was applied (e.g. reformatted, whitespace-only change) and tamper detection is now blocking normal runs with:\n\n```\nchecksum mismatch for already-applied migration [NAME]: file content changed — run `kryzhen hack fix-checksum NAME` to update\n```\n\n\u003e **Warning:** `hack fix-checksum` bypasses tamper detection. Only use it when you are certain the file change was intentional and harmless.\n\n---\n\n## Migrating from sqlx\n\nIf you have an existing project that uses [sqlx migrations](https://docs.rs/sqlx/latest/sqlx/migrate/index.html), `kryzhen hack migrate-from sqlx` imports your applied migration history into `mallard.applied_migrations` in two phases:\n\n```bash\n# Phase 1: verify checksums, rewrite files with #!migration headers, write receipt\nkryzhen hack migrate-from sqlx convert [MIGRATIONS_DIR] --database mydb\n\n# Phase 2: re-verify _sqlx_migrations against the receipt, insert into mallard\nkryzhen hack migrate-from sqlx import [MIGRATIONS_DIR] --database mydb\n\n# Or run both phases in one command:\nkryzhen hack migrate-from sqlx all [MIGRATIONS_DIR] --database mydb\n```\n\n`MIGRATIONS_DIR` defaults to `.`. The `_sqlx_migrations` table is never modified.\n\nAfter a successful import you can use `kryzhen migrate` as normal. sqlx should no longer manage those migrations.\n\n### What each phase does\n\n| Phase | Input | Output |\n|---|---|---|\n| `convert` | `_sqlx_migrations` rows + original files | Files rewritten with `#!migration` headers; `.kryzhen-import-receipt.json` written |\n| `import` | Receipt + `_sqlx_migrations` (re-verified) + converted files | Rows inserted into `mallard.applied_migrations` |\n\n### Team workflow\n\nAfter Person A runs `convert` and `import`, commit both the rewritten `.sql` files and `.kryzhen-import-receipt.json` to git. Teammates who sync the repo run only `import` — it re-verifies their `_sqlx_migrations` against the receipt and inserts any missing rows, skipping rows already present.\n\n---\n\n## Development\n\nSee [docs/development.md](docs/development.md) for how to build, test, and contribute.\nAPI documentation is available via `cargo doc --open`.\n\n---\n\n## License\n\nApache License 2.0 — see [LICENSE](LICENSE).\nContributions are subject to the [Contributor License Agreement](CLA.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkamysh%2Fkryzhen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkamysh%2Fkryzhen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkamysh%2Fkryzhen/lists"}