# 機能設計書 31-外部キー制約

## 概要

本ドキュメントは、SQLiteにおける外部キー（Foreign Key）制約機能の設計仕様を定義する。外部キー制約は、リレーショナルデータベースにおける参照整合性を保証するための重要な機能である。

### 本機能の処理概要

外部キー制約機能は、親テーブル（referenced table）と子テーブル（referencing table）間のデータ整合性を自動的に検証・維持する機能である。

**業務上の目的・背景**：リレーショナルデータベースでは、テーブル間の関係性（1対多、多対多など）を表現するために外部キーが使用される。外部キー制約を適用することで、孤立したレコード（親が存在しない子レコード）の発生を防ぎ、データの整合性を保証する。これにより、アプリケーション側での整合性チェックコードが不要となり、開発効率とデータ品質が向上する。

**機能の利用シーン**：
- 受注テーブルが顧客テーブルを参照する場合、存在しない顧客IDでの受注登録を防止
- 親レコード削除時に関連する子レコードを自動的にカスケード削除
- 親レコード更新時に関連する子レコードの外部キー値を自動更新

**主要な処理内容**：
1. INSERT時：子テーブルへの挿入時、親テーブルに対応するキーが存在するか検証
2. DELETE時：親テーブルからの削除時、子テーブルに依存レコードがないか検証、またはCASCADE/SET NULL等のアクション実行
3. UPDATE時：親キーまたは子キーの更新時、参照整合性を維持するための検証とアクション実行
4. 遅延制約（Deferred）と即座制約（Immediate）の両モードをサポート

**関連システム・外部連携**：
- VDBEバイトコードインタープリター：制約検証コードの実行
- トリガーシステム：CASCADE/SET NULL等のアクション実装に内部トリガーを使用
- B-Treeストレージエンジン：親テーブルのインデックス検索

**権限による制御**：
- PRAGMA foreign_keys = ON/OFF でデータベース接続単位での有効/無効切り替え
- sqlite3_set_authorizer() による操作の認可制御が可能

## 関連画面

本機能はSQLite内部機能のため、直接的な画面関連はない。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 8 | SQLite3 Fiddle | 参照画面 | PRAGMA foreign_keys設定、FK制約付きテーブル操作 |

## 機能種別

データ整合性検証 / CRUD操作制御 / トリガー連携

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| pTab | Table* | Yes | 操作対象のテーブル構造体 | NULLでないこと |
| regOld | int | No | 削除/更新前の行データを格納するレジスタ | DELETE/UPDATE時に必須 |
| regNew | int | No | 挿入/更新後の行データを格納するレジスタ | INSERT/UPDATE時に必須 |
| aChange | int* | No | UPDATE時の変更カラムマスク配列 | UPDATE時のみ使用 |
| bChngRowid | int | No | rowid変更フラグ | 0または1 |

### 入力データソース

- 子テーブルのFKey構造体リスト（pTab->u.tab.pFKey）
- 親テーブルを参照するFKey構造体リスト（sqlite3FkReferences(pTab)で取得）
- データベーススキーマ情報（fkeyHashハッシュテーブル）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| SQLITE_OK | int | 制約違反なし |
| SQLITE_CONSTRAINT_FOREIGNKEY | int | 外部キー制約違反エラー |
| 制約カウンター | int | 遅延制約の違反数（トランザクションコミット時に検証） |

### 出力先

- VDBEバイトコード（制約検証・アクション実行コード）
- エラーメッセージ（制約違反時）
- 制約違反カウンター（即座/遅延）

## 処理フロー

### 処理シーケンス

```
1. 外部キー有効性チェック
   └─ PRAGMA foreign_keys設定とテーブル種別の確認

2. 子テーブルとしての制約チェック（I.1, D.1）
   ├─ 各FKeyに対して親テーブル検索
   ├─ 親キーのユニークインデックスを特定
   └─ 対応する親行の存在確認

3. 親テーブルとしての制約チェック（I.2, D.2）
   ├─ 参照している子テーブルをスキャン
   └─ 依存する子行の有無を確認

4. アクショントリガーの実行（DELETE/UPDATE時）
   ├─ CASCADE: 子テーブルの関連行を削除/更新
   ├─ SET NULL: 子テーブルの外部キーをNULLに設定
   ├─ SET DEFAULT: 子テーブルの外部キーをデフォルト値に設定
   └─ RESTRICT: 子行が存在する場合エラー

5. 制約カウンターの更新
   └─ 遅延/即座に応じて適切なカウンターを増減
```

### フローチャート

```mermaid
flowchart TD
    A[開始: sqlite3FkCheck] --> B{foreign_keys ON?}
    B -->|No| Z[終了]
    B -->|Yes| C{IsOrdinaryTable?}
    C -->|No| Z
    C -->|Yes| D[子テーブルとしてのFK処理]
    D --> E[各FKeyをループ]
    E --> F{変更カラムがFK列?}
    F -->|No| E
    F -->|Yes| G[親テーブル・インデックス特定]
    G --> H{親行存在チェック}
    H -->|存在| I[制約OK]
    H -->|不在| J[制約カウンター増減]
    I --> K{次のFKey?}
    J --> K
    K -->|Yes| E
    K -->|No| L[親テーブルとしてのFK処理]
    L --> M[参照元FKeyをループ]
    M --> N[子テーブルスキャン]
    N --> O{子行存在?}
    O -->|Yes| P[アクション実行/カウンター更新]
    O -->|No| Q{次のFKey?}
    P --> Q
    Q -->|Yes| M
    Q -->|No| Z
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-31-1 | 親キー存在検証 | 子テーブルのFK列に値を挿入/更新する際、親テーブルに対応するキーが存在しなければならない | INSERT/UPDATE時 |
| BR-31-2 | 依存行削除制御 | 親テーブルから行を削除する際、子テーブルに依存行が存在する場合はアクションを実行 | DELETE時 |
| BR-31-3 | 遅延制約 | DEFERRABLE INITIALLY DEFERRED指定の場合、制約検証はトランザクションコミット時まで遅延 | トランザクション内 |
| BR-31-4 | NULL許容 | FK列がNULLの場合、参照整合性チェックはスキップされる | FK列がNULL時 |

### 計算ロジック

**制約カウンター管理**：
- INSERT時（子テーブル）：親不在なら違反カウンター+1
- DELETE時（子テーブル）：親不在だった行の削除で違反カウンター-1
- INSERT時（親テーブル）：対応する子行があれば違反カウンター-1
- DELETE時（親テーブル）：依存する子行があれば違反カウンター+1

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 制約検証 | 親テーブル | SELECT | ユニークインデックスを使用した存在確認 |
| CASCADE DELETE | 子テーブル | DELETE | 依存行の自動削除 |
| CASCADE UPDATE | 子テーブル | UPDATE | FK列の自動更新 |
| SET NULL | 子テーブル | UPDATE | FK列をNULLに設定 |
| SET DEFAULT | 子テーブル | UPDATE | FK列をデフォルト値に設定 |

### テーブル別操作詳細

#### 親テーブル

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | PRIMARY KEY/UNIQUE列 | FK列値と一致するか検証 | インデックス使用 |

#### 子テーブル

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | FK列 | CASCADE: 親の新しいキー値、SET NULL: NULL | アクショントリガー |
| DELETE | - | CASCADE: 親キーに対応する行 | アクショントリガー |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| SQLITE_CONSTRAINT_FOREIGNKEY | 制約違反 | FK列に対応する親キーが存在しない | 有効な親キー値を指定 |
| SQLITE_CONSTRAINT_FOREIGNKEY | 制約違反 | 依存する子行が存在する状態で親行を削除（RESTRICT時） | 先に子行を削除 |
| foreign key mismatch | スキーマエラー | FK定義の親キーにUNIQUE/PRIMARY KEY制約がない | スキーマ修正 |

### リトライ仕様

外部キー制約違反はデータ整合性の問題であり、自動リトライは行わない。アプリケーション側で適切な順序でデータ操作を行う必要がある。

## トランザクション仕様

- **即座制約（Immediate）**：各ステートメント実行時に検証、違反時は即座にロールバック
- **遅延制約（Deferred）**：トランザクションコミット時に検証、違反があればコミット失敗
- PRAGMA foreign_keys設定はトランザクション外で変更する必要がある

## パフォーマンス要件

- 親テーブルの検索にはUNIQUE/PRIMARY KEYインデックスを使用し、O(log n)の計算量
- 子テーブルのスキャンにはインデックスを使用（存在する場合）
- COLUMN_MASK最適化により、変更されていないFK列の検証をスキップ

## セキュリティ考慮事項

- sqlite3_set_authorizer()によりFK検証時のテーブル読み取りに対する認可チェックが可能
- SQLITE_IGNOREが返された場合、該当列の値はNULLとして扱われる
- 外部キー制約の有効/無効切り替えには適切な権限管理が必要

## 備考

- SQLITE_OMIT_FOREIGN_KEYまたはSQLITE_OMIT_TRIGGERでコンパイルした場合、本機能は無効
- デフォルトでforeign_keysはOFF、明示的に有効化が必要
- WITHOUT ROWIDテーブルでも外部キー制約をサポート

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

外部キー制約の情報を保持するデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | sqliteInt.h | `src/sqliteInt.h` | FKey構造体の定義を確認 |

**読解のコツ**: FKey構造体はテーブル定義（Table構造体）のu.tab.pFKeyメンバーからリンクリストでアクセスされる。nCol（キー列数）、aCol（列マッピング）、aAction（ON DELETE/UPDATE時のアクション）、isDeferred（遅延制約フラグ）が主要メンバー。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | fkey.c | `src/fkey.c` | sqlite3FkCheck()が主要エントリーポイント |

**主要処理フロー**:
1. **889-908行目**: 関数の入口、foreign_keys有効性チェック
2. **915-1013行目**: 子テーブルとしてのFK処理ループ
3. **1018-1085行目**: 親テーブルとしてのFK処理ループ

#### Step 3: 親テーブル検索処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | fkey.c | `src/fkey.c` | fkLookupParent()関数 |

**主要処理フロー**:
- **320-460行目**: 親テーブル内の対応行をVDBEコードで検索
- **349-356行目**: NULLチェック（NULL列は制約検証をスキップ）
- **359-437行目**: INTEGER PRIMARY KEY vs 通常インデックスの分岐処理
- **440-456行目**: 制約違反時のカウンター操作またはエラー発生

#### Step 4: 子テーブルスキャン処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | fkey.c | `src/fkey.c` | fkScanChildren()関数 |

**主要処理フロー**:
- **547-660行目**: WHERE句を構築し子テーブルをスキャン
- **583-598行目**: 親キー = 子キーの等価条件を生成
- **614-636行目**: 自己参照テーブルの場合の除外条件
- **647-652行目**: WHERE句に一致する子行ごとにカウンター更新

#### Step 5: アクショントリガー処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | fkey.c | `src/fkey.c` | fkActionTrigger()関数、sqlite3FkActions()関数 |

**主要処理フロー**:
- **1216-1410行目**: CASCADE/SET NULL/SET DEFAULT/RESTRICTのトリガーを構築
- **1269-1320行目**: UPDATE用のSET句を構築
- **1327-1350行目**: RESTRICT用のSELECT ... RAISE()を構築
- **1416-1438行目**: 構築したトリガーを実行

### プログラム呼び出し階層図

```
sqlite3FkCheck()
    │
    ├─ sqlite3FkLocateIndex()      親テーブルのユニークインデックスを特定
    │      └─ 各インデックスをスキャンしてFK列に対応するものを探索
    │
    ├─ fkLookupParent()            子テーブル→親テーブル方向の検証
    │      └─ VDBEコード生成（OP_Found/OP_NotExists等）
    │
    ├─ fkScanChildren()            親テーブル→子テーブル方向の検証
    │      ├─ sqlite3WhereBegin()  子テーブルスキャン開始
    │      └─ sqlite3WhereEnd()    スキャン終了
    │
    └─ sqlite3FkReferences()       pTabを参照するFKeyリストを取得

sqlite3FkActions()
    │
    └─ fkActionTrigger()           アクショントリガーを構築
           │
           └─ sqlite3CodeRowTriggerDirect()  トリガーを実行
```

### データフロー図

```
[入力]                      [処理]                           [出力]

子テーブル行データ ──────▶ sqlite3FkCheck() ─────────────▶ VDBEコード
(regOld/regNew)            │                               (制約検証)
                           │
FKey構造体 ───────────────▶├─ fkLookupParent() ──────────▶ 制約カウンター
(pTab->u.tab.pFKey)        │   親テーブル検索               (即座/遅延)
                           │
参照元FKeyリスト ──────────▶├─ fkScanChildren() ─────────▶ エラー
(sqlite3FkReferences)      │   子テーブルスキャン           (SQLITE_CONSTRAINT)
                           │
                           └─ fkActionTrigger() ─────────▶ 内部トリガー
                               アクション構築               (CASCADE等)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| fkey.c | `src/fkey.c` | ソース | 外部キー制約のメイン実装 |
| sqliteInt.h | `src/sqliteInt.h` | ヘッダー | FKey構造体定義 |
| build.c | `src/build.c` | ソース | CREATE TABLE時のFK定義解析 |
| insert.c | `src/insert.c` | ソース | INSERT時のFK検証呼び出し |
| update.c | `src/update.c` | ソース | UPDATE時のFK検証呼び出し |
| delete.c | `src/delete.c` | ソース | DELETE時のFK検証呼び出し |
| trigger.c | `src/trigger.c` | ソース | アクショントリガー実行 |
| where.c | `src/where.c` | ソース | 子テーブルスキャン最適化 |
| vdbe.c | `src/vdbe.c` | ソース | OP_FkCounter/OP_FkIfZero実行 |
