オープンソースのデータベース DevOps

驚きのないデータベースマイグレーション

Ptah(プタハ)は、目標スキーマと稼働中のデータベースを比較し、バージョン管理型マイグレーションを書くか、承認済みの計画を適用する。pgvector テーブルの再埋め込みのような、永続的な推論状態の移行も扱う。PostgreSQL、MySQL、MariaDB、SQLite ほか。オープンソース、MIT ライセンス、導入すべきツールチェーンはない。

$ curl -fsSL https://ptah.run/install.sh | sh

v0.13.0 · pre-GA · MITWindows: irm https://ptah.run/install.ps1 | iex

変更
sh · ptah-quick-start 49 / 49
# データベース自身のスキーマをファイルに書き出す。$ ptah db read --db-url sqlite://app.db | tee schema.sqlCREATE TABLE "users" (  "id" INTEGER PRIMARY KEY,  "email" TEXT NOT NULL); # ドリフト検査は、そのファイルと取得元のデータベースを比べる。$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbNo schema drift detected. # 目標スキーマを書き換えて、列を 1 つ要求する。$ cat > schema.sql <<'SQL'CREATE TABLE "users" (  "id" INTEGER PRIMARY KEY,  "email" TEXT NOT NULL,  "created_at" TEXT);SQL # 今度は同じ検査が報告を返す。$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_added: 1 (warning) # 差を埋めるのに何が要るかを尋ねる。ドライランは何も実行しない。$ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --dry-runPlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT; # レビュー済みの計画を実行する。使い捨てのファイルなら --auto-approve でよい。$ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --auto-approvePlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT;Auto-approval enabled; applying schema changes.Schema apply completed successfully. # そして同じ検査を、3 度目に。$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbNo schema drift detected.
schema.sql データベースと一致+1 行−1 行+7 行
CREATE TABLE users (  id INTEGER PRIMARY KEY,  email TEXT NOT NULL);
CREATE TABLE users (  id INTEGER PRIMARY KEY,  email TEXT NOT NULL,+  created_at TEXT);
CREATE TABLE users (  id INTEGER PRIMARY KEY−  email TEXT NOT NULL);
CREATE TABLE users (  id INTEGER PRIMARY KEY,  email TEXT NOT NULL);+ +CREATE TABLE sessions (+  id INTEGER PRIMARY KEY,+  user_id INTEGER NOT NULL,+  token TEXT NOT NULL,+  FOREIGN KEY (user_id) REFERENCES users(id)+);
sh
--db-url
$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --dry-runPlanned schema changes:ALTER TABLE "users" ADD COLUMN "created_at" TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --dry-runPlanned schema changes:-- Disable foreign-key enforcement for the table rebuild belowPRAGMA foreign_keys = off;-- SQLite table rebuild for changes ALTER TABLE cannot express on usersCREATE TABLE "__ptah_rebuild_users" (  "id" INTEGER PRIMARY KEY);INSERT INTO "__ptah_rebuild_users" ("id") SELECT "id" FROM "users";DROP TABLE "users";ALTER TABLE "__ptah_rebuild_users" RENAME TO "users";-- Restore foreign-key enforcement after the table rebuildPRAGMA foreign_keys = on;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url sqlite://app.dbSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: sqlite://app.db Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url sqlite://app.db --dry-runPlanned schema changes:CREATE TABLE "sessions" (  "id" INTEGER PRIMARY KEY,  "user_id" INTEGER NOT NULL,  "token" TEXT NOT NULL,  CONSTRAINT "fk_sessions_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id"));exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url postgres://app@db/appNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \    --db-url postgres://app@db/app --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url postgres://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url postgres://app@db/app --dry-runPlanned schema changes:-- Add/modify columns for table: users ---- ALTER statements: --ALTER TABLE "users" ADD COLUMN "created_at" TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url postgres://app@db/appSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url postgres://app@db/app --dry-runPlanned schema changes:-- Remove columns from table: users ---- ALTER statements: --ALTER TABLE "users" DROP COLUMN "email" CASCADE;-- WARNING: Dropping column users.email with CASCADE - This will delete data and dependent objects! --;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url postgres://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: postgres://app@db/app Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url postgres://app@db/app --dry-runPlanned schema changes:-- POSTGRES TABLE: sessions --CREATE TABLE "sessions" (  "id" INTEGER PRIMARY KEY NOT NULL,  "user_id" INTEGER NOT NULL,  "token" TEXT NOT NULL);-- ALTER statements: --ALTER TABLE "sessions" ADD CONSTRAINT "fk_sessions_user_id" FOREIGN KEY ("user_id") REFERENCES "users"("id");exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url mysql://app@db/appNo schema drift detected.exit 0 $ ptah schema apply --schema-file schema.sql \    --db-url mysql://app@db/app --dry-runSchema is synced, no changes to be made.exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url mysql://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- columns_added: 1 (warning)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url mysql://app@db/app --dry-runPlanned schema changes:-- Modify table: users ---- ALTER statements: --ALTER TABLE `users` ADD COLUMN `created_at` TEXT;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url mysql://app@db/appSchema drift detected (highest severity: destructive).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- columns_removed: 1 (destructive)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url mysql://app@db/app --dry-runPlanned schema changes:-- Modify table: users ---- ALTER statements: --ALTER TABLE `users` DROP COLUMN `email`;-- WARNING: Dropping column users.email - This will delete data! --;exit 0
$ ptah schema drift --schema-file schema.sql \    --db-url mysql://app@db/appSchema drift detected (highest severity: warning).Failure threshold: all. Failing: true.Database: mysql://app@db/app Findings:- constraints_added: 1 (warning)- tables_added: 1 (safe)exit 1 $ ptah schema apply --schema-file schema.sql \    --db-url mysql://app@db/app --dry-runPlanned schema changes:-- MYSQL TABLE: sessions --CREATE TABLE `sessions` (  `id` INTEGER PRIMARY KEY,  `user_id` INTEGER NOT NULL,  `token` TEXT NOT NULL);-- ALTER statements: --ALTER TABLE `sessions` ADD CONSTRAINT `fk_sessions_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`);exit 0
目標スキーマを書く。実行前に何が起きるかを Ptah が示し、実行後はドリフト検査がデータベースとの一致を確かめる。 記録したワークフローをもっと見る →
仕組み

比較は 1 つ、どのワークフローもそれを読む

スキーマを何で書いても、Ptah はそれを 1 つの目標スキーマに解析し、データベースとの差分を取る。ドリフト、計画、lint、適用は、その 1 つの差分を 4 通りに読んだものだ。どの検査も 0、1、2 のいずれかで終了するので、CI が接続の失敗をクリーンなスキーマと取り違えることはない。

入力元 · 組み合わせ自由schema.sqlschema.yamlschema.hclschema.dbmlmodels/*.goorm loader · stdout目標スキーマ稼働中のデータベース対応データベース →比較1 つの差分得られるものschema drift裏づけ012schema plan · --dry-runレビューの対象migrations lint関門schema apply · migrations up承認のあとで
  1. schema.sqlschema.yamlschema.hclschema.dbmlmodels/*.goorm loader · stdout
  2. 目標スキーマ稼働中のデータベース
  3. 比較
  4. schema drift裏づけ 0 1 2
  5. schema plan · --dry-runレビューの対象
  6. migrations lint関門
  7. schema apply · migrations up承認のあと → 稼働中のデータベース

ふつうの一週間にやること、3 つ

実際のコマンドと、実際の出力。終了コードはどれでも同じ意味を持つ。0 は成功、1 は検査結果が否定的だったこと、2 は使い方か接続の失敗。

安全でない、または破壊的なマイグレーションを main に入れない

$ CI で.github/workflows/ptah.yml
- uses: stokaro/ptah-action@v1
  with:
    dir: ./internal/models
    db-url: ${{ secrets.PTAH_DATABASE_URL }}
    dialect: postgres
    migration-dir: ./migrations

$ ptah migrations lint --dir ./migrations \
    --dialect postgres
migrations/1790114595_drop_bio.up.sql:1 [warning] BC104: dropping a column retires a name application versions already deployed against the old schema still select and insert, so each of them starts failing the moment this migration commits, whether or not the column held any rows; deploy readers that no longer use the column first, then drop it in a later release (dropped column breaks deployed code)
migrations/1790114595_drop_bio.up.sql:1 [error] DS102: DROP COLUMN permanently deletes the column's data; deploy readers that no longer use the column first, then drop it in a later release (column dropped)

2 finding(s).
チェック · プルリクエストstokaro/ptah-action@v1
Ptah destructive-change verdictfailure
Ptah found destructive migration statements
Safety verdict: destructive.
Destructive changes: yes.
ptah-action が投稿したコメント
## Ptah migration plan

-- Remove columns from table: users --
-- ALTER statements: --
ALTER TABLE "users" DROP COLUMN "bio" CASCADE;

このアクションは対象データベースに対してマイグレーションを計画し、計画をコメントとして投稿し、データを破壊するステートメントがあれば失敗するチェック実行を 1 件書き込む。ptah migrations lint は指摘があれば 1 で、実行できなければ 2 で終了する。だから接続の失敗がクリーンなディレクトリに見えることはない。

レビューされていないマイグレーションをデータベースに近づけない

sh封じる
$ ptah migrations hash --dir ./migrations
Wrote ./migrations/ptah.sum
4 migration file(s) hashed
$ cat ./migrations/ptah.sum
h1:wmq3WUjBVQT+Ll4SPPan7wJGghKMtqi2yDc2Tc4AKnM=
1790114505_create_users.down.sql h1:3hAVcH5B1mghhsNEDFiyIlGOdernvs+6dDRkaO79/F0=
1790114505_create_users.up.sql h1:WJmUNv9ET7Qaypt2hpFNHHrqSQ5XGu0Yi0gMV0gLEMo=
1790114510_add_created_at.down.sql h1:3E0vyUoTX+11C374nPdXruUtJycoXTRoGvl8XsHLLIk=
1790114510_add_created_at.up.sql h1:9wID/+fArtJnyBz/JvM/NDDd1Ovm5SvkaJYcygIXLc4=
sh後日、適用済みのファイルが編集されたあと
$ ptah migrations up \
    --db-url sqlite://app.db \
    --migrations-dir ./migrations
error: migration sum verification failed:
migration directory does not match ptah.sum:
  changed: 1790114505_create_users.up.sql
$ echo $?
2

ptah.sum はファイルごとにチェックサムを記録する。up、validate、lint は何かを行う前にそれを検査し、status も --verify-sum を付ければ検査する。CI がレビューしたものが、そのまま本番で実行される。

検索を止めずに埋め込みモデルを切り替える

g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルerror: not found: run g2
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: boundary_captured, running
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: backfilled, running
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: caught_up, running
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: indexed, running
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: verified, running
g1 · ptah-docs-stub · クエリが読んでいる世代run g1: cut_over, running
g2 · ptah-docs-stub-v2 · 新しいモデルrun g2: cut_over, running
$ ptah inference plan --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable'
generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85
  source.table = docs (configured: the specification)
  model.identifier = ptah-docs-stub-v2 (configured: the specification)
  model.revision = 1 (configured: the specification)
  provider.credential =  (configured: the specification, as a reference rather than a value)
  generation.reproducibility = full (inferred: the specification pins an immutable model revision)
  source.estimated_rows = 3 (measured)
  run.consistency_mode = outbox (configured: the migration specification)
  target.exists = false (measured)
  target.capability.vector_type = true (measured)
  target.capability.vector_index = true (measured)
  [prepare] create the vector column and metadata for generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85
  [capture] install the outbox mechanism and record its starting position
  [backfill] embed 3 in-scope source rows
  [catch-up] process every source change recorded after the snapshot began
  [index] build the vector index and wait for it to be valid
  [verify] run every verification layer against the exact target generation
  [cutover] point queries at generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85, which nothing currently reads

What is not established:
  - generation.reproducibility = full (inferred: the specification pins an immutable model revision)

What leaves the database:
  - ptah-docs-stub-v2 at http://embeddings:8080/v1, declared local
  - the text of title, body
  - for 3 rows

Consistency mode: outbox
  - the outbox event and the source change are one transaction, so a change that committed has an event
$ ptah inference prepare --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2
prepared run g2 for generation 4f8a8e77ec39
  - snapshot boundary: 788
$ ptah inference backfill --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --batch-rows 10
backfill finished: 3 scanned, 3 embedded, 0 skipped
$ ptah inference catchup --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --batch-rows 10
caught up to transaction 796: 0 changed rows, 0 tombstoned
  - the outbox keeps events this run has processed: floor 773 for public.docs is held by run g1 (generation b7cc9eb5c7d2ee13ad46ba3d5da8a479ec57b128d771e4151fab5bf6d1da0866)
$ ptah inference index --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2
generation 4f8a8e77ec39 has a valid index
$ ptah inference verify --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2
generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85: 3 source rows, 3 target rows
  - every deterministic layer passed
  - not measured: the stored vectors were not read back, so their dimension was checked and their values were not; the target refuses a non-finite component on write, so nothing here could have carried one
$ ptah inference cutover --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2
plan 949aa4c67b4b
cutover refused:
  - this policy requires an approval and none was given
error: cutover refused

$ ptah inference cutover --spec /work/spec-g2.yaml --db-url 'postgres://ptah:ptah@postgres:5432/ptah?sslmode=disable' --run-id g2 --approve 949aa4c67b4b --approver 'site check'
queries now read generation 4f8a8e77ec3904675efb9678ec1b3de138a135b21d267621d93decf47e581d85 (plan 949aa4c67b4b)
  - no stabilization window was asked for, so nothing is keeping the previous generation current and there is no rollback to it

Ptah は、クエリが読んでいる世代の隣に新しい世代を作り、指定された埋め込みエンドポイントを呼び出し、検証し、承認がその計画を名指ししたところでクエリを切り替える。ベクトルそのものを Ptah が作ることはない。何かを実行する前に、データベースから何が出ていくかを plan が示す。

スキーマをコードで

いま持っているスキーマがそのまま使える

目標スキーマを定義し、そこへ至るのに必要な変更をレビューする。どの入力元も、計画を立てる前に同じ内部スキーマへ解析される。ファイル、Go のアノテーション、稼働中のデータベース、スキーマを出力する任意のプログラム。ORM もその 1 つだ。

schema.sqlschema.yamlschema.hclschema.dbmlmodels/user.go
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL,
  created_at TEXT
);
tables:
  users:
    columns:
      id:
        type: INTEGER
        primary: true
      email:
        type: TEXT
        not_null: true
      created_at:
        type: TEXT
table "users" {
  column "id" {
    type = integer
    null = true
  }

  column "email" {
    type = text
    null = false
  }

  column "created_at" {
    type = text
    null = true
  }

  primary_key {
    columns = [column.id]
  }
}
Table users {
  id integer [pk]
  email text [not null]
  created_at text
}
package models

//ptah:schema:table name="users"
type User struct {
	//ptah:schema:field name="id" type="INTEGER" primary="true"
	ID int64

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

	//ptah:schema:field name="created_at" type="TEXT"
	CreatedAt *string
}
入力元が何でも、同じ計画exit 0exit 0exit 0exit 0exit 0
$ ptah schema apply --schema-file schema.sql \
    --db-url sqlite://app.db --dry-run
Planned schema changes:
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.yaml \
    --db-url sqlite://app.db --dry-run
Planned schema changes:
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.hcl \
    --db-url sqlite://app.db --dry-run
Planned schema changes:
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --schema-file schema.dbml \
    --db-url sqlite://app.db --dry-run
Planned schema changes:
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
$ ptah schema apply --root-dir ./models \
    --db-url sqlite://app.db --dry-run
Planned schema changes:
ALTER TABLE "users" ADD COLUMN "created_at" TEXT;
あるいは ORM をスキーマにする

完全なスキーマを SQL、HCL、YAML のいずれかで標準出力に書き出すプログラムなら、どれでも入力元になる。Ptah はそれをシェルを介さず、制限をかけて実行し、空の出力は拒否する。

  • GORM
  • SQLAlchemy
  • Django
  • TypeORM
  • Sequelize
  • Hibernate
  • 自作のスクリプト

リポジトリにコミット済みの、バージョン固定のレシピ · そのほかは SQL DDL を出力し、同じ契約を使える

$ ptah schema drift \
    --schema-cmd ./scripts/export-schema \
    --db-url sqlite://app.db
No schema drift detected.
参照データをコードで

ルックアップ行を、テーブルの隣で宣言する

Ptah は宣言された行と稼働中のテーブルの差分を取り、元に戻せるデータマイグレーションを書く。ドリフト検査と直接適用の計画も同じ行を読む。行を更新または削除するデータマイグレーションには --allow-destructive が必要だ。

  1. //ptah:schema:data · countries.yaml
  2. 稼働中のテーブルとキーで差分を取る
  3. INSERT · UPDATE · DELETE
  4. .up.sql / .down.sql · ptah.sum
データベースのアクセス権をコードで

ロール、権限付与、行レベルポリシーを、テーブルと一緒に宣言する

ptah schema security は稼働中のデータベースを読み、行レベルセキュリティのないまま、ロールに権限が付与されているテーブルを報告する。ptah viz --security は同じ指摘を宣言されたスキーマの上に示す。--fail-on any を付ければ、この指摘でプルリクエストを止められる。

  1. GRANT … TO role
  2. ptah schema security
  3. PRV01 · 権限付与されたテーブルに RLS がない
  4. ポリシーを宣言する
スキーマのエクスポート

1 つのスキーマを、必要とするところへエクスポートする

出力先は 7 つ。Go のアノテーションからは 7 つすべてに、スキーマファイルからは HCL 以外のすべてに出力できる。機械向けの契約、人間向けのドキュメント、さらに 2 つのスキーマ形式。データベースには一切接続せず、エクスポート結果はどれもレビューを経る候補だ。

$ ptah schema export --to openapi-v3 --root-dir ./models
$ ptah schema export --to graphql --root-dir ./models \
    --graphql-operations list,by-id
$ ptah schema export --to protobuf --root-dir ./models \
    --proto-package shop.v1 --out shop.proto
$ ptah schema export --to markdown --root-dir ./models
$ ptah schema export --to html --root-dir ./models --out schema.html
$ ptah schema export --to dbml --root-dir ./models
$ ptah schema export --to hcl --root-dir ./models --out schema.hcl
openapi: 3.0.3
info:
  title: Ptah Exported Schema
  version: 1.0.0
servers:
  - url: /
paths: {}
components:
  schemas:
    products:
      type: object
      required:
        - id
        - name
        - price
        - status
      properties:
        id:
          type: integer
          format: int32
        name:
          type: string
          maxLength: 255
        price:
          type: number
        status:
          type: string
          enum:
            - active
            - inactive
# Code generated by ptah; DO NOT EDIT.

# Operation shapes below were generated on request. They are type declarations
# only: Ptah generates no resolvers, no data access, no authorization or tenant
# isolation, and no filtering, ordering, pagination, or transaction behavior.

enum ProductStatus {
  active
  inactive
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type Query {
  products(first: Int, after: String): ProductConnection
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String!
  price: Float!
  status: ProductStatus!
}

type ProductEdge {
  node: Product!
  cursor: String!
}

type ProductConnection {
  edges: [ProductEdge!]!
  pageInfo: PageInfo!
}
Exported Protobuf schema to /tmp/app/shop.proto
Exported 1 message(s), 4 field(s), 1 enum(s)
bootstrapped new compatibility history
2 export warning(s) reported

$ cat shop.proto
edition = "2023";

package shop.v1;

message Product {
  int32 id = 1;
  string name = 2;
  string price = 3;
  ProductStatus status = 4;
}

enum ProductStatus {
  PRODUCT_STATUS_UNSPECIFIED = 0;
  PRODUCT_STATUS_ACTIVE = 1;
  PRODUCT_STATUS_INACTIVE = 2;
}

// Code generated by ptah schema export --to protobuf; DO NOT EDIT.
// ptah:protobuf-export-version=2
// ptah:content-sha256=4b1c2203313b7090dd40822f26eda0ee8d0b1df0ffb61c3691aa34d06de3c81b
# Schema reference

- [products](#products)

## products

| Column | Type | Null | Default | Key | Comment |
| --- | --- | --- | --- | --- | --- |
| id | SERIAL | no | — | PK | — |
| name | VARCHAR(255) | no | — | — | — |
| price | DECIMAL(10,2) | no | — | — | — |
| status | product_status | no | — | — | — |

## Enums

- `product_status` — active, inactive
Exported schema documentation to /tmp/app/schema.html
Found 1 table(s), 4 field(s), 1 enum(s)
Enum "product_status" {
  "active"
  "inactive"
}

Table "products" {
  "id" SERIAL [pk, not null]
  "name" VARCHAR(255) [not null]
  "price" DECIMAL(10,2) [not null]
  "status" product_status [not null]
}
Exported HCL schema to /tmp/app/schema.hcl
Found 1 table(s), 4 field(s), 1 enum(s)

$ cat schema.hcl
// Code generated by ptah; DO NOT EDIT.

enum "product_status" {
  values = ["active", "inactive"]
}

table "products" {
  column "id" {
    type = SERIAL
  }
  column "name" {
    type = VARCHAR(255)
  }
  column "price" {
    type = DECIMAL(10,2)
  }
  column "status" {
    type = enum.product_status
  }
  primary_key {
    columns = [column.id]
  }
}

api_name、api_type、api_expose は、マイグレーションエンジンには手を触れずに、公開する契約の形を決める。たとえば DECIMAL を文字列として出したり、列を契約から外したりできる。Protobuf のフィールド番号は、コミットするファイルの中に保持される。

エージェント型の開発

モデルをプロジェクトにつなぐ方法は 2 つ。ツールは 1 組。データベースには触れない

モデルへの接続を誰が持つかを選ぶ。その先はどちらも同じ Ptah だ。ワークスペースがあれば 11 個のツールが使え、書き込みはすべてプレビューされてダイジェストと照合され、判断ごとに監査ログが 1 行残る。

ptah assistモデルへの接続は Ptah が持つ。設定したプロファイルを通じて、OpenAI 互換 API か Anthropic API を使う。
あなた端末
ptah assist手元のマシンのモデル、ゲートウェイ、またはプロバイダー
プロジェクトスキーマ · マイグレーション · 開発用データベース
ptah mcpモデルはクライアントが持つ。Claude、Cursor、VS Code、Zed、あるいは stdio でローカルサーバーを起動する任意の MCP クライアント。
あなたクライアント内
MCP クライアントClaude · Cursor · VS Code · Zed
プロジェクトスキーマ · マイグレーション · 開発用データベース
  • 読む → プレビュー → 承認 → 適用承認の対象は、ダイジェストで名指しされた 1 つのパッチだ。プレビューのあとでファイルが変わっていれば、適用は拒否される。
  • Allow once · for this session · No答えるのは端末かクライアントの中。リポジトリのポリシーにできるのは、権限を取り除くことだけだ。
  • データベースには書き込まないSQL を実行するツールも、データベースを変更するツールも、マイグレーションを適用するツールもなく、それを加える設定もない。ptah migrations up は人が行う手順のままだ。
  • 監査とバイト数ワークスペースがあれば、権限に関する判断はすべて .ptah/agent-audit.jsonl に記録される。ptah assist では、各回答の最後に、プロジェクトの内容のうち何バイトがプロバイダーに届いたかが示される。
ptah assist · 承認の瞬間実際の記録
┃ Ptah is asking for artifact.write:schema.
┃
┃ apply a 1-file patch to the schema artifact
┃   artifact: schema
┃   directory: /home/developer/projects/ptah-assist-lab/workspace/schema
┃   base digest: sha256:c2faa21a30324714af7e7de289200af7f5a84908223ba1cba094b4a9
┃   result digest: sha256:fcb3f414e6936591b9510583b77fdead27f6419602161183a89a00
┃   update: schema.yaml
┃
┃ This approval covers exactly sha256:6e2a256e07315164caa524dd82d73a922ede320cd1
┃ > Allow once
┃   Allow for this session
┃   No

↑ up • ↓ down • / filter • enter submit
ptah assist · モデルが拒否することもある実際の記録
> now write the migration for that column into the migrations directory in Ptah's native format, and
 call apply_patch for it. be brief

  Held — not written. Two blockers, both mine, not yours:

  • Format unknown: no tool result in this session has shown me Ptah's native migration file
  format, and I won't invent one.
  • No preview to apply:  apply_patch  needs a preview token/digest from a fresh  preview_patch
  against the current migrations artifact (last listed: 0 files, digest  sha256:70f6b4… ). I have
  none, and I can't claim a write or a verification pass without them.

  If the migration tool surface is enabled here, I'll: preview a migration adding  tasks.active
  (matching the schema change already applied), show you the diff, then apply it with its token.

-- AcmeQuant/Qwen3.8-27B via router, 1 turn(s), 0 tool call(s), answer
-- No Ptah tool answered, so nothing above was checked against this project.

完全なオープンソース。テレメトリーなし。ネットワークが届かない場所でも動く

その場から動かせないデータのために作った。規制対象、データ主権の対象、機密扱いのデータ。どのコンポーネントもセルフホストでき、開発元へ通信するものは 1 つもない。

  • MIT完全なオープンソース

    CLI、ptah-compat、ptah-ls、Kubernetes Operator、GitHub Action、そしてこのサイト。リポジトリは 4 つ、ライセンスは 1 つ。有料プランに限定された安全性チェックはない。

  • プライベートテレメトリーなし

    Ptah が接続するのは、指定したアドレスだけだ。データベース、埋め込みエンドポイント、ptah assist が使うモデルプロバイダー、そして push または pull するレジストリ。利用状況の送信も、更新の確認も、アカウントもない。

  • 自前デフォルトでデータ主権を守る

    スキーマとマイグレーションディレクトリは、自前のレジストリに OCI アーティファクトとして置く。ECR、GAR、Harbor、GHCR、Docker Hub、あるいは素の registry:2。Operator は自分のクラスター内で動く。認証情報は値ではなく参照で書くので、spec はそのままコミットできるファイルになる。

  • エアギャップネットワーク不要

    リリースアーカイブには ptah、ptah-compat、ptah-ls が入っており、どれも CGO なしでビルドされている。アーカイブは、ネットワークなしで checksums.txt と照合できる。推論では、--release がディスクで持ち込んだ oci-layout ディレクトリを受け付ける。

約束ではなく証拠

推論の実行がプロバイダーに触れる前に、plan は何が外に出るかを正確に表示する。どのモデルか、どのエンドポイントとその宣言されたクラス、どの列、何行か。データ保護の審査が求めるのと同じ一覧だ。

$ ptah inference plan実行前に表示される
What leaves the database:
  - ptah-docs-stub-v2 at http://embeddings:8080/v1, declared local
  - the text of title, body
  - for 3 rows

Consistency mode: outbox
  - the outbox event and the source change are one transaction, so a change that committed has an event
データベースのための GitOps

OCI アーティファクトとして公開したスキーマに合わせて、データベースをリコンサイルする

Ptah Operator はタグをダイジェストに固定し、そのダイジェストだけを使う。アーティファクトを検証し、データベースを観測して、計画を公開する。デフォルトでは、その計画を名指しした承認があって初めて適用し、そのあとはジョブの終了コードを信用せず、データベースをもう一度観測する。破壊的な計画はデフォルトで無効だ。有効にしても、承認は計画のバイト列そのものに結びつけられる。

PtahSchema · リコンサイルループoperator.ptah.run
リコンサイル一定間隔ごと · 変更時適用後01タグをダイジェストに解決02アーティファクトを検証03データベースを観測04計画を公開05承認06適用07再び観測開始
  1. タグをダイジェストに解決
  2. アーティファクトを検証
  3. データベースを観測
  4. 計画を公開
  5. 承認
  6. 適用
  7. 再び観測
05
破壊的な計画はデフォルトで無効。有効にすると、承認は計画のバイト列そのものに結びつく
07
ループが信用するのはデータベースで観測したものであって、適用ジョブの終了コードではない
データベース

お使いのデータベースで何が使えるかを確かめる

エンジンは 10。選ぶと、Ptah がそこで何を管理するか、知っておくべき制限、どのリリースラインをテストしているかがわかる。

PostgreSQL主要な対象

テーブル、ビュー、関数、拡張、カスタム型、ロール、行レベルセキュリティを管理する。

最も広いオブジェクトモデル。 列挙型、ドメイン型、複合型、範囲型、トリガー、シーケンス、マテリアライズドビューもここで管理する。
PostgreSQL の対応を詳しく見る →
テスト済みのリリースライン
14 – 18Ptah 認定
13サポート終了後もテスト
CI で稼働中のサーバーに対して検査している。
SQLite組み込み

スキーマの変更。ALTER TABLE で表せない変更には、テーブルの再構築を自動で計画する。

再構築は隠さず計画に載せる。 再構築はテーブルをコピーする。計画にはそれが SQLite のテーブル再構築として明記される。
SQLite の対応を詳しく見る →
テスト済みのリリースライン
3Ptah 認定
組み込みライブラリとしてプロセス内でテストしている。サーバーは関与しない。
MySQL対応

MySQL の DDL の規則に沿った計画。インデックス、制約、外部キー、そして MySQL が実際に受け付ける SQL。

MariaDB とは別物。 プランナーの系統は共通でも、方言と機能セットは別になっている。
MySQL の対応を詳しく見る →
テスト済みのリリースライン
8.4 · 9.7 · 26.7Ptah 認定
CI で稼働中のサーバーに対して検査している。
MariaDB対応

MariaDB 固有の DDL、インデックス、制約を、MariaDB 自身の方言の規則で計画する。

独自の機能セット。 MariaDB のサーバーは mysql:// の背後にあっても自身のバージョン文字列で識別され、MySQL ではなく MariaDB のプリセットで計画される。
MariaDB の対応を詳しく見る →
テスト済みのリリースライン
10.11 · 11.4 · 11.8 · 12.3Ptah 認定
CI で稼働中のサーバーに対して検査している。
CockroachDBPostgreSQL ワイヤープロトコル

スキーマの変更と行レベルの TTL ポリシー。CockroachDB の規則を加えた PostgreSQL の経路で計画する。

PostgreSQL ではない。 CockroachDB にない操作は、SQL を出力する前に拒否されるか書き換えられ、そのまま送られることはない。
CockroachDB の対応を詳しく見る →
テスト済みのリリースライン
25.4 · 26.2 · 26.3Ptah 認定
CI で稼働中のサーバーに対して検査している。
YugabyteDBPostgreSQL ワイヤープロトコル

スキーマの変更、ロール、行レベルセキュリティを PostgreSQL の経路で扱う。

インデックス操作が異なる。 インデックスの並行作成には対応する。DROP INDEX CONCURRENTLY は YugabyteDB のプリセットで除外されている。
YugabyteDB の対応を詳しく見る →
テスト済みのリリースライン
2024.2 · 2025.2 · 2026.1Ptah 認定
CI で稼働中のサーバーに対して検査している。
SQL Server対応するサブセット

T-SQL のテーブル、フィルター付きインデックス、稼働中のカタログの読み取り。

T-SQL の保守的なサブセット。 サブセットの外の変更には、手作業のマイグレーションが必要になる。
SQL Server の対応を詳しく見る →
テスト済みのリリースライン
2019 · 2022 · 2025Ptah 認定
CI で稼働中のサーバーに対して検査している。
Oracle対応

スキーマの読み取り、計画、バージョン管理されたマイグレーション。

宣言的な収束には既知の限界がある。 宣言したファイルと稼働中の Oracle のカタログとの比較はまだ収束せず、マイグレーションの実行はアドバイザリーロックで直列化されない。
Oracle の対応を詳しく見る →
テスト済みのリリースライン
21 · 23Ptah 認定
CI で稼働中のサーバーに対して検査している。
ClickHouse分析向け

エンジン設定とソートキーを持つ MergeTree テーブル、マテリアライズドビュー、TTL ポリシー。

機能の範囲で制限される。 モデル化する外部キーもトランザクションもない。計画はエンジンが提供するものの範囲で行う。
ClickHouse の対応を詳しく見る →
テスト済みのリリースライン
26.3 · 26.7 · 26.8 · 26.9Ptah 認定
24.10サポート終了後もテスト
CI で稼働中のサーバーに対して検査している。
SpannerPostgreSQL インターフェース

PostgreSQL インターフェースを通したスキーマのワークフロー。Spanner のエミュレーターに対してテストしている。

検証の範囲。 エミュレーターでテスト済み。マネージドサービスとの互換性は認定していない。
Spanner の対応を詳しく見る →
検証の範囲
エミュレーターCI でテスト
マネージド認定なし
エミュレーターはマネージドサービスの証明にはならない。

サーバーの機能を確かめる

検出したバージョン、そのサポートレベル、Ptah が計画に使う機能プロファイルを表示する。

$ ptah db capabilities --db-url "$DATABASE_URL"
出力の読み方 →

CI に裏付けられた互換性の検査

自動化された検査が master へのプッシュのたびと毎晩実行される。Ptah 認定のリリースラインは 28。CI でテストし、検査した機能の範囲で Ptah が責任を持つラインだ。組み込みの SQLite と Spanner のエミュレーターは、稼働中のサーバーとしてではなく、そのものとして宣言している。

サポートの根拠を見る →
ワークフローで Ptah を使う

ptah はコマンドラインで。ptah-ls は Go のアノテーション向けにエディタで。GitHub Action は CI で、Kubernetes Operator はクラスターで。Go パッケージは ptah.run から自分のコードで使える。

Atlas からの移行

ptah-compat は Atlas と同じ形のコマンドを受け付け、その裏で Ptah の挙動を実行する。Ariga とは無関係の、独立したクリーンルーム実装。Atlas 互換 →