Content last updated 2026-10-13

Artifact Registry ADR 007: データベーススキーマ

レジストリのデータテーブル構成

背景

Artifact Registry は次の点を考慮したデータベース構成を必要とします。

  • 異なるアクセスパターン: アーティファクト管理クライアントは独自のプロトコルを使用し、それはフォーマットごとに大きく異なります。
  • スケーラビリティ: アーティファクトのストレージは、数百万行が保存される規模にすぐに到達し得ます。
  • パフォーマンス: 前述の 2 点を踏まえつつ、それでも私たちは操作の大部分を占める読み取りクエリにおいて高速な実行時間を維持したいと考えています。
  • 過去の落とし穴: 現行のコンテナレジストリおよびパッケージレジストリのデータ構成にはいくつかのほころびが見られており(例、例)、ここではそれらを回避します。

決定事項に踏み込む前に、以下のスキーマに関するいくつかの注記を述べます。これらは主に、提示するテーブルの数を考慮して可読性を高めるためのものです。

  • このドキュメントは機能のコアとなるテーブルについて記述します。サブ機能には追加のテーブルが必要になりますが、ここでは記述しません。例えば、blob ストレージのクリーンアップに必要な補助テーブルの背景については クリーンアップタスク を参照してください。
  • テーブル名は可読性のために短縮しています。これらはここには示していない共通のプレフィックスを共有します(例: artifacts_registry_container_repositories)。
  • Artifact Registry はネームスペースにスコープされます。根拠については ADR-001 を参照してください。
  • 主キーやタイムスタンプなど、いくつかの共通カラムは明確さのために省略しています。
  • すべてのテーブルは namespace_id カラムを含みます。Cells のシャーディングキー要件 はサテライトサービスのデータベースには適用されません。行はネームスペースのアンカータプル(platform、entity_type、entity_id)を通じて間接的に Organization に帰属します。このカラムは以下のすべてのテーブル定義に明示的に示しています。
  • すべての jsonb カラムは、無制限なペイロードを防ぎ期待される構造を強制するために、永続化前に厳格な JSON スキーマに対して検証されなければなりません。これはこのドキュメント内のすべての jsonb カラムに適用されます(例: rule_configuration および package_json)。
  • リモートリポジトリテーブルの暗号化された認証情報カラムは、単独では成り立ちません。各行には、認証情報を復号するためのラップされたデータ暗号化キーと、それをラップしたネームスペースキーの識別情報(wrapped_dek、ns_key_id、ns_key_version)も保持されるため、この 3 つがなければ暗号文を読み取れません。この 3 つとテーブルの暗号文カラムは一括して有無が決まる単位を構成し、CHECK 制約により、この単位のすべてのカラムが設定されているか、すべてが NULL であるかのどちらかを強制しなければなりません。部分的な認証情報(例: パスワードのないユーザー名)は受け付けません。また、暗号文のないラップ済み DEK や、ラップ済み DEK のない暗号文など、この 3 つによって生じ得る部分的な状態も受け付けません。npm_remote_repositories のようにテーブルが 1 つの認証情報属性しか保存しない場合でも、このような部分的な状態はレコードが保持する属性数に左右されないため、制約は必要です。キーテーブルと共通のカラム構成については、暗号化キーを参照してください。
  • リモートリポジトリテーブルの暗号化された認証情報カラム(encrypted_username、encrypted_password、encrypted_auth_token)は、平文の入力を 2048 文字に制限し、暗号化前に Go の検証レイヤーで強制します。この上限は平文に対するものであり、平文はアプリケーションレイヤーにのみ存在します。データベースが見るのは bytea の暗号文だけなので、DB 側の CHECK(例: octet_length(...) <= N)は、暗号化方式の固定オーバーヘッド(IV、認証タグ、キー ID ヘッダー)を介して間接的に平文を制約することしかできず、上限の近似となり、必須の Go チェックと冗長になります。CHECK を省略することで、スキーマを暗号フレーミングから切り離した状態にも保てます。暗号、キー ID レイアウト、エンベロープ構造の変更がスキーママイグレーションを必要としません。
  • すべての id カラムは、Artifact Registry インスタンスのスコープ内で一意でなければなりません。namespaces.id は UUIDv7(RFC 9562)を使用し、すべての Artifact Registry デプロイにまたがるグローバルな一意性を保証します。PostgreSQL の各バージョンで利用可能な生成方法を含む完全な根拠については、ネームスペース ID の型を参照してください。その他すべての API 公開テーブルも id に UUIDv7 を使用し、アプリケーションレイヤーで生成します(カラムデフォルトもシーケンスも使用しません)。これにより、API に公開されるすべてのエンティティで単一の識別子型を維持し、調整なしでデプロイ間のグローバル一意性を保証します。アプリケーション生成の UUIDv7 は論理レプリケーションにも適しています(出典)。サブスクライバー間で調整すべきサーバー側シーケンスや GENERATED カラムがなく、UUIDv7 の時刻順序性により B-tree の挿入局所性は BIGSERIAL に近く保たれます。内部 blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)は意図的な例外です。API には決して公開されず、最も大量の行を保持するため、bigint DEFAULT nextval('<table>_id_seq') の ID を維持します。その一意性は単一の Artifact Registry データベース内でローカルに強制されます。これらの行は常にネームスペース配下にスコープされるため、それで十分です。サーバー側デフォルトを持たないすべての uuid id カラムには値をバージョン 7 に制限する CHECK 制約があり、ジェネレーターを信頼するのではなくスキーマがバージョンを強制します。

決定事項

データの領域は 6 つあります。

  • ネームスペーステーブル。不変のスラッグと仮想アンカータプルを持つ内部ネームスペースエンティティを導入することで、Artifact Registry を外部識別子から切り離します。完全な根拠については ADR-022 を参照してください。
  • リポジトリコレクションテーブル。ネームスペース内のリポジトリの論理的なグループです。スキーマには初日から存在しますが、まだユーザーには公開されていません。すべてのネームスペースには「default」リポジトリコレクションが作成され、すべてのリポジトリが自動的にそこへ割り当てられます。
  • ネームスペースレベルのテーブル。これらは ライフサイクルポリシーの設定とルール、ネームスペースレベルのストレージ統計、およびこのネームスペースに保存されたシークレットをラップする 暗号化キーを、ネームスペースに直接スコープして扱います。
  • リポジトリ親テーブル。すべてのフォーマットにまたがるすべてのリポジトリ(ホスト型、仮想型、リモート型)の統一されたレジストリであり、ランディングページのハイブリッドリストとクロスフォーマットクエリを支えます。
  • アーティファクトフォーマットレベルのテーブル。ここには各フォーマット専用のテーブルがあります。ホスト型リポジトリ(Container、Maven、NPM、Generic)、リモートリポジトリ(Container、Maven、NPM)、仮想リポジトリ(Container、Maven、NPM)。それぞれ repository_id を介して親の repositories テーブルを参照します。Generic はホスト型のみです。リモートリポジトリがプロキシするメタデータインデックスも、仮想リポジトリがマージするメタデータインデックスも持たないため、リモート型や仮想型のテーブルはありません。
  • blob ストレージレベルのテーブル。実際のストレージメタデータと 進行中のアップロードセッションの追跡 を扱います。

ネームスペース

erDiagram
    namespaces {
        uuid id PK "UUIDv7, globally unique across Artifact Registry deployments"
        text slug "NOT NULL, UNIQUE, immutable, limit 255"
        text platform "NOT NULL, limit 255"
        text entity_type "NOT NULL, limit 255"
        text entity_id "NOT NULL, opaque string, limit 255"
        text billing_entity_type "NOT NULL, limit 255"
        text billing_entity_id "NOT NULL, opaque string, limit 255"
        smallint delivery_mode_override "NULLABLE, 0=redirect, 1=proxy; per-namespace override of the instance default"
        timestamptz deleted_at "NULLABLE; set on soft-delete, reclaimable until purge (ADR-015)"
        timestamptz purged_at "NULLABLE; set on hard-delete, slug retired, anchor preserved for audit (ADR-015)"
        timestamptz blocked_at "NULLABLE; set on security-block, slug reserved but not serving (ADR-015)"
        timestamptz disabled_at "NULLABLE; set while the owning organization has the registry turned off, cleared on re-enable"
        timestamptz suspended_at "NULLABLE; set while billing-suspended, read-only service, cleared when resolved"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }
  • namespaces: 他のすべてのテーブルが namespace_id を介して参照するルートエンティティです。各ネームスペースは、URL やクライアント設定で使用される不変かつグローバルに一意な slug を持ちます(スラッグの設計とグローバルな一意性の強制については ADR-022 を参照)。(platform, entity_type, entity_id) タプルは、そのセマンティクスを解釈することなく、ネームスペースを外部エンティティ(デフォルトでは Organization)にリンクします。entity_id は、基となる値が数値であっても TEXT として保存され、アンカー型間でスキーマを統一して保ちます。Organizations v1 では、すべての行が ('gitlab', 'organization', '<organizations.uuid>')、すなわち organization の 数値の organizations.id ではなく UUIDv7 を持ちます。IAM の Relationships API は、UUIDv7 でないオブジェクト ID が現れる リクエスト全体を拒否するため、数値 ID では organization の祖先を名指しできません。billing_entity_type と billing_entity_id は、使用量イベントのための課金アンカーを識別します。外部から提供されるカラム(platform、entity_type、entity_id、billing_entity_type、billing_entity_id)のいずれもスキーマレベルのデフォルトを持ちません。その根拠については ADR-022 を参照してください。delivery_mode_override カラムは、ADR-005 で定義されたネームスペースごとのアーティファクトデリバリーのオーバーライドを保持します。NULL はインスタンスのデフォルト(StorageConfig.delivery_mode)を継承し、0(redirect)はこのネームスペースに対してリダイレクトを強制し、1(proxy)はプロキシを強制します。ダウンロードリクエストの有効なデリバリーパターンは namespace.delivery_mode_override ?? instance.delivery_mode です。このカラムは、リクエストハンドラーが認可とルーティングのために実行する既存のネームスペース検索の一環として読み取られるため、別個のクエリやインデックスは必要ありません。カラム型は SMALLINT で、整数からラベルへのマッピングは Go アプリケーションで定義されます(0 = redirect、1 = proxy)。PostgreSQL の ENUM 型は安全に変更するのが難しいため避けています。アーティファクトデリバリーの選択を保存する将来のカラム(例: S17 がリポジトリごとのオーバーライドを導入する場合)は、同じ整数マッピングを再利用します。
  • ライフサイクルイベント(deleted_at、purged_at): ADR-015(内部)で定義されたスラッグのライフサイクルを実装します。deleted_at はソフト削除されたネームスペースを示し、ADR-010 のソフト削除ウィンドウ内で回収できます。purged_at はハード削除されたネームスペースを示します。スラッグは永久に廃止され、アンカーと課金のカラムは監査とフォレンジックのために保持されます。これら 2 つは一方向のイベント記録であり、一度設定されると設定されたままになります。パージ済みの行では両方が設定されます。
  • サービス条件(blocked_at、disabled_at、suspended_at): それ以外は稼働中のネームスペースに対する、元に戻せる制限です。タイムスタンプは条件が課された時刻を記録します。NULL は条件が有効でないことを意味し、繰り返しサイクルの履歴は行ではなく監査イベントストリームに保持されます。blocked_at はセキュリティ上のブロックを示します(スラッグは予約され、リクエストは処理されませんが、ネームスペースはサブスクライブされたままです)。disabled_at は、所有 Organization がレジストリを無効にしたことを示します。リクエストは処理されず、データは保持されます。Rails 側の Organization レベルの設定に基づき、内部 API を通じて設定および解除されます。suspended_at は、内部 API を通じて課された課金上の一時停止を示します。サービスは読み取り専用に制限され、ダウンロードは処理されますが、書き込みは拒否されます。そのため、支払いの途絶によって本番環境のコンシューマーが機能しなくなることはありません。各カラムは独立しているため、複数の条件が共存でき、1 つを解除しても他の条件は解除されません。
  • サービス提供可否の述語: 書き込みを処理する検索(プッシュ、変更)では、5 つすべてのカラムが NULL である必要があります。読み取りを処理する検索(ルーティング、認証、ダウンロード)では、deleted_at、purged_at、blocked_at、disabled_at が NULL である必要があります。一時停止中のネームスペースは引き続き読み取りを処理します。サブスクリプションライフサイクルのクエリ(課金、保持、およびサブスクリプション状態だけを読むスケジュール検索)は、ブロック中、無効化中、一時停止中のネームスペースもサブスクライブされたままであるため、deleted_at と purged_at のみを除外します。述語は何が検索をスケジュールしたかではなく、検索が何を行うかに従います。クライアントがすでに要求した書き込みを完了するバックグラウンドジョブは書き込み処理の検索であり、そのリクエストを受け付けたときと同じく 5 つすべてのカラムが NULL である必要があります。これは、受け付け済みクライアント書き込みの延期された半分だけを対象とします。回収、カウンターの調整、ポリシー駆動の削除など、プラットフォーム駆動の書き込みはこの項目では分類しません。API は導出されたステータス(優先順位: purged、deleted、blocked、disabled、suspended、active)を公開し、Rails は表示用にこれをキャッシュします(ADR-022)。

スラッグの不変性

PostgreSQL にはネイティブな不変カラムのサポートがありません。スラッグの不変性(ADR-022)は、値が変更された場合に例外を発生させる BEFORE UPDATE OF slug トリガーによってデータベースレベルで強制されます。これは、アプリケーションレイヤーをバイパスするあらゆるコードパス(直接的なデータベースアクセス、管理ツール、マイグレーション)を捕捉します。このトリガーは、スラッグの変更を必要とする緊急操作のために無効化できます(例: ALTER TABLE namespaces DISABLE TRIGGER trg_namespaces_immutable_slug)。

インデックス

  • namespaces: (slug) に対するユニークインデックス — スラッグによってネームスペースを検索します。(platform, entity_type, entity_id) WHERE purged_at IS NULL に対する部分ユニークインデックス — パージされていない行の間でアンカーの重複を防ぎます。アクティブな行とソフト削除済みの行は対象になりますが、パージ済みの行は対象外です。そのため、以前パージされた Organization は新しいネームスペース行で再オンボーディングでき、パージ済みの行は監査のためにアンカーデータを保持できます。delivery_mode_override、deleted_at、purged_at、blocked_at、disabled_at、suspended_at にはインデックスを設けません。これらのカラムは id または slug をキーとする既存のネームスペース検索の一環として読み取られ、サービス提供可否の述語は取得された単一行に対してフィルタリングします。

ADR-015(内部)で定義された予約済みスラッグリストはデータベースには保存しません。

リポジトリコレクション

リポジトリコレクションは、ネームスペース内のリポジトリの論理的なグループであり、チーム、セキュリティドメイン、製品ラインによってアーティファクトを整理します。リポジトリコレクションを UI と API に公開することは MVP のスコープ外です。このエンティティは純粋に将来の互換性のために初日から存在します。MVP の間、すべてのネームスペースには作成時に単一の「default」リポジトリコレクションが作成され、すべてのリポジトリがそこへ割り当てられます。MVP 後にリポジトリコレクションの概念が公開されると、ユーザーは追加のリポジトリコレクションを作成し、リポジトリをそこへ再割り当てできます。

erDiagram
    namespaces ||--o{ repository_collections : "has many"

    repository_collections {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        text name "NOT NULL, limit 255"
        boolean is_default "NOT NULL, DEFAULT false"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }
  • repository_collections: ネームスペース内のリポジトリの論理的なグループです。name はネームスペース内で一意な、人間が読めるラベルです。is_default は、すべてのネームスペースとともに自動的に作成され、MVP の間にすべてのリポジトリが割り当てられるリポジトリコレクションを示します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

すべてのネームスペース作成時に、デフォルトのリポジトリコレクション行をアトミックに挿入しなければなりません。

INSERT INTO repository_collections (namespace_id, name, is_default)
VALUES (<new_namespace_id>, 'default', true)
ON CONFLICT (namespace_id, name) DO NOTHING;

インデックス

  • repository_collections: (id, namespace_id) に対する主キー — HASH(namespace_id) パーティショニングに必要な複合 PK であり、repositories からの複合外部キーのターゲットも兼ねます。(namespace_id, name) に対するユニークインデックス — ネームスペース内で名前によってリポジトリコレクションを検索します。(namespace_id) WHERE is_default IS TRUE に対する部分ユニークインデックス — ネームスペースごとにデフォルトのリポジトリコレクションが最大 1 つであることを強制します。

クエリ例

  • ネームスペースのデフォルトのリポジトリコレクションを取得する。

    SELECT *
    FROM repository_collections
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND is_default = true;
    
  • ネームスペースのすべてのリポジトリコレクションを一覧する。

    SELECT id, name, is_default, created_at
    FROM repository_collections
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
    ORDER BY created_at;
    
  • 新しい(デフォルトではない)リポジトリコレクションを作成する。

    INSERT INTO repository_collections (namespace_id, name)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'team-backend');
    

リポジトリ

repositories テーブルは、フォーマットや種類に関係なくシステム内のすべてのリポジトリを登録する統一された親テーブルです。これはランディングページのハイブリッドリスト、すなわちすべてのフォーマットにまたがるホスト型・仮想型・リモート型のリポジトリを表示する単一のソート可能・フィルター可能・ページネーション可能なビューを支えます。各フォーマット固有のリポジトリテーブル(ホスト型、仮想型、リモート型)は、repository_id を介してここの単一の行を参照します。

このモデル(ホスト型、リモート型、仮想型を対等なスタンドアロンの型とし、参照によって構成する)は、JFrog Artifactory、Sonatype Nexus、Google Cloud AR がすべて採用しているものです。ただし各社で型の名前は異なります。

erDiagram
    namespaces ||--o{ repositories : "has many"
    repository_collections }o--o{ repositories : "linked via repository_collection_repositories"

    repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        text name "NOT NULL, limit 255"
        text description "nullable, limit 1024"
        smallint format "NOT NULL, 0=docker, 1=maven, 2=npm, 3=oci, 4=generic"
        smallint kind "NOT NULL, 0=hosted, 1=virtual, 2=remote, CHECK (format <> 4 OR kind = 0)"
        smallint visibility "NOT NULL, 0=public, 1=private, 2=internal"
        bigint artifacts_count "NOT NULL, DEFAULT 0, buffered counter"
        bigint downloads_count "NOT NULL, DEFAULT 0, buffered counter"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_updated_at "nullable, DEFAULT NOW()"
        timestamptz last_reconciled_at "NOT NULL, DEFAULT 'epoch', reconciliation bookkeeping"
        text gitlab_last_updated_by_user_id "nullable, opaque string, limit 255"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
        text gitlab_created_by_user_id "nullable, opaque string, limit 255"
    }
  • repositories: すべてのリポジトリの親エンティティです。format はアーティファクトフォーマット(Docker、Maven、npm、OCI、generic)を識別します。kind はリポジトリの種類(ホスト型、仮想型、リモート型)を識別します。Generic はホスト型のみです(Generic リポジトリを参照)。CHECK (format <> 4 OR kind = 0) がこれを強制するため、子テーブルや調整で考慮する必要のある Generic の仮想型やリモート型の行は存在できません。リポジトリは repository_collection_repositories 結合テーブルを介してリポジトリコレクションにリンクされ、リポジトリがそのネームスペース内の 1 つ以上のリポジトリコレクションに属することを可能にします。MVP の間、すべてのリポジトリはネームスペースのデフォルトのリポジトリコレクションにリンクされます。name はネームスペース内で一意でなければならず、これはすべての競合製品と一致しています。カウンターカラム(artifacts_count、downloads_count、size_bytes)は、ホット行の競合を避けるために バッファ書き込み/非同期書き込み を介して維持されます。last_updated_at は、ダウンロードではなくコンテンツの変更(アーティファクトの公開/変更/削除、キャッシュイベント)を追跡します。created_at と同様に、デフォルトは NOW() です。NOW() はトランザクションの開始時刻を返すため、created_at も last_updated_at も指定しない INSERT は、両方に同じ時刻を保存します。コンテンツ変更のないリポジトリでは last_updated_at = created_at になります。1 回限りのバックフィルによって、既存のすべての NULL をその行の created_at に設定します。これを実行するまでは、デフォルトの導入前に書き込まれた行に NULL が残る場合があります。バックフィルをロールバックすると、挿入時にデフォルトを使用した行も含め、last_updated_at がまだ created_at と等しいすべての行が NULL に戻ります。後続の変更で NOT NULL を追加するまで、カラム自体は NULL を許容します。last_reconciled_at は、同名の namespace_statistics カラムに対応するリポジトリレベルのカラムです(ストレージ使用量の計算を参照)。これは調整用の記録であり、リポジトリ自身の書き戻し時に記録され、API には決して表示されず、カウンターでもありません。ネームスペースレベルの対応するカラムとは異なり、独自のインデックスはありません。インデックスを必要とする古さのスキャンはネームスペースを選択し、リポジトリにはその独自の新しさではなく、選択済みのネームスペースを通じて到達するためです。'epoch' デフォルトによって、すでに行が存在するテーブルでもこのカラムを NOT NULL にできます。カラムを追加した時点ですべての既存行が有効になり、バックフィルも NULL 許容の暫定状態も不要です。gitlab_created_by_user_id と gitlab_last_updated_by_user_id は、どの GitLab ユーザーがリポジトリを作成し最後に変更したかを記録します。どちらも NULL 許容の不透明な参照で、外部キーもアプリケーション側の検証もありません。なぜなら、ユーザーレコードはモノリスに存在するためです。ユーザーハンドルとアバターのレンダリングはコンシューマーの責任であり、AR スキーマは ID のみを保存します。これらは namespaces.entity_id と同じ理由で TEXT として保存されます。上流のユーザー ID 形式が将来変更されても(例: UUID への変更)、スキーママイグレーションは不要です。description は、UI が仮想リポジトリだけでなくすべてのリポジトリ型の説明を表示するため、親テーブルにあります。soft_deleted_at タイムスタンプは、リポジトリがソフト削除された時刻を記録し、必要に応じて復元を可能にします。ソフト削除を親テーブルに置くことで、すべてのリポジトリ型(ホスト型、仮想型、リモート型)がフォーマット固有の処理なしに同じ削除セマンティクスを共有します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

リポジトリをハード削除すると、その構造上の子へカスケードします。関連する外部キーは、次の参照アクションを持ちます。

  • フォーマットの子テーブル(container_repositories、npm_repositories、maven_repositories、generic_repositories、および generic_repositories 以外の各テーブルの仮想型とリモート型のバリアント)で、repositories を参照する FK (repository_id, namespace_id): ON DELETE CASCADE。
  • repository_collection_repositories(コレクション結合テーブル)で、repositories を参照する FK (repository_id, namespace_id): ON DELETE CASCADE。
  • アーティファクトテーブル(container_images、npm_packages、maven_packages、generic_packages)で、そのフォーマットの子テーブルを参照する FK (<format>_repository_id, namespace_id): NO ACTION。
  • 仮想 upstream 結合テーブル(container_virtual_repository_upstreams、maven_virtual_repository_upstreams、npm_virtual_repository_upstreams)で、repositories を参照する FK (upstream_repository_id, namespace_id): NO ACTION。

したがって、1 回の DELETE FROM repositories によってフォーマットの子行とすべてのコレクションリンクが削除されます。NO ACTION キーはそれを拒否でき、いずれか 1 つの拒否でステートメント全体が中止されます。アーティファクトから子へのキーは、リポジトリがまだアーティファクトを保持している間は拒否します。結合キーは、いずれかの仮想リポジトリがフォーマットを問わず、それを upstream として一覧に含めている間は拒否します。

上記の一覧は、このドキュメントがアクションを明記するキーを対象としており、図示するすべてのキーではありません。さらに 2 つの ERD エントリ、artifact_type_repository_lifecycle_policy_settings と upload_sessions は repositories 外部キーを宣言しますが、アクションは明記していません。出荷済みスキーマにはどちらの外部キーもないため、現在はどちらも削除を拒否しません。ERD エントリの説明どおりに追加されれば、明記されないアクションは NO ACTION なので、どちらも拒否します。

接続のアクションは 2 つのテストで決まり、カスケードするには両方を通過する必要があります。1 つ目は方向です。親から子への接続だけがカスケードするため、削除は repositories から構造チェーンを下り、横方向には進みません。2 つ目は子が保持するものです。下向きの接続のうち、子が純粋な構造(フォーマットの子行、コレクションリンク)ならカスケードし、ユーザーデータ(アーティファクト)なら拒否します。upstream_repository_id は 2 つ目ではなく 1 つ目のテストに失敗します。自身が所有しないリポジトリを名指すため、結合行が何を保持するかをどう解釈してもカスケードにはなりません。これらは合わせて、前述のソフト削除パスと、アプリケーションが管理する blob_storage_attachments のクリーンアップに対応する宣言的な仕組みです。

どちらの拒否も、参照アクションだけではオペレーターに届かず、どのように失敗するかはステートメントの実行時期に依存します。削除が同期的な場合、拒否は失敗したステートメントとして到着し、API レイヤーが認識して変換する必要があります。削除が非同期、つまり今トゥームストーンを付け、後のパージで DELETE FROM repositories を実行する場合、拒否は停止したパージとして現れ、呼び出し元には届きません。したがって、オペレーターが頼れる拒否は、どちらのパスよりも前の削除リクエスト時に管理 API から返されます。リポジトリの削除 を参照してください。これらのキーはそのチェックの下にあるバックストップであり、シグナルではありません。

インデックス

  • repositories: (namespace_id, name) に対するユニークインデックス — アクティブなリポジトリとソフト削除されたリポジトリの両方にまたがって名前の一意性を強制し、名前の衝突によって復元が失敗することがないようにします。名前を再利用するには、まずハード削除が必要です。(namespace_id, name) WHERE soft_deleted_at IS NULL に対するインデックス — アクティブなリポジトリの検索と名前順の一覧表示のために最適化されたスキャンパスです。(namespace_id, format) WHERE soft_deleted_at IS NULL に対するインデックス — アクティブなリポジトリをフォーマットでフィルタリングします。(namespace_id, kind) WHERE soft_deleted_at IS NULL に対するインデックス — アクティブなリポジトリを種類でフィルタリングします。(namespace_id, visibility) WHERE soft_deleted_at IS NULL に対するインデックス — リポジトリを可視性レベルでフィルタリングします(可視性監査クエリ「このネームスペースで現在公開されているリポジトリはどれか?」を支えます)。ランディングページでソート可能なカラムごとに 1 つのインデックスを設け、すべてに WHERE soft_deleted_at IS NULL と末尾の id DESC キーセットのタイブレーカーを付けます。対象は (namespace_id, artifacts_count DESC, id DESC)、(namespace_id, downloads_count DESC, id DESC)、(namespace_id, size_bytes DESC, id DESC)、および式インデックス (namespace_id, COALESCE(last_updated_at, created_at) DESC, id DESC) です。ROW(<col>, id) キーセット境界の id 側は、インデックス内に存在する必要があります。カウンターカラム(artifacts_count、downloads_count、size_bytes)はデフォルトが 0 でカーディナリティが低いため、インデックスにタイブレーカーがないと、プランナーはスキャン後のソートまたはフィルターとして適用し、深いページではネームスペースに対して O(offset) に劣化します。式インデックスにも id DESC を含めます。これはカーディナリティのためではなく(created_at は行ごとに一意であるため、COALESCE(last_updated_at, created_at) はカーディナリティが高い)、キーセットに決定的なカーソルを与えるためです。新しさによるソートでは、生の last_updated_at ではなく COALESCE(last_updated_at, created_at) で並べます。コンテンツ変更のないリポジトリは last_updated_at に作成時刻を保持するため、どちらの式でもその時刻に基づいて順位付けされます。ソートで COALESCE を維持する理由は 2 つあります。前述の式インデックスが対応するのはその式だけであり、カラムはまだ NULL を許容します。NULL を保持する行は作成時刻で順位付けされます。結合されたキーは決して NULL にならないため、キーセット境界は単純な ROW(<col>, id) 比較となり、NULL キーに対する特別な処理なしでインデックス範囲に組み込まれます。シリアライズされる last_updated_at フィールドは、artifact-registry#1574でカラムに NOT NULL を追加するまで NULL 許容のままです。name によるソートにはタイブレーカーが不要です。(namespace_id, name) ユニークインデックスが、すでに決定的なキーセットだからです。(namespace_id, format, name) WHERE soft_deleted_at IS NULL に対するインデックス — フォーマットでフィルタリングした名前順の一覧表示(主要な閲覧ビュー: フォーマットでフィルタリングし、名前でソート)に使用します。format をインデックス適用後のフィルターとして使用し、異なるフォーマットの行を飛ばしながら (namespace_id, name) ユニークインデックスを走査するのではなく、(format, name) の範囲を直接検索してインデックスオンリーの状態を維持します。インデックスを設けるのはフォーマットと名前の組み合わせだけです。使用頻度の低いカウンターまたはタイムスタンプによるソートでは、ソートとフィルターの組み合わせごとに複合インデックスを増やすのではなく、format または kind をインデックス適用後のフィルターとして維持します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — このネームスペース内のソフト削除されたリポジトリを削除時刻順に一覧します(ゴミ箱一覧クエリ「ゴミ箱には何があり、いつ削除されたか?」を支えます)。この逆の部分述語は、前述のアクティブ行に対する部分インデックスを反映したものです。このテーブルの他の部分インデックスはすべてゴミ箱を除外し、完全な (namespace_id, name) ユニークインデックスは soft_deleted_at をキーにしないため、これがなければゴミ箱の一覧表示では、フィルタリングとソートのためにネームスペース内のすべての行を確認しなければなりません。GC の適格性は ADR-010 に従い、soft_deleted_at + retention_window から導出します。別個のカラムは不要です。

MVP の間、すべてのリポジトリは単一のデフォルトのリポジトリコレクションにリンクされるため、(namespace_id, ...) ソートインデックスはネームスペース全体のクエリとコレクションでフィルターされたクエリの両方に対応します。MVP 後、ネームスペースが複数のリポジトリコレクションを持つようになると、コレクションでフィルターされたクエリは repository_collection_repositories を介して JOIN します。追加のサポートインデックスは、リポジトリコレクションが公開される際に評価されます。

クエリ例

  • ネームスペースのすべてのリポジトリ(すべてのリポジトリコレクション)を最終更新順に一覧する。

    SELECT id, name, description, format, kind, artifacts_count,
           downloads_count, size_bytes, last_updated_at
    FROM repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND soft_deleted_at IS NULL
    ORDER BY COALESCE(last_updated_at, created_at) DESC
    LIMIT 20;
    
  • リポジトリコレクションでフィルターしたネームスペースのリポジトリを最終更新順に一覧する。

    SELECT r.id, r.name, r.description, r.format, r.kind, r.artifacts_count,
           r.downloads_count, r.size_bytes, r.last_updated_at
    FROM repositories r
    JOIN repository_collection_repositories rcr
      ON rcr.namespace_id = r.namespace_id AND rcr.repository_id = r.id
    WHERE r.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND rcr.repository_collection_id = '019a1b2c-0456-7abc-8def-000000000456' AND r.soft_deleted_at IS NULL
    ORDER BY COALESCE(r.last_updated_at, r.created_at) DESC
    LIMIT 20;
    
  • リポジトリコレクションとフォーマットでフィルターしたリポジトリを一覧する。

    SELECT r.id, r.name, r.description, r.format, r.kind, r.artifacts_count,
           r.downloads_count, r.size_bytes, r.last_updated_at
    FROM repositories r
    JOIN repository_collection_repositories rcr
      ON rcr.namespace_id = r.namespace_id AND rcr.repository_id = r.id
    WHERE r.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND rcr.repository_collection_id = 456 AND r.format = 0
      AND r.soft_deleted_at IS NULL
    ORDER BY r.name
    LIMIT 20;
    
  • 名前で単一のリポジトリを検索する。

    SELECT *
    FROM repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND name = 'my-repo' AND soft_deleted_at IS NULL;
    
  • 可視性監査: ネームスペース内のすべての公開リポジトリを一覧する((namespace_id, visibility) WHERE soft_deleted_at IS NULL の部分インデックスを使用)。

    SELECT id, name, format, kind
    FROM repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND visibility = 0 AND soft_deleted_at IS NULL
    ORDER BY name;
    
  • ゴミ箱一覧: ネームスペース内のすべてのソフト削除されたリポジトリを、最近削除された順に一覧する((namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL の部分インデックスを使用)。スコープはネームスペース全体なので、管理者は「今すぐ復元可能なものは何か?」を 1 つのクエリで回答できます。親ごとのゴミ箱ビューは別個の UI の関心事であり、必要であれば後から親をキーとするインデックスを追加することで対応できます。

    SELECT id, name, format, kind, soft_deleted_at
    FROM repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND soft_deleted_at IS NOT NULL
    ORDER BY soft_deleted_at DESC
    LIMIT 50;
    

リポジトリコレクションリポジトリ

repository_collection_repositories 結合テーブルは、リポジトリを、それが属するリポジトリコレクションにマッピングします。リポジトリはそのネームスペース内の 1 つ以上のリポジトリコレクションのメンバーになることができ、共通のユーティリティリポジトリを複数のチームのリポジトリコレクションを通じて公開するといった共有アクセスのシナリオを可能にします。

erDiagram
    repository_collections ||--o{ repository_collection_repositories : "has many"
    repositories ||--o{ repository_collection_repositories : "has many"

    repository_collection_repositories {
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id), part of composite PK (namespace_id, repository_collection_id, repository_id)"
        uuid repository_collection_id PK,FK "NOT NULL, (repository_collection_id, namespace_id) references repository_collections(id, namespace_id)"
        uuid repository_id PK,FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }
  • repository_collection_repositories: リポジトリをリポジトリコレクションにリンクします。MVP の間、すべてのリポジトリはちょうど 1 つのリポジトリコレクション(ネームスペースのデフォルト)にリンクされますが、スキーマは複数のリンクを許可するため、MVP 後にリポジトリをリポジトリコレクション間で共有できます。アプリケーションは、すべてのリポジトリが少なくとも 1 つのリポジトリコレクションリンクを持つという不変条件を強制します。Postgres はこれを宣言的に表現できません。複合外部キーにより、リポジトリコレクションとリポジトリは同じネームスペース内でのみリンクできることが保証されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

インデックス

  • repository_collection_repositories: (namespace_id, repository_collection_id, repository_id) に対する主キー — リンクの一意性を強制し、リポジトリコレクションによる検索に対応します。(namespace_id, repository_id) に対するインデックス — 特定のリポジトリが属するすべてのリポジトリコレクションを検索します。

クエリ例

  • リポジトリが属するすべてのリポジトリコレクションを一覧する。

    SELECT repository_collection_id
    FROM repository_collection_repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0789-7abc-8def-000000000789';
    
  • リポジトリをリポジトリコレクションにリンクする。

    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', '019a1b2c-0789-7abc-8def-000000000789')
    ON CONFLICT (namespace_id, repository_collection_id, repository_id) DO NOTHING;
    

ライフサイクルポリシー

erDiagram
    lifecycle_policy_settings ||--o{ lifecycle_rules : "has many"

    lifecycle_policy_settings {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, UNIQUE, references namespaces(id)"
        boolean enabled "NOT NULL"
    }

    lifecycle_rules {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid lifecycle_policy_settings_id FK "NOT NULL, (lifecycle_policy_settings_id, namespace_id) references lifecycle_policy_settings(id, namespace_id)"
        smallint rule_type "NOT NULL, 0=keep_last_downloaded_at, 1=keep_last_n, 2=keep_regex"
        jsonb rule_configuration "NOT NULL"
    }
  • lifecycle_policy_settings: ネームスペースレベルでライフサイクル管理の設定を定義し、すべてのリポジトリのデフォルトポリシーとして機能します。有効化されると、関連するライフサイクルルールがネームスペース全体に適用されます。これらのポリシーはリポジトリレベルのポリシーによって オーバーライド できます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • lifecycle_rules: ネームスペースレベルで特定のアーティファクトのライフサイクル動作を統制する、個々の保持・クリーンアップルールを指定します。これらのルールは、リポジトリレベルで オーバーライド されない限り、すべてのリポジトリに適用されます。ポリシーレコードごとのライフサイクルルールの数は、ルール評価時のパフォーマンス低下を防ぐために制限されます。これは、例えば特定のアーティファクトをどのくらいの期間保持するかをユーザーが指定するために使用されます(例: Maven スナップショットファイルは 1 か月だけ保持するなど)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

インデックス

  • lifecycle_policy_settings: (namespace_id) に対するユニークインデックス — ネームスペースごとに 1 つのポリシー設定レコード。
  • lifecycle_rules: (namespace_id, lifecycle_policy_settings_id) に対するインデックス — 特定のポリシーのすべてのルールを取得します。

リポジトリレベルのオーバーライドテーブルは同じパターンに従います。設定テーブルには (namespace_id, repository_id) に対するユニークインデックス、ルールテーブルには (namespace_id, <format>_repository_lifecycle_policy_settings_id) に対するインデックスです。

クエリ例

  • 特定のネームスペースのポリシーを取得する

    SELECT lp.*
    FROM lifecycle_policy_settings lp
    WHERE lp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';
    
  • 特定のアーティファクトリポジトリのポリシーを取得する

    SELECT *
    FROM container_repository_lifecycle_policy_settings
    WHERE container_repository_lifecycle_policy_settings.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND container_repository_lifecycle_policy_settings.repository_id = '019a1b2c-0123-7abc-8def-000000000123';
    
  • 新しいライフサイクルルールを作成する

    INSERT INTO lifecycle_rules (namespace_id, lifecycle_policy_settings_id, rule_type, rule_configuration)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', 1, '{"count": 10}'::jsonb);
    
  • ライフサイクルルールを更新する

    UPDATE lifecycle_rules
    SET rule_configuration = '{"count": 20}'::jsonb
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND id = '019a1b2c-0123-7abc-8def-000000000123';
    
  • ライフサイクルルールを破棄する

    DELETE FROM lifecycle_rules
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND id = '019a1b2c-0123-7abc-8def-000000000123';
    

リポジトリレベルのオーバーライド

各リポジトリ型(container、maven、npm、generic)は、ネームスペースレベルの値に対するオーバーライドを提供する、同様の名前のテーブルを持ちます。これにより優先順位システムが構築されます: ネームスペース(最低)-> リポジトリ(最高)。オーバーライドは repository_id を介して親の repositories テーブルを参照します。

erDiagram
    artifact_type_repository ||--|| artifact_type_repository_lifecycle_policy_settings : "has one"
    artifact_type_repository ||--o{ artifact_type_repository_lifecycle_rules : "has many"

    artifact_type_repository_lifecycle_policy_settings {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
        boolean enabled "NOT NULL"
    }

    artifact_type_repository_lifecycle_rules {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid artifact_type_repository_lifecycle_policy_settings_id FK "NOT NULL, (artifact_type_repository_lifecycle_policy_settings_id, namespace_id) references artifact_type_repository_lifecycle_policy_settings(id, namespace_id)"
        smallint rule_type "NOT NULL, 0=keep_last_downloaded_at, 1=keep_last_n, 2=keep_regex"
        jsonb rule_configuration "NOT NULL"
    }

(各アーティファクトフォーマットにオーバーライドテーブルがあるため、artifact_type は container、maven、npm、generic に置き換える必要があります。これらのオーバーライドはホスト型、仮想型、リモート型のリポジトリに同様に適用されます(generic はホスト型のみです)。repository_id 外部キーは親の repositories テーブルを参照し、フォーマット固有のテーブルはリポジトリの format カラムによって決まります。)

これらのテーブルは、ある意味で カスケード設定 のように動作します。それらの説明は ネームスペースレベル の同様の名前のテーブルとまったく同じであり、パーティショニングも同様です。すべてのオーバーライドテーブルは HASH(namespace_id) で 64 パーティションにパーティショニングされます。現在の 2 層の優先順位システム(ネームスペース → リポジトリ)は、MVP 後にリポジトリコレクションが公開される際に 3 層(ネームスペース → リポジトリコレクション → リポジトリ)に拡張できます。これには同じパターンに従ってリポジトリコレクションレベルのオーバーライドテーブルを追加する必要がありますが、既存のネームスペースレベルやリポジトリレベルのテーブルへの変更は不要です。

暗号化キー

erDiagram
    namespaces ||--o{ namespace_encryption_keys : "has many"

    namespace_encryption_keys {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        int version "NOT NULL, CHECK >= 1, UNIQUE (namespace_id, version)"
        bytea wrapped_key "NOT NULL, the namespace key wrapped by the deployment root key; emptied to zero length on crypto-shred"
        text root_key_uri "NOT NULL, limit 255, names the root key this row was wrapped under"
        boolean active "NOT NULL, DEFAULT true"
        timestamptz shredded_at "nullable, crypto-shred tombstone marker"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }
  • namespace_encryption_keys: ネームスペースが保持したキー暗号化キーごとに 1 行を保存し、各キーをデプロイのルートキーでラップします。ネームスペースが一度に持つアクティブなキーは 1 つです。以前のバージョンでラップされた行は引き続きそのバージョンを介してキーを解決するため、置き換えられたバージョンは削除せず、非アクティブ化してテーブルに残します。id は参照用の識別情報であり、認証情報の行は ns_key_id でこれを指定します。一方、version はネームスペース単位の序数で、ネームスペースのキーを順序付け、ローテーションの走査を制御します。この 2 つは相互に置き換えられません。暗号学的に消去されたネームスペースを再度有効にすると version は 1 から再開するため、ネームスペースの存続期間全体にわたってキーを識別できず、読み取りでは ns_key_id によってラップ用キーを解決します。暗号学的消去では、行を削除せずトゥームストーンとして保持します。wrapped_key を長さ 0 の bytea で上書きし、active を解除して shredded_at に時刻を記録するため、消去済みの状態が再起動後も維持され、すべてのインスタンスに適用されます。2 つの行単位の CHECK 制約によって、この構成を維持します。一方はトゥームストーンがアクティブのままになることを拒否し、もう一方はキー素材と未消去状態を双方向に関連付けて、shredded_at が NULL の場合に限りキー素材が存在するようにします。いずれかのトゥームストーンが存在すればネームスペース全体が消去済みであるという、ネームスペース全体の不変条件はアプリケーションレイヤーで強制します。これは行単位の CHECK で対応できる範囲の限界です。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

インデックス

  • namespace_encryption_keys: (namespace_id, version) に対するユニークインデックスは、ネームスペース単位のバージョン序数を維持します。(namespace_id) WHERE active = true に対する部分ユニークインデックスは、ネームスペースごとにアクティブなキーが 1 つだけであることを保証するとともに、認証情報を書き込む際にラップに使用するキーを検索するために使われます。(namespace_id) WHERE shredded_at IS NOT NULL に対する部分インデックスは、読み取りパスでキー履歴を走査せずに、ネームスペースが消去済みかを検出するために使われます。(root_key_uri) WHERE shredded_at IS NULL に対する部分インデックスは、特定のルートキーで現在もラップされている有効な行を検索するために使われ、ルートキーのローテーションで走査し、完了確認で数える対象となります。消去済みの行は、最後のインデックスから意図的に除外します。消去済みのネームスペースは以前の root_key_uri を永続的に保持し、その wrapped_key はすでに空になっているため、ローテーションでこれらの行を対象にしてはいけません。4 つのインデックスのうち、namespace_id 以外のカラムで始まるのはこれだけです。このインデックスが扱うクエリはルートキーを指定し、ネームスペースを指定しないためです。パーティショニングされたテーブルのすべてのインデックスと同様、各パーティションにローカルなため、この読み取りは 1 つのパーティションに絞られず、64 パーティションすべてに分散します。ルートキーのローテーションはバックグラウンドジョブとして一定数の行をまとめて走査するため、リクエストパスでは許容できないこの分散も、ここでは許容できます。

リモートリポジトリテーブルの認証情報カラム

3 つのリモートリポジトリテーブル(Container、Maven、NPM)は、行単位のデータ暗号化キーによる暗号文として上流の認証情報を保存し、それぞれが暗号文とともに次の 3 つのカラムを持ちます。

  • wrapped_dek(bytea): ネームスペースキーでラップされた、行のデータ暗号化キー。
  • ns_key_id(uuid): このキーをラップした namespace_encryption_keys の行。(ns_key_id, namespace_id) に対する複合外部キーで namespace_encryption_keys(id, namespace_id) を参照し、ON DELETE RESTRICT を指定します。これにより、認証情報の行が引き続き依存している間は、キーのバージョンを削除できません。
  • ns_key_version(int): 行のラップに使われたネームスペースキーのバージョン。ネームスペースキーのローテーション処理が、キーテーブルを結合せずに古いバージョンのままの行を選択できるよう、キーの行から非正規化します。この処理はキーセットカーソルを使ってネームスペースの行を行 ID 順に走査するため、ソートせずにその順序を満たす実行計画では、主キー (id, namespace_id) を先頭に使用し、スキャン内で namespace_id とバージョンの両方を適用します。後述の (namespace_id, ns_key_id) インデックスは、この読み取りではなく外部キーに使用します。namespace_id によって処理範囲が 1 つのパーティションに限定されるのは、インデックスの先頭にあるからではなく、ハッシュパーティションキーだからです。そのパーティション内でスキャン範囲を限定するには (namespace_id, id) インデックスが必要ですが、これらのテーブルにはありません。ns_key_version から始まるインデックスはありません。スキャンは 1 つのネームスペースではなく 1 つのパーティションを読み取り、オペレーターが実行するローテーション処理の一環として、そのパーティションをサイズに上限のあるバッチで走査します。そのため、そのネームスペース自身の行を超えて調べる行があっても、リクエスト処理経路とは異なり、この処理では許容できます。また、これらのテーブルは管理操作でしか変更されません。読み取りでは常に ns_key_id によってラップ用キーを解決し、このカラムは使用しません。

認証情報を必要としないリモートリポジトリも有効なため、この 3 つはいずれも NULL 許容です。背景で説明した一括有無の CHECK により、行が単位の一部だけを保持することを防ぎます。また、各テーブルは (namespace_id, ns_key_id) に対するインデックスを持ちます。PostgreSQL は外部キーの参照される側にはインデックスを作成しますが、参照する側には作成しません。そのため、このインデックスがなければ、キーの行を削除しようとするたびに、ON DELETE RESTRICT を強制するため認証情報テーブル全体が走査されます。このアクションは ON DELETE RESTRICT と記述しますが、このドキュメントでは、削除を拒否する他のキーには ON DELETE NO ACTION と記述しています。これらのキーはいずれも DEFERRABLE ではないため、2 つの表記は同じように動作します。RESTRICT を選ぶのは、チェックを遅延させる可能性も排除し、キーの行を削除するステートメントで拒否されるようにして、トランザクションのコミット時に持ち越さないためです。

行の上位には 2 段階のキーがあります。デプロイのルートキーがネームスペースキーをラップし、ネームスペースキーが各行の DEK をラップします。各段階は個別にローテーションされ、いずれも認証情報の暗号文を再暗号化しません。ルートキーをローテーションすると、namespace_encryption_keys.wrapped_key を新しいルートキーでラップして root_key_uri を更新しますが、認証情報の行には一切触れません。ネームスペースキーをローテーションすると、各認証情報の行の wrapped_dek を新しいネームスペースキーでラップし、その ns_key_id と ns_key_version を更新しますが、暗号文カラムは変更しません。暗号文を置き換えるのは認証情報への書き込み時だけで、そのたびに新しい DEK を生成します。

コンテナリポジトリ

この部分の課題は、OCI Distribution Spec v1.1 に準拠することです。

このアプローチは GitLab Container Registry のスキーマ から大きな着想を得ています。

erDiagram
    repositories ||--|| container_repositories : "has one"
    container_repositories ||--o{ container_images : "has many"
    container_images ||--o{ container_blobs : "has many"
    container_images ||--o{ container_manifests : "has many"
    container_images ||--o{ container_manifest_relationships : "has many"
    container_images ||--o{ container_tags : "has many"
    container_tags ||--|| container_manifests : "has one"
    container_blobs ||--|| blob_storage_attachments : "has one"
    container_manifests ||--|| blob_storage_attachments : "has one"
    container_manifest_relationships ||--|| container_manifests : "has one (parent_id)"
    container_manifest_relationships ||--|| container_manifests : "has one (child_id)"

    container_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
    }

    container_images {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_repository_id FK "NOT NULL, (container_repository_id, namespace_id) references container_repositories(id, namespace_id)"
        text name "NOT NULL, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    container_blobs {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
        bytea digest "NOT NULL, CHECK octet_length = 32"
        text media_type "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz soft_deleted_at "nullable"
    }

    container_manifests {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
        bytea digest "NOT NULL, CHECK octet_length = 32"
        text media_type "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        bigint size "NOT NULL, precomputed at push time"
        text gitlab_user_id "nullable, opaque string, limit 255"
        text gitlab_project_id "nullable, opaque string, limit 255"
        bytea gitlab_git_commit_sha "nullable"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    container_manifest_relationships {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
        uuid parent_container_manifest_id FK "NOT NULL, (namespace_id, container_image_id, parent_container_manifest_id) references container_manifests(namespace_id, container_image_id, id)"
        uuid child_container_manifest_id FK "NOT NULL, (namespace_id, container_image_id, child_container_manifest_id) references container_manifests(namespace_id, container_image_id, id)"
    }

    container_tags {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_image_id FK "NOT NULL, (container_image_id, namespace_id) references container_images(id, namespace_id)"
        uuid container_manifest_id FK "NOT NULL, (container_manifest_id, namespace_id) references container_manifests(id, namespace_id)"
        text name "NOT NULL, limit 255"
    }
  • container_repositories: 複数のイメージのコンテナです。各リポジトリは独立したバージョニングを持つ複数のイメージをホストできます。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_images: リポジトリ内の名前付きコンテナイメージ(例: myapp、backend)を表します。last_downloaded_at はイメージが最後にプルされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_at ライフサイクルルールによって、ダウンロードベースの保持を評価するために使用されます(ADR-010)。soft_deleted_at タイムスタンプは、イメージがソフト削除された時刻を記録し、必要に応じて復元を可能にします。イメージの削除は、リクエスト時間内のトランザクションで完了できない唯一のコンテナ削除でもあります。タグの削除では 1 行が削除され、マニフェストの削除は ADR-004 の 1,000 タグ上限によって制限されますが、1 つのイメージには最大 25,000 個のマニフェストを格納でき、各マニフェストは 200 個の参照を持つことができます。そのため、イメージを削除すると行をマークし、バックグラウンド処理に回収を委ねます。このため、このテーブルはイメージレベルの削除マーカー検出インデックスを持ち、マニフェストレベルの削除マーカーインデックスでは代用できません(以下のインデックス一覧を参照してください)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_blobs: コンテナイメージを構成する、個々のコンテンツアドレス可能なレイヤーと設定オブジェクトを保存します。マニフェストとその構成要素であるレイヤー(blob)の関係は暗黙的であり(実行時にマニフェストの内容を解析することで決定される)、データベースの外部キーとしてはモデル化されません。soft_deleted_at タイムスタンプは、blob がソフト削除された時刻を記録し、必要に応じて復元を可能にします。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_manifests: 特定のイメージバージョンの設定とレイヤーを記述するイメージマニフェストを表します。size カラムは、ここをルートとするマニフェストツリーの合計バイトサイズを保持します。すなわち、このマニフェスト自体のペイロードに加えて、そこから到達可能なすべての blob(マニフェストリストや OCI インデックスの場合は子マニフェストを通じて推移的に到達するものを含む)です。gitlab_user_id はどの GitLab ユーザーがこのマニフェストをプッシュしたかを記録します。NULL 許容の不透明なテキスト参照で外部キーはなく、repositories の同等のカラムと同じ根拠によります。ユーザーレコードはモノリスに存在し、ユーザーハンドルとアバターのレンダリングはコンシューマーの責任であり、AR スキーマは ID のみを保存し、TEXT はスキーマを上流のユーザー ID 形式の将来の変更から隔離します。gitlab_project_id と gitlab_git_commit_sha は、その帰属に公開コンテキストの残りを加えて拡張します。gitlab_project_id はプッシュ元の GitLab プロジェクト(例: CI_PROJECT_ID)であり、gitlab_user_id と同じモノリス参照の理由から NULL 許容の不透明テキストとして保存されます。gitlab_git_commit_sha は公開時の Git コミット(例: CI_COMMIT_SHA)で、ハッシュカラムのスキーマ規約に従って NULL 許容の bytea として保存されます。可変長で、SHA-1(20 バイト)と SHA-256(32 バイト)の両方に収まります。これはモノリス参照ではなく公開時の事実なので、外部キーは不要です。両方とも、CI コンテキストなしでプッシュが到着した場合(例: 開発者のワークステーションからの手動プッシュ)には NULL になります。soft_deleted_at タイムスタンプは、マニフェストがソフト削除された時刻を記録し、必要に応じて復元を可能にします。created_at はマニフェストが最初にプッシュされた時刻を記録します。ネームスペースごとの時刻順インデックスと組み合わせることで、公開履歴と時間範囲のアーティファクト出所クエリ(例: 「午前 2 時から午前 8 時の間にこのネームスペースにプッシュされたものは何か?」)を支えます。公開イベント自体は削除によって消去されないため、ソフト削除された行も公開履歴に引き続き表示されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_manifest_relationships: Docker マニフェストリストと OCI インデックス(マルチアーキテクチャイメージなど)を扱います。ここでは親マニフェストが複数の他のマニフェストを参照できます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_tags: 特定のマニフェストを指す、人間が読める名前(例: latest、v1.2.3)を提供します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

container_blobs テーブルは、他のコンテナレジストリアーキテクチャがそうするかもしれない方法で、コンテナレジストリの物理的な blob を直接保存することはしません。ここでの違いは、blob ストレージが blob ストレージ テーブルで処理される(重複排除とガベージコレクションも含む)ことです。したがって、container_* レベルでは、単に blob_storage_attachments レコードへの参照を保存するだけで済みます。

インデックス

  • container_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親リポジトリ参照によってコンテナリポジトリを検索します。
  • container_images: (namespace_id, container_repository_id, name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — イメージ名はリポジトリ内で一意なイメージを識別します。重複すると OCI の名前ベースの検索が壊れます。部分条件により、ソフト削除後に同じ名前のイメージを再作成できます。(namespace_id, container_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — keep_last_downloaded_at ライフサイクルルールの評価をサポートします。リポジトリ内のすべてのイメージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったイメージのみを返します。NULLS FIRST は、一度もダウンロードされていないイメージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたイメージを削除時刻の順に一覧表示し、イメージ粒度のゴミ箱一覧とリーパーによるイメージレベルのスキャンを支えます。この形の container_manifests インデックスは、どちらにも使用できません。このインデックスはマニフェストをキーとし、ソフト削除されたイメージのマニフェストには独自の soft_deleted_at がないため、マニフェストのスキャンでもリポジトリの走査でも、リポジトリがまだ有効な削除マーカー付きのイメージには到達できません。
  • container_blobs: (namespace_id, container_image_id, digest) WHERE soft_deleted_at IS NULL に対するユニークインデックス — blob のダイジェストはコンテンツアドレス指定です。同じイメージ内の同じダイジェストは定義上同じ blob です。部分条件により、ソフト削除後に同じダイジェストを再プッシュできます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによって blob を検索します。(namespace_id, digest) に対するインデックス — コンテンツダイジェストによるイメージ横断の検索で、blob のマウントに使用します。コンテナ blob はコンテンツアドレス指定であるため、その digest は保存された blob_sha256 と等しくなります。このため、このインデックスは別個の (namespace_id, blob_sha256) インデックスなしに、クロスフォーマットのチェックサム検索と脆弱性影響クエリ「この侵害されたダイジェストが与えられたとき、どのイメージがそれを参照しているか?」にも使用されます。これはレイヤーまたは設定のダイジェストの検索です。このドキュメントの逆引きインデックスは無条件(soft_deleted_at 述語なし)なので、かつて参照されたダイジェストは監査証跡に引き続き表示されます。現在影響を受けているアーティファクトのみを必要とする脆弱性影響は、クエリ時に soft_deleted_at IS NULL を追加します。これは小さな中間集合に対する安価な後置フィルターです。Maven、npm、generic のファイルはコンテンツアドレス指定ではなく、同等のダイジェストカラムもないため、これらのテーブルには専用の (namespace_id, blob_sha256) 逆引きインデックスがあり、コンテナテーブルにはありません。この等価性は sha256 が唯一のダイジェストアルゴリズムである間に成り立ちます。digest と blob_sha256 はどちらも octet_length = 32 CHECK の下で 32 バイトの値であり、コンテンツは書き込み時にダイジェストに対して検証されるため、(namespace_id, digest) インデックスは blob_sha256 インデックスとまったく同じ行を返します。将来、sha256 以外のダイジェストアルゴリズムが導入される場合は、2 つのカラムを分離し、再検討します。
  • container_manifests: (namespace_id, container_image_id, digest) WHERE soft_deleted_at IS NULL に対するユニークインデックス — マニフェストのダイジェストはコンテンツアドレス指定です。同じイメージ内の同じダイジェストは定義上同じマニフェストです。部分条件により、ソフト削除後に同じダイジェストを再プッシュできます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってマニフェストを検索します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたマニフェストを削除時刻順に一覧し、コンテナイメージのアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体の時系列スキャンで、公開履歴のページネーションと時間範囲のアーティファクト出所クエリを支えます。無条件(soft_deleted_at 述語なし)なので、後でソフト削除された公開イベントも監査証跡に引き続き表示されます。(namespace_id, container_image_id, id) に対するユニークインデックス — container_manifest_relationships からの、イメージにスコープされた 2 つのマニフェスト参照のターゲットです。主キーによってこの 3 つの値の組はすでに一意なので、このインデックスは一意性の保証を追加しません。このインデックスが存在するのは、PostgreSQL が外部キーの参照先カラムに対して明示的に宣言されたユニーク制約またはインデックスを必要とし、主キーからそのような制約を推論しないためです。container_blobs とは異なり、container_manifests にはスタンドアロンの (namespace_id, digest) インデックスがありません。ダイジェストはイメージ内でのみ、ユニークな (namespace_id, container_image_id, digest) によってインデックス化されます。そのため、マニフェストペイロードのイメージ横断チェックサム検索(マニフェストの digest は保存された blob_sha256 と等しい)は、ネームスペースで絞り込まれたパーティションをスキャンします。これは許容されています。これを発行する MVP エンドポイントはなく、脆弱性影響の検索は container_blobs の (namespace_id, digest) インデックスによって提供されるレイヤー/設定ダイジェスト検索であるためです。
  • container_manifest_relationships: (namespace_id, parent_container_manifest_id, child_container_manifest_id) に対するユニークインデックス — 親子関係の重複を防ぎ、特定の親マニフェストのすべての子を見つけます。(namespace_id, child_container_manifest_id, parent_container_manifest_id) に対するインデックス — 特定の子マニフェストのすべての親を、ソートではなくインデックスから親 ID 順に取得します。(namespace_id, container_image_id) に対するインデックス — 特定のイメージのすべてのマニフェスト関係を見つけます。
  • container_tags: (namespace_id, container_image_id, name) に対するユニークインデックス — イメージ内で名前によってタグを検索します。(namespace_id, container_manifest_id, name) に対するインデックス — 特定のマニフェストを指すすべてのタグを、ソートではなくインデックスから名前順に取得します。

クエリ例

  • 名前でイメージを取得する

    SELECT *
    FROM container_images
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND container_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND name = 'myapp/backend'
      AND soft_deleted_at IS NULL;
    
  • リポジトリ ID に対してダイジェストで blob を取得する

    SELECT cb.*
    FROM container_blobs cb
    JOIN container_images ci
      ON cb.container_image_id = ci.id AND cb.namespace_id = ci.namespace_id
    WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = '019a1b2c-0123-7abc-8def-000000000123'
      AND cb.digest = 'sha256:abcd1234...'::bytea
      AND ci.soft_deleted_at IS NULL AND cb.soft_deleted_at IS NULL;
    
  • リポジトリ ID に対してダイジェストでマニフェストを取得する

    SELECT cm.*
    FROM container_manifests cm
    JOIN container_images ci
      ON cm.container_image_id = ci.id AND cm.namespace_id = ci.namespace_id
    WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND ci.container_repository_id = '019a1b2c-0123-7abc-8def-000000000123'
      AND cm.digest = 'sha256:efgh5678...'::bytea
      AND ci.soft_deleted_at IS NULL AND cm.soft_deleted_at IS NULL;
    
  • チェックサム検索と脆弱性影響: 保存された blob の sha256 が与えられたとき、それを参照するネームスペース内のすべてのアーティファクトを見つけます。namespace_id の等価性により、テーブルごとに単一のパーティションに絞り込みます。Maven、npm、generic のファイルは (namespace_id, blob_sha256) インデックスを使用し、パーティションをスキャンせずに一致する行を直接返します。コンテナテーブルはコンテンツアドレス指定です。blob またはマニフェストの digest は保存された blob_sha256 と等しいため、container_blobs は (namespace_id, digest) インデックスを通じてこれに応答します。container_manifests にはスタンドアロンのダイジェストインデックスがないため、マニフェストペイロードの検索はネームスペースで絞り込まれたパーティションをスキャンします。チェックサム検索はすべての参照を返します。脆弱性影響(「この侵害されたダイジェストによって現在影響を受けているアーティファクトはどれか?」)は、結果をアクティブなアーティファクトに限定するために soft_deleted_at IS NULL を追加します。

    -- Single format: container layer/config blobs referencing the digest.
    -- container_blobs is content-addressed (digest = blob_sha256) and has a
    -- (namespace_id, digest) index, so this reverse lookup rides that index
    -- rather than a dedicated blob_sha256 one. container_manifests has no
    -- standalone digest index. See the cross-format query below.
    SELECT cb.id, cb.container_image_id, cb.digest
    FROM container_blobs cb
    WHERE cb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND cb.digest = 'sha256:abcd1234...'::bytea;
    
    -- Cross-format: every artifact referencing the digest, active rows only (vulnerability impact)
    SELECT 'container_blob' AS artifact_kind, cb.id AS artifact_id, cb.container_image_id AS parent_id
    FROM container_blobs cb
    WHERE cb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND cb.digest = 'sha256:abcd1234...'::bytea AND cb.soft_deleted_at IS NULL
    UNION ALL
    -- container_manifests has no standalone (namespace_id, digest) index, so this
    -- arm scans the namespace-pruned partition (accepted: no MVP endpoint issues
    -- a cross-image manifest-payload checksum search).
    SELECT 'container_manifest', cm.id, cm.container_image_id
    FROM container_manifests cm
    WHERE cm.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND cm.digest = 'sha256:abcd1234...'::bytea AND cm.soft_deleted_at IS NULL
    UNION ALL
    SELECT 'maven_file', mf.id, mf.maven_version_id
    FROM maven_files mf
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND mf.blob_sha256 = 'sha256:abcd1234...'::bytea AND mf.soft_deleted_at IS NULL
    UNION ALL
    SELECT 'npm_file', nf.id, nf.npm_version_id
    FROM npm_files nf
    WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND nf.blob_sha256 = 'sha256:abcd1234...'::bytea AND nf.soft_deleted_at IS NULL
    UNION ALL
    SELECT 'generic_file', gf.id, gf.generic_version_id
    FROM generic_files gf
    WHERE gf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND gf.blob_sha256 = 'sha256:abcd1234...'::bytea AND gf.soft_deleted_at IS NULL;
    

    同じ (namespace_id, blob_sha256) アクセスパスは、キャッシュ側のテーブル(container_remote_blobs、container_remote_manifests、maven_remote_files、npm_remote_files)と、npm_metadata_files / npm_remote_metadata_files にも適用されます。キャッシュされた参照もカバーするには、UNION ALL をそれらのテーブルに拡張してください。Generic はホスト型のみなので、キャッシュ側のテーブルはありません。

コンテナリモートリポジトリ

リモートリポジトリは、プロキシおよびキャッシュできる外部のコンテナレジストリを表します。これらは独自のライフサイクルを持つスタンドアロンエンティティであり、複数の仮想リポジトリ間で共有できます。これらは親の repositories テーブルを介して仮想リポジトリの上流から参照されます。

erDiagram
    repositories ||--|| container_remote_repositories : "has one"
    container_remote_repositories ||--o{ container_remote_images : "has many"
    container_remote_images ||--o{ container_remote_blobs : "has many"
    container_remote_images ||--o{ container_remote_manifests : "has many"
    container_remote_images ||--o{ container_remote_manifest_relationships : "has many"
    container_remote_images ||--o{ container_remote_tags : "has many"
    container_remote_tags ||--|| container_remote_manifests : "has one"
    container_remote_blobs ||--|| blob_storage_attachments : "has one"
    container_remote_manifests ||--|| blob_storage_attachments : "has one"
    container_remote_manifest_relationships ||--|| container_remote_manifests : "has one (parent_id)"
    container_remote_manifest_relationships ||--|| container_remote_manifests : "has one (child_id)"

    container_remote_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
        text url "NOT NULL, limit 1024"
        text auth_url "nullable, limit 1024"
        bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
        uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
        int ns_key_version "nullable, the namespace key version this row was wrapped under"
        bytea encrypted_username
        bytea encrypted_password
        smallint cache_validity_hours "NOT NULL, DEFAULT 24"
        smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
        timestamptz last_health_checked_at "nullable"
    }

    container_remote_images {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_remote_repository_id FK "NOT NULL, (container_remote_repository_id, namespace_id) references container_remote_repositories(id, namespace_id)"
        text name "NOT NULL, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    container_remote_blobs {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
        bytea digest "NOT NULL, CHECK octet_length = 32"
        text media_type "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz soft_deleted_at "nullable"
    }

    container_remote_manifests {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
        bytea digest "NOT NULL, CHECK octet_length = 32"
        text media_type "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, CHECK octet_length = 32, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        bigint size "NOT NULL, updated as children are cached"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    container_remote_manifest_relationships {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
        uuid parent_container_remote_manifest_id FK "NOT NULL, (parent_container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
        uuid child_container_remote_manifest_id FK "NOT NULL, (child_container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
    }

    container_remote_tags {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid container_remote_image_id FK "NOT NULL, (container_remote_image_id, namespace_id) references container_remote_images(id, namespace_id)"
        uuid container_remote_manifest_id FK "NOT NULL, (container_remote_manifest_id, namespace_id) references container_remote_manifests(id, namespace_id)"
        text name "NOT NULL, limit 255"
        timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
        text upstream_etag "nullable, limit 255"
    }
  • container_remote_repositories: 外部のコンテナレジストリを表します。URL、オプションの認証 URL(auth_url)、認証情報、キャッシュ TTL(cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_id を介して親の repositories テーブルを参照します。リモートリポジトリはスタンドアロンであるため、同じリモートを使用する 2 つの仮想リポジトリは 1 つのキャッシュを共有します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_remote_images: リモートリポジトリ内のキャッシュされたコンテナイメージです。container_images を反映します。last_downloaded_at はキャッシュされたイメージが最後にプルされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込み(repositories.downloads_count と同じパターン)を介して維持されます。keep_last_downloaded_at ライフサイクルルールとキャッシュ保持の評価に使用されます(ADR-010)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_remote_blobs: キャッシュされたレイヤーまたは設定 blob です。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_remote_manifests: キャッシュされたイメージマニフェストです。size カラムは、このキャッシュが把握しているサブツリーのバイトフットプリントを保持します。すなわち、キャッシュ時のマニフェスト自体のペイロードに加えて、子が到着するにつれて各子の size が加わります。イメージマニフェストの場合、値はキャッシュ時に完全です。マニフェストリストと OCI インデックスの場合、子がフェッチされるにつれて段階的にツリー全体のフットプリントに収束し、一部の子が一度もプルされなければ部分的なままになることもあります。この段階的なセマンティクスは遅延リモートキャッシングを反映しています。size を完全に保つためだけに積極的に子をフェッチすると、遅延設計が損なわれます。created_at はマニフェストが最初にキャッシュされた時刻を記録し、ホスト型の同等のもの(container_manifests)と同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_remote_manifest_relationships: キャッシュされたマルチアーキテクチャのマニフェストリスト関係です。ホスト型の同等のものと同じ構造です。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_remote_tags: キャッシュされたタグからマニフェストへのマッピングです。タグは可変なポインターであり、キャッシュ再検証時にタグが新しいマニフェストに再ポイントされることがあります。upstream_checked_at はタグが上流レジストリに対して最後に検証された時刻を記録し、cache_validity_hours と比較して再検証が必要かどうかを判断します。upstream_etag は上流が返した ETag を保存し、タグがまだ同じマニフェストを指している場合に完全なマニフェスト解決を回避するための条件付きリクエスト(If-None-Match)を可能にします。マニフェストと blob は暗号学的ハッシュによってコンテンツアドレス指定されるため、鮮度の追跡は不要です。保存されたバイトがダイジェストと一致すれば、コンテンツが正しいことが保証されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

インデックス

  • container_remote_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id) に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。
  • container_remote_images: (namespace_id, container_remote_repository_id, name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — 名前によってキャッシュされたイメージを検索します。部分条件により、ソフト削除後に同じ名前のイメージを再作成できます。
  • container_remote_blobs: (namespace_id, container_remote_image_id, digest) WHERE soft_deleted_at IS NULL に対するユニークインデックス — イメージ内でダイジェストによってキャッシュされた blob を検索します。部分条件により、ソフト削除後に同じダイジェストを再キャッシュできます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによって blob を検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされた blob への逆引きで、チェックサム検索と脆弱性影響がキャッシュ側の参照もカバーするようにします。このインデックスはリモートキャッシュ固有です。ホスト型の container_blobs テーブルはコンテンツアドレス指定であり、同じ検索を (namespace_id, digest) インデックスで処理するため、これを反映する blob_sha256 インデックスはありません。リモートテーブルには、逆引きに再利用できるスタンドアロンの (namespace_id, digest) インデックスもありません。これはプル専用で、blob マウントパスを正当化するものがないためです。そのため、チェックサム検索とサイズ照合クエリが blob_storage_blobs(namespace_id, sha256) と JOIN するカラムである blob_sha256 を直接インデックス化します。
  • container_remote_manifests: (namespace_id, container_remote_image_id, digest) WHERE soft_deleted_at IS NULL に対するユニークインデックス — イメージ内でダイジェストによってキャッシュされたマニフェストを検索します。部分条件により、ソフト削除後に同じダイジェストを再キャッシュできます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってマニフェストを検索します。(namespace_id, blob_sha256) に対するインデックス — マニフェストペイロードの保存された blob の sha256 から、それを参照するすべてのキャッシュされたマニフェストへの逆引きで、チェックサム検索と脆弱性影響がキャッシュ側の参照もカバーするようにします。このインデックスはリモートキャッシュ固有です。ホスト型の container_manifests テーブルには同等のインデックスがありません。また、リモート blob テーブルと同様に、リモートマニフェストテーブルにも再利用できるスタンドアロンの (namespace_id, digest) インデックスがないため、逆引きには blob_sha256 を直接インデックス化します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたキャッシュ済みマニフェストを削除時刻順に一覧し、キャッシュされたコンテナイメージのアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体の時系列スキャンで、ホスト型の container_manifests インデックスを反映し、キャッシュ側の公開履歴と出所をカバーします。ホスト型のインデックスと同じ監査証跡の理由から無条件(soft_deleted_at 述語なし)です。
  • container_remote_manifest_relationships: (namespace_id, parent_container_remote_manifest_id, child_container_remote_manifest_id) に対するユニークインデックス — 親子関係の重複を防ぎます。(namespace_id, child_container_remote_manifest_id) に対するインデックス — 特定の子マニフェストのすべての親を見つけます。ホスト型側のインデックスではカラム数を増やしますが、対応するこちらのインデックスは 2 カラムの形式を維持します。これは、このテーブルに書き込む処理がないためです。遅延キャッシュされたインデックスは子へのリンクを記録しないため、キャッシュ側で関係を記録する処理が導入されるまで、どの読み取りも空の結果を返します(artifact-registry#264)。(namespace_id, container_remote_image_id) に対するインデックス — 特定のイメージのすべてのマニフェスト関係を見つけます。
  • container_remote_tags: (namespace_id, container_remote_image_id, name) に対するユニークインデックス — イメージ内で名前によってタグを検索します。(namespace_id, container_remote_manifest_id) に対するインデックス — 特定のマニフェストを指すすべてのタグを見つけます。ホスト型側のインデックスではカラム数を増やしますが、対応するこちらのインデックスは 2 カラムの形式を維持します。これは読み取りコストが低いからではありません。キャッシュされた名前が任意の順序に並ぶ場合、同じ 100 ms の許容時間に対し、測定値は 110 〜 144 ms です。このテーブルのサイズを制限する書き込み経路がないため、キャッシュ行数にはインデックスのサイズ設計の基準となる上限がありません。対処には、キャッシュ格納側で上限を設けるかという問題の解決が必要です(artifact-registry#669)。

クエリ例

  • リモートリポジトリを作成する

    -- Resolve the default repository collection for the namespace
    SELECT id FROM repository_collections WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND is_default = true;
    -- Create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'docker-hub', 0, 2, 1)
    RETURNING id;
    -- Link the repository to the repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <repository_collection_id>, <returned_id>);
    -- Then create the format-specific record
    INSERT INTO container_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_username, encrypted_password)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://registry.hub.docker.com', $1, $2, $3, $4, $5);
    
  • キャッシュされたマニフェストが新鮮かどうかを確認する

    SELECT crm.digest
    FROM container_remote_manifests crm
    JOIN container_remote_tags crt
      ON crt.container_remote_manifest_id = crm.id AND crt.namespace_id = crm.namespace_id
    JOIN container_remote_images cri
      ON crt.container_remote_image_id = cri.id AND crt.namespace_id = cri.namespace_id
    WHERE cri.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND cri.container_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789'
      AND cri.name = 'library/nginx'
      AND crt.name = 'latest'
      AND cri.soft_deleted_at IS NULL AND crm.soft_deleted_at IS NULL;
    
  • ダイジェストでキャッシュされた blob をプルする(blob ストレージへの読み取りパスのショートカット)

    SELECT bsb.object_storage_key, bsb.size
    FROM container_remote_blobs crb
    JOIN blob_storage_blobs bsb
      ON bsb.namespace_id = crb.namespace_id AND bsb.sha256 = crb.blob_sha256
    WHERE crb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND crb.container_remote_image_id = '019a1b2c-0456-7abc-8def-000000000456'
      AND crb.digest = 'sha256:abcd1234...'::bytea
      AND crb.soft_deleted_at IS NULL;
    

仮想コンテナリポジトリ

erDiagram
    repositories ||--|| container_virtual_repositories : "has one"
    container_virtual_repositories ||--o{ container_virtual_repository_upstreams : "has many"
    container_virtual_repository_upstreams ||--|| repositories : "references upstream"
    container_virtual_repository_upstreams ||--o{ container_virtual_upstream_rules : "has many"

    container_virtual_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE CASCADE"
    }

    container_virtual_repository_upstreams {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid container_virtual_repository_id FK "NOT NULL, (container_virtual_repository_id, namespace_id) references container_virtual_repositories(id, namespace_id) ON DELETE CASCADE"
        uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
        int position "NOT NULL, CHECK position >= 1"
    }

    container_virtual_upstream_rules {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid container_virtual_repository_upstream_id FK "NOT NULL, (container_virtual_repository_upstream_id, namespace_id) references container_virtual_repository_upstreams(id, namespace_id) ON DELETE CASCADE"
        smallint rule_type "NOT NULL, 0=allow, 1=deny, CHECK rule_type IN (0, 1)"
        text pattern "NOT NULL, CHECK char_length(pattern) <= 255"
        smallint target_field "NOT NULL, 0=image, 1=tag, CHECK target_field IN (0, 1)"
    }
  • container_virtual_repositories: コンテナイメージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは upstream_repository_id を介して上流リポジトリを参照し、これは repositories(id, namespace_id) を指します。複合外部キー (namespace_id, upstream_repository_id) は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。position は 1 始まりで連続します。CHECK position >= 1 が下限を定め、管理サーフェスは一覧を変更するトランザクション内で番号を振り直すため、値に隙間はありません。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • container_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。ルールは(リモートリポジトリごとではなく)上流参照ごとのままで、包含/除外パターンが仮想-上流の関連付けごとに設定される JFrog モデルと一致します。データベースは 2 つの列挙型とパターン長を、CHECK rule_type IN (0, 1)、CHECK target_field IN (0, 1)、CHECK char_length(pattern) <= 255 で強制します。これは境界での拒否であり、範囲外の識別子は読み取りパスに処理を任せず、書き込み時に拒否されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

これら 3 つのテーブルの外部キーには、次の参照アクションがあります。

  • container_virtual_repositories の repositories を参照する FK (repository_id, namespace_id): フォーマット子テーブルの一般ルールで定められた ON DELETE CASCADE。
  • container_virtual_repositories の namespaces を参照する FK namespace_id: NO ACTION。
  • container_virtual_repository_upstreams の namespaces を参照する FK namespace_id: NO ACTION。
  • container_virtual_repository_upstreams の container_virtual_repositories を参照する FK (container_virtual_repository_id, namespace_id): ON DELETE CASCADE。
  • container_virtual_repository_upstreams の repositories を参照する FK (upstream_repository_id, namespace_id): NO ACTION。
  • container_virtual_upstream_rules の namespaces を参照する FK namespace_id: NO ACTION。
  • container_virtual_upstream_rules の container_virtual_repository_upstreams を参照する FK (container_virtual_repository_upstream_id, namespace_id): ON DELETE CASCADE。

container_virtual_repository_upstreams の 2 つの外部キーが反対のアクションを取るのは意図的です。container_virtual_repository_id は親から子へのリンクなので、仮想リポジトリを削除すると upstream の関連付けも削除されます。upstream_repository_id は兄弟参照なので、まだ upstream として一覧に含まれるリポジトリの削除は拒否されます。そこでカスケードすると、一覧に含めるすべての仮想リポジトリが縮小し、空になる可能性もありますが、オペレーターには見えません。キーはシグナルを出さずに縮小を拒否するため、repositories の削除パスがそれを扱います。upstream の削除は意図的な管理操作でなければならず、以下の逆引きインデックスもそのために存在します。

インデックス

  • container_virtual_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。
  • container_virtual_repository_upstreams: (namespace_id, container_virtual_repository_id, position) に対する DEFERRABLE INITIALLY DEFERRED ユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にするために遅延可能(遅延可能)です。(namespace_id, container_virtual_repository_id, upstream_repository_id) に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id) に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記の NO ACTION ガードと関連付けサーフェスが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。
  • container_virtual_upstream_rules: (namespace_id, container_virtual_repository_upstream_id) に対するインデックス — 特定の上流のすべてのルールを取得します。

順序付き upstream のエントリはユニークインデックスではなくユニーク制約です。遅延は制約の属性なので、ALTER TABLE ... ADD CONSTRAINT ... UNIQUE (...) DEFERRABLE INITIALLY DEFERRED で追加します。PostgreSQL では CREATE UNIQUE INDEX ... DEFERRABLE は構文エラーです。npm の仮想テーブルには同じ方法で npm_virtual_repository_upstreams に追加された unique_nvru_ns_id_repository_id_position がすでにあります。コンテナのマイグレーションはパターンを新設せず、兄弟のものをコピーします。ただし、npm では 0、ここでは 1 である position の下限はコピーしません。遅延可能なユニーク制約は ON CONFLICT の調停にも使えないため、position の書き込みはこのキーへのアップサートではなく、UPDATE の後に INSERT を行います。

クエリ例

  • 仮想リポジトリを作成する

    -- First create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 0, 1, 1)
    RETURNING id;
    -- Link the repository to a repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>);
    -- Then create the format-specific record
    INSERT INTO container_virtual_repositories (namespace_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);
    
  • 仮想リポジトリを上流に関連付ける

    INSERT INTO container_virtual_repository_upstreams (namespace_id, container_virtual_repository_id, upstream_repository_id, position)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
    

Maven リポジトリ

Maven パッケージは、ファイルの集合(.jar、.pom、maven-metadata.xml)を表します。したがって、単一の Maven パッケージのダウンロードは 4 〜 15 個の API リクエストを表すことがあります。

erDiagram
    repositories ||--|| maven_repositories : "has one"
    maven_repositories ||--o{ maven_packages : "has many"
    maven_packages ||--o{ maven_versions : "has many"
    maven_packages ||--o{ maven_files : "has many"
    maven_versions ||--o{ maven_files : "has many"
    maven_files ||--|| blob_storage_attachments : "has one"

    maven_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
    }

    maven_packages {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_repository_id FK "NOT NULL, (maven_repository_id, namespace_id) references maven_repositories(id, namespace_id)"
        text group_id "NOT NULL, limit 255"
        text artifact_id "NOT NULL, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    maven_versions {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_package_id FK "NOT NULL, (maven_package_id, namespace_id) references maven_packages(id, namespace_id)"
        text version "NOT NULL, limit 255"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        text gitlab_user_id "nullable, opaque string, limit 255"
        text gitlab_project_id "nullable, opaque string, limit 255"
        bytea gitlab_git_commit_sha "nullable"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    maven_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_package_id FK "NOT NULL, (maven_package_id, namespace_id) references maven_packages(id, namespace_id)"
        uuid maven_version_id FK "nullable, (maven_version_id, maven_package_id, namespace_id) references maven_versions(id, maven_package_id, namespace_id)"
        text file_name "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        bytea sha1 "NOT NULL"
        bytea md5 "nullable"
        bytea sha512 "NOT NULL"
        timestamptz soft_deleted_at "nullable"
    }
  • maven_repositories: 複数のパッケージのコンテナです。各リポジトリはグループ ID とアーティファクト ID で識別される複数のパッケージをホストできます。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_packages: そのグループ ID とアーティファクト ID で識別される Maven パッケージ(例: com.example:myapp)を表します。last_downloaded_at はパッケージのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。NULL はパッケージが一度もダウンロードされていないことを意味し、keep_last_downloaded_at ライフサイクルルールの評価では可能な限り最も古いダウンロード時刻として扱われます(すなわち、ダウンロードベースの保持の下で削除の対象となります)。keep_last_downloaded_at ライフサイクルルールによって、ダウンロードベースの保持を評価するために使用されます(ADR-010)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_versions: Maven パッケージの個々の バージョン(例: 1.0.0、2.1.3-SNAPSHOT)を保存します。last_downloaded_at はバージョンのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_at ライフサイクルルールによって使用されます。gitlab_user_id、gitlab_project_id、gitlab_git_commit_sha は、どの GitLab ユーザーがこのバージョンを公開したか、および公開の背後にある CI コンテキスト(プロジェクト、コミット)を記録し、container_manifests の同等のカラムと同じ形状・根拠によります。created_at はバージョンが最初に公開された時刻を記録し、container_manifests と同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_files: Maven パッケージに関連付けられた個々のファイルを表します。ファイルは、maven_version_id が設定されたバージョン固有のもの(JAR、POM、ソース、Javadoc、チェックサム)か、maven_version_id が NULL のパッケージレベルのもの(maven-metadata.xml とそのチェックサムなど)のいずれかです。maven_package_id は常に設定されており、パッケージからそのすべてのファイルへの直接のパスを提供します。また、レジストリがパフォーマンスのボトルネックを改善するために使用する補助ファイルである場合もあります。sha1 と md5 カラムは、整合性検証のために Maven プロトコルが要求するチェックサム を保存します。Maven クライアントは、すべてのアーティファクトとともに .sha1 と .md5 のサイドカーファイルを期待します。これらのカラムは blob_storage_blobs ではなく maven_files にあります。なぜなら、これらは Maven プロトコルの関心事であり、普遍的な blob プロパティではないためです。他のフォーマット(OCI コンテナ)は SHA256 のみを使用します。これらをここに置くことで、blob_storage_blobs をフォーマット固有のカラムやインデックスを持たないフォーマット非依存のテーブルとして保ちます。sha1 は Maven プロトコルがそれを要求するため NOT NULL です。md5 は Maven 3.9+ が MD5 チェックサムを非推奨にした ため NULL 許容です。sha512 は NOT NULL です。なぜなら、Maven プロトコルはレジストリが提供できなければならない .sha512 サイドカーを公開しており、その値はバイトが永続化される前にハンドラーを通過する際のアップロード中に常に計算可能だからです。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

私たちは、パッケージ名(この場合はグループ ID とアーティファクト ID)とバージョンを同じテーブルに保存していません。その理由は、UI がこのデータにパッケージ名でアクセスするためです。パッケージ名がフォルダであり、それを開くと各バージョンに 1 つずつサブフォルダがあるツリー状の UI を想像してください。この最初のリクエストはフォルダ、すなわちパッケージ名を一覧する必要があります。フォルダを開くと、すべてのサブフォルダ、すなわちパッケージバージョンを一覧するリクエストがトリガーされます。したがって、このアクセスパターンを容易にするために、2 つの専用テーブル(maven_packages と maven_versions)を持っています。

インデックス

  • maven_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親リポジトリ参照によって Maven リポジトリを検索します。
  • maven_packages: (namespace_id, maven_repository_id, group_id, artifact_id) WHERE soft_deleted_at IS NULL に対するユニークインデックス — リポジトリ内で Maven 座標によってパッケージを検索します。部分条件により、ソフト削除後に同じ座標のパッケージを再作成できます。(namespace_id, maven_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — keep_last_downloaded_at ライフサイクルルールの評価をサポートします。リポジトリ内のすべてのパッケージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったパッケージのみを返します。NULLS FIRST は、一度もダウンロードされていないパッケージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたパッケージを削除時刻順に一覧し、リーパーのパッケージレベルスキャンを支えます。maven_versions の同形インデックスでは対応できません。パッケージ削除はパッケージ行をマークしますが子行を書き込まないため、トゥームストーンが付いたパッケージ配下のバージョンは自身の soft_deleted_at を持たず、リポジトリが生存している間はバージョンレベルスキャンでもリポジトリ走査でもそのパッケージに到達できません。このインデックスがなければ、リーパーはそのパッケージを見つけるために maven_packages をシーケンシャルスキャンする必要があります。
  • maven_versions: (namespace_id, maven_package_id, version) WHERE soft_deleted_at IS NULL に対するユニークインデックス — パッケージ内の特定のバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, maven_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — maven_packages と同じ範囲スキャン戦略を使用し、パッケージのバージョンにスコープされた keep_last_downloaded_at ライフサイクルルールの評価を支えます。(namespace_id, maven_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL に対するインデックス — ランディングページのリポジトリソートと同様に、バージョン一覧表示でパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたバージョンを削除時刻順に一覧し、Maven アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体を時系列でスキャンし、公開履歴のページネーションと時間範囲を指定したアーティファクトの来歴クエリを支えます。ソフト削除された公開イベントも監査証跡に表示されるよう、無条件のインデックスとします。(namespace_id, maven_package_id, id) に対するインデックス — maven_package_id の外部キーチェックに対応します。チェックには述語がないため、非部分インデックスです。ソフト削除されたバージョンも自身のパッケージを参照するため、前述の部分ユニークインデックスでは対応できません。
  • maven_files: (namespace_id, maven_version_id, file_name) WHERE soft_deleted_at IS NULL AND maven_version_id IS NOT NULL に対するユニークインデックス — バージョン固有のファイル名はバージョン内で一意でなければなりません。部分条件はソフト削除された行とパッケージレベルのファイルを除外します。(namespace_id, maven_package_id, file_name) WHERE soft_deleted_at IS NULL AND maven_version_id IS NULL に対するユニークインデックス — パッケージレベルのファイル名(maven-metadata.xml など)はパッケージ内で一意でなければなりません。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべての Maven ファイルへの逆引きで、クロスフォーマットのチェックサム検索を支えます。既存の親をキーとするインデックスはバージョンまたはパッケージをキーとするため、ダイジェストをキーとするスキャンを直接満たすことはできません。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL AND maven_version_id IS NULL AND file_name = 'maven-metadata.xml' に対するインデックス — メタデータパージの検出スキャン用に、ネームスペース内のパッケージレベルの maven-metadata.xml のトゥームストーンを古い順に一覧表示します。メタデータのみを対象とする述語により、インデックスをパージ処理が読み取るパッケージレベルのメタデータファイルに限定します。また、DESC により、古い順に読み取る処理は、新しい順に読み取る処理が順方向にスキャンするのと同じコストで、インデックスを逆方向にスキャンできます。同じキーの単純なトゥームストーンインデックスでもこのスキャンに対応できますが、すべてのファイルのトゥームストーンを保持するため、3 条件の論理積からなる述語によってインデックスをパージ処理が読み取る行に限定します。(namespace_id, maven_package_id, blob_sha256) に対するインデックス — maven_package_id の外部キーチェックと、インデックスから blob_sha256 を読み取るリポジトリレベルのサイズ走査の結合を支えます。チェックには述語がなく、走査はソフト削除された行にもバージョンに紐づく行にも到達するため、非部分インデックスです。(namespace_id, maven_version_id, blob_sha256) に対するインデックス — maven_version_id の外部キーチェックと、インデックスから blob_sha256 を読み取るアーティファクトレベルのサイズ再計算に対応します。チェックには述語がなく、再計算はソフト削除された行にも到達するため、非部分インデックスです。ソフト削除されたファイルも自身のバージョンを参照するため、maven_version_id に対する部分ユニークインデックスではどちらにも対応できません。

クエリ例

  • 特定のリポジトリ ID とパッケージ名のパッケージバージョンを取得する。

    SELECT mv.*
    FROM maven_versions mv
    JOIN maven_packages mp
      ON mv.maven_package_id = mp.id AND mv.namespace_id = mp.namespace_id
    WHERE mp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mp.maven_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND mp.group_id = 'com.example' AND mp.artifact_id = 'myapp'
      AND mv.version = '1.0.0'
      AND mp.soft_deleted_at IS NULL AND mv.soft_deleted_at IS NULL;
    
  • バージョン ID とファイル名でファイルを取得する。

    SELECT mf.*
    FROM maven_files mf
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND mf.file_name = 'myapp-1.0.0.jar'
      AND mf.soft_deleted_at IS NULL;
    
  • 特定のパッケージのパッケージレベルのファイル(例: maven-metadata.xml)を取得する。

    SELECT mf.*
    FROM maven_files mf
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_package_id = '019a1b2c-0123-7abc-8def-000000000123' AND mf.maven_version_id IS NULL
      AND mf.soft_deleted_at IS NULL;
    
  • ゴミ箱一覧: ネームスペース内のすべてのソフト削除された Maven バージョンを、最近削除された順に一覧する((namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL の部分インデックスを使用)。コンプライアンスのユースケースはネームスペース全体(「今ゴミ箱に何があるか?」)です。親にスコープしたビュー(「このパッケージのゴミ箱に入ったバージョン」)には、別個の (namespace_id, maven_package_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL インデックスが有益で、その UI が構築される場合には後から追加できます。同じパターンは npm_versions、container_manifests、およびそれらのリモート版に適用されます。

    SELECT mv.id, mv.maven_package_id, mv.version, mv.soft_deleted_at
    FROM maven_versions mv
    WHERE mv.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mv.soft_deleted_at IS NOT NULL
    ORDER BY mv.soft_deleted_at DESC
    LIMIT 50;
    
  • メタデータパージの検出: メタデータパージのバックグラウンドスキャン用に、ネームスペース内のパッケージレベルの maven-metadata.xml のトゥームストーンを古い順に一覧表示します((namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL AND maven_version_id IS NULL AND file_name = 'maven-metadata.xml' の部分インデックスを使用)。パージは、1 回の実行でサイズに上限のある 1 ページを読み取るバックグラウンドジョブです。インデックスは新しい順に保存されるため、古い順に読み取る処理は、新しい順に読み取る処理が順方向にスキャンするのと同じコストで、インデックスを逆方向にスキャンします。

    SELECT mf.id, mf.maven_package_id, mf.file_name, mf.soft_deleted_at
    FROM maven_files mf
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.soft_deleted_at IS NOT NULL AND mf.maven_version_id IS NULL AND mf.file_name = 'maven-metadata.xml'
    ORDER BY mf.soft_deleted_at ASC
    LIMIT 50;
    

Maven リモートリポジトリ

erDiagram
    repositories ||--|| maven_remote_repositories : "has one"
    maven_remote_repositories ||--o{ maven_remote_packages : "has many"
    maven_remote_packages ||--o{ maven_remote_versions : "has many"
    maven_remote_packages ||--o{ maven_remote_files : "has many"
    maven_remote_versions ||--o{ maven_remote_files : "has many"
    maven_remote_files ||--|| blob_storage_attachments : "has one"

    maven_remote_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
        text url "NOT NULL, limit 1024"
        bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
        uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
        int ns_key_version "nullable, the namespace key version this row was wrapped under"
        bytea encrypted_username
        bytea encrypted_password
        smallint cache_validity_hours "NOT NULL, DEFAULT 24"
        smallint metadata_cache_validity_hours "NOT NULL, DEFAULT 24"
        smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
        timestamptz last_health_checked_at "nullable"
    }

    maven_remote_packages {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_remote_repository_id FK "NOT NULL, (maven_remote_repository_id, namespace_id) references maven_remote_repositories(id, namespace_id)"
        text group_id "NOT NULL, limit 255"
        text artifact_id "NOT NULL, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    maven_remote_versions {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_remote_package_id FK "NOT NULL, (maven_remote_package_id, namespace_id) references maven_remote_packages(id, namespace_id)"
        text version "NOT NULL, limit 255"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    maven_remote_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_remote_package_id FK "NOT NULL, (maven_remote_package_id, namespace_id) references maven_remote_packages(id, namespace_id)"
        uuid maven_remote_version_id FK "nullable, (maven_remote_version_id, maven_remote_package_id, namespace_id) references maven_remote_versions(id, maven_remote_package_id, namespace_id)"
        text file_name "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        bytea sha1 "NOT NULL"
        bytea md5 "nullable"
        bytea sha512 "NOT NULL"
        timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
        text upstream_etag "nullable, limit 255"
        timestamptz soft_deleted_at "nullable"
    }
  • maven_remote_repositories: 外部の Maven リポジトリを表します。URL、認証情報、アーティファクトのキャッシュ TTL(cache_validity_hours)、および maven-metadata.xml などのメタデータレスポンス用の別個の TTL(metadata_cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_id を介して親の repositories テーブルを参照します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_remote_packages: グループ ID とアーティファクト ID で識別される、キャッシュされた Maven パッケージです。maven_packages を反映します。last_downloaded_at はパッケージのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_at ライフサイクルルールとキャッシュ保持の評価に使用されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_remote_versions: Maven パッケージのキャッシュされたバージョンです。maven_versions を反映します。last_downloaded_at はバージョンのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_at ライフサイクルルールとキャッシュ保持の評価に使用されます。created_at はバージョンが最初にキャッシュされた時刻を記録し、maven_versions を反映してキャッシュ側の公開履歴と出所スキャンを支えます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_remote_files: キャッシュされたファイル(JAR、POM、チェックサム、maven-metadata.xml)です。NULL 許容の maven_remote_version_id は、ホスト型と同じパターン(バージョン固有のファイル対 maven-metadata.xml のようなパッケージレベルのファイル)を保ちます。sha1 と md5 は、コンテンツがホストされているかキャッシュされているかに関係なく Maven プロトコルがこれらのチェックサムの提供を要求するため保持されます。sha512 は、ホスト型の maven_files のカラム形状を反映するために整合性の観点から追加され、Maven Virtual 仕様(S30)が単一のクエリパスでどちらのバックエンドからも .sha512 サイドカーを提供できるようにします。値はプロキシ書き込みステップ中に他のチェックサムとともにキャッシュされたバイトから計算されるため、初日から NOT NULL を達成できます。upstream_checked_at はファイルが上流リポジトリに対して最後に検証された時刻を記録し、アーティファクトファイルについては cache_validity_hours、メタデータファイル(例: maven-metadata.xml)については metadata_cache_validity_hours と比較して再検証が必要かどうかを判断します。upstream_etag は上流が返した ETag を保存し、変更されていないファイルの再ダウンロードを回避するための条件付きリクエスト(If-None-Match)を可能にします。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

インデックス

  • maven_remote_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id) に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。
  • maven_remote_packages: (namespace_id, maven_remote_repository_id, group_id, artifact_id) WHERE soft_deleted_at IS NULL に対するユニークインデックス — Maven 座標によってキャッシュされたパッケージを検索します。部分条件により、ソフト削除後に同じ座標のパッケージを再作成できます。
  • maven_remote_versions: (namespace_id, maven_remote_package_id, version) WHERE soft_deleted_at IS NULL に対するユニークインデックス — パッケージ内のキャッシュされたバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, maven_remote_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL に対するインデックス — バージョン一覧表示で、キャッシュされたパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたキャッシュ済みバージョンを削除時刻順に一覧し、キャッシュされた Maven アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体を時系列でスキャンします。ホスト型の maven_versions インデックスを反映し、キャッシュ側の公開履歴と来歴を対象にします。ホスト型のインデックスと同じ監査証跡上の理由から、無条件(soft_deleted_at の述語なし)のインデックスとします。
  • maven_remote_files: (namespace_id, maven_remote_version_id, file_name) WHERE soft_deleted_at IS NULL AND maven_remote_version_id IS NOT NULL に対するユニークインデックス — バージョン固有のファイル名は、バージョン内で一意でなければなりません。(namespace_id, maven_remote_package_id, file_name) WHERE soft_deleted_at IS NULL AND maven_remote_version_id IS NULL に対するユニークインデックス — パッケージレベルのファイル名は、パッケージ内で一意でなければなりません。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存済み blob の sha256 から、それを参照するすべてのキャッシュ済み Maven ファイルを逆引きします。ホスト型の maven_files インデックスを反映し、チェックサム検索でキャッシュ側の参照も対象にします。

クエリ例

  • リモートリポジトリを作成する

    -- First create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'central', 1, 2, 0)
    RETURNING id;
    -- Link the repository to a repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>);
    -- Then create the format-specific record
    INSERT INTO maven_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_username, encrypted_password)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://repo.maven.apache.org/maven2', $1, $2, $3, $4, $5);
    
  • 座標でキャッシュされた Maven ファイルを検索する

    SELECT mrf.*, bsb.object_storage_key
    FROM maven_remote_files mrf
    JOIN maven_remote_versions mrv
      ON mrf.maven_remote_version_id = mrv.id AND mrf.namespace_id = mrv.namespace_id
    JOIN maven_remote_packages mrp
      ON mrv.maven_remote_package_id = mrp.id AND mrv.namespace_id = mrp.namespace_id
    JOIN blob_storage_blobs bsb
      ON bsb.namespace_id = mrf.namespace_id AND bsb.sha256 = mrf.blob_sha256
    WHERE mrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND mrp.maven_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789'
      AND mrp.group_id = 'com.example'
      AND mrp.artifact_id = 'myapp'
      AND mrv.version = '1.0.0'
      AND mrf.file_name = 'myapp-1.0.0.jar'
      AND mrp.soft_deleted_at IS NULL AND mrv.soft_deleted_at IS NULL AND mrf.soft_deleted_at IS NULL;
    
  • パッケージのキャッシュされた maven-metadata.xml を検索する

    SELECT mrf.*
    FROM maven_remote_files mrf
    JOIN maven_remote_packages mrp
      ON mrf.maven_remote_package_id = mrp.id AND mrf.namespace_id = mrp.namespace_id
    WHERE mrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND mrp.maven_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789'
      AND mrp.group_id = 'com.example'
      AND mrp.artifact_id = 'myapp'
      AND mrf.maven_remote_version_id IS NULL
      AND mrf.file_name = 'maven-metadata.xml'
      AND mrp.soft_deleted_at IS NULL AND mrf.soft_deleted_at IS NULL;
    

Maven 仮想リポジトリ

erDiagram
    repositories ||--|| maven_virtual_repositories : "has one"
    maven_virtual_repositories ||--o{ maven_virtual_repository_upstreams : "has many"
    maven_virtual_repository_upstreams ||--|| repositories : "references upstream"
    maven_virtual_repository_upstreams ||--o{ maven_virtual_upstream_rules : "has many"

    maven_virtual_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
    }

    maven_virtual_repository_upstreams {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_virtual_repository_id FK "NOT NULL, (maven_virtual_repository_id, namespace_id) references maven_virtual_repositories(id, namespace_id)"
        uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
        int position "NOT NULL"
    }

    maven_virtual_upstream_rules {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid maven_virtual_repository_upstream_id FK "NOT NULL, (maven_virtual_repository_upstream_id, namespace_id) references maven_virtual_repository_upstreams(id, namespace_id)"
        smallint rule_type "NOT NULL, 0=allow, 1=deny"
        text pattern "NOT NULL, limit 255"
        smallint target_field "NOT NULL, 0=group_id, 1=artifact_id, 2=version"
    }
  • maven_virtual_repositories: Maven パッケージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは upstream_repository_id を介して上流リポジトリを参照し、これは repositories(id, namespace_id) を指します。複合外部キー (namespace_id, upstream_repository_id) は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • maven_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

インデックス

  • maven_virtual_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。
  • maven_virtual_repository_upstreams: (namespace_id, maven_virtual_repository_id, position) に対する DEFERRABLE INITIALLY DEFERRED ユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にするために遅延可能(遅延可能)です。(namespace_id, maven_virtual_repository_id, upstream_repository_id) に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id) に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記の NO ACTION ガードが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。
  • maven_virtual_upstream_rules: (namespace_id, maven_virtual_repository_upstream_id) に対するインデックス — 特定の上流のすべてのルールを取得します。

クエリ例

  • 仮想リポジトリを作成する

    -- First create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 1, 1, 1)
    RETURNING id;
    -- Link the repository to a repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>);
    -- Then create the format-specific record
    INSERT INTO maven_virtual_repositories (namespace_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);
    
  • 仮想リポジトリを上流に関連付ける

    INSERT INTO maven_virtual_repository_upstreams (namespace_id, maven_virtual_repository_id, upstream_repository_id, position)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
    

NPM リポジトリ

Node パッケージは基本的に .tar.gz ファイルであり、各バージョンが単一のアーカイブです。ただし、Node クライアントはより豊富な機能セットを持ち、例えば私たちが扱う必要のあるディストリビューションタグの使用などがあります。

erDiagram
    repositories ||--|| npm_repositories : "has one"
    npm_repositories ||--o{ npm_packages : "has many"
    npm_packages ||--o{ npm_versions : "has many"
    npm_packages ||--o{ npm_tags : "has many"
    npm_versions ||--o{ npm_files : "has many"
    npm_tags ||--|| npm_versions : "has one"
    npm_packages ||--o{ npm_metadata_files : "has many"
    npm_files ||--|| blob_storage_attachments : "has one"
    npm_metadata_files ||--|| blob_storage_attachments : "has one"

    npm_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
    }

    npm_packages {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_repository_id FK "NOT NULL, (npm_repository_id, namespace_id) references npm_repositories(id, namespace_id)"
        text name "NOT NULL, limit 255"
        text scope "nullable, limit 255"
        integer versions_count "NOT NULL, DEFAULT 0, buffered counter"
        integer tags_count "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    npm_versions {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
        text version "NOT NULL, limit 255"
        jsonb package_json "NOT NULL"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        text gitlab_user_id "nullable, opaque string, limit 255"
        text gitlab_project_id "nullable, opaque string, limit 255"
        bytea gitlab_git_commit_sha "nullable"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    npm_tags {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
        uuid npm_version_id FK "NOT NULL, (npm_version_id, namespace_id) references npm_versions(id, namespace_id)"
        text name "NOT NULL, limit 255"
    }

    npm_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_version_id FK "NOT NULL, (npm_version_id, namespace_id) references npm_versions(id, namespace_id)"
        text file_name "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz soft_deleted_at "nullable"
    }

    npm_metadata_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_package_id FK "NOT NULL, (npm_package_id, namespace_id) references npm_packages(id, namespace_id)"
        smallint kind "NOT NULL, 0=full, 1=dist_tags, 2=abbreviated"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz expires_at "NOT NULL"
    }
  • npm_repositories: 複数のパッケージのコンテナです。各リポジトリはオプションのスコープを持つ複数のパッケージをホストできます。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_packages: npm パッケージを表します。name カラムはスコープを含む完全なパッケージ名(例: @myorg/mypackage または lodash)を保存します。versions_count はソフト削除されたものを含むパッケージの npm_versions 行をカウントし、ガベージコレクションが行をハード削除したときにのみデクリメントします。tags_count はその npm_tags 行をカウントします(npm_tags にはソフト削除カラムがないため、その問題は発生しません)。両方とも、ADR-004 のパッケージごとのエンティティ数制限(25,000 バージョン、1,000 タグ)を強制するバッファカウンターであり、バッファ書き込み/非同期書き込み を介して維持されます。ソフト削除されたバージョンを含めることは namespace_statistics.deduplicated_size_bytes の扱いを反映し、不正利用のベクトルを塞ぎます。すなわち、上限からソフト削除された行を除外できる顧客は、繰り返しソフト削除と再公開を行うことで 25,000 バージョンの制限を無期限に下回り続けることができてしまいますが、ソフト削除されたすべての行は依然としてストレージを占有し、復元可能なままです。両方の上限が 32 ビットの上限を十分に下回るため、bigint ではなく integer 型です。他の場所の上限のないカウンター(downloads_count、size_bytes)は無制限に増加するため bigint が必要です。last_downloaded_at はパッケージのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_at ライフサイクルルールによって使用されます。npm の単一バージョン非公開化では、最後のアクティブなバージョンがなくなるとこの行をソフト削除するため、ソフト削除されたパッケージは配下のすべてのバージョンがなくなった後も残り得ます。この非対称性が、このテーブルがバージョンレベルのものに頼らず独自のトゥームストーンインデックスを持つ理由です。maven_packages は同じ形のインデックスを持ち、逆方向から同じ状態に到達します。Maven のパッケージ削除はパッケージ行をマークし、バージョンを生存させるため、親だけにトゥームストーンが付き、子には付きません(以下のインデックス一覧を参照)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_versions: 埋め込まれた package.json メタデータとともに、npm パッケージの個々のバージョンを保存します。last_downloaded_at はバージョンのいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込み を介して維持されます。keep_last_downloaded_at ライフサイクルルールによって使用されます。gitlab_user_id、gitlab_project_id、gitlab_git_commit_sha は、どの GitLab ユーザーがこのバージョンを公開したか、および公開の背後にある CI コンテキスト(プロジェクト、コミット)を記録し、container_manifests の同等のカラムと同じ形状・根拠によります。created_at はバージョンが最初に公開された時刻を記録し、container_manifests と同じ公開履歴・時間範囲出所スキャンを支えます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_tags: 特定のパッケージバージョンを指す NPM ディストリビューションタグ(例: latest、next、beta)を提供します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_files: npm パッケージバージョンのファイルを表します。これらは主に tarball アーカイブです。また、レジストリがパフォーマンスのボトルネックを改善するために使用する補助ファイルである場合もあります。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_metadata_files: npm パッケージの事前計算されたメタデータファイルを、kind ごとに 1 つずつ保存します。kind カラムはメタデータのバリアントを区別します。full(0)はすべてのバージョンを含む完全な packument を含み、dist_tags(1)はディストリビューションタグのマッピングのみを含み、abbreviated(2)はリクエストが Accept: application/vnd.npm.install-v1+json を持つときに提供されるインストール専用の射影です。適切なファイルは、クライアントのリクエストに基づいて npm メタデータエンドポイントで提供されます。メタデータがパッケージのすべてのバージョンにまたがるため、(npm_versions ではなく)npm_packages にリンクされます。メタデータファイルはキャッシュ再構築によって生成されます。書き込みは非同期実行を 1 つキューに追加し、新鮮な行がないことを検出した読み取りは、応答前に同じ再構築をリクエスト goroutine 上で実行します。expires_at カラムはキャッシュの鮮度を駆動します。ライター(公開、非推奨化、非公開化、dist-tag の変更)は、データ書き込みと同じトランザクション内で、影響を受けるパッケージのすべての行に expires_at = NOW() を設定することでキャッシュを強制的に期限切れにします。再構築は、新しく生成された blob で行をアップサートする際に expires_at = NOW() + npm.packument_cache_ttl を設定します。キャッシュジョブと読み取り時の補充のどちらも、この再構築を実行できます。リーダーは expires_at > NOW() でフィルターし、ミス時には読み取り時補充にフォールスルーするため、期限切れの行がクライアントに提供されることはありません。このカラムはハード削除の期限ではなく、キャッシュの鮮度シグナルです。強制的な期限切れは blob とアタッチメントをそのまま残すため、それらに対してすでに解決中のレスポンスは、再構築がアタッチメントを入れ替えるまで正常に完了します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

Maven と同様に、パッケージ名とバージョンはまったく同じ理由で 2 つの異なるテーブルに保存されます。

インデックス

  • npm_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親リポジトリ参照によって NPM リポジトリを検索します。
  • npm_packages: (namespace_id, npm_repository_id, name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — リポジトリ内で名前によってパッケージを検索します。部分条件により、ソフト削除後に同じ名前のパッケージを再作成できます。(namespace_id, npm_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — keep_last_downloaded_at ライフサイクルルールの評価をサポートします。リポジトリ内のすべてのパッケージをスキャンして行ごとにフィルターするのではなく、境界のある範囲スキャンによって期限切れになったパッケージのみを返します。NULLS FIRST は、一度もダウンロードされていないパッケージを最も古い行とグループ化し、両方が同じ範囲スキャンで返されるようにします。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたパッケージを削除時刻順に一覧します。同じ形のバージョンレベルのインデックスとは異なり、主な目的はゴミ箱の一覧表示ではありません。最後のアクティブなバージョンがなくなったときにソフト削除されたパッケージは、その配下のバージョンより長く残ります。そのため、それらのバージョンがパージされると、バージョンレベルのスキャンでもリポジトリの走査でもパッケージ行に到達できず、このインデックスがなければリーパーはテーブルをシーケンシャルスキャンしてその行を探す必要があります。(namespace_id, npm_repository_id, name) WHERE soft_deleted_at IS NOT NULL に対するインデックス — 公開座標にあるトゥームストーン行の versions_count を合計し、ソフト削除をまたぐ公開前のバージョン上限チェックを支えます。versions_count はソフト削除されたバージョンもカウントし、ハード削除時にのみデクリメントします(前述の上限回避防止ルール)。パッケージ全体の非公開化ではカウント全体がトゥームストーン行に移る一方、部分的なユニーク名前インデックスにより再公開時にはゼロの新しいアクティブ行が挿入されるため、上限のリセットを防ぐにはトゥームストーン行を名前で読む必要があります。ユニーク名前インデックスではこの読み取りに対応できません。PostgreSQL が部分インデックスを使用するのは、クエリの述語がインデックスの述語を含意する場合だけであり、このクエリの述語はそのインデックスの述語の補集合だからです。
  • npm_versions: (namespace_id, npm_package_id, version) WHERE soft_deleted_at IS NULL に対するユニークインデックス — パッケージ内の特定のバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, npm_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — npm_packages と同じ範囲スキャン戦略を使用し、パッケージのバージョンにスコープされた keep_last_downloaded_at ライフサイクルルールの評価を支えます。(namespace_id, npm_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL に対するインデックス — ランディングページのリポジトリソートと同様に、バージョン一覧表示でパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたバージョンを削除時刻順に一覧し、npm アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体を時系列でスキャンし、公開履歴のページネーションと時間範囲を指定したアーティファクトの来歴クエリを支えます。ソフト削除された公開イベントも監査証跡に表示されるよう、無条件のインデックスとします。(namespace_id, npm_package_id, id) に対するインデックス — npm_package_id の外部キーチェックに対応します。チェックには述語がないため、非部分インデックスです。ソフト削除されたバージョンも自身のパッケージを参照するため、前述の部分ユニークインデックスでは対応できません。
  • npm_tags: (namespace_id, npm_package_id, name) に対するユニークインデックス — パッケージ内で名前によってディストリビューションタグを検索します。(namespace_id, npm_version_id) に対するインデックス — 特定のバージョンを指すすべてのタグを見つけます。
  • npm_files: (namespace_id, npm_version_id, file_name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — ファイル名はバージョン内で一意でなければなりません。部分条件により、ソフト削除後に同じ名前のファイルを再作成できます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべての npm ファイルへの逆引きで、クロスフォーマットのチェックサム検索を支えます。既存のバージョンをキーとするインデックスは、ダイジェストをキーとするスキャンを直接満たすことはできません。(namespace_id, npm_version_id) に対するインデックス — npm_version_id の外部キーチェックに対応します。チェックには述語がないため、非部分インデックスです。ソフト削除されたファイルも自身のバージョンを参照するため、前述の部分ユニークインデックスでは対応できません。
  • npm_metadata_files: (namespace_id, npm_package_id, kind) に対するユニークインデックス — パッケージごと・kind ごとに 1 つのメタデータファイル。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってメタデータファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのメタデータファイルへの逆引きで、npm_files を反映し、単一の sha256 検索で tarball と packument スタイルのメタデータの両方をカバーします。

クエリ例

  • 特定のリポジトリ ID とパッケージ名のすべてのバージョンを取得する

    SELECT nv.*
    FROM npm_versions nv
    JOIN npm_packages np
      ON nv.npm_package_id = np.id AND nv.namespace_id = np.namespace_id
    WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND np.npm_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND np.name = '@myorg/mypackage'
      AND np.soft_deleted_at IS NULL AND nv.soft_deleted_at IS NULL;
    
  • 公開パスの制限事前チェックのためにパッケージごとのエンティティ数カウンターを読み取る(アドバイザリ。npm_versions と npm_tags の部分ユニークインデックスが、レースのない信頼できるガードです)。

    SELECT versions_count, tags_count
    FROM npm_packages
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456' AND soft_deleted_at IS NULL;
    
  • バージョン ID とファイル名でファイルを取得する

    SELECT nf.*
    FROM npm_files nf
    WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nf.npm_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND nf.file_name = 'mypackage-1.0.0.tgz'
      AND nf.soft_deleted_at IS NULL;
    
  • パッケージの事前計算された完全メタデータファイルを取得する(npm メタデータエンドポイントで提供)

    SELECT bsb.object_storage_key, bsb.size
    FROM npm_metadata_files nmf
    JOIN blob_storage_blobs bsb ON bsb.namespace_id = nmf.namespace_id AND bsb.sha256 = nmf.blob_sha256
    WHERE nmf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nmf.npm_package_id = 456 AND nmf.kind = 0
      AND nmf.expires_at > NOW();
    

    読み取りは expires_at > NOW() でフィルターします。ミス(行がない、またはライターが 強制的に期限切れにしたか TTL が経過したために expires_at <= NOW())では、リクエスト goroutine 上で 下記のキャッシュ再構築を実行し、その再構築がコミットした行を提供します。したがってミスは、レスポンスへ直接 レンダリングされる本文ではなく、キャッシュ書き込みと blob のオープンで終わります。

  • 書き込み時に packument キャッシュを強制的に期限切れにする

    公開、非推奨化、非公開化、dist-tag の変更は、データ書き込みと同じトランザクション内で、影響を受ける パッケージのすべての kind について expires_at を NOW() に切り替えることでキャッシュを無効化します。 blob とアタッチメントはそのまま残されるため、すでに進行中のレスポンスは、再構築が アタッチメントを入れ替えるまで既存の blob に対して解決し続けます。

    UPDATE npm_metadata_files
    SET expires_at = NOW()
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND npm_package_id = '019a1b2c-0456-7abc-8def-000000000456';
    

    初回公開の場合はまだ行が存在しないため、UPDATE は 0 行に影響します。最初に実行される再構築が、 キャッシュジョブと読み取り時補充のどちらであっても、キャッシュ行を挿入します。

  • バージョンの公開または非公開化の後にメタデータファイルをアップサートする

    キャッシュ再構築は、キャッシュジョブと読み取り時補充のどちらが実行していても、パッケージの kind ごとにこれを 1 回実行します。古いアタッチメントは、 孤立したアタッチメントが blob のガベージコレクションをブロックするのを防ぐために、 同じトランザクション内で削除しなければなりません(クリーンアップタスク を参照)。

    -- The new blob and attachment (id=789) are created earlier in the same transaction.
    -- The interval below mirrors the configured `npm.packument_cache_ttl` (default 7 days).
    WITH old AS (
      SELECT blob_storage_attachment_id, blob_sha256
      FROM npm_metadata_files
      WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND npm_package_id = '019a1b2c-0456-7abc-8def-000000000456' AND kind = 0
    ),
    upsert AS (
      INSERT INTO npm_metadata_files (namespace_id, npm_package_id, kind, blob_storage_attachment_id, blob_sha256, expires_at)
      VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', 0, 789, 'abcd1234...'::bytea, NOW() + interval '7 days')
      ON CONFLICT (namespace_id, npm_package_id, kind)
      DO UPDATE SET blob_storage_attachment_id = EXCLUDED.blob_storage_attachment_id,
                    blob_sha256 = EXCLUDED.blob_sha256,
                    expires_at = EXCLUDED.expires_at
    )
    DELETE FROM blob_storage_attachments bsa
    USING old
    WHERE bsa.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND bsa.id = old.blob_storage_attachment_id
      AND bsa.sha256 = old.blob_sha256;
    

    初回挿入時、old CTE は行を返さないため、アタッチメントは削除されません。 競合時(更新)には、以前のアタッチメントが削除されます。他のアタッチメントが 参照していない場合、古い blob はガベージコレクションされます(重複排除に対して安全: 各クライアントは独自のアタッチメントを保持するため、1 つを削除しても同じ blob を 共有する他のものには影響しません)。

NPM リモートリポジトリ

erDiagram
    repositories ||--|| npm_remote_repositories : "has one"
    npm_remote_repositories ||--o{ npm_remote_packages : "has many"
    npm_remote_packages ||--o{ npm_remote_versions : "has many"
    npm_remote_packages ||--o{ npm_remote_metadata_files : "has many"
    npm_remote_versions ||--o{ npm_remote_files : "has many"
    npm_remote_metadata_files ||--|| blob_storage_attachments : "has one"
    npm_remote_files ||--|| blob_storage_attachments : "has one"

    npm_remote_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
        text url "NOT NULL, limit 1024"
        bytea wrapped_dek "nullable, this row's data-encryption key, wrapped by the namespace key"
        uuid ns_key_id FK "nullable, (ns_key_id, namespace_id) references namespace_encryption_keys(id, namespace_id), ON DELETE RESTRICT"
        int ns_key_version "nullable, the namespace key version this row was wrapped under"
        bytea encrypted_auth_token
        smallint cache_validity_hours "NOT NULL, DEFAULT 24"
        smallint metadata_cache_validity_hours "NOT NULL, DEFAULT 24"
        smallint last_health_status "NOT NULL, DEFAULT 0, 0=unknown, 1=healthy, 2=unhealthy"
        timestamptz last_health_checked_at "nullable"
    }

    npm_remote_packages {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_remote_repository_id FK "NOT NULL, (npm_remote_repository_id, namespace_id) references npm_remote_repositories(id, namespace_id)"
        text name "NOT NULL, limit 255"
        text scope "nullable, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    npm_remote_versions {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_remote_package_id FK "NOT NULL, (npm_remote_package_id, namespace_id) references npm_remote_packages(id, namespace_id)"
        text version "NOT NULL, limit 255"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    npm_remote_metadata_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_remote_package_id FK "NOT NULL, (npm_remote_package_id, namespace_id) references npm_remote_packages(id, namespace_id)"
        smallint kind "NOT NULL, 0=full, 1=dist_tags, 2=abbreviated"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
        text upstream_etag "nullable, limit 255"
    }

    npm_remote_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid npm_remote_version_id FK "NOT NULL, (npm_remote_version_id, namespace_id) references npm_remote_versions(id, namespace_id)"
        text file_name "NOT NULL, limit 255"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz upstream_checked_at "NOT NULL, DEFAULT NOW()"
        text upstream_etag "nullable, limit 255"
        timestamptz soft_deleted_at "nullable"
    }
  • npm_remote_repositories: 外部の npm レジストリを表します。URL、認証情報、アーティファクトのキャッシュ TTL(cache_validity_hours)、およびパッケージメタデータレスポンス用の別個の TTL(metadata_cache_validity_hours)を含みます。ヘルスチェックのステータスは監視のために追跡されます。repository_id を介して親の repositories テーブルを参照します。上流の認証情報は、それを復号するラップ済みデータ暗号化キーとともに暗号文として保存します。カラムの構成と制約については、暗号化キーを参照してください。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_remote_packages: キャッシュされた npm パッケージです。last_downloaded_at はパッケージのいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録し、ホット行の競合を避けるためにバッファ書き込み/非同期書き込みを介して維持されます。keep_last_downloaded_at ライフサイクルルールとキャッシュ保持の評価に使用されます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_remote_versions: キャッシュされたバージョンで、そのバージョンの最初の tarball がプルされたときに作成され、バージョンは tarball のファイル名から取得します。バージョンごとのメタデータは保持しません。リモートリポジトリは upstream 自身の packument ドキュメント(npm_remote_metadata_files を参照)を行に解析せずに中継するため、このテーブルは npm_remote_files のキーとなり、バージョンレベルのキャッシュライフサイクルを保持します。last_downloaded_at は、バージョン内のいずれかのキャッシュされたファイルが最後にダウンロードされた時刻を記録します。ホット行の競合を避けるため、バッファ書き込み/非同期書き込みによって維持します。keep_last_downloaded_at ライフサイクルルールとキャッシュ保持の評価に使用します。created_at はバージョンが最初にキャッシュされた時刻を記録し、npm_versionsと同様に、キャッシュ側の公開履歴と来歴のスキャンを支えます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_remote_metadata_files: upstream レジストリからキャッシュした事前計算済みのメタデータファイルを、パッケージごとに種類につき 1 つ保存します。kind は、すべてのバージョンを含む完全な packument(0)、dist-tags のみのマッピング(1)、およびリクエストに Accept: application/vnd.npm.install-v1+json が含まれる場合に提供するインストール専用の形式である簡略化された packument(2)を区別します。各行は、そのバリアントについて upstream 自身のドキュメントを保持し、すべての dist.tarball をリモートリポジトリに書き換えます。リモートリポジトリはタグごとの行を保持しません。クライアントは中継された packument の dist-tags メンバーからタグを解決します。upstream_checked_at は、メタデータを upstream レジストリに照らして最後に検証した時刻を記録し、metadata_cache_validity_hours と比較して再検証が必要かを判断します。upstream_etag は upstream が返した ETag を保存し、条件付きリクエスト(If-None-Match)によって変更のないメタデータの再ダウンロードを回避できます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_remote_files: キャッシュされた tarball です。file_name は上流の dist.tarball から導出した名前を保持するため、上流がファイル名内でスコープを繰り返している場合は、パス区切り文字を含みます(ADR-009を参照)。したがって、limit 255 は単一の名前ではなく相対パス全体を制限し、要素数を制限するルールはないため、これが唯一の制限です。対応するホスト型の npm_files は常に単一セグメントです。upstream_checked_at はファイルが上流レジストリに対して最後に検証された時刻を記録し、cache_validity_hours と比較して再検証が必要かどうかを判断します。upstream_etag は上流が返した ETag を保存し、変更されていない tarball の再ダウンロードを回避するための条件付きリクエスト(If-None-Match)を可能にします。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は blob ストレージ セクションを参照してください。

インデックス

  • npm_remote_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によってリモートリポジトリを検索します。(namespace_id, ns_key_id) に対するインデックス — 暗号化キーの外部キーを支え、キーの行を削除する際にテーブルを走査せず参照元の行を確認できるようにします。
  • npm_remote_packages: (namespace_id, npm_remote_repository_id, name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — 名前によってキャッシュされたパッケージを検索します。部分条件により、ソフト削除後に同じ名前のパッケージを再作成できます。
  • npm_remote_versions: (namespace_id, npm_remote_package_id, version) WHERE soft_deleted_at IS NULL に対するユニークインデックス — パッケージ内のキャッシュされたバージョンを検索します。この部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, npm_remote_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL に対するインデックス — バージョン一覧表示で、キャッシュされたパッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — ソフト削除されたキャッシュ済みバージョンを削除時刻順に一覧し、キャッシュされた npm アーティファクトに対するアーティファクト粒度のゴミ箱一覧クエリを支えます。(namespace_id, created_at DESC) に対するインデックス — ネームスペース全体を時系列でスキャンします。ホスト型の npm_versions インデックスを反映し、キャッシュ側の公開履歴と来歴を対象にします。ホスト型のインデックスと同じ監査証跡上の理由から、無条件(soft_deleted_at の述語なし)のインデックスとします。
  • npm_remote_metadata_files: (namespace_id, npm_remote_package_id, kind) に対するユニークインデックス — パッケージごと・kind ごとに 1 つのメタデータファイルを強制します。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってメタデータファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされたメタデータファイルへの逆引きで、ホスト型の npm_metadata_files インデックスを反映します。
  • npm_remote_files: (namespace_id, npm_remote_version_id, file_name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — ファイル名はバージョン内で一意でなければなりません。部分条件により、ソフト削除後に同じ名前のファイルを再作成できます。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべてのキャッシュされた npm ファイルへの逆引きで、ホスト型の npm_files インデックスを反映し、チェックサム検索がキャッシュ側の参照もカバーするようにします。

クエリ例

  • リモートリポジトリを作成する

    -- First create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'npm-registry', 2, 2, 0)
    RETURNING id;
    -- Link the repository to a repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>);
    -- Then create the format-specific record
    INSERT INTO npm_remote_repositories (namespace_id, repository_id, url, wrapped_dek, ns_key_id, ns_key_version, encrypted_auth_token)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>, 'https://registry.npmjs.org', $1, $2, $3, $4);
    
  • キャッシュされた packument を読み取る(packument レスポンスを提供)

    -- kind 0 serves the full packument; kind 2 serves the abbreviated one
    SELECT nrmf.blob_storage_attachment_id, nrmf.blob_sha256, nrmf.upstream_etag, nrmf.upstream_checked_at
    FROM npm_remote_metadata_files nrmf
    JOIN npm_remote_packages nrp
      ON nrmf.npm_remote_package_id = nrp.id AND nrmf.namespace_id = nrp.namespace_id
    WHERE nrp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND nrp.npm_remote_repository_id = '019a1b2c-0789-7abc-8def-000000000789'
      AND nrp.name = '@myorg/mypackage'
      AND nrp.soft_deleted_at IS NULL
      AND nrmf.kind = 0;
    
  • キャッシュされた tarball をプルする(読み取りパスのショートカット)

    SELECT bsb.object_storage_key, bsb.size
    FROM npm_remote_files nrf
    JOIN blob_storage_blobs bsb
      ON bsb.namespace_id = nrf.namespace_id AND bsb.sha256 = nrf.blob_sha256
    WHERE nrf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
      AND nrf.npm_remote_version_id = '019a1b2c-0456-7abc-8def-000000000456'
      AND nrf.file_name = 'mypackage-1.0.0.tgz'
      AND nrf.soft_deleted_at IS NULL;
    

NPM 仮想リポジトリ

erDiagram
    repositories ||--|| npm_virtual_repositories : "has one"
    npm_virtual_repositories ||--o{ npm_virtual_repository_upstreams : "has many"
    npm_virtual_repository_upstreams ||--|| repositories : "references upstream"
    npm_virtual_repository_upstreams ||--o{ npm_virtual_upstream_rules : "has many"

    npm_virtual_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE CASCADE"
    }

    npm_virtual_repository_upstreams {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid npm_virtual_repository_id FK "NOT NULL, (npm_virtual_repository_id, namespace_id) references npm_virtual_repositories(id, namespace_id) ON DELETE CASCADE"
        uuid upstream_repository_id FK "NOT NULL, (upstream_repository_id, namespace_id) references repositories(id, namespace_id) ON DELETE NO ACTION"
        int position "NOT NULL, CHECK position >= 0"
    }

    npm_virtual_upstream_rules {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE NO ACTION"
        uuid npm_virtual_repository_upstream_id FK "NOT NULL, (npm_virtual_repository_upstream_id, namespace_id) references npm_virtual_repository_upstreams(id, namespace_id) ON DELETE CASCADE"
        smallint rule_type "NOT NULL, 0=allow, 1=deny, CHECK rule_type IN (0, 1)"
        text pattern "NOT NULL, CHECK char_length(pattern) <= 255"
        smallint target_field "NOT NULL, 0=full_package_name, 1=scope, 2=version, CHECK target_field IN (0, 1, 2)"
    }
  • npm_virtual_repositories: npm パッケージの仮想リポジトリです。名前、可視性、クロスフォーマットクエリのために repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_virtual_repository_upstreams: 仮想リポジトリとその上流を結合するテーブルです。各仮想リポジトリは上流の順序付きリストを持ちます。各エントリは upstream_repository_id を介して上流リポジトリを参照し、これは repositories(id, namespace_id) を指します。複合外部キー (namespace_id, upstream_repository_id) は、上流が同じネームスペース内にあることを強制します。これはレジストリがネームスペースにスコープされること(ADR-001)と一貫しています。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • npm_virtual_upstream_rules: 上流の許可/拒否フィルタールールを定義します。各ルールは、この上流を通じて解決する際にどのアーティファクトが含まれるか除外されるかを制御するために、ワイルドカードパターンと対象フィールドを指定します。パターンは MVP ではワイルドカードのみです。正規表現のサポートは顧客のフィードバックがそれを正当化するまで延期されます(ディスカッション)。データベースは列挙型とパターン長を CHECK rule_type IN (0, 1)、CHECK target_field IN (0, 1, 2)、CHECK char_length(pattern) <= 255 で強制します。範囲外の識別子は読み取りパスに任せず、書き込み時に拒否されます。パターンの境界はバイトではなく文字数を数えるため、UTF-8 のサイズにかかわらず 255 文字のパターンを受け付けます。HASH(namespace_id) で 64 パーティションにパーティショニングされます。

ここで position は 0 始まりです。CHECK position >= 0 が下限を定め、0 が最優先の位置です。この下限は npm 固有で、リファレンス実装とは異なります。モノリスの仮想レジストリは npm を含むすべての出荷済みフォーマットで 1 始まりかつ隙間を詰めます。virtual_registries_packages_npm_registry_upstreams は "position" smallint DEFAULT 1 NOT NULL と CHECK ((1 <= "position") AND ("position" <= 20)) を宣言し、共通の VirtualRegistries::RegistryUpstream は作成時に maximum(:position).to_i + 1 を割り当て、削除時にはより大きいすべての位置をデクリメントします。コンテナテーブルの下限 1 はこの前例と一致します。S32 は、S17 の 1 始まりの連続した番号振り直しと、このドキュメントの position が 1 から始まるクエリ例を根拠にしています。npm の >= 0 と削除による隙間の許容は、S31 がその前例に反して意図的に選択したものです。

2 つの下限は互いに調整されておらず、データベースに存在するのは一方だけです。npm は artifact-registry!2066 で出荷済みですが、コンテナは S32 で指定されているだけで、まだマイグレーションがありません。S32 は、position 値が書き込みサーフェスより下のレイヤーに届かず、解決では一覧を昇順に読み位置をインデックスとして扱わないことを理由に、差異の決着を S17 の管理 API またはこのドキュメントに委ねています。そのため、この段落は分岐を追認せず記録します。#985 で追跡されています。

これら 3 つのテーブルの外部キーには、次の参照アクションがあります。

  • npm_virtual_repositories の repositories を参照する FK (repository_id, namespace_id): フォーマット子テーブルの一般ルールで定められた ON DELETE CASCADE。
  • npm_virtual_repositories の namespaces を参照する FK namespace_id: NO ACTION。
  • npm_virtual_repository_upstreams の namespaces を参照する FK namespace_id: NO ACTION。
  • npm_virtual_repository_upstreams の npm_virtual_repositories を参照する FK (npm_virtual_repository_id, namespace_id): ON DELETE CASCADE。
  • npm_virtual_repository_upstreams の repositories を参照する FK (upstream_repository_id, namespace_id): NO ACTION。
  • npm_virtual_upstream_rules の namespaces を参照する FK namespace_id: NO ACTION。
  • npm_virtual_upstream_rules の npm_virtual_repository_upstreams を参照する FK (npm_virtual_repository_upstream_id, namespace_id): ON DELETE CASCADE。

npm_virtual_repository_upstreams の 2 つの外部キーが反対のアクションを取るのは意図的です。npm_virtual_repository_id は親から子へのリンクなので、仮想リポジトリを削除すると upstream の関連付けも削除されます。upstream_repository_id は兄弟参照なので、まだ upstream として一覧に含まれるリポジトリの削除は拒否されます。そこでカスケードすると、一覧に含めるすべての仮想リポジトリが縮小し、空になる可能性もありますが、オペレーターへのシグナルはありません。upstream の削除は意図的な管理操作でなければならず、以下の逆引きインデックスもそのために存在します。

カスケードチェーンは 3 レベルなので、1 つの repositories 行の削除は、npm_virtual_repositories、npm_virtual_repository_upstreams を通じてルールまで到達します。

インデックス

  • npm_virtual_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親参照によって仮想リポジトリを検索します。
  • npm_virtual_repository_upstreams: (namespace_id, npm_virtual_repository_id, position) に対する DEFERRABLE INITIALLY DEFERRED ユニーク制約 — 仮想リポジトリの順序付き上流を取得します。トランザクション内での並べ替えを可能にします。(namespace_id, npm_virtual_repository_id, upstream_repository_id) に対するユニークインデックス — 同じ上流が仮想リポジトリに二度追加されるのを防ぎます。(namespace_id, upstream_repository_id) に対するインデックス — 特定のリポジトリを upstream として一覧に含めるすべての仮想リポジトリを検索します。これは、上記の NO ACTION ガードと関連付けサーフェスが読み取るものです。どちらのユニークエントリにも、この列順を使用可能な接頭辞として持つものはなく、PostgreSQL は外部キーカラムを自動的にインデックス化しません。
  • npm_virtual_upstream_rules: (namespace_id, npm_virtual_repository_upstream_id) に対するインデックス — 特定の上流のすべてのルールを取得します。namespace_id が先頭なので、読み取りは自身の namespace_id 等価条件を運ぶ必要があります。外部キーカラムだけでは使用可能な接頭辞に一致せず、すべてのパーティションを走査します。

順序付き upstream のエントリはユニークインデックスではなくユニーク制約です。遅延は制約の属性なので、ALTER TABLE ... ADD CONSTRAINT ... UNIQUE (...) DEFERRABLE INITIALLY DEFERRED で追加します。PostgreSQL では CREATE UNIQUE INDEX ... DEFERRABLE は構文エラーです。遅延可能なユニーク制約は ON CONFLICT の調停にも使えないため、position の書き込みはこのキーへのアップサートではなく、UPDATE の後に INSERT を行います。

クエリ例

  • 仮想リポジトリを作成する

    -- First create the parent repository
    INSERT INTO repositories (namespace_id, name, format, kind, visibility)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'my-virtual-repo', 2, 1, 1)
    RETURNING id;
    -- Link the repository to a repository collection
    INSERT INTO repository_collection_repositories (namespace_id, repository_collection_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', <returned_id>);
    -- Then create the format-specific record
    INSERT INTO npm_virtual_repositories (namespace_id, repository_id)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', <returned_id>);
    
  • 仮想リポジトリを上流に関連付ける

    INSERT INTO npm_virtual_repository_upstreams (namespace_id, npm_virtual_repository_id, upstream_repository_id, position)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0123-7abc-8def-000000000123', '019a1b2c-0789-7abc-8def-000000000789', 1);
    

Generic リポジトリ

Generic パッケージは、パッケージ名とバージョンの下で公開される不透明なファイルの集合です。レジストリはファイル内容の解析、検証、インデックス作成を行わず、生成または提供するフォーマット定義のメタデータドキュメントもありません。そのため、Generic リポジトリはこのドキュメントで最も薄いフォーマットであり、アーティファクトテーブルは 3 つ、補助テーブルはありません。

erDiagram
    repositories ||--|| generic_repositories : "has one"
    generic_repositories ||--o{ generic_packages : "has many"
    generic_packages ||--o{ generic_versions : "has many"
    generic_versions ||--o{ generic_files : "has many"
    generic_files ||--|| blob_storage_attachments : "has one"

    generic_repositories {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, UNIQUE (namespace_id, repository_id), (repository_id, namespace_id) references repositories(id, namespace_id)"
    }

    generic_packages {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid generic_repository_id FK "NOT NULL, (generic_repository_id, namespace_id) references generic_repositories(id, namespace_id)"
        text name "NOT NULL, limit 255"
        timestamptz last_downloaded_at "nullable, buffered"
        timestamptz soft_deleted_at "nullable"
    }

    generic_versions {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid generic_package_id FK "NOT NULL, (generic_package_id, namespace_id) references generic_packages(id, namespace_id)"
        text version "NOT NULL, limit 255"
        bigint size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_downloaded_at "nullable, buffered"
        text gitlab_user_id "nullable, opaque string, limit 255"
        text gitlab_project_id "nullable, opaque string, limit 255"
        bytea gitlab_git_commit_sha "nullable"
        timestamptz soft_deleted_at "nullable"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
    }

    generic_files {
        uuid id PK "UUIDv7, application-generated, part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid generic_version_id FK "NOT NULL, (generic_version_id, namespace_id) references generic_versions(id, namespace_id)"
        text file_path "NOT NULL, CHECK (octet_length(file_path) <= 1024)"
        bigint blob_storage_attachment_id FK "NOT NULL, (namespace_id, blob_storage_attachment_id, blob_sha256) references blob_storage_attachments(id, namespace_id, sha256)"
        bytea blob_sha256 FK "NOT NULL, (namespace_id, blob_sha256) references blob_storage_blobs(namespace_id, sha256)"
        timestamptz soft_deleted_at "nullable"
    }
  • generic_repositories: 複数のパッケージを格納するコンテナです。名前、可視性、クロスフォーマットのクエリのために、repository_id を介して親の repositories テーブルを参照します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • generic_packages: 単一の自由形式の名前(例: release-artifacts)で識別される Generic パッケージを表します。Maven とは異なり、カラムに分割する構造化された座標はないため、1 つの name カラムが識別子全体を保持します。last_downloaded_at は、パッケージ内のいずれかのファイルが最後にダウンロードされた時刻を記録し、バッファ書き込み/非同期書き込みによって維持します。NULL はパッケージが一度もダウンロードされていないことを意味し、keep_last_downloaded_at ライフサイクルルールの評価では、可能な限り古いダウンロード時刻として扱います(ADR-010)。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • generic_versions: Generic パッケージの個々のバージョン(例: 1.0.0、2024-06-01-nightly)を保存します。Generic はバージョンの文法を課さないため、このカラムは解析済みのバージョンではなく不透明な文字列です。レジストリはバージョンを等価性のみで比較し、意味的な順序では並べません。last_downloaded_at、gitlab_user_id、gitlab_project_id、gitlab_git_commit_sha、created_at は、maven_versionsの同等のカラムと同じ形状、バッファリング動作、来歴の根拠を持ちます。Generic では複数回の公開によって既存のバージョンにファイルを追加でき、それらは異なるパイプラインやプロジェクトから来る場合があるため、来歴カラム(gitlab_user_id、gitlab_project_id、gitlab_git_commit_sha)はバージョンを作成した公開のみを記録し、その後のアップロードでは変更しません。よくあるケースである、1 つのパイプライン内の複数のジョブでは、同じプロジェクトとコミットを共有します。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • generic_files: バージョンの下で公開される個々のファイルを表します。generic_version_id は NOT NULL であり、これが maven_filesとの構造上の違いです。Maven は maven-metadata.xml などのパッケージレベルのファイルをモデル化するためにバージョンの null を許容しますが、Generic はメタデータドキュメントを生成しないため、パッケージレベルのファイルを持ちません。構造は npm_filesと一致するため、npm と同様に、このテーブルはパッケージの FK を持ちません。パッケージは常に generic_versions.generic_package_id であり、2 つ目の独立した FK はそれと不一致になる可能性があります。Generic の公開ではバージョンの下にファイルを入れ子にでき、レジストリはそのレイアウトを平坦化せずにそのまま保存するため、file_path はディレクトリセグメントを含めた、バージョンからの相対的な公開パス(例: bin/linux/amd64/tool)を保持します。無料プランと同様に、最後のセグメントは Generic のファイル名文法(ASCII の英字、数字、.、_、-、+、~、@。ただし ~ または @ で始まったり終わったりしない)に従い、ディレクトリセグメントには / 以外の任意の文字を含められます。空のセグメント、.、.. のセグメントは拒否するため、保存される各パスの表記は 1 つです。パスはデコードした状態で保存します。CHECK (octet_length(file_path) <= 1024) は文字数ではなくバイト数を制限するため、使用する文字にかかわらず、このカラムに対するユニークインデックスの btree エントリ上限内に収まります。値が単一の名前ではなくパスであるため、この制限は、このドキュメントの他のファイル名に使用する 255 よりも大きくなっています。バージョン内の既存の file_path を再公開すると、Maven のリリースファイルや npm のバージョンと同様に 409 Conflict で拒否します。(namespace_id, generic_version_id, file_path) に対する部分ユニークインデックスが挿入を決定するため、同時アップロードでは成功するのは 1 つだけです。ファイルを変更する場合、ユーザーはそれを削除して再公開します。これは、重複を許可 を無効にした無料プランの Generic Package Registry と一致します。無料プランのデフォルト(重複を許可)、そのグループレベルの切り替え、および例外の正規表現は GA の対象範囲外なので、スキーマにはそれらの設定はありません。maven_files と異なり、ここにはフォーマット固有のチェックサムカラムはありません。Generic プロトコルが公開するのは blob_sha256 がすでに保持する SHA256 のみなので、重複させるものがないためです。HASH(namespace_id) で 64 パーティションにパーティショニングされます。
  • blob_storage_attachments: 詳細は Blob ストレージセクションを参照してください。

Generic がパッケージとバージョンを分離する理由は Mavenと同じです。ツリー状の UI は、最初にパッケージ名を一覧表示し、展開するとバージョン識別子を表示するため、2 つのアクセスパターンには 2 つのテーブルが必要です。フォーマット自体にインデックスを作成するメタデータがないことは、UI が発行する読み取りの形状を変えません。

インデックス

  • generic_repositories: (namespace_id, repository_id) に対するユニークインデックス — 親リポジトリへの参照によって Generic リポジトリを検索します。
  • generic_packages: (namespace_id, generic_repository_id, name) WHERE soft_deleted_at IS NULL に対するユニークインデックス — リポジトリ内で名前によってパッケージを検索します。部分条件により、ソフト削除後に同じ名前のパッケージを再作成できます。(namespace_id, generic_repository_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — 境界のある範囲スキャンによって keep_last_downloaded_at ライフサイクルルールの評価を支え、NULLS FIRST によって一度もダウンロードされていないパッケージを最も古い行とグループ化します。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — リーパーのパッケージレベルスキャンのために、ソフト削除されたパッケージを削除時刻順に一覧します。パッケージ削除では子行を書き込まないため、バージョンレベルのインデックスではこのスキャンに対応できません。
  • generic_versions: (namespace_id, generic_package_id, version) WHERE soft_deleted_at IS NULL に対するユニークインデックス — パッケージ内の特定のバージョンを検索します。部分条件により、ソフト削除後に同じ識別子でバージョンを再作成できます。(namespace_id, generic_package_id, last_downloaded_at NULLS FIRST) WHERE soft_deleted_at IS NULL に対するインデックス — generic_packages のライフサイクルスキャンに相当する、パッケージ単位のスキャンです。(namespace_id, generic_package_id, size_bytes DESC) WHERE soft_deleted_at IS NULL に対するインデックス — バージョン一覧表示のために、パッケージのバージョンをサイズ順に並べます。(namespace_id, soft_deleted_at DESC) WHERE soft_deleted_at IS NOT NULL に対するインデックス — アーティファクト粒度のゴミ箱一覧です。(namespace_id, created_at DESC) に対するインデックス — 公開履歴と時間範囲を指定した来歴クエリを支える時系列スキャンです。ソフト削除された公開イベントも監査証跡に表示されるよう、無条件とします。(namespace_id, generic_package_id) に対するインデックス — generic_package_id の外部キーチェックと、リポジトリレベルのサイズ走査でバージョンからパッケージに進む処理に対応します。チェックには述語がなく、走査はソフト削除されたバージョンにも到達するため、非部分インデックスです。
  • generic_files: (namespace_id, generic_version_id, file_path) WHERE soft_deleted_at IS NULL に対するユニークインデックス — ファイルパスはバージョン内で一意でなければなりません。バージョンが null の状態は表現できないため、maven_files では 2 つ必要なユニークインデックスが、ここでは 1 つで十分です。(namespace_id, blob_storage_attachment_id) に対するインデックス — ストレージアタッチメントによってファイルを検索します。(namespace_id, blob_sha256) に対するインデックス — 保存された blob の sha256 から、それを参照するすべての Generic ファイルへの逆引きで、クロスフォーマットのチェックサム検索を支えます。(namespace_id, generic_version_id, blob_sha256) に対するインデックス — generic_version_id の外部キーチェック、リポジトリレベルのサイズ走査の結合、アーティファクトレベルのサイズ再計算に対応し、これらはすべてインデックスから blob_sha256 を読み取ります。チェックには述語がなく、走査はソフト削除された行にも到達するため、非部分インデックスです。ソフト削除されたファイルも自身のバージョンを参照するため、前述の部分ユニークインデックスでは外部キーチェックに対応できません。

クエリ例

  • 指定されたリポジトリ ID とパッケージ名のパッケージバージョンを取得する。

    SELECT gv.*
    FROM generic_versions gv
    JOIN generic_packages gp
      ON gv.generic_package_id = gp.id AND gv.namespace_id = gp.namespace_id
    WHERE gp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND gp.generic_repository_id = '019a1b2c-0123-7abc-8def-000000000123' AND gp.name = 'release-artifacts'
      AND gv.version = '1.0.0'
      AND gp.soft_deleted_at IS NULL AND gv.soft_deleted_at IS NULL;
    
  • バージョン ID と公開パスを指定してファイルを取得する。

    SELECT gf.*
    FROM generic_files gf
    WHERE gf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND gf.generic_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND gf.file_path = 'bin/linux/amd64/tool'
      AND gf.soft_deleted_at IS NULL;
    
  • バージョン詳細ビュー用に、バージョンの下で公開されたすべてのファイルを一覧する。

    SELECT gf.id, gf.file_path, gf.blob_sha256
    FROM generic_files gf
    WHERE gf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND gf.generic_version_id = '019a1b2c-0456-7abc-8def-000000000456'
      AND gf.soft_deleted_at IS NULL
    ORDER BY gf.file_path;
    

blob ストレージ

blob ストレージのデータ構成は、次の前提の下で行われています。

  • blob への一対多の関連付けを扱う必要はありません。これは blob ストレージのクライアント領域で処理されます。したがって、一対一の関連付けのみが必要です。
  • 適切な クリーンアップ処理 のために、いくつの blob ストレージクライアントが単一の blob を使用しているか(重複排除)を追跡する必要があります。
  • さらに、単一の blob に対する各使用の異なる起源を追跡したい場合があります。

ここで提示するスキーマは、データのストレージ側のみを考慮しています。メトリクスや クリーンアップ などの追加の側面のために補助テーブルが必要になる場合がありますが、これらの部分はまだ評価中のためここでは記述しません。アップロードセッションの追跡については アップロードセッション で記述します。

erDiagram
    blob_storage_attachments ||--|| blob_storage_blobs : "has one"

    blob_storage_attachments {
        bigint id PK "DEFAULT nextval('blob_storage_attachments_id_seq')"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        bytea sha256 PK,FK "NOT NULL, (namespace_id, sha256) references blob_storage_blobs(namespace_id, sha256)"
    }

    blob_storage_blobs {
        bigint id PK "DEFAULT nextval('blob_storage_blobs_id_seq')"
        uuid namespace_id PK "NOT NULL, UNIQUE with sha256, no namespaces(id) reference by design"
        bytea sha256 PK "NOT NULL, UNIQUE with namespace_id"
        text object_storage_key "NOT NULL, limit 1024"
        bigint size "NOT NULL"
        bytea metadata_sha1 "nullable, CHECK octet_length = 20"
    }
  • blob_storage_attachments: 特定の blob の使用を追跡します。各クライアント(Container、Maven、npm、または Generic リポジトリテーブル)は、blob レコードを使用(作成または再利用)したいたびに、ここにレコードを作成する必要があります。各使用は、ここに単一のレコードを 持たなければなりません。クライアントは、参照しているアーティファクトレコード(ファイル、blob、キャッシュエントリ)を削除する際に、アタッチメントレコードを削除する責任があります。両方の削除は、孤立したアタッチメントが blob クリーンアップをブロックするのを防ぐために、同じトランザクション内で行わなければなりません。クライアントテーブルから blob_storage_attachments への外部キーは参照整合性を強制しますが(ダングリング参照を防ぐ)、ON DELETE CASCADE は使用しません。クリーンアップはアプリケーション管理です。例えば、まったく同じファイルを持つ 2 つの Maven パッケージは、それぞれ異なるアタッチメントレコードを参照し、それが同じ blob レコードを参照する必要があります。namespace_id カラムは Cells のシャーディングに必要です。sha256 カラムは、パーティションプルーニングされた JOIN を可能にするために、参照される blob_storage_blobs レコードから伝播されます(パーティショニング戦略 を参照)。主キーは、従来の (id) ではなく (id, namespace_id, sha256) です。sha256 が必要なのは、PostgreSQL がハッシュパーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを強制するためであり、namespace_id が必要なのは、PK をデプロイ間でグローバルに一意に保つためです。ローカルな bigint id は単一の Artifact Registry データベース内でのみ一意なので(ネームスペース ID 型 を参照)、デプロイ間のネームスペースマイグレーション(ADR-022)では、同じ (id, sha256) ペアがターゲットデータベースにすでに存在する可能性があります。UUIDv7 の namespace_id を PK に追加することで、その衝突を構造的に排除します。クライアントテーブルは、この複合 PK を (namespace_id, blob_storage_attachment_id, blob_sha256) を介して参照します。
  • blob_storage_blobs: このテーブルは、オブジェクトストレージ上に存在するすべてのファイルコンテンツを blob として一覧します。オブジェクトストレージキーは専用カラムに完全な形で保存され、blob が使用されるたびに計算されることはありません。sha256 は基本となるコンテンツアドレス可能な識別子であり、常に存在します(NOT NULL)。namespace_id カラムは重複排除を Organization にスコープし、設計上 namespaces(id) への参照を持ちません。この行は保存オブジェクトへの唯一のハンドルである object_storage_key を所有し、ADR-025 ではガベージコレクションが行より先にオブジェクトを削除します。そのため、namespaces からのカスケードはオブジェクトがまだ存在する間にハンドルを破壊し、オブジェクトが回収されずに残り、ブロックする参照は、意図的に延期されたガベージコレクションサイクルの完了までネームスペースの削除を止めます。したがってネームスペースのハード削除後も、ガベージコレクションが回収するまで blob 行は残ります。フォーマット固有のチェックサム(例えば Maven の SHA1 と MD5)はここではなくフォーマット固有のファイルテーブルに保存し、このテーブルをフォーマットに依存しない状態に保ちます。同じ理由から、コンテンツタイプも除外します。これはフォーマットが blob をどのように解釈するかという特性であって blob 自体の特性ではなく、フォーマット固有のテーブルに属します。metadata_sha1 カラムは、フォーマットに依存しないというルールに対する意図的かつ限定的な例外です。コミット時に blob に付加される MVP ユーザーメタデータの許可リストにある SHA-1 を反映し、SHA-1 が指定されなかった場合は NULL になります。このカラムをフォーマット固有のテーブルではなく blob_storage_blobs に置くのは、ストレージレイヤーの blob 情報の検索がプッシュとプルのホットパスで 1 回の DB ラウンドトリップとなることが契約上定められているためです。DB のミラーなしでユーザーメタデータを公開すると、ダイジェストごとのオブジェクトストレージ HEAD のファンアウト、または部分的な API 公開が必要になります。同じ値は、コミット時にバックエンドネイティブの x-amz-meta-checksum-sha1 / x-goog-meta-checksum-sha1 ヘッダーとしてストレージオブジェクトに付加されます。行は不変であるため、DB とストレージオブジェクトのコピーにずれは生じません。将来、許可リストに項目を追加する際は、改訂によって項目ごとの NULL 許容カラムを追加します。主キーが (id, namespace_id, sha256) である理由は、前述の blob_storage_attachments と同じです。sha256 は PostgreSQL のパーティションキー包含ルールを満たし、UUIDv7 の namespace_id は PK をデプロイ間でグローバルに一意に保ちます。また、代理 bigint id により、blob ストレージ層全体で行識別子の形を一貫させています。Organization ごとの重複排除は、別個の UNIQUE (namespace_id, sha256) 制約によって強制されます。この制約はコンテンツハッシュによる検索インデックスも兼ね、このテーブルを参照するすべての外部キーのターゲットになります。PK を直接参照する FK はありません。(namespace_id, sha256) はすでに行を一意に識別し、UUIDv7 の namespace_id によってそれ自体がグローバルに一意であるため、呼び出し元は代理 id を保持せずに自然キーで JOIN します。

blob ストレージテーブルは、Artifact Registry の外部でも再利用できるように設計されています。これにより、他の機能が同じ重複排除とストレージインフラを活用できます。

すべてのハッシュカラム(digest と sha256、sha1、md5、sha512 — Maven 固有)は、テキストエンコーディングやインラインアルゴリズムプレフィックスを持たない、生のハッシュバイトを保持する bytea として保存されます。コンテナの digest と blob_sha256 カラムは生の 32 バイト SHA-256 を保持し、CHECK octet_length = 32 によって強制されます。OCI のワイヤ形式 sha256:<64 hex chars> はサービスレイヤーで生のバイトへ、また生のバイトから変換されるため、blob_sha256 は blob_storage_blobs.sha256 と直接バイト比較で JOIN します。MVP は sha256 のみをサポートします。sha256 以外の参照はこれらのテーブルに到達する前にサービスレイヤーで拒否されるため、別個の digest_algorithm カラムは保存しません。コンテナ blob の digest と blob_sha256 は同じバイトになるため、(namespace_id, digest) インデックスはコンテンツハッシュの逆引きインデックスも兼ね、ホスト型コンテナテーブルに別個の blob_sha256 インデックスはありません(コンテナリポジトリのインデックス を参照)。将来、sha256 以外のダイジェストアルゴリズムが導入される場合は、2 つのカラムを分離し、再検討します。

アップロードセッション

アップロードセッションは、ADR-008 で記述された 2 フェーズアップロードライフサイクルを通じて、進行中の blob アップロードを追跡します。各セッションは、ネームスペースのストレージパーティション内の uploads/{upload_id} にある一時的なストレージオブジェクトにマッピングされます。セッションは、アップロード API(再開可能なアップロード、同時アップロードの解決)をサポートし、オブジェクトストレージの列挙なしに アップロードパージ を可能にするために、初期スキーマからデータベースで追跡されます(ADR-011)。

erDiagram
    namespaces ||--o{ upload_sessions : "has many"
    repositories ||--o{ upload_sessions : "has many"

    upload_sessions {
        bigint id PK "DEFAULT nextval('upload_sessions_id_seq'), part of composite PK (id, namespace_id)"
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id)"
        uuid repository_id FK "NOT NULL, (repository_id, namespace_id) references repositories(id, namespace_id)"
        uuid upload_id "NOT NULL"
        bigint size_bytes "NOT NULL, DEFAULT 0, bytes uploaded so far"
        bytea hash_state "nullable, serialized intermediate SHA-256 hash state"
        boolean dirty "NOT NULL, DEFAULT FALSE, concurrent-writer poison bit"
        timestamptz created_at "NOT NULL, DEFAULT NOW()"
        timestamptz expires_at "NOT NULL"
        timestamptz updated_at "NOT NULL, DEFAULT NOW(), chunk-append latency metrics"
    }
  • upload_sessions: 各 blob アップロードが進行中の間、それを追跡します。テーブルはバイナリ存在モデルに従い、コンテナレジストリのパターン を反映します。行が存在すれば、アップロードは進行中であるかクリーンアップが必要です。存在しなければ、アップロードは完了したかパージされました。完了時、ストレージレイヤーは blob をコンテンツアドレス可能なストアに移動し、その後 blob_storage_blobs レコードを作成するのと同じトランザクション内でセッション行を削除します。フォーマット固有の行(blob_storage_attachments とフォーマットテーブル)は、その後、呼び出し側のフォーマットサブシステムによって別個のトランザクションで作成されます。これにより、ストレージレイヤーをフォーマット非依存に保ちます。upload_id(UUID)は、一時オブジェクトパス(uploads/{upload_id})で使用されるストレージレベルの識別子です。repository_id はアップロードを開始したリポジトリを記録します。後続のリクエストでは、サーバーは URL 内のリポジトリが session.repository_id と一致することを検証し、upload_id が漏洩した場合のリポジトリ間の再利用を防ぎます。各リクエストの認可は、URL のリポジトリに対してリクエストミドルウェアによって実行され、このカラムには依存しません。複合外部キー (namespace_id, repository_id) は、アップロードがターゲットリポジトリと同じネームスペース内にあることを強制します。size_bytes は一時ストレージに書き込まれたバイト数を追跡します。再開可能なアップロードの場合、各チャンクが到着するにつれて更新され、クライアントにどこから再開するかを伝える Range レスポンスヘッダーを生成するために使用されます(OCI Distribution Spec)。モノリシックアップロードの場合、blob データが書き込まれた後に設定されます。created_at はアップロードが開始された時刻を記録します。これは、アップロード期間メトリクス(期間と blob サイズの相関)と、アプリケーションの TTL 設定が引き下げられた場合の遡及的な期限切れ(WHERE created_at < NOW() - :new_ttl)を可能にします。既存のセッションは元の期限を保持するため、expires_at だけではこれをサポートできません。expires_at はセッション期限のタイムスタンプで、作成時にアップロードタイプに基づいて NOW() + :configured_ttl として計算されます(再開不可能なものは短く、再開可能なアップロードは長く)。期限切れのセッションはアップロードパージの候補です。パージャーは一時ストレージオブジェクトを削除し、行を削除します(ADR-008)。再開可能なアップロードのハッシュ状態は、シリアライズされた中間 SHA-256 状態として hash_state カラムに保存されます。単一行の UPDATE は PATCH ごとのオブジェクトストレージのラウンドトリップよりも単純です(ADR-008 を参照)。その UPDATE は行ロックを取りません。1 つの upload_id に対する同時ライターは、SELECT ... FOR UPDATE ロックではなく、size_bytes の compare-and-swap と dirty ポイズンビットによって調整されます(乖離時に終了)。dirty はそのポイズンビットで、CAS の敗者によって設定され、行の削除によってのみクリアされます。updated_at はセッションの最終変更時刻を記録し、チャンク追加レイテンシメトリクスと最終アクティビティの可観測性をサポートします。これはアクセスログから導出するのではなく保存されたカラムです。なぜなら、リクエスト時に同期的な再開パスの決定を支えるためです。書き込みコストは無視できます。既存の size_bytes/hash_state の UPDATE に相乗りするためです。HASH(namespace_id) で 64 パーティションにパーティショニングされ、スキーマ内の他のすべての namespace_id スコープのテーブルと一貫しています。セッションは短命ですが、アップロードパージャーは延期されている(ADR-011)ため、それが出荷されるまで期限切れの行が蓄積され、初日からパーティショニングすることで後のマイグレーションを回避し、repositories とのパーティションワイズ JOIN の適格性を保ち、空のパーティションではコストがかかりません。主キーは、従来の (id) ではなく (id, namespace_id) です。PostgreSQL はハッシュパーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを必要とし、この PK のパーティションキーは UUIDv7 の namespace_id なので、すでにデプロイ間でグローバルに一意です。これは、sha256 でパーティショニングし同じ保証のために namespace_id を追加する blob_storage_attachments と blob_storage_blobs とは異なります。

インデックス

  • upload_sessions: (namespace_id, upload_id) に対するユニークインデックス — ネームスペース内でアップロード UUID によってセッションを検索します。expires_at に対するインデックス — アップロードパージのために期限切れのセッションを見つけます。(namespace_id, repository_id) に対するインデックス — 特定のリポジトリのすべてのセッションを見つけます。認可チェックとリポジトリ削除時のクリーンアップに使用されます。

クエリ例

  • アップロードセッションを作成する

    INSERT INTO upload_sessions (namespace_id, repository_id, upload_id, expires_at)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', '019a1b2c-0456-7abc-8def-000000000456', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', NOW() + INTERVAL '1 hour')
    RETURNING id, upload_id;
    
  • チャンクアップロード中にセッションを検索する

    SELECT *
    FROM upload_sessions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND upload_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11';
    
  • チャンク追加後にセッション状態を更新する(hash_state + updated_at、size_bytes に対する compare-and-swap)

    UPDATE upload_sessions
    SET size_bytes = 1048576, hash_state = 'a1b2c3...'::bytea, updated_at = NOW()
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND upload_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
      AND size_bytes = 524288;  -- CAS: only persist if no concurrent writer advanced the row; a zero-rowcount result is a conflict
    
  • アップロードパージのために期限切れのセッションを見つける

    SELECT id, namespace_id, upload_id
    FROM upload_sessions
    WHERE expires_at < NOW()
    ORDER BY expires_at
    LIMIT 100;
    

    このクエリはパーティションプルーニングされません。述語が namespace_id を含まないため、64 パーティションすべてをスキャンします。これはここでは許容できます。パージャーは境界のあるバックグラウンドジョブ(LIMIT 100、expires_at のインデックスに支えられる)であり、ホットパスのクエリではないため、ファンアウトはパフォーマンス上クリティカルではありません。

  • クリーンアップ後にセッションを削除する

    DELETE FROM upload_sessions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = 789;
    

パーティショニングの不変条件

namespace_id を含むすべてのテーブルはパーティショニングされます。 デフォルトのパーティションキーは HASH(namespace_id) で 64 パーティションです。特定のテーブルは、文書化された理由がある場合に異なるキーを使用することがあります(HASH(sha256) の例外については blob ストレージのパーティショニング戦略 を参照)。namespace_id を含まないテーブルはパーティショニングされません。

このルールは、テーブルごとの判断ではなく、行のプロパティとして述べられています。namespace_id がスキーマの一部であれば、テーブルはパーティショニングされます。「このテーブルは小さい」「このテーブルは親と 1:1 である」「後でパーティショニングを追加できる」といった例外はありません。小さなテーブルも大きなテーブルと同じようにパーティショニングされます。均一性が要点です。低ボリュームのテーブルをパーティショニングするコストは無視できます。64 個のほぼ空の子、計測可能なランタイムオーバーヘッドはありません。一方、後でパーティショニングを 追加する コストは、本番データが配置された後のテーブルの書き換え、主キーの再構成、カスケードする外部キーの変更によって支配されます。

以下の例外も同様に構造的であり、ここで却下した判断に基づく例外には該当しません。

機械的な帰結

PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを要求します。これはスキーマ全体の主キーと外部キーを形作ります。

  • 主キー。 すべてのパーティショニングされたテーブルの主キーは namespace_id を吸収します。(id) は (id, namespace_id) になります。パーティショニングされたテーブルのユニークインデックスは、先頭カラムとして namespace_id を含みます。
  • パーティショニングされたテーブル間の外部キー。 namespace_id で複合になります。子は (<parent>_id, namespace_id) を介して親を参照し、親の (id, namespace_id) を参照します。repositories、workspaces、フォーマット固有のリポジトリテーブル、中間層テーブル、ファイルテーブル、リモートキャッシュテーブルではこの形式を使用しますが、祖先 ID を含む形式という 1 つのバリエーションがあります。親への参照とともに祖先 ID を非正規化して保持するテーブルは、その ID を含むように参照を拡張します。その結果、キーは namespace_id、祖先 ID、親 ID の組となり、親の同じ 3 カラムに対するセカンダリユニークインデックスを参照します。このスキーマのもう 1 つの 3 カラムの系列である blob_storage_attachments への参照は、このバリエーションではありません。それぞれが親 ID とともに保持するのは、非正規化された祖先 ID ではなく、親自身の sha256 です。これにより、データベースは、行が指定する祖先とその親が属する祖先が異なる行を拒否します。2 カラム形式ではそのような行が許容されます。container_manifest_relationships は container_manifests への 2 つの参照を両方とも container_image_id で拡張し、maven_files は maven_versions への参照を maven_package_id で拡張し、maven_remote_files は maven_remote_versions への参照を maven_remote_package_id で拡張します。カラムの順序は共通の規約ではなく、それぞれの親のユニークインデックスに従うため、この 3 つで統一されてはいません。3 カラムのキーが存在すれば (<parent>_id, namespace_id) のペアも存在するため、拡張された参照は 2 カラムの参照に追加するのではなく、それを置き換えます。拡張は個々の参照に属するもので、祖先カラムが NOT NULL であることから導かれるわけではありません。container_remote_manifest_relationships は container_remote_manifests に対して 2 カラム形式を維持し、対応するホスト型では参照を拡張します。これは、現時点ではキャッシュ側の子リンクを書き込む処理がないためです(artifact-registry#264)。NULL を許容する祖先カラムでは、この拡張を行うこと自体ができません。MATCH SIMPLE はいずれかのカラムが NULL であるキーをスキップするため、そのような行では拡張された参照が何も強制しなくなるためです。
  • namespaces への外部キー。 単一カラムです。namespace_id は namespaces(id) を参照します。namespaces は主キーが (id) のままである唯一のテーブルです。これはパーティショニングされておらず、独自の namespace_id を持ちません(それを 定義する)ため、子テーブルは複合 PK の小細工なしにそれを参照します。

複合外部キーの形状は、ネームスペース境界をスキーマレベルでエンコードします。任意のパーティショニングされたテーブルの行は、外部キーがそれを禁止するため、異なるネームスペースに属する別のパーティショニングされたテーブルの行を参照できません。これは Cells のシャーディングキー(namespace_id)がアプリケーションレベルで引く境界と同じものであり、データベース自体の中で冗長化されています。

例外

2 つの構造的条件のいずれかを満たす場合にのみ、テーブルはパーティショニングされません。

namespace_id を欠くテーブル。 今日の主な例は namespaces 自体です。これは namespace_id が判明する前に slug から解決されるルーティングルートであり、その ID を定義するため namespace_id カラムを持ちません。namespace_id を持たない将来のテーブル(例: インスタンス全体の設定、グローバルな cron 状態、デプロイにスコープされたライフサイクルメタデータ)は、このデフォルトを自動的に継承し、パーティショニングされません。

主キー全体が namespace_id であるテーブル。 独立した id カラムを持たないため、ネームスペースごとに 1 行を保持し、2 行目を表現できず、どの外部キーからも参照されないテーブルです。現在、この条件を満たすのは namespace_statistics だけです。今後この条件を適用するテーブルは、主キーが正確に (namespace_id) であり、どこからも参照されないことを独自の説明に明記します。これにより、その主張は単なる表明ではなく監査可能になります。

この 2 つ目の条件は、不変条件が却下する「このテーブルは親と 1:1 である」という例外とは意図的に異なります。それは実装者がどちらとも主張できる関係性に関する判断でしたが、こちらはキー定義と、そのテーブルを参照する制約のカタログ一覧によって確定するスキーマのプロパティです。独自の id を持つテーブルは、どれほど小さくても対象外です。また、id または参照元の外部キーを取得したテーブルは、その時点で対象外になります。不変条件の根拠はいずれも、この形状のテーブルには当てはまりません。パーティショニングは namespace_id にスコープされた読み取りが 64 ではなく 1 つのパーティションに触れるようにするために存在しますが、ここでは主キーが namespace_id そのもの なので、すべての読み取りはすでに単一行のキー検索です。また、後からの変換は、両方が成立しなければならない 2 つの理由から低コストです。まず、不変条件が回避するために書かれた主キーの再構成、すなわち (id) から (id, namespace_id) への変更はすでに完了しています。namespace_id だけをキーとするテーブルは、パーティションキーをすべてのユニーク制約に含めるという PostgreSQL の要件を満たすためです。次に、PostgreSQL は通常のテーブルをパーティショニングされたテーブルへ ALTER できないため、変換ではパーティショニングされたテーブルを作成し、その下で既存テーブルと交換します。この処理では、すべての参照元の外部キーを削除して新しいテーブルに対して再作成し、各再作成時に参照元テーブルのスキャンを伴う再検証が必要です。どこからも参照されないテーブルには削除すべき外部キーがありません。そのため、参照元の外部キーがないことは、現在のスキーマに関する単なる観察ではなく条件の一部です。後から参照を取得したテーブルがこの例外にとどまると、その根拠がもはや成り立たなくなるためです。

どちらの例外の述語も構造的です。行内の namespace_id の有無、およびそのカラムがどこからも参照されないテーブルの主キー全体であるかどうかです。いずれも行数、書き込み頻度、現在のアクセスパターンに依存せず、これらはすべてシステムが進化するにつれて変化し得ます。

シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)も例外ではありません。これらは 64 パーティションすべてを保持し、1 つが入力され 63 が空になります。空のパーティションはこの規模では無視できます(それぞれ数 KB のカタログとインデックスのオーバーヘッド)。パーティションプルーニングは影響を受けず、デプロイ間でのスキーマの均一性は、シングルテナントのバリアントを切り出すことよりも価値があります。病的な「1 つのパーティションがすべてを保持する」ケースは、完全なマルチテナント規模での blob_storage_blobs / blob_storage_attachments にのみ適用され、これがそれら 2 つのテーブルが代わりに HASH(sha256) を使用する理由です。blob ストレージのパーティショニング戦略 を参照してください。

blob ストレージのパーティショニング戦略

帰結 で述べたように、blob_storage_blobs と blob_storage_attachments は、すべての Organization にわたってすべてのアーティファクトフォーマットを提供するため、非常に高い行数を蓄積します。意図的なパーティショニング戦略がなければ、これは次の問題につながります。

  • テーブルが数十億行に成長するにつれて、インデックスの肥大化とクエリパフォーマンスの低下。
  • すべてのアーティファクトタイプを同時にブロックするテーブル全体のロック(例: インデックス作成やスキーママイグレーション中)。
  • 高い書き込みレートでの autovacuum の競合。

念頭に置くべき重要な制約: PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約にパーティションキーを含めることを要求します。blob_storage_blobs の場合、重複排除制約は UNIQUE (namespace_id, sha256) です。パーティションキーがこれらのカラムのサブセットでない戦略は、その制約に追加のカラムを強制することになり、それは同じ Organization 内の同じ blob が異なるパーティションにまたがって二度保存されるのを防げなくなり、重複排除モデルを完全に損ないます。

以下が候補となる戦略です。

オプション A: sha256 によるハッシュパーティショニング

両方のテーブルを PARTITION BY HASH (sha256) で 64 パーティションにパーティショニングします。

sha256 はコンテンツアドレス可能なダイジェストなので、その値は本質的に均一に分布します。均等なデータ分布のために追加の作業は不要です。これはシングルテナント問題を解決します。シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)は、namespace_id のみを使用すると、すべての行を単一のパーティションに集中させてしまいます。sha256 をパーティションキーとすると、Organization がいくつ存在するかに関係なく、行は 64 パーティションすべてに均等に広がります。

[namespace_id, sha256] に対する既存のユニーク制約はすでに sha256 を含んでいるため、このスキームと互換性があります。パーティションキーが制約の一部であるため、PostgreSQL はハッシュパーティションにまたがって一意性を強制できます。

このアプローチでは、blob_storage_blobs への JOIN が単一のパーティションをターゲットにできるように、sha256 を blob_storage_attachments とフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)に伝播する必要があります。これは、blob 識別子(namespace_id + sha256)が *_files と blob_storage_attachments の両方の行に保存され、単純な bigint 外部キーよりも多くの物理ストレージを使用することを意味します(sha256 は bytea として 32 バイト、bigint は 8 バイト)。しかし、このトレードオフは正当化されます。読み取りパス(アーティファクトプル)、すなわちシステム内で最もホットなクエリは、(namespace_id, sha256) を介して *_files から blob_storage_blobs に直接 JOIN でき、blob_storage_attachments を完全にスキップして 1 つの JOIN を排除します。アタッチメントは、クリーンアップ 中に「この blob はまだ誰かに使用されているか?」に答えるライフサイクルパスのために依然として必要です。

5 つの重要なアクセスパターンは次のように動作します。

#操作頻度ヒットするパーティション
AP1アーティファクトのプル(namespace_id + sha256 を介した *_files → blob_storage_blobs)最高1
AP2孤立チェック(WHERE namespace_id = ? AND sha256 = ?)高1
AP3重複排除アップサート(ON CONFLICT (namespace_id, sha256) DO NOTHING)中〜高1
AP4アタッチメントの CRUD(blob から伝播された namespace_id + sha256)中1
AP5Organization ごとのストレージ会計(WHERE namespace_id = ?、sha256 なし)低全 64(緩和済み)

ポジティブ:

  • テナントの集中に関係なく均一な分布: シングルテナントデプロイは、1 つに集中させる代わりに、データを 64 パーティションすべてに広げます。
  • すべての高頻度アクセスパターン(プル、孤立チェック、重複排除アップサート、アタッチメント CRUD)がちょうど 1 つのパーティションにヒットします。
  • ユニーク制約 (namespace_id, sha256) はパーティションキーを含みます。重複排除アップサートは単一のパーティションをターゲットにし、外部ロックなしに ON CONFLICT DO NOTHING を介して同時アップロードを解決します。
  • 読み取りパス(アーティファクトプル)は blob_storage_attachments の JOIN を完全にスキップし、(namespace_id, sha256) を介して *_files から blob_storage_blobs に直接進みます。

ネガティブ:

  • sha256 をより多くのテーブルに伝播する必要があります。blob_storage_attachments とフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)は、blob_storage_attachment_id 外部キーに加えて (namespace_id, sha256) を保持します。これは blob 識別子を行にわたって複製し、行ごとのストレージを増加させます。
  • namespace_id のみ(sha256 なし)のクエリはパーティションをプルーニングできず、64 パーティションすべてをスキャンします。主なケースはストレージ会計(Organization ごとに blob サイズを合計する)です。これは、blob の挿入/削除時に遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます。これは GitLab で既に確立されているパターンです(例: プロジェクト統計)。ロールアップテーブルがなくても、64 パーティションにわたる並列集約は数秒で完了します。

オプション B: namespace_id によるハッシュパーティショニング

両方のテーブルを PARTITION BY HASH (namespace_id) で固定数のパーティションにパーティショニングします。

すべての一般的なアクセスパターンはすでに WHERE 句に namespace_id を含んでいるため、クエリプランナーはすべての操作に対して単一のパーティションをターゲットにできます。Cells のシャーディングキー(namespace_id)がパーティションキーを兼ね、より広範なアーキテクチャと一貫しています。

[namespace_id, sha256] に対するユニーク制約はすでに namespace_id を含んでいるため、このスキームと変更なしで互換性があります。PostgreSQL はすべてのハッシュパーティションにわたって一意性をグローバルに強制します。

ポジティブ:

  • すべての Organization スコープのクエリが単一のパーティションにヒットします。クエリプランナーは他のすべてを自動的にプルーニングします。
  • パーティションプルーニングはクリーンアップパスに直接適用されます。blob_storage_attachments の孤立チェック(WHERE namespace_id = ? AND sha256 = ?)は単一のパーティションをターゲットにすることが保証され、検索コストをテーブル全体のボリュームではなくパーティションサイズで境界づけます。
  • スキーマ変更とロックは単一のパーティションにスコープされ、他の Organization への影響を軽減します。
  • Cells のシャーディングキーと整合します。一般的なアクセスパターンに対してクロスパーティション作業はありません。
  • [namespace_id, sha256] に対する既存の制約は変更なしで正しく動作します。

ネガティブ:

  • Organization のサイズが大きく異なる場合、非常に高い blob 数を持つ Organization が自身のハッシュパーティションを支配し得ます。シングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)では、すべての行が単一のパーティションに集中します。VACUUM に数時間かかり、インデックスは数百 GB に達します。
  • WHERE 句から namespace_id を省略するクエリは、すべてのパーティションをスキャンします。

オプション C: id(主キー)による範囲パーティショニング

両方のテーブルを自動インクリメントする主キーの範囲でパーティショニングします。これは GitLab の既存の テーブルパーティショニングフレームワーク で使用されているアプローチであり、既存のツールによって十分にサポートされています。

ポジティブ:

  • パーティションサイズは予測可能に成長します。データが蓄積するにつれて新しいパーティションを簡単に追加できます。
  • GitLab の既存のパーティション管理インフラと互換性があります。

ネガティブ:

  • 重複排除の一意性を壊します。PostgreSQL は、パーティショニングされたテーブルのすべてのユニーク制約に id を含めることを要求します。[namespace_id, sha256] に id を追加すると、同じ Organization の同じ sha256 が複数のパーティションに現れ得るようになり、重複排除モデルが完全に壊れます。
  • クエリは Organization スコープですが、パーティションは id 範囲ベースなので、すべての Organization スコープのクエリが複数のパーティションにまたがります。
  • ロックスコープの削減が Organization 境界と整合しません。

オプション D: created_at による範囲パーティショニング

両方のテーブルを時間範囲(例: 月単位または四半期単位のウィンドウ)でパーティショニングします。

ポジティブ:

  • blob がクリーンアップされた後、古いパーティションを簡単にアーカイブまたはドロップできます。
  • パーティションは既知の時間ウィンドウに対応し、明確な運用モデルです。

ネガティブ:

  • ホットパーティション問題: すべての書き込みが最新のパーティションをターゲットにし、書き込みの競合を集中させます。
  • blob は年齢ではなく、すべてのアタッチメントを失ったときに期限切れになります。時間ベースのパーティショニングは実際の blob ライフサイクルと整合しません。
  • オプション C と同じユニーク制約の問題: created_at をユニーク制約に追加する必要があり、クロスパーティション重複排除を壊します。
  • アクセスパターンは時間スコープではなく Organization スコープなので、クエリはすべてのパーティションにまたがります。

オプション E: パーティショニングなし

Cells レベルのシャーディング(namespace_id)と標準的なインデックス作成を主要なスケーラビリティメカニズムとして頼ります。パーティショニングは、メトリクスが必要性を示すまで延期されます。

ポジティブ:

  • 単純なスキーマと運用: パーティション管理のオーバーヘッドがなく、マイグレーションとスキーマ変更が簡単です。
  • 初期段階の規模では十分: 行数が単一の Cell 内で扱いやすいうちはうまく機能します。

ネガティブ:

  • Cell 内での無制限の成長: テーブルが成長するにつれて、テーブルレベルのロックがすべての Organization に同時に影響します。
  • 適切に設計されたインデックスでさえ、非常に高い行数ではパフォーマンスの圧力に直面します。

決定

sha256 によるハッシュパーティショニング(オプション A)が選択されました。blob_storage_blobs と blob_storage_attachments の両方について。

これは次のことを満たす唯一のオプションです。

  1. すべての高頻度アクセスパターン(アーティファクトプル、孤立チェック、重複排除アップサート、アタッチメント CRUD)を単一のパーティション内に保ちます。
  2. テナントの集中に関係なく行を均一に分布させます。これは、namespace_id ベースのパーティショニングがすべての行を 1 つのパーティションに集中させるシングルテナントデプロイ(Dedicated、Self-Managed、単一 Organization の Cells)にとってクリティカルです。
  3. [namespace_id, sha256] に対する既存のユニーク制約と変更なしで互換性があり、ON CONFLICT (namespace_id, sha256) DO NOTHING を介してレースのない重複排除アップサートを可能にします。

両方のテーブルについて、初期値として 64 パーティションが選択されています。これは、運用オーバーヘッドを管理可能に保ちつつ、十分な分布とロック分離を提供します。

トレードオフは、sha256 を blob_storage_attachments とフォーマット固有のテーブル(*_files、container_blobs、container_manifests、キャッシュエントリ)に伝播する必要があることです。これは blob 識別子(namespace_id + sha256)を行にわたって複製し、bigint 外部キーのみよりも多くの物理ストレージを使用します。利点は、読み取りパス、すなわちシステム内で最もホットなクエリが、(namespace_id, sha256) を介して *_files から blob_storage_blobs に直接 JOIN し、blob_storage_attachments を完全にスキップして 1 つの JOIN を排除することです。アタッチメントは クリーンアップライフサイクルパス のためにのみ残ります。

namespace_id のみ(sha256 なし)のクエリ、例えば Organization レベルのストレージ会計は、パーティションをプルーニングできず、64 パーティションすべてをスキャンします。これは、遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます。これは GitLab で既に確立されているパターンです(例: プロジェクト統計)。

フォーマット固有のテーブルのパーティショニング戦略

フォーマット固有のテーブル(ホストされたコンテンツテーブルとそのリモートの対応物)は、パーティショニングの不変条件 によって確立された HASH(namespace_id) デフォルトに従います。各テーブルの箇条書きがそれを明示的に記録します。ホスト型とリモート型は同じアクセス形状を共有するため 1 つの戦略を共有します。すべての主要なアクセスパターンは namespace_id スコープです。テーブルごとの違い(キャッシュ TTL、上流メタデータ)はパーティショニングと直交し、テーブルごとの説明に存在します。

このグループに固有の根拠:

  • すべての主要なアクセスパターンが namespace_id スコープです(リポジトリとアーティファクト座標による検索、パッケージまたはイメージのファイルの一覧表示、上流のキャッシュエントリの一覧表示)。したがって、HASH(namespace_id) はすべての操作に対して単一パーティションのプルーニングを与えます。読み取りパスのショートカット((namespace_id, sha256) を介した *_files → blob_storage_blobs、blob_storage_attachments をスキップ)、すなわちシステム内で最もホットなクエリは、このパーティショニングから直接恩恵を受けます。
  • blob_storage_blobs を HASH(sha256) に駆動するシングルテナント集中の懸念は適用されません。各フォーマット固有のテーブルは 1 つのフォーマット(リモートの場合は 1 つの上流)にスコープされるため、ネームスペースごとのフットプリントは構造的に、blob_storage_blobs が保持するクロスフォーマット集約の一部にすぎません。
  • (namespace_id, blob_sha256) を介した blob_storage_blobs への JOIN は、namespace_id リテラルでフォーマットテーブルのパーティションをプルーニングします。blob 側が計画時にプルーニングされるのは、クエリがダイジェストを名指す場合だけです。blob_storage_blobs.sha256 = maven_files.blob_sha256 のようなカラム間の述語では sha256 がプランナーにとって不明なため、64 個すべての blob パーティションに対する Append を構築し、実行時に外側の行ごとにプルーニングします。スキャンは実行時に回避されますが、コストは計画時に発生します。

PostgreSQL 17.10 上で、blob_storage_blobs、blob_storage_attachments、maven_files にそれぞれ単一ネームスペースの 5,000 行を持つフィクスチャを使用して計測しました。どちらの行も同じ読み取り、すなわち 1 つの maven_files 行とその blob のサイズで、blob 側が参照するテーブルだけが異なります。2 行目はネームスペースレベルの調整用のシャドウです。これは PARTITION BY HASH (namespace_id) と PRIMARY KEY (namespace_id, sha256) INCLUDE (size) を持ちます。各値は同一セッションで 5 回繰り返した範囲です。

blob のサイズを得るために JOIN したテーブルblob 側のプラン計画実行
blob_storage_blobs64 パーティションに対する Append、うち 63 は never executed3.85〜4.70 ms0.45〜0.72 ms
blob_storage_blobs_by_namespace単一パーティションの Index Only Scan、Heap Fetches: 00.26〜0.37 ms0.11〜0.17 ms

namespace_id リテラルは計画時にシャドウをプルーニングしますが、ベーステーブルをプルーニングするダイジェストリテラルはありません。新しいバックエンドでのベーステーブルの最初のプランは計画に 22.07 ms を要し、5,738 個の計画バッファを読みます。ファンアウトはフィクスチャの行数ではなく、64 パーティションに対する HASH(sha256) とダイジェストの欠如から生じます。blob_storage_blobs [namespace_id, sha256] のユニーク制約によってこれは変わりません。パーティショニングされたテーブルのインデックスは各パーティションにローカルなので、パーティションを開くコストは下げても Append の大きさは変えません。

パーティション数の根拠

すべての HASH(namespace_id) テーブルは 64 パーティションを使用し、blob_storage_blobs と blob_storage_attachments(HASH(sha256))に選ばれた 64 パーティションと一致します。この数は、既存の Container Registry と Package Registry のデータベースからの本番データに基づいています。

パーティション数は、最大の想定テーブル(container_blobs)によって駆動され、その本番アナログはすでに同等の規模で 64 パーティションを使用しています。他のフォーマット固有のテーブルは大幅に小さく、64 パーティションはそれらすべてにとって余裕があります。

この決定の主要な要因:

  • スキュー耐性: HASH(namespace_id) は均一な分布を保証しません。ネームスペースのサイズは大きく偏っています。少数の大きなネームスペースが不均衡な割合の行を保持します。パーティション数が少ないと、同じパーティションにハッシュされる大きなネームスペースが不均衡を増幅します。64 パーティションでは、最悪のスキューでもパーティションサイズを管理可能に保ちます。
  • アンダーパーティショニングの修正は高コスト: 後でパーティション数を変更するには、テーブルの完全な再構築が必要です。小さなテーブルをオーバーパーティショニングするオーバーヘッドは無視できますが、大きなテーブルをアンダーパーティショニングすると実際の運用リスクが生じます。
  • パーティションワイズ JOIN: PostgreSQL は、同じパーティションスキーム(同じキー、同じ方法、同じ数)を共有するテーブル間の JOIN を、一致するパーティションを直接 JOIN することで最適化できます。すべての HASH(namespace_id) テーブルが 64 パーティションを使用するため、この最適化が利用可能です。実際には、クエリはすでに namespace_id = ? を含むため、プランナーは各側について 1 つのパーティションにプルーニングしますが、パーティションワイズ JOIN は無料の最適化のままです。
  • 運用上の一貫性: すべての namespace_id パーティショニングされたテーブルにわたる単一のパーティション数は、特定の namespace_id のすべてのテーブルが同じパーティション番号にハッシュされることを意味し、メンテナンススクリプト、監視、一括操作を簡素化します。

どのテーブルがパーティショニングされるかは、ここで列挙されるのではなく パーティショニングの不変条件 によって決定されます。

バッファ書き込みと非同期書き込み

ダウンロードまたはアップロードのリクエストごとに、複数のカラムが更新されます。repositories のカウンターカラム(artifacts_count、downloads_count、size_bytes)、エンティティ数の上限チェックに使用する npm_packages のパッケージごとのカウンター(versions_count、tags_count)、Maven、npm、Generic のバージョンテーブル(maven_versions、maven_remote_versions、npm_versions、npm_remote_versions、generic_versions)の size_bytes カウンター、そして container_images、maven_packages、maven_versions、npm_packages、npm_versions、generic_packages、generic_versions の last_downloaded_at タイムスタンプです。これらをリクエストパスで直接書き込むと、同じ行に対する同時リクエストが直列化され(人気のあるパッケージでホット行の競合が発生し)、リクエストのレイテンシがデータベースの書き込みスループットに結び付きます。

これを避けるために、これらのカラムはバッファ書き込み/非同期書き込みを介して維持されます。リクエストハンドラーは高速な中間ストア(例: Redis)に更新を記録し、バックグラウンドプロセスが定期的にバッファされたエントリを行にマージし直します。これは GitLab の ProjectStatistics と同じパターンを再利用します。

この方法で維持されるカラムは、スキーマ図で buffered とフラグ付けされています。

マージのセマンティクス

マージ戦略はカラムの種類に依存します。

  • カウンター(artifacts_count、downloads_count、size_bytes、versions_count、tags_count): バッファされたデルタを既存の値に合計します。すべてのインクリメントを保持しなければなりません。インクリメントを失うと永続的なアンダーカウントが生じます。エンティティ数制限チェック(versions_count、tags_count)については、境界での小さな上限超過は許容されます。制限は(データ整合性ルールではなく)製品の上限であり、ドリフトはバッファウィンドウで境界づけられ、次のフラッシュで再同期されます。重複するバージョン名は、カウンターに関係なく、npm_versions と npm_tags のユニークインデックスによって別個にブロックされます。
  • タイムスタンプ(last_downloaded_at): バッファされた値と既存の値の最大値を取ります(最新が勝つ)。最も新しいダウンロード時刻のみが重要です。中間の値は破棄できます。

両方の戦略は同じバッファリングインフラを共有し、書き込み前にバッファされたエントリがどのように削減されるかだけが異なります。

トレードオフ

  • 古さ: バッファされたカラムは、最大 1 つのフラッシュ間隔だけ現実から遅れます。これは現在のコンシューマーにとって許容できます。ライフサイクルルールの評価(keep_last_downloaded_at)はフラッシュ間隔を十分に上回るスケジュールで実行され、ランディングページのカウンターは短時間の乖離を許容します。これは、自身の書き込みを同期的に観測しなければならない読み取りや、ダウンロードイベントの正確な順序を必要とする決定には 適していません。
  • バッファ損失: フラッシュ前にバッファが失われると、最近の更新が失われます。カウンターについてはこれは永続的なアンダーカウントです。タイムスタンプについては、次のダウンロードが正しい(ただしわずかに遅延した)値を復元します。

ネームスペース ID 型

以下の決定は namespaces.id だけでなく、この ADR が定義するすべてのテーブルの id に適用されます。内部 blob ストレージ層だけが例外です。型選択による影響が最も広い namespaces.id について根拠を論じます。

namespaces.id カラムの型はスキーマ全体にカスケードします。すべてのパーティショニングされたテーブルがシャーディングキーとして namespace_id を保持し、それらのテーブルの本質的にすべての複合主キー、外部キー、複合インデックスがこのカラムを含みます。外部キーと複合インデックスでは先頭にあり、複合主キーでは id の後にあります。機械的な帰結では、(id) が (id, namespace_id) になると述べています。後で型を変更するには、すべてのパーティショニングされたテーブルとすべての物理的な子リレーションにわたる多段階のマイグレーションが必要であり、スキーマが本番データを保持すると実質的に取り消し不可能な決定になります。

選択を駆動する 3 つのプロパティ:

  1. デプロイモデル間でのグローバルな一意性。 Artifact Registry は、複数の独立したデプロイ(GitLab.com、Dedicated、Self-Managed、Cell ごと、および潜在的には GitLab Rails から独立したスタンドアロン製品)として実行されるように設計されています(ADR-022 を参照)。ローカルなシーケンスから引かれる連続した整数 ID はデプロイ間で衝突し、ネームスペース行が Artifact Registry インスタンス間を移動するあらゆるシナリオ(MVP 後のマイグレーションツール、Cell の統合、デプロイ間の参照)を排除します。
  2. 運用上のデバッグ容易性。 namespace_id = 42 はデプロイ間で曖昧です。同じ整数が、異なる Cell やインストールで無関係なネームスペースを指し得ます。サポートチケット、インシデントのランブック、デプロイ間のログ相関はすべて、識別子が一見して一意であるときに恩恵を受けます。
  3. ID 生成のための調整依存がない。 デプロイ間で重複しない bigint 範囲を割り当てるには、中央権威(Topology サービスまたは同等のもの)が必要です。UUIDv7 はデータベース上でローカルに、調整なしで生成されます。

オプション

オプション A: UUIDv7

namespaces.id は UUIDv7 値(RFC 9562)が入力される uuid です。スキーマ全体のすべての namespace_id カラムは uuid です。生成は、データベース側(PG18 ネイティブの uuidv7()、または PG13〜17 の pg_uuidv7 拡張)でも、RFC 9562 準拠のライブラリを使用したアプリケーション側でも行えます。カラム型はすべての場合で同じで、パスは後でデータを書き換えることなく変更できます。完全なマトリックスについては下記の決定セクションを参照してください。

ポジティブ:

  • すべての Artifact Registry デプロイにわたって構造的にグローバルに一意です。調整も、中央アロケーターも、範囲管理もありません。数千のデプロイが同時に生成しても、衝突は暗号学的に極めて起こりにくいものです。
  • 時刻順: 新しい ID は各パーティション内の B-tree の右端に追加されます。credativ による PG18、100 万行の比較 では、UUIDv7 の主キーインデックスは約 90% のリーフ密度(bigint シーケンスも達成するデフォルトの fillfactor)と約 0% のフラグメンテーションを達成しました。同じワークロードでの UUIDv4 の約 71% のリーフ密度と約 50% のフラグメンテーションと比べてです。
  • WAL ボリュームは UUIDv4 よりも bigint にはるかに近いです。UUIDv7 のシーケンシャル挿入の局所性は、ランダムな UUID が被るフルページ書き込みの増幅を回避します。挿入スループットは、現実的な複数カラムスキーマで bigint と数パーセント以内で一致します(kkm-mako、PG18、100 万行・13 カラムの e コマーステーブル: bigint 76.5 秒対 UUIDv7 77.0 秒、Ardent Performance、PG17-dev、10 個の同時クライアントを持つ 2000 万行のテーブル: bigint 3,480 tps 対 UUIDv7 3,420 tps)。むき出しの 2 カラムのトイスキーマでは差がより顕著です。kkm-mako の最小スキーマ は同じ行数で bigint 1.63 秒対 UUIDv7 2.16 秒(約 32% 遅い)を計測しました。これは、より幅広い ID カラムが行のより大きな割合を占めるためです。絶対値はワークロード依存です。
  • 埋め込まれたミリ秒タイムスタンプにより、ID は BRIN フレンドリーで、診断のために自明に抽出可能です。
  • Artifact Registry が実行され得るすべての PostgreSQL バージョンで利用可能です。PG18 は uuidv7() をネイティブに出荷します(2025 年 9 月)。PG13〜17 では pg_uuidv7 拡張 が公開ベンチマークによればネイティブに対して 2% 未満のオーバーヘッドで uuid_generate_v7() を提供し、どのバージョンでも RFC 9562 準拠のライブラリでアプリケーション側生成をサポートします。
  • デプロイ間のネームスペースの可搬性を構造的に可能にします。MVP 後のマイグレーションツール(ADR-011)、Cell の統合、ADR-022 のスタンドアロン製品パスは、関連するすべての行の namespace_id を書き換えることなく、Artifact Registry インスタンス間でネームスペース行を移動します。

ネガティブ:

  • ストレージ: 値ごとに 16 バイト対 bigint の 8 バイト。namespace_id はパーティショニングされたテーブルのほぼすべての複合インデックスの先頭カラムなので、この幅広化はすべての物理的な子リレーションにわたって複合的に効きます。Jamauriceholt の PG 15.4 における 2000 万行の外部キーインデックスのベンチマーク は、UUIDv7 で 847 MB 対 BIGSERIAL で 423 MB(約 2 倍)、1 万行の一括挿入で 1,847 バッファ書き込みページ対 847(約 2.2 倍)を計測しました。エントリごとの幅広化はインデックスタプルの約 20 バイト中の約 8 バイト(約 40%)です。観測された合計インデックスサイズは、そのエントリごとの下限から、インデックスがキーか固定オーバーヘッドかの割合に応じて約 2 倍までの範囲です。Artifact Registry のマルチ TB のメタデータ規模では、これは実際のものですが境界のあるコストであり、テーブル全体ではなく namespace_id を先頭とするインデックスに集中します。
  • キー幅に実質的に依存するクエリの読み取りレイテンシは、bigint よりも計測可能に遅くなり得ます。合成的な 500 万ユーザー / 2000 万注文 / 5000 万 audit_log スキーマ(Jamauriceholt) では、1 対多の JOIN が UUIDv7 で BIGSERIAL より約 26 倍遅く、単一行検索が約 15 倍遅く、範囲/ページネーションが約 16 倍遅くなりました。これらの数値は最悪ケースの合成クエリを反映しており、このスキーマに外挿すべきではありません。すべてのホットパスは複合キーに対する単一パーティションの namespace_id = ? インデックス付き検索です。それらの条件下では、オーバーヘッドは上記のページごとのバイトコストで境界づけられ、クエリ形状のコストに増幅されません。レビュアーがより強力な経験的下限を望む場合、マージ前に PG18 上の代表的な行幅でのパーティションローカルなインデックス付き検索ベンチマークを依頼するのが適切です。
  • 時刻順は HASH(namespace_id) テーブルでのパーティションプルーニングを可能にしません。ハッシングはタイムスタンプ成分に関係なく値をパーティションに散らします。パーティション内の B-tree の局所性は保たれますが、これは bigint シーケンスもより低いストレージコストで提供します。UUIDv7 のパーティションプルーニングの利点は RANGE(uuid) スキームにのみ適用され、ここでは使用されません。
  • クライアントライブラリ、管理ツール、API レスポンスは整数ではなく 36 文字の文字列をレンダリングします。軽微ですが広範です。namespace_id を運ぶあらゆるエンドポイントで JSON レスポンスサイズが増加します。
オプション B: 調整された範囲割り当てを伴う bigint

namespaces.id は bigint DEFAULT nextval('namespaces_id_seq') のままです。各 Artifact Registry デプロイには、Topology サービスによって重複しない bigint 範囲(例: デプロイ X: 1 〜 10^12、デプロイ Y: 10^12+1 〜 2×10^12)がプロビジョニングされます。Artifact Registry はスラッグの取得のためにすでに Topology サービスに依存しています(ADR-022 を参照)。

ポジティブ:

  • 現在のドラフトに対してストレージの差分がゼロです。考慮すべきインデックス、WAL、JOIN のコストはありません。
  • 既存の依存を再利用します。Topology サービスはスラッグの取得のためにすでに必要です。
  • ID 生成はシーケンスの nextval のままです。自明に高速で、拡張は不要です。
  • Cells 間で調整された bigint シーケンスという GitLab Rails の確立されたパターンと一致します(Cells 開発ガイドライン)。

ネガティブ:

  • デプロイ間のネームスペースの可搬性が構造的にサポートされません。Y の割り当て範囲がソース ID を含まない場合、デプロイ X からデプロイ Y へネームスペースを移動するには、依然としてすべての行の namespace_id を書き換える必要があります。
  • 範囲割り当ては、すべての新しい Artifact Registry デプロイにブートストラップステップを追加し、範囲サイズと回収のためのガバナンスモデルを追加します。範囲を重複させてしまう誤割り当ては、早期に検出するのが難しいグローバルな一意性違反です。
  • 後でデプロイ間の可搬性をサポートする決定を下すと、この ADR が回避しようとしている bigint から UUID への完全なマイグレーションが必要になります。
オプション C: Snowflake パックされた bigint

アプリケーション側で 64 ビットをビットパックします: デプロイ ID(14 ビット、16K デプロイ)+ タイムスタンプ(41 ビット、エポックから 69 年)+ バックエンドごとのシーケンス(9 ビット、512 ID/ms/バックエンド)。小さなライブラリで Go サービスで生成されます。

ポジティブ:

  • bigint に対してストレージの差分がゼロです。同じインデックス、WAL、JOIN プロファイルです。
  • 自己識別: デプロイの起源が任意の namespace_id から抽出可能です。
  • UUIDv7 のように時刻順で、同じパーティション内の B-tree 局所性の利点を与えます。
  • 拡張依存がありません。ID 生成は数個のビット演算です。

ネガティブ:

  • PostgreSQL プリミティブの代わりに Go サービスで保守されるカスタムジェネレーター。すべてのライターは同じライブラリバージョンとクロックソースを使用しなければなりません。
  • クロックスキューに敏感: デプロイごとのカウンターはクロックの巻き戻しとバーストトラフィックを生き延びなければなりません。モノトニッククロックの規律と、ミリ秒内のシーケンスカウンターの慎重な扱いが必要です。
  • 業界で広く使用されています(Twitter、Discord、Instagram の 41+13+10 バリアント)が、PostgreSQL ネイティブのパターンではありません。ツール、監査可能性、チーム間の親しみやすさは UUID よりも弱いです。
  • ビットフィールドの分割は一度限りの設計上の決定です。デプロイビットが少なすぎたり、タイムスタンプ範囲が狭すぎたりすると、後で変更するのが困難です。
  • デプロイ間のマイグレーションを解決しません。デプロイ X で生成された ID は X の 14 ビットプレフィックスを永遠に運ぶため、ネームスペースをデプロイ Y へ再配置しても、依然として書き換えか、起源について嘘をつく ID のいずれかを意味します。

決定

namespaces.id にはオプション A(UUIDv7)が選択されました。その結果、スキーマ全体のすべての namespace_id カラムと、API に公開されるすべてのテーブルの id(repositories.id、container_images.id、maven_packages.id など)にも適用されます。これらの id カラムはサーバー側デフォルトを持たない uuid です。アプリケーションレイヤーが INSERT 時に値を供給するため、論理レプリケーションで調整するシーケンスも GENERATED カラムもなく、ネームスペースマイグレーション(ADR-022)に伴うデプロイ間の行再挿入にもシーケンス管理は不要です。内部 blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)は bigint DEFAULT nextval('<table>_id_seq') を維持します。これらのテーブルは API に公開されず、最も大量の行を保持するため、より狭いキーがインデックスサイズを目に見えて削減します。

決定的な要因:

  1. ネームスペースは可搬性の単位です。 何らかの Artifact Registry 識別子がデプロイ間の移動を生き延びなければならないとすれば、それは namespace_id です。ネームスペースより下のすべてはそれとともに移動します。ネームスペースより上のすべては不変のスラッグとアンカータプルを通じて表現されます(ADR-022)。
  2. コストは集中していて境界があります。 namespace_id を 8 バイトから 16 バイトに幅広化することは多くのインデックスの先頭カラムに効きますが、合計ストレージを倍増させません。大きなパーティショニングされたテーブルの行幅は他のカラム(リポジトリ/イメージ/マニフェスト ID、タイムスタンプ、カウンター、32 バイトの bytea ダイジェスト)によって支配されます。予備的なサイジングでは、影響を合計メタデータストレージの数十パーセントに置いており、Artifact Registry のキャパシティ範囲内です。
  3. 利点はインクリメンタルではなく構造的です。 デプロイ間の移動に触れるすべての MVP 後の機能(ADR-011 のマイグレーションツール、Cell の統合、ADR-022 のスタンドアロン製品パッケージング)は、namespace_id が構造的にグローバルに一意であるときに意味のある形で簡単になり、アロケーターの不在は調整依存を取り除きます。
  4. ストレージコストは一度、挿入時に、まだ空のスキーマで支払われます。 オプション B は、デプロイモデルが後でグローバルな一意性を要求する場合、すべてのパーティショニングされたテーブルにわたる取り消し不可能なマイグレーションを必要とします。私たちは、後の無制限なマイグレーションリスクを回避するために、既知の境界のあるコストを今日受け入れます。
  5. UUIDv7 はホットパスのパフォーマンスプロファイルを保ちます。 単一パーティションの namespace_id = ? 検索は単一パーティションのままです。bigint が提供するパーティション内の B-tree 局所性は、UUIDv7 の時刻順プレフィックスによっても提供されます。失われる唯一のプロパティ(UUID 範囲によるパーティションプルーニング、8 バイトのインデックス先頭カラム)は、HASH パーティショニングに適用されないか、コストが境界づけられています。

実装上の注記:

  • 3 つの実行可能な生成パスが存在します。選択はデプロイ時に利用可能な PostgreSQL バージョンに依存し、カラム型とは独立です。
    • PG18+ ネイティブ: カラムデフォルト DEFAULT uuidv7()。拡張は不要です。
    • PG13〜17 で pg_uuidv7 拡張を使用: カラムデフォルト DEFAULT uuid_generate_v7()。ネイティブパスとの関数名の違いに注意してください。マイグレーションとスキーマダンプは、ターゲット環境に対して正しい名前を参照しなければなりません。
    • アプリケーション側生成: 任意の PostgreSQL バージョン、拡張不要。Go サービスが RFC 9562 準拠のライブラリで値を生成し、INSERT で供給します。
  • これらのパス間で後から切り替えるのはメタデータのみ(ALTER COLUMN SET DEFAULT)であり、すべてのジェネレーターが RFC 9562 準拠の UUIDv7 値を発行する限り、データを書き換えません。これにより、初期パスはスキーマのコミットメントではなくランタイム/運用上の選択になります。
  • その準拠はジェネレーターに任せず、データベースが強制します。サーバー側デフォルトを持たない uuid の id を持つすべてのテーブルには、CHECK ((get_byte(uuid_send(id), 6) >> 4) = 7) というバージョンチェックがあります。uuid_send は値の 16 個の生のバイトを返し、バイト 6 の上位ニブルが RFC 9562 のバージョンフィールドです。両方の関数は不変なので、この式を CHECK に使用できます。UUIDv7 以外を発行するパスの値は、後から発見されるのではなく書き込み時に拒否されます。INSERT がフィールドを割り当てなかった場合のすべてゼロの UUID や、紛れ込んだ uuid.New() によるバージョン 4 の値も同様です。これら最後の 2 つの値は、どちらも正当な主キーとして読み取れますが、他のすべての行が占める時刻順の範囲から外れて並びます。
  • 残りのテーブルには制限対象の uuid id がないため、チェックを持ちません。blob ストレージ層、id カラムなしでキー付けされる 3 つの Artifact Registry テーブル(repository_collection_repositories、namespace_statistics、(namespace_id, sha256) をキーとする blob_storage_blobs_by_namespace シャドウ)、およびキー形状をこのスキーマが設定しないライブラリ所有のスキーママイグレーションとジョブキューのテーブル goose_db_version と river_* です。
  • すでに行を保持するテーブルへのバージョンチェック追加は、空のテーブルへの追加とは異なります。最初に ADD CONSTRAINT ... NOT VALID、次に別の VALIDATE を行い、1 つのトランザクションですべてを扱わず、マイグレーションごとに 1 テーブルを扱います。パーティショニングされた親の CHECK は親と 64 個すべてのパーティションで ACCESS EXCLUSIVE を取るためです。
  • 未解決の問題(GA に近づいたら解決): どの初期パスを取るかは、GA 時点で .com、Dedicated、Self-Managed にわたって利用可能な PostgreSQL バージョンに依存します。すべてのインストールタイプにわたって PG18 を保証できない場合、アプリケーション側生成が最も安全な暫定的選択です。カラムデフォルトは、PG18 がどこでも下限になったら、ネイティブの uuidv7() に移動できます。
  • この ADR のすべての Mermaid 図では、namespace_id カラムと API に公開されるすべてのテーブルの id を uuid として示します。blob ストレージ層(blob_storage_attachments、blob_storage_blobs、upload_sessions)だけが bigint の id を維持します。
  • UUIDv7 のモノトニック性は、同じミリ秒内の単一バックエンド(データベース側)またはプロセス(アプリケーション側)内で厳格であり、バックエンドやプロセスをまたいでは厳格ではありません。これはインデックスの局所性とデバッグ容易性には十分です。ホットパスのロジックが接続をまたいだ厳格なグローバル順序を想定することはありません。
  • スラッグから namespace_id への検索キャッシュ(ADR-022 を参照)は影響を受けません。それは不変のスラッグをキーにします。
  • パーティショニングされたテーブルで使用する複合主キーパターン(例えば、PostgreSQL のパーティショニングテーブルの制約ルールで必要となる upload_sessions の (id, namespace_id))は引き続き維持されます。namespace_id の構成要素は uuid です。id の構成要素は blob ストレージ層を除くすべてのテーブルで uuid であり、blob ストレージ層(例えば upload_sessions)では bigint です。

パーティションスキーマの構成

パーティショニングされたテーブルごとに 64 個の HASH パーティションがあり、後で中間層テーブルがパーティショニングされるにつれて成長するパーティションセットがあるため、子リレーションは論理テーブルを大きく上回ります。これらの子がどこに存在するか(public で親と並べるか、専用のネームスペースに置くか)は、スキーマの可読性、ツールの整合性、そしてパーティショニングされたテーブルの周りに構築するマイグレーションツールを形作ります。

オプション A: パーティション子のための専用スキーマ

親テーブルは public に存在し、すべてのパーティション子は専用の partitions スキーマに存在します。パーティション DDL は、すべての CREATE TABLE ... PARTITION OF で明示的にパーティションスキーマをターゲットにします。そうでなければ PostgreSQL は子を親のスキーマに配置します。

ポジティブ:

  • カタログの可読性: \dt public.*、information_schema、ER 図、IDE のスキーマビューは、すべてのパーティション子ではなく論理テーブルのみを表示します。スキーマレビュー、オンボーディング、DB コンソール作業は、エンジニアが実際に推論する抽象化で動作します。
  • アプリケーションレイヤーは影響を受けません。アプリケーションは public の親テーブルを通じてクエリし、partitions スキーマを参照することはありません。マイグレーションツールのみが、明示的な partitions.<name> 修飾を使用して子パーティションをターゲットにします。
  • パーティションライフサイクル操作のためのクリーンなスコープ化: 権限、pg_dump -n、ロジカルレプリケーションパブリケーション、監視エクスポーターは、テーブル名パターンの代わりに単一のネームスペースをターゲットにします。
  • 偶発的なパーティションレベルのクエリを抑制します。特定の子に到達するには partitions.<name> が必要で、パーティション抽象化をバイパスするのを難しくします。

ネガティブ:

  • Postgres のデフォルトが規約に反します。CREATE TABLE ... PARTITION OF parent は、明示的にオーバーライドされない限り子を親のスキーマに配置するため、強制はデータベース自体ではなくマイグレーションツール、リンター、または CI に存在します。
  • パーティショニングヘルパーは子の作成をパーティションスキーマにルーティングしなければならず、サービスのブートストラップはマイグレーション実行前にスキーマとその権限をプロビジョニングしなければなりません(ADR-006)。
  • ランタイムの利点はありません。プルーニング、ロック、VACUUM、クエリパフォーマンスは変わりません。このケースは完全に組織的なものです。

オプション B: すべてのテーブルを public に

親とその子パーティションはデフォルトスキーマに一緒に存在します。これは追加設定なしの PostgreSQL のすぐ使える動作です。

ポジティブ:

  • 最も単純なブートストラップ: 追加スキーマ、権限の分割、マイグレーションツールのパーティションルーティングヘルパーがありません。ローカル開発、CI、マイグレーションはセットアップなしで動作します。
  • Postgres のデフォルトとサードパーティツールの想定(イントロスペクション、ORM、クエリアナライザー)と一致し、ツールごとの設定を回避します。

ネガティブ:

  • カタログの煩雑さ: すべてのパーティション子が論理テーブルとネームスペースを共有し、すぐにあらゆる \dt、information_schema クエリ、ER 図を支配します。新しいテーブルがパーティショニングされるにつれて問題は複合的になります。
  • パーティションライフサイクルツールのためのスキーマレベルのスコープ化がありません。pg_dump、ロジカルレプリケーション、監視はテーブル名パターン(blob_storage_blobs_*、*_files_* など)として表現しなければなりません。
  • パーティションレベルのクエリ(例: SELECT FROM blob_storage_blobs_37)は通常のテーブル参照と区別がつかず、パーティション抽象化をバイパスするのを容易にします。

決定

オプション A(専用の partitions スキーマ)が選択されました。

決定的な要因は、アプリケーション向けのテーブルとパーティショニングの内部の区別です。論理テーブルは、アプリケーションが読み書きする公開インターフェースです。パーティション子はパーティショニングメカニズムの内部であり、パーティションライフサイクルツールによってのみ触れられるべきです。両方を単一のスキーマに保つことはその境界をぼかします。スキーマのイントロスペクション、権限、運用ツールはすべて、それらを区別するために名前でフィルターしなければなりません。専用の partitions スキーマは、その区別をデータベース自体の中で構造的にします。パーティションライフサイクル操作は 1 つのネームスペースにスコープされ、public を読むものにはアプリケーションに公開される操作対象だけが見えます。

可読性の議論がこの選択を補強します。パーティション子は最初のデプロイから論理テーブルを大きく上回り、より多くのテーブルがパーティショニングされるにつれてギャップが広がるため、単一スキーマのレイアウトは最初のデプロイから不格好で、時間とともに悪化します。ブートストラップコスト(マイグレーションツールのパーティションルーティングヘルパー、起動時のスキーマ作成)は一度限りであり、同じマイグレーション抽象化を採用するすべてのサテライトサービスにわたって償却されます(ADR-006)。

このパターンは規模で検証されています。GitLab Rails はパーティション子を専用の gitlab_partitions_static と gitlab_partitions_dynamic スキーマに整理しています。

パーティション子のみが専用スキーマに移動します。親テーブルと明示的なパーティショニングのないテーブルは public のままです。

クリーンアップタスク

上記のアプローチを理解するには、クリーンアップに関する blob ストレージ部分の課題を理解することが重要です。

一方では、親オブジェクトが破棄される一環として削除される 1 つまたは複数のアタッチメントを持つことができます(パッケージが破棄される、またはクリーンアップポリシーが実行されて数百のファイルを削除する)。

他方では、blob テーブルからレコードを単純に削除することはできません。それらはオブジェクトストレージ上のファイルを参照するためです。したがって、blob レコードを取得し、それを削除し、オブジェクトストレージ上のファイルも削除するクリーンアップタスクが必要です。これはデータベースでは実行できません。バックグラウンドプロセスとして実装されるコールバックが必要です。

blob を破棄のために扱う前に、バックエンドは(重複排除のため)それがもうどの部分にも使用されていないことを確認する必要があります。そこでアタッチメントテーブルが重要な役割を果たします。それは特定の blob の使用を記録します。クリーンアップタスクは、(namespace_id, sha256) ペアがアタッチメントテーブルにまだ存在するかどうかを単純に尋ねるだけで済みます(孤立チェッククエリ を参照)。それが「いいえ」であれば、blob は削除して問題ありません。

このアプローチは、各 blob ストレージクライアントに取り組むエンジニアにとってクリーンアップ契約を単純に保ちます。アーティファクトレコード(単一ファイル、一括破棄、またはクリーンアップポリシーの実行)を削除する際、アプリケーションは対応する blob_storage_attachments レコードも同じトランザクション内で削除しなければなりません。これがクライアントレベルでの唯一のクリーンアップ責任です。オブジェクトストレージとの対話は不要です。その時点から、blob ストレージのバックグラウンドプロセスが引き継ぎます。それは残りのアタッチメントを持たない blob_storage_blobs 行を特定し(孤立チェック)、データベースレコードとオブジェクトストレージファイルの両方を削除します。

アップロードセッションのクリーンアップも同様のパターンに従います。upload_sessions テーブルはバイナリ存在モデルを使用します。行が存在すれば、アップロードは進行中であるかクリーンアップが必要です。したがって、期限切れのセッション(expires_at < NOW() のもの)はパージの候補です。パージャーは一時ストレージオブジェクトを削除し、行を削除します。テーブルは、ストレージ内のオブジェクトを列挙することなく、候補を特定しストレージパス(ネームスペースパーティション下の uploads/{upload_id})を導出するために必要なすべての情報を提供します。アップロードパージの出荷スケジュールについては ADR-011 を参照してください。

この設計図は、クリーンアッププロセスを可能にし得る高レベルのデータベースプリミティブ(アタッチメント追跡、blob ストレージ構成、アップロードセッション追跡)を確立しますが、具体的な実装の詳細(トリガー、バックグラウンドジョブのロジック、パフォーマンス分析)は、後の詳細な仕様作業に委ねられます。

ストレージ使用量の計算

ストレージ使用量は、ネームスペース、リポジトリ、アーティファクトという 3 つのスコープで追跡します。各スコープには、サブミリ秒の読み取りで表示パスを提供する事前計算済みのカウンターと、ドリフトが疑われる場合やオンデマンドの検証が必要な場合にソースデータから正確な値を計算する調整パスがあります。blob ストレージスキーマは、これらの計算と帰属を正確かつ効率的にするように設計されています。

  • blob とアタッチメントは Organization にスコープされ、重複排除は Organization 内でのみ発生します(ADR-002 を参照)。
  • blob_storage_blobs は Organization ごとに一意の保存済み blob ごとに 1 行を持ちます。オブジェクトストレージ内の各物理オブジェクトは、Organization ごとに一度表現されます。
  • 物理 blob と blob_storage_blobs レコードは、すべてのアタッチメントを失ったときに(クリーンアッププロセス を通じて)非同期にクリーンアップされるため、blob_storage_blobs はまだ使用中(または非同期削除を保留中)の blob のみを参照します。結果として、ストレージ使用量クエリはアタッチメント数でフィルターする必要がありません。

したがって、特定の Organization のストレージ使用量を計算することは、blob_storage_blobs に列挙されたその blob のサイズを合計するだけのことです。これはマニフェストごとの container_manifests.size(コンテナリポジトリ を参照)とは異なります。後者は「このマニフェストツリーはどのくらいの大きさか」に答え、マニフェスト間やマニフェストリストの子間で共有される blob を二重カウントすることがあるため、Organization レベルの使用量の代替にはなりません。

別個の ADR が、ストレージ使用量の計算と帰属をより詳細に記述します。この ADR は、それらの計算を容易にするデータベースプリミティブを定義します。

erDiagram
    namespaces ||--|| namespace_statistics : "has one"

    namespace_statistics {
        uuid namespace_id PK,FK "NOT NULL, references namespaces(id) ON DELETE CASCADE"
        bigint deduplicated_size_bytes "NOT NULL, DEFAULT 0, buffered counter"
        bigint components_count "NOT NULL, DEFAULT 0, buffered counter"
        timestamptz last_reconciled_at "NOT NULL, DEFAULT 'epoch', reconciliation bookkeeping"
    }
  • namespace_statistics: バッファカウンター(非同期フラッシャー)を介して維持される、事前計算されたネームスペースレベルのカウンターを保存します。これは、表示パスと課金システムが読み取るテーブルであり、サブミリ秒のレスポンスを提供します(ベンチマーク表 を参照)。調整メカニズム は、ドリフトが疑われるときにこれらのカウンターを検証・修正するために存在します。構造上、各ネームスペースには 1 行が存在します。テーブル作成時に存在するすべてのネームスペースをシードし、その後 namespaces に挿入されるネームスペースごとに AFTER INSERT トリガーが 1 行を作成するため、読み取りパスは行が存在しない場合とゼロになったカウンターを区別する必要がありません。namespace_id 外部キーは、このスキーマ内で references namespaces(id) に ON DELETE CASCADE を伴う 2 つの外部キーの一方です。もう一方は同じルールの同じ側にある blob_storage_blobs_by_namespace シャドウテーブルのものであり、トリガーが必要とする参照アクションです。トリガーはすべてのネームスペースに子行があることを保証するため、カスケードがなければ、ネームスペース自身が存在させた行によってネームスペースのハード削除が拒否されます。この行はユーザーデータではなく導出された記録であるため、repositories で述べたルール、すなわち子が純粋な構造であればカスケードし、ユーザーデータであれば拒否するというルールのカスケード側に該当します。主キーは正確に (namespace_id) で、独立した id カラムはなく、このスキーマ内のどの外部キーからも参照されないため、パーティショニングの不変条件の例外にある 2 つ目の条件によりパーティショニングされません。この条件を適用する唯一のテーブルです。
    • deduplicated_size_bytes: ネームスペースが使用する合計ストレージで、blob の重複排除がすでに適用されています(ADR-002 を参照)。このカラムは、将来の生サイズや論理サイズのメトリクスと区別するために、将来の互換性を持たせるべく(size_bytes ではなく)このように命名されています。

    • components_count: ネームスペースのホスト型およびリモート型リポジトリに保存されたアーティファクトバージョンの合計数:

      • Container: container_manifests + container_remote_manifests。
      • Maven: maven_versions + maven_remote_versions。
      • npm: npm_versions + npm_remote_versions。
      • Generic: generic_versions(ホスト型のみ。リモート型のバリアントはありません)。

      ソフト削除された行は、ソフト削除ウィンドウ が期限切れになった後にガベージコレクションがハード削除するまでカウントされ続けます。これは deduplicated_size_bytes と一致します。これは、ガベージコレクションが基となる blob を回収するまで、ソフト削除されたアーティファクトのバイトを保持します。仮想リポジトリは、独自のバージョンテーブルを持たないため、別途カウントされません。仮想リポジトリは上流の順序付きリストを通じてリクエストを解決し(container_virtual_repository_upstreams とその Maven および npm の同等のものを参照)、各上流はそれ自体がホスト型またはリモート型のリポジトリであり、そのバージョンは上記のテーブルを介してすでに含まれています。その上で仮想リポジトリをカウントすると、その上流を二重カウントすることになります。これは消費ベースの価格設定とメータリングのためのネームスペースレベルの次元であり、deduplicated_size_bytes を補完します。ストレージ使用量とともにネームスペース概要に表示されます。

    • last_reconciled_at: ネームスペースで完全な調整パスが最後に完了した実時間であり、完全に成功した場合にのみ記録されます。これは記録用であり、カウンターでも課金入力でもありません。キャッチアップ候補の選択と古さのスキャンを駆動し、API には決して表示されません。'epoch' デフォルト(1970-01-01)により、新しくシードされた行はテーブル内で最も古い行としてソートされるため、最初に成功したパスで実際の時刻が記録されるまで必ず選択されます。また、このカラムに NULL を保持する行はありません。'-infinity' の方がこの意図を明確に表し、当初はこちらを選択していましたが、読み戻すことができません。time.Time はこれを保持できないため、このカラムを time.Time として型付けする生成モデルは、デフォルト値を保持している行をスキャンできません。最初のパスが実行されるまではすべての行が該当し、テーブルを読み取るたびにこのカラムを射影から除外するか、無限大をモデル化する型で保持しなければなりません。通常のモデルで印を付けた行を読み取れなくするセンチネルは、記録用カラムには適切なトレードオフではありません。'epoch' を使うと、実際のパスがセンチネル値を記録することは絶対にないという保証は失われますが、これは許容します。パスが 1970-01-01 を記録するにはシステムクロックが数十年単位で誤っている必要があり、このカラムは候補選択だけを駆動する記録用です。課金入力ではなく API にも表示されないため、誤読が起きても調整パスが遅延または繰り返されるだけで、カウンターが破損することはありません。

ネームスペースレベルのストレージ会計の調整

namespace_statistics.deduplicated_size_bytes カウンターとリポジトリレベルの repositories.size_bytes カウンターは、サブミリ秒の読み取りで表示パスを提供します。しかし、2 つの調整シナリオでは、キャッシュされたカウンターではなくソースデータから正確なストレージを計算する必要があります。

  1. オンデマンドの検証: 顧客が「私の課金は正確か?」と尋ね、私たちはソースデータからネームスペースの正確なストレージを計算する必要があります。これは、すべての 64 個の sha256 パーティションにわたる SUM(size) FROM blob_storage_blobs WHERE namespace_id = ? を意味します。
  2. ドリフトの修正: 失敗した GC 実行、部分的なフラッシュ、またはその他のイベントがキャッシュされたカウンターを非同期化し、私たちはそれを修正するために正確な値を再計算する必要があります。

blob_storage_blobs は HASH(sha256) でパーティショニングされているため、任意の namespace_id のみのクエリは 64 パーティションすべてにファンアウトします。CloudSQL PostgreSQL 18 インスタンス上の ベンチマーク(シード されたデータセット: 64 個の sha256 パーティションにわたる約 160 万個の blob、Zipf 分布の blob 所有権を持つ 50 万のネームスペース、blob が最も重いネームスペースは 35.3 万個の blob)は、最も重いネームスペースについてベースラインで 78 ms と約 3K+ のバッファヒットを示しています。2 つの加算的な保険ポリシーがこれを改善できます。

オプション A — blob_storage_blobs のカバリングインデックス: 各パーティションの既存の namespace_id インデックスに INCLUDE (size) を追加します。これは 64 パーティションのファンアウトを、最小限またはゼロのヒープフェッチで 64 個のインデックスオンリースキャンに変えます。スペースのオーバーヘッドは無視できます(既存のインデックスのリーフページに size カラムが追加されるだけです)。

オプション B — ネームスペースパーティショニングされたシャドウテーブル: HASH(namespace_id) で 64 パーティションにパーティショニングされ、blob_storage_blobs の AFTER INSERT/DELETE トリガーを介して維持される専用の blob_storage_blobs_by_namespace テーブルです。これは調整クエリを単一パーティションのインデックスオンリースキャンに折りたたみます。スペースのオーバーヘッドは中程度です(blob データの最小サブセット — namespace_id、sha256、size — を 64 個の新しいパーティションとインデックスにわたって複製し、blob 数に比例して線形に成長します)。トレードオフは、すべての blob の INSERT/DELETE での書き込み増幅ですが、調整負荷をメインの blob_storage_blobs テーブル(ホットパス)から遠ざけます。

erDiagram
    blob_storage_blobs_by_namespace {
        uuid namespace_id PK,FK "NOT NULL, PK with sha256, references namespaces(id) ON DELETE CASCADE"
        bytea sha256 PK "NOT NULL, PK with namespace_id"
        bigint size "NOT NULL, INCLUDEd in both indexes"
    }

blob_storage_blobs のトリガーがこのテーブルを維持します。AFTER INSERT は (namespace_id, sha256, size) をシャドウテーブルにコピーし、AFTER DELETE は一致する行を削除します。AFTER UPDATE トリガーは不要です。blob_storage_blobs の行は不変だからです。コンテンツアドレス可能なストレージは、コンテンツへの変更が新しい sha256、したがって新しい行を生成することを意味します(ADR-008 を参照)。主キー (namespace_id, sha256) INCLUDE (size) はパーティションキー(namespace_id)を含まなければならず、blob_storage_blobs のユニークキーを反映します。size も主キーに含まれるため、ダイジェストによる検索では一致する行ごとのヒープフェッチを避け、インデックスオンリーで処理できます。テーブルは他の HASH(namespace_id) テーブルと同じ 64 パーティション数を使用します。(namespace_id) INCLUDE (size) のカバリングインデックスは、ネームスペース全体の合計を求めるインデックスオンリースキャンを可能にします。主キーも namespace_id が先頭にあるため、その合計をインデックスオンリーで処理できます。それでもカバリングインデックスを維持するのは機能上の理由ではなくスキャンコストのためです。カバリングインデックスのエントリには、合計で読み取らない 32 バイトの sha256 が含まれず、幅の広い主キーインデックスの大部分を占めるこのカラムを走査せずに済みます。

namespace_id 外部キーは namespaces(id) を ON DELETE CASCADE で参照するため、namespace_statistics と同じ理由で、リポジトリで述べたルールのカスケード側に置かれます。シャドウ行はユーザーデータではなく、blob_storage_blobs 行から導出されたコピーであるためです。 シャドウ行の生存期間は 2 つのうち短い方で制限され、2 つのリーパーがこれを強制します。ガベージコレクションが対応する blob_storage_blobs 行を回収すると AFTER DELETE トリガーが行を削除し、ネームスペースがなくなるとこのカスケードが行を削除します。 シャドウ行はネームスペースごとの読み取りを低コストにするためだけに存在し、どちらのイベントもその読み取りの可能性を終わらせるため、どちらが先に発火しても正しい動作です。 シャドウとそのソースが同じ生存期間を持たないのは、設計が許容する乖離ではなく、設計そのものです。シャドウは blob_storage_blobs のレプリカではなく、そこから導出された読み取りアクセラレーターです。

blob_storage_blobs は意図的に namespaces(id) 参照を持たず、その欠如は未完の延期ではなく、決定です。 この行は、保存オブジェクトへの唯一のハンドルである object_storage_key を所有し、ADR-025ではガベージコレクションがそのオブジェクトを元の行より先に削除します。したがって、namespaces からのカスケードは、オブジェクトがまだ存在する間にハンドルを破壊し、その存在を把握するものがデータベースに何も残らない状態でオブジェクトを取り残します。 ブロックする参照も適切ではありません。意図的に延期され、レート制限されるガベージコレクションのサイクルが終わるまで、すべてのネームスペース削除を停止させるためです。 このテーブルを作成したマイグレーション 20260612130000_create_blob_storage_blobs.sql は、欠けている 2 つの参照をどちらも後続のマイグレーションへ延期すると説明しています。namespaces(id) 側についてはその位置付けが置き換えられ、repositories(id, namespace_id) 側は別の問いであり、このセクションでは立場を取りません。

したがって、ネームスペースのハード削除はシャドウ行を削除し、それがコピーした blob_storage_blobs 行は、ガベージコレクションが独自のスケジュールで回収するまで残します。 シャドウを読み取る処理がこの差を観測することはありません。repositories と blob_storage_attachments は参照アクションなしで namespaces を参照するため、ネームスペース配下のすべてのリポジトリとアタッチメントがなくなるまで削除は進まず、調整処理を駆動する namespace_statistics 行も同じステートメントでカスケード削除されます。

アプローチタイミングバッファスキャンされたパーティション書き込みオーバーヘッド
namespace_statistics カウンター(表示パス)0.013 ms10非同期フラッシャー
シャドウテーブル + カバリングインデックス(オプション B)29 ms1,3611トリガー
blob のカバリングインデックス(オプション A)43 ms1,59964なし
ベースライン(変更なし)78 ms~3K+64なし

両方のオプションは純粋に加算的であり(blob_storage_blobs 自体への変更なし)、独立して追加または削除できます。相互排他的ではありません。両方とも初期スキーマに含まれます。より多くのカバレッジで始めて、本番メトリクスが不要だと確認したら後でインデックスや補助テーブルを削除する方が簡単です。

ネームスペースレベルのコンポーネント数の調整

namespace_statistics.components_count カウンターは、表示パスとメータリングパイプラインを提供します。ストレージカウンターと同様に、2 つのシナリオでソースデータから正確な値を再計算する必要があります。

  1. オンデマンドの検証: 顧客(または課金)がコンポーネント数が正確かどうかを尋ね、私たちはそれをソース行から導出する必要があります。
  2. ドリフトの修正: 失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグがカウンターを非同期化し、私たちはそれを再計算する必要があります。

調整は、ネームスペースの行にスコープされた 7 つの独立したカウントを合計します。4 つのホスト型(container_manifests、maven_versions、npm_versions、generic_versions)と 3 つのリモート型(container_remote_manifests、maven_remote_versions、npm_remote_versions)です。 再計算された値が components_count が追跡するものと一致するように、ソフト削除された行も含まれます(挿入はインクリメント、ガベージコレクションのハード削除はデクリメント、ソフト削除と復元は何も変更しない操作)。 その括弧内の「ガベージコレクションによるハード削除」は、どのパスが実行するかにかかわらず、カウント対象の行のあらゆるハード削除を指し、それを実行するパスは 2 つあります。 フォーマット自身の削除は独自のトランザクションで行を削除し、そのようなルートでは扱えない行はライフサイクルのパージ処理が削除します。

SELECT
  (SELECT COUNT(*) FROM container_manifests        WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM container_remote_manifests WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM maven_versions             WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM maven_remote_versions      WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM npm_versions               WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM npm_remote_versions        WHERE namespace_id = $1)
+ (SELECT COUNT(*) FROM generic_versions           WHERE namespace_id = $1)
  AS components_count;

各サブクエリは単一のソーステーブルでの namespace_id によるカウントであり、soft_deleted_at 述語はありません。そのため、テーブルにまだある行(ライブ + ソフト削除ウィンドウ 内のソフト削除されたもの)が components_count が追跡するものと一致します。7 つのソーステーブルはすべて HASH(namespace_id) でパーティショニングされているため、各サブクエリは単一のパーティションにプルーニングされ、そのパーティションでネームスペースの行をスキャンします。既存の部分ユニークインデックス(WHERE soft_deleted_at IS NULL)はライブ行のみをカバーするため、カウントを直接満たすことはできません。ネームスペースごとのカーディナリティはデータモデルによって境界づけられ(バージョンごとに 1 行、ファイルや blob 参照ごとではない)、調整は頻繁ではない(オンデマンドまたはドリフト修正であり、ホットパスではない)ため、境界のあるスキャンは許容できます。追加の保険ポリシー(カバリングインデックスやシャドウテーブル)は導入されません。本番メトリクスがこれが遅すぎることを示した場合、各ソーステーブルの非部分的な (namespace_id) インデックスが、シャドウテーブルを検討する前の最も安価な次のステップです。

リポジトリレベルのストレージ会計の調整

repositories.size_bytes カウンターはバッファ書き込み/非同期書き込みによって維持され、サブミリ秒の読み取りでランディングページのハイブリッドリストを提供します。ネームスペースレベルのカウンターと同様に、次の 2 つのシナリオではソースデータから正確な値を再計算する必要があります。

  1. オンデマンドの検証: ユーザーが「このリポジトリは実際にどれだけのストレージを使用しているか?」と尋ね、ソース行から正確な値を導出する必要があります。
  2. ドリフトの修正: 失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグによってカウンターの同期がずれ、再計算する必要があります。

各組み合わせが異なるテーブルの連鎖を通じて blob ストレージに到達するため、調整は (repositories.format, repositories.kind) で分岐します。各フォーマットについて、クエリはリポジトリ内のアーティファクトから参照されるすべての blob の sha256 を収集し、リポジトリ内で重複排除するために DISTINCT を適用し、サイズを得るために blob_storage_blobs_by_namespace へ JOIN します。クエリは、中間層のテーブルを各 *_repository_id カラムでフィルタリングします。これらのカラムは repositories.id ではなく、フォーマット固有のスタブテーブル自体の id を参照します。以下のクエリ例では、小さな CTE を介して repositories.id からスタブの id を解決するため、親の識別子を使用して呼び出せます。

  • Container: container_images(container_repository_id でフィルタリング)→ container_blobs と container_manifests。両方のテーブルが blob_sha256 を保持し、UNION によってレイヤー blob とマニフェストペイロードを対象にします。
  • Maven: maven_packages(maven_repository_id でフィルタリング)→ maven_files。単一のファイルテーブルが、パッケージのバージョン固有ファイルとパッケージレベルファイルを対象にします。
  • npm: npm_packages(npm_repository_id でフィルタリング)→ npm_versions → npm_files に加え、npm_metadata_files(バージョンレベルではなくパッケージレベルをキーとする)との UNION を使用します。
  • Generic: generic_packages(generic_repository_id でフィルタリング)→ generic_versions → generic_files。npm と同様に、generic_files はパッケージの FK を持たないため、走査はバージョンを経由します。Generic にはメタデータファイルがないため、メタデータの結合はありません。この 3 階層の走査は計測されていません。計測されたのはコンテナ走査と Maven のパッケージを先頭とするインデックスだけです(後述)。
  • リモート型のバリアント(kind = remote)は、キャッシュテーブル(*_remote_*)に対して同じ形を使用します。キャッシュもリポジトリのフットプリントの一部です。

SUM(DISTINCT bsb.size) は誤りです。異なる blob が同じ size 値を持つ場合があり、同じ長さの小さなファイルがまとめられてしまいます。調整では、最初に SELECT DISTINCT blob_sha256 を実行し、その後でのみ合計を求めるために blob_storage_blobs_by_namespace へ JOIN しなければなりません。サイズは blob_storage_blobs 自体ではなく、ネームスペースのシャドウテーブルから取得します。このシャドウテーブルは (namespace_id, sha256) INCLUDE (size) をキーとしているため、各ダイジェストは 1 つのパーティション内でインデックスオンリーに解決されます。ここでは namespace_id がリテラルなので単一のパーティションに絞り込まれ、size が主キーに含まれるため、その後のヒープフェッチは発生しません。シャドウテーブルのもう 1 つのインデックスであるカバリングインデックス (namespace_id) INCLUDE (size) は、sha256 で行を特定できないため、この読み取りには使用できません。リポジトリから到達できるすべての sha256 は、そのリポジトリのネームスペースに必ず存在するよう設計されています。以下のクエリ例はすべて、この方法でサイズを解決します。

再計算した値を repositories.size_bytes が追跡する値と一致させるため、ソフト削除された行も含めます。 カウンターはリポジトリ内で重複排除されます(調整の DISTINCT blob_sha256 と一致します)。ある sha256 がリポジトリに初めてアタッチされた場合にのみインクリメントし、その sha256 の最後のアタッチメントがリポジトリからなくなったときにデクリメントします。削除するパスは 2 つあります。 フォーマット自身の削除は、独自のトランザクションでアタッチメントを削除します。 ソフト削除されたアーティファクト配下のアタッチメントはそのルートで扱えないため、代わりにライフサイクルのパージ処理が削除します。 どちらのパスもガベージコレクションの実行を待ちません。ガベージコレクションは blob を回収し、別スコープのカウンターであるネームスペース単位の deduplicated_size_bytes をデクリメントします。 ソフト削除と復元は何も変更せず、前述したネームスペースレベルの動作と一致します。

調整のコストは、リポジトリのアーティファクト数ではなく、ネームスペースがハッシュされるパーティションによって制限されます。フォーマット固有のファイル、blob、マニフェストテーブル(Container の container_blobs と container_manifests、Maven の maven_files、npm の npm_files と npm_metadata_files、Generic の generic_files、さらに Generic 以外の各フォーマットの *_remote_* キャッシュバリアント)は HASH(namespace_id) で 64 パーティションに分割されるため、走査の各側は単一パーティションに絞り込まれます。その中でプランナーは逐次走査のパスを取ります。リポジトリ述語は blob 保持テーブルより 2 JOIN 上のフォーマット固有スタブテーブルにあり、PostgreSQL はこのパスのテーブル間相関統計を保持しないため、実際には 200 行でも 30,000 行と見積もります。PostgreSQL 17.10 で 11 個の EXPLAIN (ANALYZE, BUFFERS) プランを計測し(artifact-registry!1712)、コンテナ走査でリポジトリを 200 ダイジェストに保った結果は次のとおりです。

収集テーブルのパーティションに共存するもの走査プラン時間
なし。対象リポジトリの行だけネステッドループ、blob_storage_blobs_by_namespace の Index Only Scan0.508 ms
同じネームスペースの兄弟リポジトリ、59,800 行収集テーブルとシャドウに対する Filter: (namespace_id = ...) 付き Seq Scan18.157 ms

ハッシュパーティションは全ネームスペースの約 1/64 の行を保持するため、逐次走査はネームスペース規模ではなく全体規模の量になります。この最後の点は計測ではなく、ノードの種類とパーティション数からの推論です。これらのテーブルのフォーマット固有インデックスは、一様に部分インデックスではありません。npm_metadata_files と npm_remote_metadata_files には soft_deleted_at カラム自体がありません。container_blobs と container_manifests では、このドキュメントが指定するカラムは GA 時にソフト削除とともに導入されるため(ADR-010)、現時点のユニークインデックスは述語を持たず、ソフト削除を含めるこの枠組みはそれらには及びません。npm_files、npm_remote_files、generic_files、container_remote_blobs、container_remote_manifests は正確に soft_deleted_at IS NULL の部分インデックスです。maven_files と maven_remote_files のユニークインデックスにはバージョン ID に関する 2 つ目の連言があり、パッケージレベルのインデックスでは maven_version_id IS NULL、バージョンレベルのインデックスでは IS NOT NULL で、maven_remote_version_id にも同じ組み合わせがあります。したがって、リポジトリ比例のコストは排除されておらず、走査を逐次走査のパスに保つのはインデックスの欠如や部分インデックスではなく、行数見積もりです。調整は頻繁ではなく(オンデマンドまたはドリフト修正であり、ホットパスではありません)、パーティションスキャンは許容できます。ネームスペースレベルの調整からの最後の blob_storage_blobs_by_namespace 検索は、計測したすべてのプランで単一パーティションに絞り込まれ、プランナーが走査サイズを正しく見積もる場合はインデックスオンリーで解決され、同じ過大見積もりではそのパーティションのシーケンシャルスキャンにフォールバックします。

リポジトリレベルの調整には、現在、保険となる構造が 1 つあります。maven_files (namespace_id, maven_package_id, blob_sha256) に対する非部分インデックス index_maven_files_on_ns_id_pkg_id_blob_sha256 です。これは maven_package_id の外部キーチェックを支えるために導入され、3 番目のカラムは recomputeMavenFilesSizeStmt が読み取るダイジェストです。このインデックスにより、解放バイト数の測定セルは 1,001,427 バッファ・3,066 ms から 318 バッファ・1.8 ms に減少します。したがって、これらの連鎖に含まれるすべてのテーブルが、maven_files も含め、(namespace_id, <its parent id>) で始まる非部分インデックスを持ちます。artifact-registry#684では 2 カラムの (namespace_id, maven_package_id) 形式を提案していましたが、決定となるのは導入済みの 3 カラム形式であり、3 番目のカラムは結合が読み取るものです。このインデックス以外では、走査はネームスペースレベルの調整ですでに導入する blob_storage_blobs_by_namespace のシャドウを再利用し、独自のシャドウテーブルは追加しません。アタッチ/デタッチ時にフォーマット固有テーブルから維持される、(namespace_id, repository_id, sha256, size) をマッピングするリポジトリパーティショニングのシャドウテーブルであれば、オプション Bの形をより細かいスコープで反映できます。

アーティファクトレベルのストレージ会計

アーティファクトレベルのストレージ使用量は、単一のアーティファクトバージョン(Container マニフェスト、Maven バージョン、npm バージョン、Generic バージョン)のバイト単位のフットプリントです。アーティファクトごとの UI 表示(例えば「このイメージは 142 MB」「このパッケージバージョンは 4 MB」)を支え、サイズによるフィルタリングまたはソートを行うライフサイクルルールとレポートクエリによって使用されます。

基となるアーティファクトの形が異なるため、会計モデルはフォーマットごとに異なります。

  • Container マニフェスト: container_manifests.size はプッシュ時に事前計算され、不変です。マニフェストはコンテンツアドレス可能であるため(ADR-008)、バイトが変更されると、新しいダイジェストと新しい size を持つ新しいマニフェストが生成されます。このカラムが信頼できる唯一の情報源であるため、調整は不要です。リモートキャッシュでは container_remote_manifests.size がこれを反映し、子が遅延キャッシュされるマニフェストリストに対して段階的なセマンティクスを持ちます。
  • Maven、npm、Generic のバージョン: 各バージョン行は、バージョンのフットプリントについて信頼できる唯一の情報源となる事前計算済みの size_bytes カラム(maven_versions、maven_remote_versions、npm_versions、npm_remote_versions、generic_versions)を持ちます。 container_manifests.size とは異なり、プッシュ時に不変の値として設定することはできません。Maven、npm、Generic のバージョンはバージョンレベルでコンテンツアドレス可能ではなく、バージョンの存続期間中にファイルを追加または削除できるためです。 そのため、repositories.size_bytes と同様に、バッファ書き込み/非同期書き込みを介してバッファカウンターとして維持します。blob_sha256 がバージョンに初めてアタッチされたときにインクリメントし、そのバージョンから当該 sha256 の最後のアタッチメントがなくなったときにデクリメントします(バージョン内で重複排除し、調整で使用する DISTINCT blob_sha256 と一致させます)。削除するパスは 2 つあります。 フォーマット自身のファイル削除は独自のトランザクションで行を削除し、カラムをそこで再計算します。そのルートで扱えないファイルはライフサイクルのパージ処理が削除します。 どちらもガベージコレクションを待ちません。 表示パスとバージョン一覧は、サイズでソートまたはフィルタリングする場合も含め、インデックス付きカラムを直接読み取ります。

ソフト削除されたファイルは、ガベージコレクションによってハード削除されるまで、バージョンごとの size_bytes に引き続き寄与します。ソフト削除と復元はカウンターに対して何も変更しない操作であり、ネームスペースおよびリポジトリのセマンティクスと一致します。 寄与を終わらせるのはハード削除であり、実行するパスは 2 つあります。 Maven、npm、Generic のファイル削除は、独自のトランザクションで行を削除し、そこでバージョンの size_bytes を再計算します。 ソフト削除されたバージョン配下のファイルはそれらのルートで扱えないため、代わりにライフサイクルのパージ処理が削除します。 パージ処理は同じ回収処理でバージョン行も削除するため、カウンターは再計算されず行とともになくなります。 どちらのパスもガベージコレクションの実行を待ちません。ガベージコレクションは blob を回収し、別スコープのカウンターである deduplicated_size_bytes をデクリメントします。

読み取り時に導出せずカラムを事前計算することで、バージョン一覧をサイズでソートおよびフィルタリングできます。単一バージョンのサイズを導出する処理はいずれの方法でも低コストです(少数のファイルに限定された JOIN)が、サイズがバージョン一覧でソートまたはフィルタリング可能なカラムになると、すべての行で導出する方法はスケールしません。Cloud SQL PostgreSQL 17 インスタンス(large プロファイル、最も深いネームスペースに約 26K の Maven バージョンと約 26K の npm バージョン)で検証しました。

サイズ上位 50 バージョンMavennpm
読み取り時に導出(各バージョンの blob を合計)58 ms29 ms
事前計算済みの size_bytes カラム + インデックス0.06 ms0.08 ms

これは 3 〜 4 桁の差であり、重複排除を行う読み取り時導出のバリアントはさらに低速です(Maven で約 190 ms)。また、バージョン数の増加とともに差は広がります。ランディングページではすでにリポジトリを size_bytes でソートしており、バージョン一覧にも同じ期待が当てはまるため、スコープ間の一貫性を保つためにカラムを事前計算します。

アーティファクトレベルのストレージ会計の調整

container_manifests.size は不変かつコンテンツアドレス可能であるため、調整は不要です。Maven、npm、Generic の size_bytes カウンターは、リポジトリレベルおよびネームスペースレベルのカウンターと同様に、失敗したフラッシュ、部分的なバッファ損失、またはバックグラウンドジョブのバグによってドリフトする可能性があります。そのため、オンデマンドの検証またはドリフトの修正のために、ソースデータから正確な値を再計算します。

1 つのバージョンを調整する際は、フォーマット固有のファイルテーブルから、単一の maven_version_id、npm_version_id、または generic_version_id にスコープして、そのバージョンの重複しない blob_sha256 値を選択し、ネームスペース・リポジトリレベルの走査と同じシャドウである blob_storage_blobs_by_namespace に対して各ダイジェストのサイズを解決します。再計算した値をカウンターが追跡する値と一致させるため、ソフト削除されたファイルも含め、パッケージレベルの行はバージョン ID の等価条件で除外します。

バージョンごとの走査はサイズを読む前にダイジェストを保持するため、ダイジェストを事前に名指せないネームスペース・リポジトリレベルの読み取りをシャドウに送るプルーニングの根拠は、このスコープには当てはまりません。ダイジェストセットを単一の bytea[] 配列引数(blob_storage_blobs.sha256 = ANY($n))として束縛するとダイジェストのハッシュパーティションへ静的にプルーニングでき、約 15 個のダイジェストまでは、異なるダイジェストごとに 1 パーティションという数が適切です。プルーニングが制限するのはパーティション数であってプローブ数ではありません。各残ったパーティションは自身のインデックスに対して配列全体を評価するため、コストはダイジェスト数ではなくパーティション数×ダイジェスト数になります。本番カーディナリティでの計測では、PostgreSQL 16〜18、ネームスペースの blob 数 100〜1,000,000、ダイジェスト数 1〜512 の全条件でシャドウは 0.09〜2.44 ms でした。配列形式が勝るのはダイジェスト数 1、3、5 だけで、15 以上では勝らず、15 で 2〜6 倍、100 で 9〜48 倍遅くなります。Maven で一般的な 4〜15 ファイルではどちらもサブミリ秒なので、シャドウは通常のカーディナリティで何も犠牲にせず、15 ダイジェスト以上で優位です。

ダイジェストを配列として束縛せず、派生テーブルとしてベーステーブルに JOIN するとまったくプルーニングされません。呼び出しごとに 64 パーティションすべてを開き、計測した 123 条件のどれでもシャドウに勝りませんでした。Artifact Registry サービスはシンプルクエリプロトコルを使用し、ステートメントを毎回再計画するため、読み取りではなく 64 パーティションに対する計画が走査を支配します。

このスコープでシャドウを読み取っても、スキーマがまだ負担していないコストは生じません。トリガー維持による書き込み増幅は、ネームスペースレベルとリポジトリレベルの走査のために blob_storage_blobs がすでに持つ AFTER INSERT と AFTER DELETE トリガーで、blob 書き込みごとに発生するため、バージョンごとの読み取りはそれを増やしません。size の 2 つ目のコピーは、既存のどの書き込みパスでも古くなりません。blob サイズはコンテンツアドレス指定であり、blob のバイト列が変わると異なる sha256、したがって異なる行になるためであり、行をその場で更新する書き込みパスはありません。size は挿入後に不変で、訂正は削除と再挿入によって行い、トリガーがそれを伝播します。シャドウは更新を追跡しないため、UPDATE は決して行いません。この不変性はスキーマが強制するものではなく規約です。blob_storage_blobs.size の UPDATE を拒否するものはなく、AFTER UPDATE トリガーもないため、シャドウは更新前の値を保持し、例外を発生させずにその値から合計を返します。このリスクは、ネームスペースレベルとリポジトリレベルの走査がすでに持つものであり、このスコープが導入するものではありません。

バージョンごとのカーディナリティはフォーマットのプロトコルによって制限されるため(通常、Maven は 4〜15 ファイル、npm は 1〜3 ファイル)、再計算はサブミリ秒の単一パーティション読み取りになります。Generic はバージョンごとのファイル数にプロトコル上の制限を設けません。シャドウの読み取りは、この場合に拡張性があると計測された形式であり、512 個のダイジェストで最大 2.44 ms です。

カラムの導入時に行うバックフィルは、バージョンでグループ化した集合ベースの再計算です。現在の量では低コストです(最も深いネームスペースにある約 26K バージョンを約 284 ms でバックフィルできるため、この規模ではテーブル全体でも数秒です)。事前計算を先送りし、後から本番環境の量に対してバックフィルするよりもはるかに低コストです。今実施することで、すべてのフォーマットにわたってデータモデルと API の一貫性も保たれます。

インデックス

  • blob_storage_blobs: (namespace_id, sha256) に対するユニークインデックス — 重複排除を強制し、Organization 内で sha256 によって blob の存在を確認します。この制約はパーティションキー(sha256)を含むため、PostgreSQL はすべてのハッシュパーティションにわたって正しく強制します。(namespace_id) INCLUDE (size) のカバリングインデックス — ヒープフェッチなしで ネームスペースレベルのストレージ会計の調整 のためのインデックスオンリースキャンを可能にします。
  • blob_storage_attachments: (namespace_id, sha256) に対するインデックス — blob のコンテンツハッシュが与えられたときにアタッチメントの存在を確認します(クリーンアッププロセス が孤立チェックに使用)。
  • blob_storage_blobs_by_namespace: (namespace_id, sha256) INCLUDE (size) に対する主キー — blob_storage_blobs 行との 1:1 対応を強制し、リポジトリレベルおよびアーティファクトレベルの調整で行うダイジェストごとのサイズ検索を、ヒープフェッチなしで解決します。(namespace_id) INCLUDE (size) のカバリングインデックス — sha256 を読み取らない ネームスペースレベルのストレージ会計の調整 でネームスペース全体を合計するための、より低コストな単一パーティションのインデックスオンリースキャンです。主キーもこのスキャンに使用できますが、合計では使用しない 32 バイトのキーカラムを含むエントリを走査することになります。
  • namespace_statistics: (namespace_id) に対する主キー — ネームスペースごとに 1 つの統計レコード。このテーブルで必要な一意性は主キーだけで正確に満たされるため、同じ単一カラムに別のユニークインデックスは作成しません。追加しても、それ以上の制約は何も強制されません。(last_reconciled_at, namespace_id) に対するインデックス — 最後に成功した調整パスが最も過去のネームスペースを選択する、キャッチアップの古さのスキャンと、同じ述語によるバックログ数の計算に使用します。どちらもこのテーブルだけを読み取ります。ここに行がないネームスペースを捉えるために namespaces を結合するスキャンでは、このインデックスをまったく使用できません。結果の IS NULL 選言はインデックス条件ではなく結合後のフィルターになるためです。したがって、このインデックスを使用するクエリは namespace_statistics に限定されます。このインデックスは、このドキュメントのほぼすべてのほかのインデックスと異なり、意図的に namespace_id を先頭にしません。スキャンはネームスペース内ではなく、テーブル全体を新しさで並べ替えます。また、このテーブルはパーティショニングされていないため、先頭の namespace_id はプルーニングにも順序付けにも役立ちません。namespace_id はフィルターではなく同順位を決めるカラムとして後ろに置きます。これによりソートキーが一意になり、スキャンをオフセットではなくキーセットでページングできます。特にカラムの作成直後は、すべての行が同じ 'epoch' デフォルトを共有し、タイムスタンプだけでは順序が決まらないため重要です。また、各クエリがインデックスの 2 カラムだけを選択するため、両方のクエリをカバーし、vacuum 済みのテーブルではどちらもインデックスだけから応答できます。

ハッシュパーティショニングされたテーブルでは、インデックスはパーティションごとにローカルです。インデックス操作は単一のパーティションにスコープされ、テーブル全体をロックしません。

blob ストレージのクエリ例

  • アーティファクトをプルする(読み取りパスのショートカット: *_files → blob_storage_blobs、アタッチメントをスキップ — 1 パーティション)

    SELECT bsb.object_storage_key, bsb.size
    FROM maven_files mf
    JOIN blob_storage_blobs bsb ON bsb.namespace_id = mf.namespace_id AND bsb.sha256 = mf.blob_sha256
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456' AND mf.file_name = 'myapp-1.0.0.jar'
      AND mf.soft_deleted_at IS NULL;
    
  • blob アップロード時の重複排除アップサート(1 パーティション、レースなし)

    INSERT INTO blob_storage_blobs (namespace_id, sha256, size, object_storage_key, metadata_sha1)
    VALUES ('018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8', 'abcd1234efgh5678...'::bytea, 1048576, 'artifact_registry/.../objects/ab/cd/abcd1234efgh5678...', NULL)
    ON CONFLICT (namespace_id, sha256) DO NOTHING
    RETURNING id, sha256;
    
  • Organization 内で sha256 によって blob の存在を確認する(1 パーティション)

    SELECT 1 AS one
    FROM blob_storage_blobs
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND sha256 = 'abcd1234efgh5678...'::bytea
    LIMIT 1;
    
  • 孤立チェック: この blob はまだいずれかのアタッチメントから参照されているか?(1 パーティション)

    SELECT 1 AS one
    FROM blob_storage_attachments
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND sha256 = 'abcd1234efgh5678...'::bytea
    LIMIT 1;
    
  • blob のカバリングインデックス(オプション A)を介したストレージ会計の調整: ソースデータからネームスペースの正確なストレージを計算する(64 パーティション、インデックスオンリースキャン)

    SELECT SUM(size) AS total_size_bytes
    FROM blob_storage_blobs
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';
    
  • シャドウテーブル(オプション B)を介したストレージ会計の調整: ネームスペースの正確なストレージを計算する(1 パーティション、インデックスオンリースキャン)

    SELECT SUM(size) AS total_size_bytes
    FROM blob_storage_blobs_by_namespace
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';
    
  • 表示パス: 事前計算されたネームスペースカウンターを読み取る(単一行検索)

    SELECT deduplicated_size_bytes, components_count
    FROM namespace_statistics
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8';
    
  • 表示パス: 事前計算済みのリポジトリカウンターを読み取る(単一行の検索、ランディングページのハイブリッドリストで使用)

    SELECT size_bytes
    FROM repositories
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';
    
  • リポジトリレベルの調整: 単一リポジトリの正確なストレージをフォーマット別に計算します。各先頭 CTE は、repositories.id からフォーマット固有のスタブ id(container_repositories、maven_repositories、npm_repositories、generic_repositories)を解決します。中間層の *_repository_id カラムは repositories.id ではなく、スタブテーブル自体の id を参照します。

    • Container: イメージを走査し、container_blobs と container_manifests の blob 参照を UNION し、blob_sha256 で重複排除して、ネームスペースのシャドウテーブルを介して合計します。

      WITH cr AS (
        SELECT id
        FROM container_repositories
        WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456'
      ),
      uniq_blobs AS (
        SELECT cb.blob_sha256
        FROM container_blobs cb
        JOIN container_images ci
          ON ci.namespace_id = cb.namespace_id AND ci.id = cb.container_image_id
        WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND ci.container_repository_id = (SELECT id FROM cr)
        UNION
        SELECT cm.blob_sha256
        FROM container_manifests cm
        JOIN container_images ci
          ON ci.namespace_id = cm.namespace_id AND ci.id = cm.container_image_id
        WHERE ci.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND ci.container_repository_id = (SELECT id FROM cr)
      )
      SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes
      FROM uniq_blobs u
      JOIN blob_storage_blobs_by_namespace bsb
        ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
      
    • Maven: 単一のファイルテーブルが、バージョン固有ファイルとパッケージレベルファイルを対象にします。

      WITH mr AS (
        SELECT id
        FROM maven_repositories
        WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456'
      ),
      uniq_blobs AS (
        SELECT DISTINCT mf.blob_sha256
        FROM maven_files mf
        JOIN maven_packages mp
          ON mp.namespace_id = mf.namespace_id AND mp.id = mf.maven_package_id
        WHERE mp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND mp.maven_repository_id = (SELECT id FROM mr)
      )
      SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes
      FROM uniq_blobs u
      JOIN blob_storage_blobs_by_namespace bsb
        ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
      
    • npm: パッケージ → バージョン → ファイルを走査し、パッケージレベルのメタデータファイルを UNION して、blob_sha256 で重複排除します。

      WITH nr AS (
        SELECT id
        FROM npm_repositories
        WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456'
      ),
      uniq_blobs AS (
        SELECT nf.blob_sha256
        FROM npm_files nf
        JOIN npm_versions nv
          ON nv.namespace_id = nf.namespace_id AND nv.id = nf.npm_version_id
        JOIN npm_packages np
          ON np.namespace_id = nv.namespace_id AND np.id = nv.npm_package_id
        WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND np.npm_repository_id = (SELECT id FROM nr)
        UNION
        SELECT nmf.blob_sha256
        FROM npm_metadata_files nmf
        JOIN npm_packages np
          ON np.namespace_id = nmf.namespace_id AND np.id = nmf.npm_package_id
        WHERE np.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND np.npm_repository_id = (SELECT id FROM nr)
      )
      SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes
      FROM uniq_blobs u
      JOIN blob_storage_blobs_by_namespace bsb
        ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
      
    • Generic: npm と同様にパッケージ → バージョン → ファイルを走査します。Generic にはメタデータファイルがないため、パッケージレベルの結合はなく、blob_sha256 で重複排除します。

      WITH gr AS (
        SELECT id
        FROM generic_repositories
        WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND repository_id = '019a1b2c-0456-7abc-8def-000000000456'
      ),
      uniq_blobs AS (
        SELECT DISTINCT gf.blob_sha256
        FROM generic_files gf
        JOIN generic_versions gv
          ON gv.namespace_id = gf.namespace_id AND gv.id = gf.generic_version_id
        JOIN generic_packages gp
          ON gp.namespace_id = gv.namespace_id AND gp.id = gv.generic_package_id
        WHERE gp.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8'
          AND gp.generic_repository_id = (SELECT id FROM gr)
      )
      SELECT COALESCE(SUM(bsb.size), 0) AS total_size_bytes
      FROM uniq_blobs u
      JOIN blob_storage_blobs_by_namespace bsb
        ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
      
  • アーティファクトレベル: 事前計算済みの Container マニフェストサイズを読み取る(不変、単一行の検索)

    SELECT size
    FROM container_manifests
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0789-7abc-8def-000000000789';
    
  • アーティファクトレベル: 事前計算済みの Maven バージョンサイズを読み取る(信頼できる唯一の情報源、単一行の検索)

    SELECT size_bytes
    FROM maven_versions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';
    
  • アーティファクトレベル: バージョン一覧表示のために、Maven パッケージのバージョンをサイズ順に一覧する((namespace_id, maven_package_id, size_bytes DESC) インデックスを使用。npm も同様)

    SELECT id, version, size_bytes
    FROM maven_versions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND maven_package_id = '019a1b2c-0123-7abc-8def-000000000123'
      AND soft_deleted_at IS NULL
    ORDER BY size_bytes DESC
    LIMIT 50;
    
  • アーティファクトレベルの調整: カウンターを検証または修正するために、ソースデータから Maven バージョンの正確なサイズを再計算する(通常 4〜15 ファイル、カーディナリティは限定的)。ステップ 1 で maven_files からバージョンの重複しないダイジェストを収集し、ステップ 2 で blob_storage_blobs_by_namespace からサイズを合計します。リテラルの namespace_id が 1 パーティションにプルーニングし、各ダイジェストはその中でインデックスオンリーに解決されます。

    -- Step 1: the version's distinct digests (keyset-paged on blob_sha256)
    SELECT DISTINCT mf.blob_sha256
    FROM maven_files mf
    WHERE mf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND mf.maven_version_id = '019a1b2c-0456-7abc-8def-000000000456';
    
    -- Step 2: the digests from step 1, resolved against the namespace shadow
    SELECT COALESCE(SUM(bsb.size), 0) AS bytes
    FROM blob_storage_blobs_by_namespace bsb
    WHERE bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = ANY($1::bytea[]);
    
  • アーティファクトレベル: 事前計算済みの npm バージョンサイズを読み取る(信頼できる唯一の情報源、単一行の検索)

    SELECT size_bytes
    FROM npm_versions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';
    
  • アーティファクトレベルの調整: ソースデータから npm バージョンの正確なサイズを再計算する(通常、バージョンごとに 1〜3 ファイル。npm_metadata_files はパッケージレベルをキーとし、単一バージョンのフットプリントには含まれない)。blob_storage_blobs_by_namespace から、バージョンの重複しないダイジェストを JOIN してサイズを合計します。これは調整セクションで説明した読み取りです。

    WITH uniq_blobs AS (
      SELECT DISTINCT nf.blob_sha256
      FROM npm_files nf
      WHERE nf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND nf.npm_version_id = '019a1b2c-0456-7abc-8def-000000000456'
    )
    SELECT COALESCE(SUM(bsb.size), 0) AS bytes
    FROM uniq_blobs u
    JOIN blob_storage_blobs_by_namespace bsb
      ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
    
  • アーティファクトレベル: 事前計算済みの Generic バージョンのサイズを読み取る(信頼できる唯一の情報源、単一行の検索)

    SELECT size_bytes
    FROM generic_versions
    WHERE namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND id = '019a1b2c-0456-7abc-8def-000000000456';
    
  • アーティファクトレベルの調整: ソースデータから Generic バージョンの正確なサイズを再計算します。npm と同じ形状で、Generic には除外するメタデータファイルがないため、より単純です。(namespace_id, generic_version_id, blob_sha256) インデックスによって、ダイジェストの収集をインデックスオンリーで実行できます。

    WITH uniq_blobs AS (
      SELECT DISTINCT gf.blob_sha256
      FROM generic_files gf
      WHERE gf.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND gf.generic_version_id = '019a1b2c-0456-7abc-8def-000000000456'
    )
    SELECT COALESCE(SUM(bsb.size), 0) AS bytes
    FROM uniq_blobs u
    JOIN blob_storage_blobs_by_namespace bsb
      ON bsb.namespace_id = '018f4d6f-0e10-7e3a-9bfd-23a4c5d6e7f8' AND bsb.sha256 = u.blob_sha256;
    

帰結

ポジティブ

  1. 各アーティファクトフォーマットに合わせたデータ構成: 各アーティファクトフォーマットに専用のテーブルを使用することで、テーブル構成に最大の柔軟性が得られます。フォーマットプロトコルが要求する任意の数の追加カラムを持つことができます。すでに専用のテーブルを使用しているため、追加の補助テーブルは不要です。

  2. 各フォーマットのデータテーブルは関連する使用パターンを持つ: 各フォーマット専用のテーブルは、REST および GraphQL API と関連するアーティファクト管理クライアントから使用パターンを受け取ります。これにより、他のフォーマットの使用パターンからの分離が提供されます。

  3. フォーマット関連データのパフォーマンスの分離: 特定のアーティファクトフォーマットのテーブルでのパフォーマンスのボトルネックは、他のフォーマットに即時の影響を与えません。

  4. 透過的なオブジェクトストレージのクリーンアップ: オブジェクトストレージのクリーンアップタスク が blob ストレージ ドメインに集約されているため、親ドメイン(この場合は各フォーマット固有のドメイン)はこの部分を扱う必要がありません。さらに、このクリーンアップは削除操作がどのように発生したか(単一要素の破棄、一括破棄、選択された要素のセットに対して破棄を実行するバックグラウンドクリーンアップポリシー)に影響されません。

  5. blob ストレージの分離が再利用性を提供する: blob ストレージテーブルは、ここで記述する Artifact Registry 機能に縛られません。したがって、この部分は他の領域でのファイルアップロードのニーズに再利用できます。

  6. 効率的なストレージ会計: Organization スコープの重複排除と Organization ごとの重複排除された blob レコードにより、ストレージ使用量クエリは単純かつ効率的です。注意: sha256 ベースのパーティショニングでは、Organization レベルの集約は 64 パーティションすべてをスキャンします。これは、遅延インクリメントを介して更新される専用のロールアップテーブルによって緩和されます(パーティショニング戦略 を参照)。

  7. 統一されたクロスフォーマットの一覧表示: 親の repositories テーブルは、ネームスペース内のすべてのフォーマットと種類(ホスト型、仮想型、リモート型)にわたるすべてのリポジトリを一覧表示する単一のソースを提供し、複数のテーブルにわたる UNION ALL なしにランディングページのハイブリッドリストを支えます。

  8. スタンドアロンのリモートリポジトリが共有を可能にする: 独自のライフサイクルを持つスタンドアロンエンティティとしてのリモートリポジトリは、複数の仮想リポジトリ間で共有でき、設定とキャッシュエントリの重複を減らします。

ネガティブ

  1. クロスフォーマットの詳細クエリは依然として JOIN を必要とする: 親の repositories テーブルはランディングページの一覧表示ユースケースを解決しますが、フォーマット固有の詳細(例: コンテナイメージ、Maven パッケージ)へのアクセスには依然としてフォーマット固有のテーブルへの JOIN が必要です。

  2. blob ストレージのための集約されたテーブル: これは 2 つの欠点をもたらします。第一に、これらのテーブルには非常に大量の行が存在します。この状況に対処するには慎重なテーブル設計が必要です。第二に、これらのテーブルの問題(テーブル全体のロックなど)は、すべてのアーティファクトタイプに潜在的に影響します。

  3. リポジトリごとのストレージ帰属は JOIN を必要とする: リポジトリレベルでの正確なストレージ使用量の帰属は、フォーマット固有のテーブルから blob_storage_attachments を通じて blob_storage_blobs への JOIN を通じて導出されます。これは blob ストレージを汎用的かつ重複排除された状態に保ちますが、非正規化されたリポジトリごとのカウンターと比べていくらかの複雑さを追加します。

  4. 2 ステップのリポジトリ作成: リポジトリの作成には、親の repositories テーブルとフォーマット固有のテーブルの両方への挿入が必要です。これは単一テーブルへの挿入と比べてトランザクションの複雑さを追加します。

代替案

共通データの集約

ここでの異なるアプローチは、アーティファクトフォーマット領域のすべての共通データを共通の集約されたテーブルに保存することです。

これは、複数のソースを一緒に JOIN することなくそれらのクエリに答えられるため、混在するアーティファクトフォーマットのデータアクセスに非常に役立ちます。

このアプローチは Package Registry 機能 ですでに使用されており、この執筆時点で、それらの共通テーブルは予想どおり大量の行を持つだけでなく、大量の特化したインデックスも持っています。これらのインデックスのそれぞれは、アーティファクトフォーマットに固有のアクセスパターンをサポートします。インデックスの量が今日かなり多いため、新しいインデックスを追加すること(例えば Package Registry 機能に新しいフォーマットサポートが追加される場合)は、より多くの精査や反対さえも受けることになります。

さらに、各アーティファクトフォーマットには保存する必要のある固有のデータがあります(例: 正規化されたパッケージ名)。この固有のデータは、一部の行だけが使用するカラムを作ることになるため、共通テーブルには保存できません。これはいくつかの補助テーブルの作成につながります。これらの補助テーブルは、特定のアーティファクトタイプのアクセスパターンに必要な JOIN の量を増加させます。

repositories 親テーブルの導入は、このアプローチの限定版を採用しています。一覧表示とフィルタリングに必要なクロスフォーマットメタデータ(名前、可視性、フォーマット、種類、カウンター)のみが集約されます。フォーマット固有のデータは専用のテーブルに残り、上記で記述したインデックスの増殖と補助テーブルの問題を回避します。

参考文献