Ptah(プタハ)の実践例
端末で記録した Ptah の実行 24 件。ページのために書かれたものは一つもない。開けば読めるし、再生を押せば打ち込まれていく。
-
# データベース自身のスキーマをファイルに書き出す。$ 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.
-
# 宣言をバージョン管理型マイグレーションにする。$ ptah migrations generate --root-dir ./entities \ --db-url sqlite://app.db --migrations-dir ./migrations \ --name create_users_and_postsGenerated migration files for sqlite://app.db:UP: /tmp/app/migrations/1788809709_create_users_and_posts.up.sqlDOWN: /tmp/app/migrations/1788809709_create_users_and_posts.down.sql # down も同時に書かれる。$ ptah migrations up --db-url sqlite://app.db \ --migrations-dir ./migrations=== MIGRATE UP ===Current version: 0Total migrations: 1Pending migrations: 1 ✅ Migrations completed successfully!Database is now at version: 1788809709 # リビジョンテーブルも一致している。$ ptah migrations status --db-url sqlite://app.db \ --migrations-dir ./migrations=== MIGRATION STATUS ===Database: ***Current Version: 1788809709Applied Migrations: 1Pending Migrations: 0
-
# 生成されたマイグレーションには必ず down が付いてくる。$ ls migrations1788810699_create_users.down.sql1788810699_create_users.up.sql # 戻したら何が起きるかを尋ねる。何も実行されない。$ ptah migrations down --db-url sqlite://app.db \ --migrations-dir ./migrations --dry-run=== DRY RUN MODE ===No actual changes will be made to the database === MIGRATE DOWN ===Target version: 0Current version: 1788810699Migrations to roll back: 1 ✅ Dry run completed successfully!Would have rolled back to version: 0Would have rolled back these migrations: [1788810699] # まず計画、次にロールバック。前進のときと同じ規則。
-
# この 2 つのファイルの間にある SQL は何か。$ ptah schema diff --from file://schema.sql \ --to file://schema-next.sql \ --dev-url "sqlite://dev.db?mode=memory"ALTER TABLE "users" ADD COLUMN "created_at" TEXT;CREATE INDEX IF NOT EXISTS "idx_users_email" ON "users" ("email"); # 任意の 2 状態。どちらも稼働中のデータベースではない。
-
# 新しい埋め込みモデル。まず計画を立てる。これには何が要るのか。$ ptah inference plansource.estimated_rows = 3 (measured)target.capability.vector_type = true (measured)[backfill] embed 3 in-scope source rowsConsistency mode: outbox # 配信中の世代の隣に、新しい世代を作る。$ ptah inference prepareprepared run quick-start for generation b7cc9eb5c7d2 - snapshot boundary: 753$ ptah inference backfill --batch-rows 10backfill finished: 3 scanned, 3 embedded, 0 skipped # 実行中に変わった行を追いつかせ、そのあとインデックスを張る。$ ptah inference catchup --batch-rows 10caught up to transaction 761: 0 changed rows, 0 tombstoned$ ptah inference indexgeneration b7cc9eb5c7d2 has a valid index # 何かが読む前に検証する。$ ptah inference verify3 source rows, 3 target rowsevery deterministic layer passed # クエリはまだ古いベクトルを読んでいる。切り替えは独立した手順。$ ptah inference cutoverplan 5bbecb32c113cutover refused: - this policy requires an approval and none was givenerror: cutover refused # その計画をダイジェストで承認する。承認されるのはその 1 つだけ。$ export PTAH_APPROVE=5bbecb32c113$ ptah inference cutover --approver 'quick-start check'queries now read generation b7cc9eb5c7d2ee13ad46ba3d5da8a479ec57b128d771e4151fab5bf6d1da0866 (plan 5bbecb32c113) - this is the first generation over this target, so there is no previous one to keep current and nothing to roll back to # クエリがたどるポインタが、新しい世代を指している。$ psql -c 'SELECT target_table, active_generation FROM ptah_embedding_pointer;' target_table | active_generation--------------+------------------------------------------------------------------ docs | b7cc9eb5c7d2ee13ad46ba3d5da8a479ec57b128d771e4151fab5bf6d1da0866(1 row)
-
# このマイグレーションを追加するプルリクエストが出た。$ cat migrations/1700000100_drop_email.up.sqlALTER TABLE "users" DROP COLUMN "email"; # データベースに届く前に検査する。$ ptah migrations lint --dir ./migrations \ --dialect postgresmigrations/1700000100_drop_email.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/1700000100_drop_email.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).warning: DS110P ran without the baseline schema it reads, so this analysis is thinner than the same directory would get against a dev database the run can read # ビルドを落とすのは終了コード。$ echo $?1
-
# 計画を保存する。レビューされたものが、そのまま実行される。$ ptah schema plan --schema-file schema.sql \ --db-url sqlite://app.db --name add-active-users \ --output plan.jsonPlanned schema changes:CREATE TABLE "users" ( "id" INTEGER PRIMARY KEY, "email" TEXT NOT NULL, "legacy_ref" TEXT);Plan saved to file://plan.json # 署名する。鍵を読むのは ssh-keygen であって、Ptah ではない。$ ptah schema approve --plan plan.json --key ./release_keyApproved plan.jsonSignature: plan.json.sig$ ptah schema verify-approval --plan plan.jsonApproved by release@example.comPlan digest: cb74d5837d3efb8c1a8c93bd930fd0321d12abc5e13fd731ff129c43bf0bfc41 # 承認済みの計画で、識別子を 1 つ書き換えてみる。$ sed -i 's/legacy_ref/legacy_id/g' plan.json$ ptah schema verify-approval --plan plan.jsonerror: approval does not verify against .ptah/allowed_signers: either the plan changed after it was approved, or it was signed by a key that file does not list # 終了コード 2。書き換えられた計画は、誰も承認していない計画である。
-
# シードファイルは環境名をファイル名に含む。$ ls seeds001_demo_accounts.dev.sql002_reference_data.prod.sql$ ptah seed --db-url sqlite://app.db --seeds-dir ./seeds --env dev=== SEED ===Environment: dev Matching seeds: 1Applied seeds: 1Seeds completed successfully. # もう一度実行する。適用済みのシードは記録されているので、何も起きない。$ ptah seed --db-url sqlite://app.db --seeds-dir ./seeds --env devMatching seeds: 1Applied seeds: 0Skipped seeds: 1Database seed data is already up to date. # そして prod は、打ち込めば届く環境ではない。$ ptah seed --db-url sqlite://app.db --seeds-dir ./seeds --env proderror: refusing to seed protected environment "prod" without --allow-prod
-
# スキーマは Go の構造体。宣言はアノテーションのほう。$ cat entities/user.gopackage entities //ptah:schema:table name="users"type User struct { //ptah:schema:field name="id" type="SERIAL" primary="true" ID int64 //ptah:schema:field name="email" type="VARCHAR(255)" not_null="true" Email string //ptah:schema:field name="plan" type="ENUM" enum="free,team,enterprise" Plan string} # PostgreSQL 向けに出力する。$ ptah schema render --root-dir . --dialect postgres-- Statement 1/2CREATE TYPE "enum_user_plan" AS ENUM ('free', 'team', 'enterprise'); -- Statement 2/2-- POSTGRES TABLE: users --CREATE TABLE "users" ( "id" SERIAL PRIMARY KEY NOT NULL, "email" VARCHAR(255) NOT NULL, "plan" enum_user_plan); # 同じファイル、違うのはフラグ 1 つ。$ ptah schema render --root-dir . --dialect sqlite-- Statement 1/1CREATE TABLE "users" ( "id" INTEGER PRIMARY KEY, "email" TEXT NOT NULL, "plan" TEXT CHECK (plan IN ('free', 'team', 'enterprise'))); # SQLite に enum 型はないので CHECK になる。落ちたものは何もない。
-
# 誰もスキーマファイルを書かなかったデータベース。$ ptah introspect --db-url sqlite://app.db --out ./modelsGenerated 2 Go file(s) in /tmp/app/modelsImported 2 table(s), 5 field(s), 0 enum(s) # 出てきたのは、自分で書いたであろうアノテーション付きの Go。$ cat models/posts.go// Code generated by ptah introspect; DO NOT EDIT. package models //ptah:schema:table name="posts"type Posts struct { //ptah:schema:field name="id" type="INTEGER" primary="true" Id *int //ptah:schema:field name="user_id" type="INT" not_null="true" foreign="users(id)" foreign_key_name="fk_posts_user_id" on_delete="NO ACTION" on_update="NO ACTION" UserId int //ptah:schema:field name="title" type="TEXT" not_null="true" Title string} # 制約は、名前と参照動作を伴って返ってきた。
-
# 接続先のサーバーについて Ptah が何を判定したかを尋ねる。$ ptah db capabilities --db-url sqlite://app.dbDialect: sqliteServer version: 3.53.4Capability preset: SQLite3 (sqlite)Preset source: version-ladderSupport level: certified Behavior: identifier_limit unlimited enum_modeling unsupported foreign_key_reference unique Capabilities: advisory_locks unsupported catalog_partitions supported # 宣言した enum がここで CHECK になる理由が enum_modeling。
-
# 別のツールが読める形でデータベースを読み戻す。$ ptah schema inspect --db-url sqlite://app.db --format dbmlTable "users" { "id" INTEGER [pk] "email" TEXT [not null]} # 人間以外が読むなら JSON で。$ ptah schema inspect --db-url sqlite://app.db --format json{"schemas":[{"name":"main","tables":[{"name":"users","columns":[{"name":"id","type":"INTEGER","null":true},{"name":"email","type":"TEXT"}],"primary_key":{"parts":[{"column":"id"}]}}]}]} # 残る 2 つは hcl と sql。--out-dir を付ければファイルに書く。
-
# どのビュー列が、どの実テーブル列に依存しているのか。$ ptah schema lineage --schema-file schema.sql \ --dialect sqliteSOURCE FEEDS KINDusers.email active_users.email viewusers.id active_users.id view # 解析は静的。--db-url がなければ何にも接続しない。
-
# スキーマの形を、メトリクス基盤がすでに読める形式で。$ ptah schema stats --db-url sqlite://app.db# HELP ptah_schema_tables Tables.# TYPE ptah_schema_tables gaugeptah_schema_tables{dialect="sqlite"} 2# HELP ptah_schema_columns Columns across all tables.# TYPE ptah_schema_columns gaugeptah_schema_columns{dialect="sqlite"} 5# HELP ptah_schema_indexes Indexes.# TYPE ptah_schema_indexes gaugeptah_schema_indexes{dialect="sqlite"} 0 # 数えるのはオブジェクトで、行ではない。中身ではなく形。
-
# テーブルは、それを読む API をすでに記述している。$ ptah schema export --to graphql --root-dir ./entities \ --out schema.graphqlExported GraphQL schema to /tmp/app/schema.graphqlFound 2 table(s), 5 field(s), 0 enum(s)$ cat schema.graphql# Code generated by ptah; DO NOT EDIT. type User { id: ID! email: String!} type Post { id: ID! user_id: Int! title: String! user: User!} # 最後のフィールドは外部キーで、リレーションとして読まれている。
-
# テーブル定義は、その行を運ぶペイロードの形も決めている。$ ptah schema export --to openapi-v3 --from sql \ --schema-file schema.sql --out openapi.yamlExported OpenAPI schema to /tmp/app/openapi.yamlFound 2 table(s), 5 field(s), 0 enum(s)$ head -20 openapi.yamlopenapi: 3.0.3info: title: Ptah Exported Schema version: 1.0.0paths: {}components: schemas: users: type: object required: - id - email properties: id: type: integer format: int32 # NOT NULL は required になった。両者がずれることはない。
-
# 同じスキーマを、ワイヤ契約として。$ ptah schema export --to protobuf --from sql \ --schema-file schema.sql --out schema.proto \ --proto-package acme.app.v1Exported Protobuf schema to /tmp/app/schema.protoExported 2 message(s), 5 field(s), 0 enum(s)bootstrapped new compatibility history1 export warning(s) reported # 警告こそが要点。フィールド番号は約束である。$ head -14 schema.protoedition = "2023"; package acme.app.v1; message Post { int32 id = 1; int32 user_id = 2; string title = 3;} message User { int32 id = 1; string email = 2;} # 次のエクスポートは、この番号を再利用するか、書き込みを拒否する。
-
# 作図ツールが求めるのは DBML。フラグを 1 つ変えるだけ。$ ptah schema export --to dbml --root-dir ./entities \ --out schema.dbmlExported DBML schema to /tmp/app/schema.dbmlFound 2 table(s), 5 field(s), 0 enum(s)$ cat schema.dbmlTable "posts" { "id" SERIAL [pk] "user_id" INT [not null] "title" VARCHAR(200) [not null]} Table "users" { "id" SERIAL [pk] "email" VARCHAR(255) [not null]} Ref "posts"."user_id" > "users"."id" # これを得るために二重に記述したものは何もない。
-
# 誰も手では最新に保てないリファレンスページ。$ ptah schema export --to markdown --from sql \ --schema-file schema.sql --out SCHEMA.mdExported schema documentation to /tmp/app/SCHEMA.mdFound 2 table(s), 5 field(s), 0 enum(s)$ head -13 SCHEMA.md# Schema reference - [posts](#posts)- [users](#users) ## posts | Column | Type | Null | Default | Key | Comment || --- | --- | --- | --- | --- | --- || id | INTEGER | yes | — | PK | — || user_id | INTEGER | no | — | FK → users(id) | — || title | TEXT | no | — | — | — | # HTML は、--to を変えただけの同じコマンド。
-
# 宣言は、そのまま図でもある。$ ptah viz --root-dir ./entities --format mermaid \ --include-columnserDiagram users { SERIAL id PK VARCHAR_255 email } posts { SERIAL id PK INT user_id FK VARCHAR_200 title } users ||--o{ posts : "fk_posts_user_id" # 関連は外部キーから読まれる。2 つ目のファイルからではない。
-
# スキーマそのものを、ダイジェスト付きのアーティファクトとして公開する。$ ptah schema push oci://registry.example/acme/schema:v1 \ --schema-file schema.sql --dialect sqlite --latestPushed schema as oci://registry.example/acme/schema:v1Digest: sha256:66fed834333daafa24448c7c6b5fe548c4f429d036977194f5b417c505fb933fVersion: Tags: [v1 latest] # 2 つのタグがそれを指している。指定した 2 つだけ。$ ptah oci tags oci://registry.example/acme/schemalatestv1 # 動くタグは、動かないダイジェストに解決される。$ ptah oci resolve oci://registry.example/acme/schema:latestoci://registry.example/acme/schema@sha256:66fed834333daafa24448c7c6b5fe548c4f429d036977194f5b417c505fb933f
-
# そのアーティファクトは何を宣言しているのか。ダウンロードせずに尋ねる。$ ptah oci inspect oci://registry.example/acme/schema:v1Reference: oci://registry.example/acme/schema:v1Digest: sha256:66fed834333daafa24448c7c6b5fe548c4f429d036977194f5b417c505fb933fMedia type: application/vnd.oci.image.manifest.v1+jsonSize: 843Artifact type: application/vnd.stokaro.ptah.schema.v1Discovery: none Annotations: io.stokaro.ptah.kind application/vnd.stokaro.ptah.schema.v1 io.stokaro.ptah.schema-format hcl org.opencontainers.image.revision 112a72244a81db149899df0ce43afc704a152bc4 Files: NAME MEDIA TYPE SIZE DIGEST schema.hcl application/vnd.stokaro.ptah.schema.hcl.v1 483 sha256:11719c3690781f43aadc70a236e5494dd5435e9418b6197e4048fc3a5fd23366 # 答えたのは 843 バイトのマニフェスト。ペイロードは動いていない。
-
# レジストリ参照は、ファイルと同じくスキーマの入力元である。$ ptah schema render \ --schema-file oci://registry.example/acme/schema:v1 \ --dialect postgres-- Statement 1/3-- POSTGRES TABLE: users --CREATE TABLE "users" ( "id" INTEGER PRIMARY KEY NOT NULL, "email" TEXT NOT NULL); # あるいは、保存されたままの正規形の HCL として取り出す。$ ptah schema pull oci://registry.example/acme/schema:v1 \ --out pulled.hclPulled oci://registry.example/acme/schema:v1 to /tmp/app/pulled.hclDigest: sha256:66fed834333daafa24448c7c6b5fe548c4f429d036977194f5b417c505fb933f$ head -12 pulled.hcl// Code generated by ptah; DO NOT EDIT. table "posts" { column "id" { type = INTEGER } primary_key { columns = [column.id] } foreign_key "fk_posts_user_id" { columns = [column.user_id] ref_columns = [table.users.column.id] }}
-
# 既存の Atlas パイプラインと、同じフラグ。$ ptah-compat migrate diff add_users --dir file://migrations \ --to file://schema.sql \ --dev-url "sqlite://dev.db?mode=memory"Created migration file: /tmp/app/migrations/20260907193826_add_users.sqlUpdated migration checksum: /tmp/app/migrations/atlas.sum # 書かれたのは Atlas のディレクトリ。タイムスタンプ付きファイルと atlas.sum。$ cat migrations/atlas.sumh1:9q+8PcnP03kKT2aZzOj4MHlWEilRmbPXaScNvs2FJzI=20260907193826_add_users.sql h1:eXKVFuXvHFnneZKiWkpyJSHeF5q8KlZCCXJqM7JNymE= # apply は、その同じディレクトリを読む。$ ptah-compat migrate apply --dir file://migrations \ --url sqlite://app.dbMigrating to version 20260907193826 from 1 pending migrations.Migration complete. Current version: 20260907193826 # スクリプトは 1 つも変えていない。下にはネイティブのコマンド体系がそのままある。