コンテンツにスキップ

飛行計画・AssetドメインER図

飛行計画ドメインとAssetドメインの論理データモデルを示す。上位の概念モデルは utm-design-docs/docs/data-model/data-model.md、 API仕様はflight-planning.yaml、状態遷移は flight-plan-statemachine.md、 DIPS通報の項目対応はflight-plan-field-mapping.mdを参照。

各設計判断の理由は末尾の「設計方針」に、図中に収まらないenum値の全列挙・制約・参照先は 図の直後の「カラム補足」に記載する。

本ドキュメントで繰り返し使う用語を先に定める。

用語意味
DIPS国土交通省が提供するドローン情報基盤システム。飛行計画の通報・参照のAPIを提供するシステムであり、制度そのものではない
飛行計画の通報義務日本の航空法が定める義務。特定飛行を行う場合に飛行計画を国土交通大臣へ通報しなければならないというもの
DIPS通報上記の義務を果たすために、DIPSの飛行計画情報登録・更新APIへ飛行計画を送信すること
DIPS通報の対象日本国内の運航でDIPS通報の枠組みが適用されること。本ドキュメントで「対象外」と書く場合は枠組み自体が適用されない運航(将来の海外展開など通報先が別の制度になるケース)を指す。10月デモの範囲ではすべての飛行計画が対象であり、この運航をどう表すかは本設計では決めていない(行の不在では表さない。理由はDIPS通報固有の属性は飛行計画本体から分離する参照)
report_required=false枠組みは適用されるが、機体重量・飛行空域・飛行方法から特定飛行に当たらず通報義務が生じないこと(100g未満など)。屋内飛行も事由に当たるが、それを申告する入力項目がAPIにないため判定には用いない(report_requiredのカラム補足参照)。行は存在し、根拠をreport_exemption_reasonに記録する。上の「対象外」とは別概念
(用語の使い分け)本ドキュメントで「対象外」はDIPS通報の枠組みが適用されないことのみを指す。設計・デモの範囲に含めないことは「スコープ外」と書く

図の色分け・カラムのマーカー(PKFKUKPK,FKFK,UK)・カーディナリティ記法は データモデル設計ドキュメントの規約に定める。

本ドキュメントで概念モデルに対して追加したテーブルは以下の12テーブル。他のテーブルは概念モデルに定義済みのものである。 既存テーブルへのカラム追加(ASSET_UAS_ATTRS.max_takeoff_weight_kgをDIPS通報にも用いるなど)は色では表現しない。

追加テーブル追加した理由
FLIGHT_PLAN_PILOT_ASSIGNMENT飛行計画に紐づく操縦者・機体が複数指定可のため、リビジョン単位のN:M中間テーブルが必要(操縦者・機体はマスタ参照と中間テーブルで表す
FLIGHT_PLAN_DRAFT一時保存(DRAFT)中の入力内容を、本登録後のFLIGHT_PLAN_REVISIONとは別の制約で保持するため(一時保存は専用テーブルで表す
FLIGHT_PLAN_STATE_EVENT運航状態・DIPS通報状態の遷移を、内容リビジョンとは分けて追記のみのイベントとして記録するため(状態遷移は内容リビジョンと分けてイベントで記録する
FLIGHT_PLAN_AREA_CIRCLE円形エリアの半径を形状ごとのサブテーブルに分けて保持するため(飛行領域は形状ごとにサブテーブルへ分ける
FLIGHT_PLAN_AREA_ROUTE経路エリアのバッファ幅を形状ごとのサブテーブルに分けて保持するため(同上)
FLIGHT_PLAN_DIPS_ATTRSDIPS通報固有の属性を飛行計画本体から分離するため(DIPS通報固有の属性は飛行計画本体から分離する
FLIGHT_PLAN_DIPS_PURPOSE飛行目的が「コード+そのコードに対する補足」の組の配列となったため、コード単位の行として保持する必要がある(同上)
DIPS_FLIGHT_PLANDIPSから収集した他社飛行計画をUTM内部の飛行計画と別の独立モデルとして持つため(DIPSから収集した他社飛行計画は独立モデルとして管理する
FLIGHT_PLAN_DIPS_NEARBY_LINK上記の独立モデルと自社飛行計画をN:Mで関連付けるため(同上)
PILOT操縦者を組織直下の独立マスタとして管理するため(概念モデルには操縦者のエンティティがない)
PILOT_EVENTPILOTの可変属性をイベント由来の導出値とするため(可変属性はイベント由来の導出値として持つ)。forDemoでは実装対象外(図3 カラム補足のASSET_EVENT / PILOT_EVENTの注記参照)
ASSET_UAS_DIPS_ATTRS機体のDIPS通報用属性(機体認証書番号・登録記号・機体認証・DIPS機体種別コード)を一般的な機体属性から分離するため(機体のDIPS通報用属性は専用テーブルに分離する

飛行計画の中核(本体・リビジョン・一時保存・状態遷移・計画内容・競合検出・飛行実績)を図1に、DIPS連携 (通報とDIPSからの周辺計画収集)を図2に分ける。DIPS通報は日本の航空法に基づく日本固有の要件であり、中核のモデルと分離して 差し替え可能にしておく設計方針(DIPS通報固有の属性は飛行計画本体から分離する)を 図の構成にも反映したものである。

ORGANIZATIONUSERはIAMドメイン、PILOTASSETは図3、本図から関係線が伸びるDIPS連携の 4テーブル(FLIGHT_PLAN_DIPS_ATTRSDIPS_REPORTDIPS_FLIGHT_PLANFLIGHT_PLAN_DIPS_NEARBY_LINK)は 図2で定義する。COORDINATION_CONFLICTIONは運航調整ドメイン(coordinationスキーマ)で定義される他ドメインの テーブルである。いずれも本図では枠と関係線のみを示す。

erDiagram
    ORGANIZATION ||--o{ FLIGHT_PLAN : "所有"
    USER ||--o{ FLIGHT_PLAN : "作成者"
    USER ||--o{ FLIGHT_PLAN_REVISION : "変更者"
    FLIGHT_PLAN ||--o| FLIGHT_PLAN_DRAFT : "一時保存 (DRAFT中のみ存在)"
    USER ||--o{ FLIGHT_PLAN_DRAFT : "更新者"
    FLIGHT_PLAN ||--o{ FLIGHT_PLAN_REVISION : "リビジョン履歴 (本登録後)"
    FLIGHT_PLAN ||--o| FLIGHT_PLAN_REVISION : "current_revision"
    FLIGHT_PLAN ||--|{ FLIGHT_PLAN_STATE_EVENT : "状態遷移イベント"
    FLIGHT_PLAN_REVISION |o--o{ FLIGHT_PLAN_STATE_EVENT : "遷移時点のリビジョン"
    USER ||--o{ FLIGHT_PLAN_STATE_EVENT : "実行者"
    FLIGHT_PLAN_REVISION |o--o| FLIGHT_PLAN_REVISION : "parent_revision"
    FLIGHT_PLAN_REVISION ||--|| FLIGHT_PLAN_AREA : "飛行領域"
    FLIGHT_PLAN_AREA ||--o| FLIGHT_PLAN_AREA_CIRCLE : "円の付随パラメータ"
    FLIGHT_PLAN_AREA ||--o| FLIGHT_PLAN_AREA_ROUTE : "経路の付随パラメータ"
    FLIGHT_PLAN_REVISION ||--o{ FLIGHT_PLAN_PILOT_ASSIGNMENT : "操縦者・機体割当"
    PILOT ||--o{ FLIGHT_PLAN_PILOT_ASSIGNMENT : "操縦者"
    ASSET ||--o{ FLIGHT_PLAN_PILOT_ASSIGNMENT : "使用機体"
    FLIGHT_PLAN ||--o| FLIGHT_RECORD : "実飛行"
    FLIGHT_PLAN_REVISION ||--o{ FLIGHT_RECORD : "飛行開始時リビジョン"
    FLIGHT_PLAN ||--o{ CONFLICT_DETECTION : "競合検出"
    DIPS_REPORT }o--o{ COORDINATION_CONFLICTION : "DIPS飛行計画IDで対応 (運航調整ドメイン)"
    DIPS_FLIGHT_PLAN |o--o{ COORDINATION_CONFLICTION : "DIPS飛行計画IDで対応 (運航調整ドメイン)"
    FLIGHT_PLAN_REVISION ||--o{ CONFLICT_DETECTION : "検出対象リビジョン"
    FLIGHT_PLAN_REVISION ||--|| FLIGHT_PLAN_DIPS_ATTRS : "DIPS通報属性 (図2)"
    FLIGHT_PLAN ||--o{ DIPS_REPORT : "通報履歴 (図2)"
    FLIGHT_PLAN_REVISION ||--o{ DIPS_REPORT : "通報されたリビジョン (図2)"
    FLIGHT_PLAN ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "周辺収集結果 (図2)"
    FLIGHT_PLAN_REVISION ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "収集時の起点リビジョン (図2)"
    DIPS_FLIGHT_PLAN ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "収集された計画 (図2)"

    FLIGHT_PLAN {
        uuid id PK
        uuid organization_id FK
        uuid created_by FK "USER"
        uuid current_revision_id FK "最新リビジョン。DRAFT中はNULL"
        enum status "運航状態の現在値 (STATE_EVENT由来の導出)"
        timestamp created_at
        timestamp updated_at "最終更新時刻の現在値 (導出)"
        timestamp deleted_at "論理削除。ACTIVATED・ENDEDは削除不可"
    }
    FLIGHT_PLAN_DRAFT {
        uuid flight_plan_id PK,FK "FLIGHT_PLANと1:0..1。本登録またはキャンセルで消える"
        uuid updated_by FK "USER (直近更新者)"
        timestamp updated_at
        string name "一時保存で唯一の必須項目"
        timestamp planned_start_at "一覧の期間絞り込み用。Nullable"
        timestamp planned_end_at "一覧の期間絞り込み用。Nullable"
        jsonb draft_fields "入力途中の全項目。Nullable"
    }
    FLIGHT_PLAN_STATE_EVENT {
        uuid id PK
        bigserial seq "追記順。同一時刻の順序を確定させる"
        uuid flight_plan_id FK
        uuid flight_plan_revision_id FK "遷移時点のリビジョン。DRAFT中はNULL"
        enum event_type "遷移を起こした操作"
        enum status_after "遷移後の運航状態。常に記録 (NOT NULL)"
        enum report_status_after "遷移後のDIPS通報状態。常に記録 (NOT NULL)"
        uuid actor_user_id FK "USER (実行者)"
        timestamp occurred_at
    }
    FLIGHT_PLAN_REVISION {
        uuid id PK
        uuid flight_plan_id FK
        int revision_no "1始まり連番"
        uuid parent_revision_id FK "Nullable: 初版はNULL"
        uuid changed_by FK "USER"
        enum change_type "REGISTER(本登録)/UPDATE。内容変更のみ"
        string change_reason "変更理由。Nullable: API入力項目が未提供"
        timestamp changed_at
        string name "リビジョン時点の値を不変保持"
        timestamp planned_start_at "UTC"
        timestamp planned_end_at "UTC"
    }
    FLIGHT_PLAN_AREA {
        uuid id PK
        uuid flight_plan_revision_id FK,UK "リビジョン不変紐付け。1リビジョン1件"
        enum area_type "ROUTE/CIRCLE/POLYGON (APIのflyRoute.type)"
        geometry geometry "入力された元形状。CIRCLE時は中心点。Z値はAGL"
        geometry plan_geometry "dataType=FLIGHT_PLAN の返却形状。Z値はAGL"
        geometry plan_geometry_wgs84 "同左のWGS84版"
        geometry operational_intent_geometry "dataType=OPERATIONAL_INTENT。ROUTE時のみ。Z値はAGL"
        geometry operational_intent_geometry_wgs84 "同左のWGS84版"
        geometry top_bottom_3d_geometry "dataType=TOP_BOTTOM_3D の上下面。Z値はAGL"
        geometry top_bottom_3d_geometry_wgs84 "同左のWGS84版"
        float min_altitude_m_agl "最低対地高度。現時点は常に0固定"
        float max_altitude_m_agl "最高対地高度。APIのflightSpec.altitude"
        float min_altitude_m_amsl "標高から換算した参考値 (導出)"
        float max_altitude_m_amsl "標高から換算した参考値 (導出)"
        float min_altitude_m_wgs84 "ジオイド高から換算した参考値 (導出)"
        float max_altitude_m_wgs84 "ジオイド高から換算した参考値 (導出)"
        enum altitude_reference "値域はAGL/WGS84。接尾辞のない列は現時点では常にAGL"
        enum altitude_unit "値域はM。geometry系カラムのZ値の単位"
    }
    FLIGHT_PLAN_AREA_CIRCLE {
        uuid area_id PK,FK "FLIGHT_PLAN_AREAと1:0..1 (area_type=CIRCLE時のみ)"
        float radius_m "半径。APIのflyRoute.radiusM"
    }
    FLIGHT_PLAN_AREA_ROUTE {
        uuid area_id PK,FK "FLIGHT_PLAN_AREAと1:0..1 (area_type=ROUTE時のみ)"
        float buffer_m "バッファ幅。APIのflyRoute.bufferM"
    }
    FLIGHT_PLAN_PILOT_ASSIGNMENT {
        uuid id PK
        uuid flight_plan_revision_id FK
        uuid pilot_id FK "Pilotマスタ(/api/v1/asset/pilots)"
        uuid aircraft_id FK "ASSET.id (kind=UAS)"
    }
    FLIGHT_RECORD {
        uuid id PK
        uuid flight_plan_id FK,UK "1計画1件"
        uuid start_revision_id FK "飛行開始時のリビジョン"
        timestamp actual_start_at
        timestamp actual_end_at
    }
    CONFLICT_DETECTION {
        uuid id PK
        uuid flight_plan_id FK
        uuid detected_against_revision_id FK "検出対象のリビジョン"
        uuid airspace_restriction_id "AIRSPACE_RESTRICTION.id (空域制限ドメイン)"
        enum airspace_restriction_type "空域制限の種別"
        enum status "DETECTED/RESOLVING/RESOLVED/IGNORED"
        timestamp detected_at
        jsonb detection_params "閾値・距離"
    }

    classDef added fill:#fdd,stroke:#c00,stroke-width:2px,color:#c00
    class FLIGHT_PLAN_DRAFT,FLIGHT_PLAN_STATE_EVENT,FLIGHT_PLAN_AREA_CIRCLE,FLIGHT_PLAN_AREA_ROUTE,FLIGHT_PLAN_PILOT_ASSIGNMENT,PILOT,FLIGHT_PLAN_DIPS_ATTRS,DIPS_FLIGHT_PLAN,FLIGHT_PLAN_DIPS_NEARBY_LINK added
テーブルカラム補足
FLIGHT_PLANcurrent_revision_id現在の内容を指す最新リビジョン。DRAFTの間はFLIGHT_PLAN_REVISIONが存在しないためNULL、本登録以降はNOT NULL
FLIGHT_PLANupdated_at最終更新時刻の導出キャッシュ。全レスポンスで必須のupdatedAtに対応する。内容変更(FLIGHT_PLAN_REVISION.changed_at)・状態遷移(FLIGHT_PLAN_STATE_EVENT.occurred_at)・**一時保存中の上書き(FLIGHT_PLAN_DRAFT.updated_at)**の3者の最大値をキャッシュする。DRAFT中の更新はリビジョンもイベントも作らないため、3つ目を含めないとupdateFlightPlanで何度更新してもupdatedAtが動かない。全レスポンスの必須項目であり一覧の全行で必要になるため実体化する(listFlightPlansはデモではソートを提供しないため、ソートは根拠にならない)
FLIGHT_PLANstatus運航状態の現在値。DRAFT/ACCEPTED/ACTIVATED/CANCELLED/ENDEDFLIGHT_PLAN_STATE_EVENTから導かれる導出キャッシュ。GeoSpatialの領域検索がFlightPlanStatusFilterで絞り込み軸に使うため実体化する(listFlightPlansの絞り込み軸はperiodFrom/periodTo/registeredUserIdのみで、運航状態は含まない)
FLIGHT_PLANdeleted_at論理削除。**削除不可はACTIVATED(飛行中)とENDED(終了)**とし、DRAFTACCEPTEDCANCELLEDは削除できる。設定するのはdeleteFlightPlanと、DRAFTを対象としたcancelFlightPlan(削除と同等に扱うため。一時保存は専用テーブルで表す参照)。ENDEDを不可とするのは、flight-plan-statemachine.mdで飛行終了がreport_statusを変えずREPORTEDのまま終端になる一方、deleteFlightPlanreportStatus=REPORTEDなら削除前にDIPS側の取り下げを実行するため、実際に行われた飛行のDIPS通報を取り下げることになるからである。CANCELLEDは取り下げ済み(WITHDRAWN)または未通報でDIPSへの副作用がないため削除できる
FLIGHT_PLAN_DRAFT(関係)FLIGHT_PLANと1:0..1。DRAFTの間のみ存在し、本登録またはキャンセルで削除される。行の有無は状態を表さないDRAFTかどうかを表すのはFLIGHT_PLAN.statusだけとする。理由は一時保存は専用テーブルで表す参照)
FLIGHT_PLAN_DRAFTname一時保存で唯一の必須項目。更新時は上書きし履歴を持たない
FLIGHT_PLAN_DRAFTplanned_start_at / planned_end_atlistFlightPlansの期間絞り込み(periodFrom/periodTo)でACCEPTED以降と統合するため個別カラム化。一時保存では未入力を許すためNullable
FLIGHT_PLAN_DRAFTdraft_fieldsflyRoute/pilotInfo/flightPurposesなど入力途中の全項目。正規化・マスタ参照の整合性検証・競合判定を行わないため単一のjsonbで保持する
FLIGHT_PLAN_STATE_EVENT(関係)FLIGHT_PLANと1:N(1件以上)。INSERT-onlyで状態遷移を1遷移1行として記録する。内容を持たないため、状態が変わってもFLIGHT_PLAN_AREAなどの子テーブルを複製しない
FLIGHT_PLAN_STATE_EVENTflight_plan_revision_id遷移が起きた時点の内容リビジョン。DRAFT中の遷移(仮登録)はリビジョンが存在しないためNULL
FLIGHT_PLAN_STATE_EVENTevent_typeCREATE(仮登録)/REGISTER(本登録)/UPDATE(内容変更に伴う通報状態のリセット)/REPORT_START/REPORT_COMPLETE/REPORT_FAIL/REPORT_TIMEOUT/WITHDRAW_START/WITHDRAW_COMPLETE/WITHDRAW_FAIL/WITHDRAW_TIMEOUT/ACTIVATE/END/CANCELUPDATEは通報済み計画の内容変更でreport_statusUNREPORTEDへ戻す遷移、WITHDRAW_FAILは取り下げ失敗でREPORTEDへ戻す遷移。REPORT_TIMEOUT/WITHDRAW_TIMEOUTは504で成否不明のまま滞留に入った事実を記録するもので、状態は変えない(両軸とも直前の値を複写する)
FLIGHT_PLAN_STATE_EVENTseq追記順を確定させる連番(BIGSERIAL)。occurred_atだけでは、内容更新時にリビジョン作成とUNREPORTEDイベントを同一トランザクションで追記する場合など、同一時刻の複数行の順序が定まらないため。両軸の現在値の導出は本カラムの最大値で行う
FLIGHT_PLAN_STATE_EVENTstatus_after遷移後の運航状態。変化しない遷移でも直前の値を複写して常に記録する(NOT NULL)。理由は状態イベントは両軸の結果状態を常に記録する参照
FLIGHT_PLAN_STATE_EVENTreport_status_after遷移後のDIPS通報状態(UNREPORTED/REPORTING/REPORTED/WITHDRAWING/WITHDRAWN)。status_afterと同じく、変化しない遷移でも直前の値を複写して常に記録する(NOT NULL)。現在値はseqが最大の行の値として導出する
FLIGHT_PLAN_REVISIONchange_typeREGISTER(本登録。初版リビジョン)/UPDATE(内容変更)。状態遷移はリビジョンを作らずFLIGHT_PLAN_STATE_EVENTに記録するため、本enumは内容変更の2値のみ
FLIGHT_PLAN_REVISION(UK)(flight_plan_id, id)にUNIQUE制約を付与する。FLIGHT_PLAN_DIPS_NEARBY_LINKflight_plan_idを非正規化して持つため、その複合外部キーの参照先として必要
FLIGHT_PLAN_REVISIONrevision_no1始まり連番。UNIQUE(flight_plan_id, revision_no)を付与し、同一計画内で番号が重複しないことを担保する
FLIGHT_PLAN_REVISIONchange_reason変更理由(Gitコミットメッセージ相当)。Nullable。現在のAPI(createFlightPlan/updateFlightPlan/registerFlightPlan)に理由を渡す入力項目がないため、当面はUTM側が操作種別から定型文を設定する。Operatorが理由を入力できるようにするかは検討したが、どの操作が行われたかは呼ばれたoperationから導出できるためAPIからの指定は不要と決定した(確定した事項(10月デモ向け)参照)。当面はUTM側が操作種別から生成する(OAS・他ドメインへ反映が必要な項目参照)
FLIGHT_PLAN_REVISIONnameAPIのnameに対応。リビジョン時点の値を不変保持
FLIGHT_PLAN_AREAflight_plan_revision_idリビジョン不変紐付け。APIのflyRouteは単一オブジェクトのため、1リビジョンにつき1件(UNIQUE(flight_plan_revision_id))とする
FLIGHT_PLAN_AREAarea_typeUTM内部の飛行領域の形状。ROUTE/CIRCLE/POLYGON。UTM内部(API・DB)はこの3語に統一する(飛行領域の呼び名はUTM内部でROUTEに統一する参照)。OASのflyRoute.typePR #126route/circle/polygonに揃えたため、API境界での読み替えは発生しない
FLIGHT_PLAN_AREAgeometry入力された元形状(WGS84/EPSG:4326)。ROUTELineStringPOLYGONPolygonCIRCLEは中心点のPoint。UTM内部の正確な距離判定(CIRCLEST_DWithin)とPOLYGONの交差判定はこの列で行う(ROUTEの競合判定はoperational_intent_geometryで行う)。PostGIS型はgeometry(Geometry, 4326)相当(形状がarea_typeで変わるためサブタイプを絞らない。型修飾子は用いず、素のgeometry+CHECK制約でSRIDを保証する)。
FLIGHT_PLAN_AREAplan_geometrydataType=FLIGHT_PLANとして返す形状。CIRCLEは中心点+radius_mを多角形化したPolygon(GeoSpatialのGeometryPointを許さないため)、ROUTE/POLYGONgeometryと同値。応答生成をarea_typeで分岐させないため常に保持する。PostGIS型は同じ理由でgeometry(Geometry, 4326)相当(型修飾子は用いず、素のgeometry+CHECK制約でSRIDを保証する)。
FLIGHT_PLAN_AREAoperational_intent_geometrydataType=OPERATIONAL_INTENTとして返す形状。ROUTEのみST_Buffer(geometry::geography, buffer_m, 'quad_segs=1 endcap=square join=mitre')::geometryの結果を保持し、CIRCLE/POLYGONではNULL(GeoSpatialがROUTEのときだけ返す契約のため)PostGIS型はgeometry(Polygon, 4326)相当(型修飾子は用いず、素のgeometry+CHECK制約でSRIDと種別を保証する)。DIPS通報の形状であると同時に、ROUTEの空域制限との競合判定の対象でもある(両者で形状を揃えるため)。
FLIGHT_PLAN_AREAtop_bottom_3d_geometrydataType=TOP_BOTTOM_3Dとして返す上面・下面のMultiPolygon。水平方向の占有範囲と最低・最高高度から組み立てた結果を保持する。PostGIS型はgeometry(MultiPolygon, 4326)相当で、上面のPolygonと下面のPolygonの2つを1つの値として持つ。どちらが天面かはZ値で決まる(天面はmax_altitude_m_agl、床面はmin_altitude_m_agl)。天面・床面をZ軸方向に離して組み立てるため値は3次元(MultiPolygonZ)である(2Dを意味する型修飾子では登録できない。DDLはST_NDims = 3を必須にして表現する)。
FLIGHT_PLAN_AREAplan_geometry_wgs84 / operational_intent_geometry_wgs84 / top_bottom_3d_geometry_wgs84左の各形状のZ値を、地表標高(DEM)とジオイド高を用いて頂点ごとにWGS84楕円体高へ換算したもの。GeoSpatialは同一FeatureをaltitudeReference=AGLWGS84の2件返すため、接尾辞のない列と本列を対にして使い分ける。換算は登録・収集の時点で一度だけ行い、応答時にDEMへ問い合わせない(高度は換算値をカラムに保持する参照)。elevation.enabled=trueの環境では、対になる接尾辞のない列がNOT NULLの場合は本列も常にNOT NULL(登録・更新の時点でいずれかの頂点が変換できない場合はその登録・更新自体を拒否するため、対になる列の形状が存在する行は必ず換算値も持つ。operational_intent_geometry_wgs84は対になるoperational_intent_geometryROUTE以外でNULLになる場合はそれに合わせてNULLのままである)。elevation.enabled=false(既定値・ローカル開発)ではこの保証はなく、NULLのままになる高度は換算値をカラムに保持する参照。DBのCHECK/NOT NULL制約はいずれの環境でも課さない)。入力された元形状のgeometryには_wgs84列を持たない(GeoSpatialが返すgeometrydataTypeごとの実体化列から生成し、geometry列はCIRCLEの中心・半径の復元にのみ使うため、換算値の消費者がいない)PostGIS型は対になる接尾辞のない列と同じ。
FLIGHT_PLAN_AREAmin_altitude_m_agl最低対地高度[m AGL]。area_typeによらず現時点では常に0(地表)固定。対応するAPI入力項目が存在せず、DIPSでは高度の競合チェックを行わないため最高高度のみを管理する(詳細はflight-plan-field-mapping.md参照)
FLIGHT_PLAN_AREAmax_altitude_m_agl最高対地高度[m AGL]。APIのflightSpec.altitudeに対応。DIPS通報時はflightAltitudeとして送信する
FLIGHT_PLAN_AREAmin_altitude_m_amsl / max_altitude_m_amsl平均海面高度[m AMSL]。地表の標高データを用いてAGL値から換算した参考値の導出キャッシュ。elevation.enabled=trueの環境では常にNOT NULL_wgs84列と同じ理由。高度は換算値をカラムに保持する参照。enabled=falseのローカル開発ではNULLを許容)。geometryのZ値には使用しない
FLIGHT_PLAN_AREAmin_altitude_m_wgs84 / max_altitude_m_wgs84WGS84楕円体高[m]。ジオイド高を用いてAMSL値から換算した参考値の導出キャッシュ。elevation.enabled=trueの環境では常にNOT NULL(同上)
FLIGHT_PLAN_AREAaltitude_reference / altitude_unit接尾辞のないgeometry系カラムのZ値が準拠する高度基準(現時点では常にAGL)と単位(値域はM。APIのaltitudeUnitm)。_wgs84列の基準は列名で自明なため対象外。基準・単位の追加変更に備えて自己記述的に保持する。AGL基準の列と_wgs84列を対にして持つため、応答生成時はこの2系統を使い分ける(高度は換算値をカラムに保持する参照)
FLIGHT_PLAN_AREA_CIRCLEarea_idFLIGHT_PLAN_AREAと1:0..1(PK兼FK)。area_type=CIRCLEの行にのみ存在する
FLIGHT_PLAN_AREA_CIRCLEradius_m半径。APIのflyRoute.radiusMに対応
FLIGHT_PLAN_AREA_ROUTEarea_idFLIGHT_PLAN_AREAと1:0..1(PK兼FK)。area_type=ROUTEの行にのみ存在する
FLIGHT_PLAN_AREA_ROUTEbuffer_mバッファ幅。APIのflyRoute.bufferMに対応。上限はflight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」を参照
FLIGHT_PLAN_PILOT_ASSIGNMENT(UK)(flight_plan_revision_id, pilot_id, aircraft_id)にUNIQUE制約を付与し、同一組の重複登録を防ぐ
FLIGHT_PLAN_PILOT_ASSIGNMENT(組織スコープ)PILOT.organization_idASSET.organization_idFLIGHT_PLAN.organization_idと一致することを登録・更新時に検証する(DB制約は置かない)。他組織の操縦者・機体を割り当てるとPilotAssignmentpilotNameaircraftNamesから他組織の情報が読み取れてしまうため。理由は操縦者・機体はマスタ参照と中間テーブルで表す参照
FLIGHT_PLAN_PILOT_ASSIGNMENTpilot_idPilotマスタ(/api/v1/asset/pilots
FLIGHT_PLAN_PILOT_ASSIGNMENTaircraft_idASSET.idkind=UAS/api/v1/asset/aircrafts
FLIGHT_RECORD(関係)1つの飛行計画に対する実飛行は最大1件(ACTIVATED到達時に作成し、ENDEDで確定する)
FLIGHT_RECORD(UK)UNIQUE(flight_plan_id)を付与する。1つの飛行計画に対する実飛行が最大1件であることを、図のカーディナリティだけでなくDBレベルで担保するため
FLIGHT_RECORDstart_revision_id飛行開始時のリビジョン
CONFLICT_DETECTIONdetected_against_revision_id判定対象のリビジョン。ACCEPTED以降のリビジョンは必ず空域制限チェック済みであり、current_revision_idに紐づく行が0件なら「チェック済みで競合なし」を意味する
CONFLICT_DETECTIONairspace_restriction_id競合した空域制限(空域制限ドメインのAIRSPACE_RESTRICTION.id)。データソース側の元ID(FISS由来の文字列)ではなくUTM内部のuuidで参照する。スキーマが異なるためFK制約は張らない。理由は空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する参照
CONFLICT_DETECTIONairspace_restriction_type検出時点の空域制限の種別。APIのAirspaceRestrictionTypeに対応。API応答の再構成に必要であり、かつ検出履歴として検出時点の値を保つため、別ドメインへ引き直さずスナップショットとして保持する
CONFLICT_DETECTION(対象範囲)空域制限との競合のみを扱う。飛行計画同士の重複は運航調整ドメインのcoordination.conflictionが担い、有人機・逸脱・侵入に相当するAPIは未定義のため、種別カラム(conflict_type/counterpart_kind)は持たない。詳細は空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する参照
CONFLICT_DETECTIONstatusDETECTED/RESOLVING/RESOLVED/IGNORED
COORDINATION_CONFLICTION(参照)運航調整ドメイン(coordinationスキーマ)のconflictionテーブル。self_flight_plan_id/opponent_flight_plan_idにDIPSの飛行計画IDを持ち、conflict_flagで重複/重複解消を表す。本ドメインとはDIPS飛行計画IDで対応づけ、FK制約は張らない(スキーマが異なるため)
COORDINATION_CONFLICTIONopponent_mail_address相手Operatorの連絡先。運航調整で必要な連絡先はこちらが保持するため、収集側(FLIGHT_PLAN_DIPS_NEARBY_LINK)にはemailを持たせない
COORDINATION_CONFLICTION(カーディナリティ)self_flight_plan_id/opponent_flight_plan_idはDIPSの飛行計画IDによる論理対応であり、FK制約も一意制約もない。DIPS_REPORTは同じ受付番号の行が複数並ぶためN:M、DIPS_FLIGHT_PLANは相手計画が未収集でありうるため0..1側になる

図2: DIPS連携(通報・周辺計画収集)

Section titled “図2: DIPS連携(通報・周辺計画収集)”

DIPSへの飛行計画通報(FLIGHT_PLAN_DIPS_ATTRS / FLIGHT_PLAN_DIPS_PURPOSE / DIPS_REPORT)と、DIPSから 収集した周辺の他社飛行計画(DIPS_FLIGHT_PLAN / FLIGHT_PLAN_DIPS_NEARBY_LINK)の計5テーブルを扱う。 FLIGHT_PLANFLIGHT_PLAN_REVISIONは図1で定義するため、本図では枠と関係線のみを示す。

erDiagram
    FLIGHT_PLAN_REVISION ||--|| FLIGHT_PLAN_DIPS_ATTRS : "DIPS通報属性"
    FLIGHT_PLAN_DIPS_ATTRS ||--|{ FLIGHT_PLAN_DIPS_PURPOSE : "飛行目的"
    FLIGHT_PLAN ||--o{ DIPS_REPORT : "通報履歴"
    FLIGHT_PLAN_REVISION ||--o{ DIPS_REPORT : "通報されたリビジョン"
    FLIGHT_PLAN ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "周辺収集結果"
    FLIGHT_PLAN_REVISION ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "収集時の起点リビジョン"
    DIPS_FLIGHT_PLAN ||--o{ FLIGHT_PLAN_DIPS_NEARBY_LINK : "収集された計画"

    FLIGHT_PLAN_DIPS_ATTRS {
        uuid flight_plan_revision_id PK,FK "FLIGHT_PLAN_REVISIONと1:1"
        bool report_required "通報義務の有無 (飛行空域・飛行方法・重量から判定)"
        string report_exemption_reason "report_required=false時の根拠"
        string departure_point "出発地。一覧で必須のため個別カラム"
        string destination_point "目的地。一覧で必須のため個別カラム"
        string contact_email "調整連絡先。競合通知の宛先"
        enum_array flight_airspace "飛行空域コード配列。通報要否の判定入力"
        enum_array flight_type "飛行方法コード配列。一覧で必須・判定入力"
        jsonb dips_report_detail "DIPS通報詳細のパススルー項目一式"
    }
    FLIGHT_PLAN_DIPS_PURPOSE {
        uuid id PK
        uuid flight_plan_revision_id FK "FLIGHT_PLAN_DIPS_ATTRS"
        enum code "飛行目的コード (FlightPurposeCode)"
        string note "codeごとの補足"
    }
    DIPS_REPORT {
        uuid id PK
        uuid flight_plan_id FK
        uuid flight_plan_revision_id FK "通報対象リビジョン"
        string dips_receipt_no "DIPS発行の受付番号。計画内で不変のため非UK"
        enum status "REPORTED/WITHDRAWN"
        timestamp reported_at
        timestamp last_synced_at
    }
    DIPS_FLIGHT_PLAN {
        uuid id PK
        string dips_flight_plan_id UK "項番3 flightPlanId"
        string identification_name "項番4 識別名称"
        timestamp planned_start_at "項番5 飛行開始予定日時"
        timestamp planned_end_at "項番6 飛行終了予定日時"
        float flight_speed_kmh "項番7 飛行速度(km/h)"
        enum area_type "項番10 CIRCLE/POLYGON"
        geometry geometry "raw_payloadからの変換。CIRCLE時は中心点。Z値はAGL"
        geometry plan_geometry "dataType=FLIGHT_PLAN の返却形状。Z値はAGL"
        geometry plan_geometry_wgs84 "同左のWGS84版"
        geometry top_bottom_3d_geometry "dataType=TOP_BOTTOM_3D の上下面。Z値はAGL"
        geometry top_bottom_3d_geometry_wgs84 "同左のWGS84版"
        float radius_m "項番14 CIRCLE時の半径(m)"
        float min_altitude_m_agl "最低対地高度"
        float max_altitude_m_agl "最高対地高度 (項番8 flightAltitude)"
        float min_altitude_m_amsl "標高から換算した参考値 (導出)"
        float max_altitude_m_amsl "標高から換算した参考値 (導出)"
        float min_altitude_m_wgs84 "ジオイド高から換算した参考値 (導出)"
        float max_altitude_m_wgs84 "ジオイド高から換算した参考値 (導出)"
        enum altitude_reference "値域はAGL/WGS84。接尾辞のない列は現時点では常にAGL"
        enum altitude_unit "値域はM。geometry系カラムのZ値の単位"
        string certification_usp_name "項番20 認定USP名称"
        string certification_usp_code "項番21 認定USPコード"
        jsonb raw_payload "DIPS応答の当該計画分の原文"
        timestamp collected_at "最終収集時刻"
    }
    FLIGHT_PLAN_DIPS_NEARBY_LINK {
        uuid id PK
        uuid flight_plan_id FK "飛行計画をキーに引くための非正規化"
        uuid flight_plan_revision_id FK "周辺検索の起点となるリビジョン"
        uuid dips_flight_plan_ref_id FK "収集したDIPS飛行計画"
        timestamp collected_at "当該自組織計画に対する収集時刻"
    }

    classDef added fill:#fdd,stroke:#c00,stroke-width:2px,color:#c00
    class FLIGHT_PLAN_DIPS_ATTRS,FLIGHT_PLAN_DIPS_PURPOSE,DIPS_FLIGHT_PLAN,FLIGHT_PLAN_DIPS_NEARBY_LINK added

DIPS_FLIGHT_PLANFLIGHT_PLAN_DIPS_NEARBY_LINKの「項番N」は「DIPS2.0 API(FPR)Guideline USP v1.0」 飛行計画参照APIのレスポンスボディの項番。

テーブルカラム補足
FLIGHT_PLAN_DIPS_ATTRSflight_plan_revision_idFLIGHT_PLAN_REVISIONと1:1(PK兼FK)。リビジョン時点のDIPS通報内容を不変保持する。本登録の必須項目を配下に持つため、リビジョンが存在すれば必ず1行存在する(NOT NULLの1:1として作る)。通報義務が生じないこと(report_required=false)は行の有無ではなく本テーブルのカラムの値で表す
FLIGHT_PLAN_DIPS_ATTRSreport_required通報義務の有無。flight_airspaceflight_type(いずれも特定飛行の類型)・機体重量から判定する。屋内飛行も義務が生じない事由だが、それを申告する入力項目がAPIに存在しないため判定には用いず、report_exemption_reason=INDOOR_ONLYとして個別に設定される想定である。ただし屋内飛行は10月デモの対象外(飛行場所は屋外で確定)であり、入力項目も新設しないと決定した(確定した事項(10月デモ向け)参照)。飛行領域や高度の変更でflight_airspaceが変われば値も変動しうる計画プロパティ
FLIGHT_PLAN_DIPS_ATTRSreport_exemption_reasonreport_required=false時の根拠(例: WEIGHT_UNDER_100G/INDOOR_ONLY)。取りうる値の全体が未確定のためstringとし、確定後にenum化する
FLIGHT_PLAN_DIPS_ATTRSdeparture_point / destination_point / contact_email / flight_airspace / flight_typeFlightPlanListItemが必須項目として返すため、dips_report_detailjsonb)ではなく個別カラムとして持つ。flight_airspaceFlightAirspaceCodeflight_typeFlightTypeCodeの配列型カラム(FLIGHT_PLAN_DIPS_PURPOSEと違い、コードごとに付随する属性がないため子テーブルにはしない)。前者は通報要否の判定入力、後者はそれに加えて一覧の必須項目である。contact_emailは競合通知の宛先として通知ドメインが参照する。DIPS通報時はそれぞれflightPlanInfo.departurePoint/destinationPointcontactInfo.emailflightPlanInfo.flightAirspace/flightTypeへ渡す(コードは数値へ変換)
FLIGHT_PLAN_DIPS_ATTRSdips_report_detailDIPSへのパススルー項目一式(assistantsNumber/plannedMaxTime/flightSpeed/riskMitigation*/exceptionalConditionsMooring/insuranceInformation/otherInformation/flightPermitApplicationInfo)。値が業務判定・一覧取得・通知の入力にならないため(本登録・更新時の入力検証以外では参照しない。詳細はDIPS通報固有の属性は飛行計画本体から分離する)個別カラム化せずjsonbに集約する。DIPSのothergyomutext/othergyomugaitextFLIGHT_PLAN_DIPS_PURPOSE.noteから通報時に導出するため本項目には含まない
FLIGHT_PLAN_DIPS_PURPOSE(関係・UK)FLIGHT_PLAN_DIPS_ATTRSと1:N(1件以上必須)。(flight_plan_revision_id, code)にUNIQUE制約を付与し、同一コードの重複指定を防ぐ
FLIGHT_PLAN_DIPS_PURPOSEcode飛行目的コード(APIのFlightPurposeCode)。APIのflightPurposes[].codeに対応
FLIGHT_PLAN_DIPS_PURPOSEnote当該目的コードの補足説明(DIPSのothergyomutext/othergyomugaitextの制約に合わせる。別紙2 ID29・ID31。桁数はflight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」を参照)。APIのflightPurposes[].noteに対応。codeOTHER_BUSINESS/OTHER_NON_BUSINESSの場合は必須で、DIPS通報時にそれぞれothergyomutext/othergyomugaitextへ変換する。それ以外のコードでは通報に使用しない
DIPS_REPORT(関係)概念モデルに定義済みのテーブル。1計画1リビジョンに閉じず、通報のたびに行を追加して受付番号・状態を追跡する(FLIGHT_PLANFLIGHT_PLAN_REVISIONの双方にFKを持つのはこのため)
DIPS_REPORTdips_receipt_noDIPS API(飛行計画登録)レスポンスのflightPlanId(受付番号)。単独のUNIQUE制約は置かない。受付番号は初回の通報成功時に発行され、以降は再通報しても同じ値であるため(dipsFlightPlanIdの定義)、通報のたびに行を追加する本テーブルでは同じ値の行が複数並ぶ。検索用に非UNIQUEの索引を張り、「1つの受付番号が複数の飛行計画にまたがらない」ことはアプリケーション側で保証する。DIPSへの更新(mode=1)・削除(mode=2)では本項目が必須となる(別紙2の更新・削除シート)ため、最新行の値を参照する
DIPS_REPORTstatusREPORTED/WITHDRAWN。本テーブルはDIPSが受け付けた通報の履歴であり、通報失敗時は行を作らないため却下を表す値を持たない。理由はDIPS通報の失敗は応答の区分に応じて扱いを分ける参照
DIPS_FLIGHT_PLANdips_flight_plan_id項番3 flightPlanId。UUIDではなくDIPS独自形式(例: O40SIFXUOCCAAXXTJXNZ.FP20250221050353423.001)のためstringとし、単独でUKとする
DIPS_FLIGHT_PLANidentification_name項番4 identificationName(識別名称)
DIPS_FLIGHT_PLANplanned_start_at項番5 startTime(飛行開始予定日時)
DIPS_FLIGHT_PLANplanned_end_at項番6 finishTime(飛行終了予定日時)。参照APIは終了日時を直接返すため算出は不要
DIPS_FLIGHT_PLANflight_speed_kmh項番7 flightSpeed(単位km/h)
DIPS_FLIGHT_PLANarea_typeDIPSから収集した飛行領域の形状。項番10 flyRoute.typeに対応するCIRCLE/POLYGONの2値。DIPSはCircle/Polygonのみを扱い、UTM内部のROUTE(バッファ付き経路)に相当する形式を持たない
DIPS_FLIGHT_PLANgeometryraw_payloadのジオメトリから変換したPostGIS型(WGS84/EPSG:4326)。CIRCLE時は中心点Point(項番12-13)、POLYGON時はPolygon(項番17-18)。PostGIS型はFLIGHT_PLAN_AREA.geometryと同じgeometry(Geometry, 4326)相当(area_typeで形状が変わるためサブタイプを絞らない。型修飾子は用いず、素のgeometry+CHECK制約でSRIDを保証する)。個人情報は格納しない: duplicateFlightPlan[].contactInfoは格納前に除去する(運航調整で必要な連絡先はcoordination.confliction.opponent_mail_addressが保持するため、本ドメインでは連絡先を持たない)
DIPS_FLIGHT_PLANplan_geometry / top_bottom_3d_geometryFLIGHT_PLAN_AREAと同じく、GeoSpatialの他Operator計画検索が返すdataTypeごとの形状を実体化して保持する。収集した計画はCIRCLE/POLYGONのみでROUTEが存在しないため、operational_intent_geometryは持たない。PostGIS型はFLIGHT_PLAN_AREAの同名列と同じ。
DIPS_FLIGHT_PLANplan_geometry_wgs84 / top_bottom_3d_geometry_wgs84FLIGHT_PLAN_AREAと同じく、左の各形状のZ値を頂点ごとにWGS84楕円体高へ換算したもの。DIPS側にOPERATIONAL_INTENTは存在しないため2列で対称になる。再収集でUPSERTする際は接尾辞のない列とあわせて作り直す。PostGIS型は対になる接尾辞のない列と同じ。
DIPS_FLIGHT_PLANradius_m項番14 radiusCIRCLE時の半径、単位m)。POLYGON時はNULL
DIPS_FLIGHT_PLANmin_altitude_m_agl / max_altitude_m_agl最低・最高対地高度[m AGL]。max_altitude_m_aglは項番8 flightAltitudeに対応。GeoSpatialの近傍他Operator計画検索(OtherFlightPlanAreaProperties)が両方を返すため、自組織側FLIGHT_PLAN_AREAと同じくmin/maxの2本で保持する
DIPS_FLIGHT_PLANmin_altitude_m_amsl / max_altitude_m_amsl / min_altitude_m_wgs84 / max_altitude_m_wgs84AGL値から標高・ジオイド高で換算した参考値の導出キャッシュ。標高データ未取得の場合はNULL。geometryのZ値には使用しない。FLIGHT_PLAN_AREAと異なり本カラムはNULLを許容する高度は換算値をカラムに保持するの「常にNOT NULL」の決定はPR-02/PR-03(自組織の登録・更新)の範囲であり、収集(PR-12)による本テーブルへの書き込みは対象外)。周辺収集は多数の他Operator計画を一括で取り込むバッチ処理であるため、自組織側(1頂点でも変換できなければ登録・更新自体を拒否)とは方針が異なり、1件が標高データのカバー範囲外でもその計画のWGS84/AMSL列だけを除外し、収集全体は失敗させないPR #276で決定・実装。issue #271)
DIPS_FLIGHT_PLANaltitude_reference / altitude_unit接尾辞のないgeometry系カラムのZ値が準拠する高度基準(現時点では常にAGL)と単位(値域はM。APIのaltitudeUnitm)。FLIGHT_PLAN_AREAと同じ扱い
DIPS_FLIGHT_PLANcertification_usp_name項番20 certificationUspName(認定USP名称)。模擬DIPSの04応答には項目自体が存在しないため、10月デモでは収集時にダミーの固定値を入れる(値はBusinessLogicSpecifications.mdの4.5節を正とする)
DIPS_FLIGHT_PLANcertification_usp_code項番21 certificationUspCode(認定USPコード)。USP単位のコードであり、同一UTM上のどの組織の計画でも同じ値になるため組織の判別には使えない。certification_usp_nameと同じく模擬DIPSからは取得できず、10月デモではダミーの固定値を入れる
DIPS_FLIGHT_PLANraw_payloadDIPS応答の当該飛行計画分をそのまま保持。型付きカラムは本項目からの導出であり、未使用項目・DIPS側の項目追加は本項目で吸収する。応答全体のメタ項目(項番22 totalCount・項番28 duplicateTotalCount)は個々の計画の属性ではないため保持しない
DIPS_FLIGHT_PLANcollected_atDIPSから収集した時刻。再収集はdips_flight_plan_idをキーにしたUPSERTで更新するため、値は常に最新の収集時点を指す
FLIGHT_PLAN_DIPS_NEARBY_LINK(UK)(flight_plan_revision_id, dips_flight_plan_ref_id)にUNIQUE制約を付与する。あわせて(flight_plan_id, flight_plan_revision_id)FLIGHT_PLAN_REVISION(flight_plan_id, id)への複合外部キーとし、非正規化したflight_plan_idがリビジョンの所属計画と食い違わないようDBレベルで担保する(FLIGHT_PLAN_REVISION側にUNIQUE(flight_plan_id, id)が必要)
FLIGHT_PLAN_DIPS_NEARBY_LINKflight_plan_id周辺検索の起点となった飛行計画(FLIGHT_PLAN)。「この飛行計画の周辺にいるDIPS飛行計画」を引くのが主な参照経路であり、リビジョン経由のJOINを挟まずに済むよう非正規化して保持する
FLIGHT_PLAN_DIPS_NEARBY_LINKflight_plan_revision_id収集を実行した時点の起点リビジョン(FLIGHT_PLAN_REVISION)。リビジョンが変わるとflyRouteが変わり「周辺」の範囲も変わるため、どの範囲で収集した結果かを示すために保持する
FLIGHT_PLAN_DIPS_NEARBY_LINKdips_flight_plan_ref_id収集した他社飛行計画(DIPS_FLIGHT_PLAN
FLIGHT_PLAN_DIPS_NEARBY_LINKcollected_at当該リビジョンに対する収集時刻

組織が管理するHW資産(機体・コントローラー・地上局等)と操縦者を扱う。飛行計画との関係は中間テーブル FLIGHT_PLAN_PILOT_ASSIGNMENT経由で表すため(図1に記載)、本図には含めない。

erDiagram
    ORGANIZATION ||--o{ ASSET : "保有"
    USER ||--o{ ASSET : "登録者"
    ASSET ||--o| ASSET_UAS_ATTRS : "UAS属性"
    ASSET_UAS_ATTRS ||--o| ASSET_UAS_DIPS_ATTRS : "DIPS通報用属性"
    ASSET ||--o| DIPS_UAS_LINK : "DIPS紐付け"
    ASSET ||--o{ ASSET_RELATION : "親側"
    ASSET ||--o{ ASSET_RELATION : "子側"
    ASSET ||--o{ ASSET_EVENT : "資産イベント"
    ASSET_RELATION ||--o{ ASSET_RELATION_EVENT : "連結イベント"
    ORGANIZATION ||--o{ PILOT : "所属"
    USER ||--o{ PILOT : "登録操作をした利用者"
    PILOT ||--o{ PILOT_EVENT : "操縦者イベント"

    ASSET {
        uuid id PK
        uuid organization_id FK
        uuid registered_by FK "USER"
        enum kind "UAS/CONTROLLER/GROUND_STATION等 (不変)"
        timestamp acquired_at "取得日 (不変)"
        timestamp created_at
        string nickname "現在値 (ASSET_EVENT由来の導出)"
        string manufacturer "現在値 (導出)"
        string model "現在値 (導出)"
        string serial_number "現在値 (導出)"
        enum status "ACTIVE/RETIRED/MAINTENANCE (導出)"
        timestamp deleted_at "論理削除 (導出)"
    }
    ASSET_UAS_ATTRS {
        uuid asset_id PK,FK "ASSETと1:0..1。FKはASSET(id,kind)の複合FKでkind=UASを強制"
        float weight_kg "正の値のみ"
        float max_takeoff_weight_kg "総重量。DIPS通報のmaxWeightに対応。正の値かつweight_kg以上"
        enum airframe_type "MULTIROTOR/FIXED_WING/VTOL"
        jsonb specs "サイズ・最大速度等"
    }
    ASSET_UAS_DIPS_ATTRS {
        uuid asset_id PK,FK "ASSET_UAS_ATTRSと1:0..1"
        string certification_number "機体認証書番号"
        string registration_symbol UK "登録記号。国交省採番の法的識別子のため一意"
        bool certification1 "機体認証(第一種)の有無"
        bool certification2 "機体認証(第二種)の有無"
        string dips_aircraft_type "DIPS機体種別コード(1-6のいずれか)"
    }
    ASSET_RELATION {
        uuid id PK
        uuid parent_asset_id FK "上位 (例: UAS)"
        uuid child_asset_id FK "下位 (例: CONTROLLER/BATTERY)"
        enum relation_type "CONTROLLER_OF/BATTERY_OF/SENSOR_ON等"
        timestamp first_linked_at "初回連結時刻 (不変)"
        enum current_status "LINKED/UNLINKED (導出)"
    }
    DIPS_UAS_LINK {
        uuid id PK
        uuid asset_id FK,UK "ASSET.kind=UASを期待。1機体1件"
        string dips_registration_no UK
        timestamp linked_at
        timestamp last_synced_at
    }
    ASSET_EVENT {
        uuid id PK
        uuid asset_id FK
        enum event_type "REGISTERED/PROFILE_CHANGED/STATUS_CHANGED等"
        uuid actor_user_id FK "実行者"
        jsonb payload "変更後の属性値・状態"
        timestamp occurred_at
    }
    ASSET_RELATION_EVENT {
        uuid id PK
        uuid asset_relation_id FK
        enum event_type "LINKED/UNLINKED"
        uuid actor_user_id FK "実行者"
        timestamp occurred_at
    }
    PILOT {
        uuid id PK
        uuid organization_id FK "操縦者を登録した組織"
        uuid registered_by FK "USER (登録操作をした利用者。操縦者本人ではない)"
        timestamp created_at
        string name "現在値 (PILOT_EVENT由来の導出)"
        string country "現在値 (導出。別紙1_1_国コード)"
        string prefectures "現在値 (導出。別紙1_2_都道府県コード)"
        string address "現在値 (導出。番地まで含む住所全体)"
        string telephone_country "現在値 (導出)"
        string telephone "現在値 (導出)"
        string email "現在値 (導出)"
        string skill_certification_number "現在値 (導出)"
        bool first_class "現在値 (導出。技能証明(一等)の有無)"
        bool second_class "現在値 (導出。技能証明(二等)の有無)"
        enum status "ACTIVE/RETIRED (現在値・導出)"
        timestamp deleted_at "論理削除 (導出)"
    }
    PILOT_EVENT {
        uuid id PK
        uuid pilot_id FK
        enum event_type "REGISTERED/PROFILE_CHANGED/STATUS_CHANGED/DELETED"
        uuid actor_user_id FK "実行者"
        jsonb payload "変更後の属性値"
        timestamp occurred_at
    }

    classDef added fill:#fdd,stroke:#c00,stroke-width:2px,color:#c00
    class PILOT,PILOT_EVENT,ASSET_UAS_DIPS_ATTRS added

「導出」はASSET_EVENT/ASSET_RELATION_EVENT/PILOT_EVENT(いずれもINSERT-only)から導かれる現在値 キャッシュを指す(理由: 可変属性はイベント由来の導出値として持つ)。

[!NOTE] forDemoの実装対象は本図9エンティティのうち4つASSET / ASSET_UAS_ATTRS / ASSET_UAS_DIPS_ATTRS / PILOT)。飛行計画ドメインがAssetドメインを参照する経路が ①FK参照(FLIGHT_PLAN_PILOT_ASSIGNMENT.pilot_id / .aircraft_id)②画面表示とDIPS通報での マスタ解決、の2本しかなく、そのどちらにも現れないため。下記5エンティティはdb/schema/asset.sqlに DDLを持たない(本図はAssetドメインの設計としては9エンティティのままを正とする)。

エンティティ対象外の理由
ASSET_EVENT / PILOT_EVENT機体・操縦者のCRUDを対象外とし、可変属性は現在値をアプリケーション側で直接更新する方針としたため。この結果、上表の「導出」はforDemoでは成立せず、現在値を直接UPDATEする列として扱う
ASSET_RELATION / ASSET_RELATION_EVENT機体構成(コントローラー・バッテリー等の連結)をデモで登録・解除しないため
DIPS_UAS_LINKDIPSからの機体情報取得を対象外とするため
テーブルカラム補足
ASSETkindUAS/CONTROLLER/GROUND_STATION/SENSOR/BATTERY/OTHER(不変)。10月デモで管理するのはUASのみであり、種別固有の属性テーブルはASSET_UAS_ATTRSだけを用意する(クラステーブル継承)。他の種別の値域は将来の拡張に備えて残すが、対応する属性テーブルは必要になった時点で追加する
ASSET(UK)(id, kind)にUNIQUE制約を付与する。idPKのため単独でも一意だが、この複合UNIQUEはASSET_UAS_ATTRS側の複合FKがkindを検査できるようにするためだけに存在する(クラステーブル継承で「kind=UASASSETにしかASSET_UAS_ATTRSを持てない」ことをDBで強制する。下記ASSET_UAS_ATTRSの補足参照)
ASSETorganization_id / registered_byそれぞれにINDEXを張る(idx_asset_organization_id / idx_asset_registered_by)。組織別・登録者別の一覧取得を想定
ASSETacquired_at取得日(不変)
ASSETstatusACTIVE/RETIRED/MAINTENANCE(現在値・導出)
ASSETdeleted_at論理削除(DELETEDイベントの導出キャッシュ)
ASSET_UAS_ATTRSasset_idASSET(id, kind)への複合FK(kindは下記の生成列)。ASSET.kind=UASの行にしか本テーブルの行を持てないことをDBレベルで強制する(クラステーブル継承の唯一の不変条件)
ASSET_UAS_ATTRS(生成列。DDLのみ)kindという名前の生成列(値は常に'UAS'固定)を持つ。上記複合FKが参照する側の列を揃えるためだけの技術的な列であり、概念モデル・APIには現れない
ASSET_UAS_ATTRSweight_kg機体重量(kg)。正の値のみ
ASSET_UAS_ATTRSmax_takeoff_weight_kg総重量。機体登録時に保持する値で、DIPS通報のflightPlanInfo.aircraftInfo[].maxWeight(通報時に申告する値)に対応する。取得元は異なるがいずれも飛行時の機体重量を表すため同一の値として扱い、通報時は本カラムをそのまま送信する。正の値かつweight_kg以上
ASSET_UAS_ATTRSairframe_typeMULTIROTOR/FIXED_WING/VTOL。有人ヘリ(回転翼-ヘリ)はAsset管理の対象外のため値域に持たない
ASSET_UAS_ATTRSspecsサイズ・最大速度等
ASSET_UAS_DIPS_ATTRScertification_number機体認証書番号。DIPS通報時にflightPlanInfo.aircraftInfo[].certificationNumとして送信する。機体認証(第一種・第二種)がいずれも無の場合はNULL、いずれか一方でも有の場合はNOT NULLをCHECKで強制する
ASSET_UAS_DIPS_ATTRSregistration_symbol登録記号。DIPS通報時にflightPlanInfo.aircraftInfo[].symbolとして送信する。DIPS_UAS_LINK.dips_registration_no同一の値であり、DIPSにおいて機体関係の登録番号とされているものは登録記号を正とする。したがって通報時は本カラムを参照する
ASSET_UAS_DIPS_ATTRS(UK)UNIQUE(registration_symbol)を付与する。登録記号は国土交通省が採番する法的識別子であり、複数の機体に同じ値が紐づくとDIPS通報が別機体として処理されうるため
ASSET_UAS_DIPS_ATTRScertification1 / certification2機体認証(第一種/第二種)の有無。DIPS通報時にflightPlanInfo.aircraftInfo[].certification1/certification2として送信する
ASSET_UAS_DIPS_ATTRSdips_aircraft_typeDIPS機体種別コード(1:飛行機/2:回転翼-ヘリ/3:回転翼-マルチローター/4:回転翼-その他/5:滑空機/6:飛行船)。ASSET_UAS_ATTRS.airframe_typeとは別軸のDIPS固有コード。値域は1〜6の数字1文字のみであることをCHECKで強制する(airframe_type=VTOLに対応するDIPSコードは未定。followups.md参照)
ASSET_RELATIONparent_asset_id / child_asset_id上位(例: UAS)/下位(例: CONTROLLER・BATTERY)。UASに紐づかないHW(独立した地上局・検査用センサー等)は親不在のASSETとして登録する
ASSET_RELATIONrelation_typeCONTROLLER_OF/BATTERY_OF/SENSOR_ON/PAYLOAD_OF/OTHER
ASSET_RELATION(UK)同時に有効な連結が1つであることを担保するため、(parent_asset_id, child_asset_id, relation_type)current_status = 'LINKED'を条件とした部分UNIQUEインデックスを張る。単純なUNIQUEでは連結・解除を繰り返す運用で再連結できなくなる
ASSET_RELATIONcurrent_statusLINKED/UNLINKEDASSET_RELATION_EVENT由来の導出)。occurred_at <= Tで過去時点の機体構成を再現できる
DIPS_UAS_LINKasset_idASSET.kind=UASを期待
DIPS_UAS_LINK(UK)UNIQUE(asset_id)を付与する。1機体につきDIPS紐付けは最大1件であることをDBレベルで担保するため。あわせてUNIQUE(dips_registration_no)も付与する(図のUKマーカーに対応。DIPS側の登録記号が複数の機体に紐づかないことを担保する)
ASSET_EVENTevent_typeREGISTERED/PROFILE_CHANGED/STATUS_CHANGED/MAINTENANCE_IN/MAINTENANCE_OUT/RETIRED/DELETED
ASSET_EVENTpayload変更後の属性値・状態
ASSET_RELATION_EVENTevent_typeLINKED/UNLINKED
PILOTcountry国コード(別紙1_1_国コード)。APIのCountryCode
PILOTregistered_by登録操作をした利用者(USER)。操縦者本人ではない。操縦者はUTMの利用者アカウントを持たなくてよく、外注先の操縦者も委託元組織のPILOTレコードとして登録する
PILOTorganization_id / registered_byそれぞれにINDEXを張る(idx_pilot_organization_id / idx_pilot_registered_by)。組織別・登録者別の一覧取得を想定
PILOTprefectures都道府県コード(別紙1_2_都道府県コード)。APIのPrefectureCode
PILOTaddress住所。番地まで含む住所全体を1カラムで保持する(海外住所も含む)。市区町村単位への分割は行わない
PILOTskill_certification_number技能証明書番号。DIPS通報時にpilotInfo[].skillCertificationNumberとして送信する。技能証明(一等・二等)がいずれも無の場合はNULL、いずれか一方でも有の場合はNOT NULLをCHECKで強制する
PILOTfirst_class / second_class技能証明(一等/二等)の有無
PILOTstatusACTIVE/RETIRED(現在値・導出)
PILOTdeleted_at論理削除(導出)
PILOT_EVENTevent_typeREGISTERED/PROFILE_CHANGED/STATUS_CHANGED/DELETED
PILOT_EVENTpayload変更後の属性値

テーブル名・カラム名の規約と、本ドキュメントの既存の逸脱4件の扱いは データモデル設計ドキュメントの規約に定める。

飛行計画は不変リビジョンの連なりとして管理する

Section titled “飛行計画は不変リビジョンの連なりとして管理する”

航空局への報告要件(通報後に変更した場合は変更履歴も含めて報告する)を満たすため、任意のリビジョン時点の 計画内容を完全に再現できる構造とする。

  • FLIGHT_PLANは不変な識別情報(ID・所有組織・作成者・作成日時)+現在リビジョンへのポインタ+ 現在の運航状態(status。イベント由来の導出キャッシュ)を持つ。

  • FLIGHT_PLAN_REVISIONはINSERT onlyで不変とし、UPDATEはDBレベルで禁止する。

  • 計画内容を構成するテーブル(FLIGHT_PLAN_AREAFLIGHT_PLAN_AREA_CIRCLE/_ROUTEFLIGHT_PLAN_PILOT_ASSIGNMENTFLIGHT_PLAN_DIPS_ATTRSFLIGHT_PLAN_DIPS_PURPOSE)はすべてリビジョンに 紐付ける。リビジョンを特定すれば飛行領域・操縦者・機体・DIPS通報内容まで一意に定まる。

  • parent_revision_idで履歴をチェーン化する(Gitのコミットparent相当)。

  • 同時更新の排他はADR-019に従い悲観ロック(SELECT ... FOR UPDATE)で行う。 1 UseCase = 1 トランザクションの中でFLIGHT_PLAN行をロックしてから、新リビジョンをINSERTしcurrent_revision_idを 差し替える。parent_revision_idcurrent_revision_idの一致検証は、ロストアップデート防止ではなくリビジョン鎖の 整合性検証として併用する。この排他はER図の構造としては表現されないため、想定する更新手順を以下に示す。

    -- 1 UseCase = 1 トランザクション(ADR-013)
    BEGIN;
    SELECT current_revision_id FROM flight_plan WHERE id = :flight_plan_id FOR UPDATE;
    -- 取得した current_revision_id を parent として新リビジョンを作成
    INSERT INTO flight_plan_revision (id, flight_plan_id, parent_revision_id, ...) VALUES (...);
    UPDATE flight_plan SET current_revision_id = :new_revision_id WHERE id = :flight_plan_id;
    COMMIT;

    ロックの範囲はFLIGHT_PLAN行の取得からcurrent_revision_idの差し替えまでであり、リビジョン作成に 伴う重い処理(空域制限との競合判定など)をロック内に含めるかは実装時に判断する。ADR-019は単純な 状態遷移を対象としたものであり、すべての更新に同じ方式を適用することを求めるものではない。

  • DIPS API呼び出しはトランザクションの外で行うため、ロックだけでは排他しきれない。外部API呼び出しを トランザクション内に置くと、応答待ちの間ロックと接続を占有し、タイムアウト時にロールバックの判断も できない。したがってDIPS通報は次の3つのトランザクションに分割する。

    1. FLIGHT_PLANFOR UPDATEで取得し、report_statusREPORTINGへ遷移させるイベントを追記してコミット
    2. トランザクション外でDIPS APIを呼び出す
    3. 結果に応じてREPORTEDDIPS_REPORTをINSERT)またはUNREPORTEDへ遷移させるイベントを追記してコミット

    取り下げ(cancelFlightPlandeleteFlightPlan)も同じ3分割とする。1でWITHDRAWINGへ遷移して コミットし、2でDIPS削除APIを呼び、3でWITHDRAWNまたはREPORTEDへ戻す。OASが「キャンセル全体が 成功した場合のみstatus=CANCELLEDへ確定する」と定めるのは3の結果を見てから運航状態を変えるという 意味であり、この分割と矛盾しない。

    REPORTINGWITHDRAWINGという呼び出し中の状態を持つのはこの構造のためである。1でコミットして しまうため、2の最中は他の操作から見ても「通報中」であり、report_statusREPORTING以外であることを 前提とする操作(再通報・取り下げ)を弾ける。DBのロックではなく状態そのもので排他する。 504(成否不明)でREPORTINGのまま残るのもこの帰結であり、滞留した場合の復旧手段は未整備 (ADR-022の残課題。DIPS通報の失敗は応答の区分に応じて扱いを分ける参照)。

  • 新リビジョンを作るのは内容変更の操作(本登録・飛行計画更新)のときだけとし、状態遷移(通報・ 取り下げ・飛行開始・終了・キャンセル)はリビジョンを作らずFLIGHT_PLAN_STATE_EVENTに記録する (理由は状態遷移は内容リビジョンと分けてイベントで記録するchange_typeREGISTER/UPDATEの2値のみである理由も同じ。図2 カラム補足参照)。 これはリビジョンを作る契機を操作の種類で区分する規則である。そのうえで、内容変更の操作でも 内容が実際に変わらなかった場合はリビジョンを作らない(飛行計画更新に空のリクエストボディや 現在値と同じ値を与えた場合。判定の詳細はdocs/flight-planning/BusinessLogicSpecifications.md 5.2節手順3-2)。リビジョンが増えるたびに通報済みの状態が失われ再通報が必要になるためで、 この規則も「状態遷移でリビジョンを作らない」のと同じく、内容の履歴を内容が変わった時点だけに保つ ことを意図している。 「通報した瞬間の計画内容」はDIPS_REPORT.flight_plan_revision_idから取り出せる。

  • 削除は論理削除(FLIGHT_PLAN.deleted_at)のみとする。報告のための保存期間中は物理削除しない。

運航状態とDIPS通報状態を直交2軸で持つ

Section titled “運航状態とDIPS通報状態を直交2軸で持つ”

運航のライフサイクルと航空局への通報進捗は独立して遷移するため、別の軸として保持する。

  • 運航状態軸: ASTM F3548-21のOperational Intent状態をベースとし、DRAFT/ACCEPTED/ACTIVATED/CANCELLED/ ENDEDを扱う。遷移の履歴はFLIGHT_PLAN_STATE_EVENT.status_after、現在値はFLIGHT_PLAN.status(導出キャッシュ)。
  • DIPS通報軸: UNREPORTED/REPORTING/REPORTED/WITHDRAWING/WITHDRAWN。遷移の履歴は FLIGHT_PLAN_STATE_EVENT.report_status_after、現在値は最新イベントから導出する(DIPS固有の値を FLIGHT_PLAN本体に持たせないため、キャッシュカラムは設けない)。
  • report_requiredは状態ではなく計画のプロパティで、flight_airspaceflight_type・機体重量から判定する。領域や高度の変更で truefalseに変動しうる。falseの場合はreport_exemption_reasonに根拠を記録する。
  • 飛行開始条件はstatus=ACCEPTED AND(report_required=false OR report_status=REPORTED)。 通報義務があり未通報のままACTIVATEDへ遷移させる操作はアプリ層で拒否する。
  • 2軸を分けることで「通報不要扱いで飛行した計画」「ACTIVATEDに到達したが未通報だった計画」といった 監査クエリをFLIGHT_PLAN_STATE_EVENTに対して直接書ける(status_after='ACTIVATED'のイベント時点で report_status_afterREPORTEDになっていない計画を抽出する等)。

DIPS通報固有の属性は飛行計画本体から分離する

Section titled “DIPS通報固有の属性は飛行計画本体から分離する”

DIPS通報固有の属性は飛行計画本体(FLIGHT_PLAN_REVISION)から切り離し、FLIGHT_PLAN_REVISIONと1:1の FLIGHT_PLAN_DIPS_ATTRSreport_requiredreport_exemption_reasondips_report_detail)と、 その子テーブルFLIGHT_PLAN_DIPS_PURPOSE(飛行目的)に持たせる。

  • 理由: これらはいずれも日本の航空法に基づく飛行計画通報(DIPS通報)に固有の概念であり、飛行計画そのものの本質的属性ではない。 本体に混在させると、国際化(他国の通報制度への対応)や他ATM/USPとの整合を取る際に、飛行計画テーブルの スキーマ自体を変更せざるを得なくなる。制度固有の関心事をサテライトテーブルに切り出しておけば、 「制度別のサテライトを差し替える/追加する」形で吸収できる。ER図も中核(図1)とDIPS連携(図2)に分け、 この境界が図の上でも見えるようにしている。
  • 本体に残す属性の判断基準: 国・制度に依存しない中核属性のみをFLIGHT_PLANFLIGHT_PLAN_REVISIONに置く。 運航状態は ASTM F3548-21 をベースとした国際標準寄りの軸のためFLIGHT_PLAN.statusとして本体に持ち、 DIPS通報の進捗(report_status)は本体にキャッシュせずFLIGHT_PLAN_STATE_EVENTから導出する。
  • リビジョン単位で持つ理由: 「通報した瞬間の計画内容」をDIPS_REPORT.flight_plan_revision_idから再現する 要件があるため、通報内容もリビジョン時点のスナップショットとして不変保持する必要がある。PKを flight_plan_revision_idとし、リビジョンと同じライフサイクル(INSERT only)に揃える。
  • 1:1とする理由: 本登録リクエストの必須項目(flightPurposesdeparturePointdestinationPointemailflightSpecriskMitigation)が本テーブルとその配下に置かれるため、行が存在しない状態を 許すと必須入力の保存先が消える。また飛行開始条件が参照するreport_requiredもNULLとなり、条件全体が unknownになって飛行開始できなくなる。リビジョンが存在すれば必ず1行存在するものとする。
    • 「通報の枠組み自体が適用されない運用」は行の不在で表さない: 将来の海外展開など通報先が別の制度に なるケースは、行の有無ではなく別の手段(FLIGHT_PLAN側に制度を示す属性を持たせる等)で表す。 行の不在はFLIGHT_PLAN_REVISIONが存在しない状態(DRAFT)と区別できず、意味が多重化するためである。 10月デモの範囲ではすべての飛行計画がDIPS通報の対象であり、この検討は不要。
  • dips_report_detailjsonb)に集約する項目の範囲: DIPSへのパススルーのみで、値が業務判定・ 一覧取得・通知の入力にならない項目に限る。具体的にはassistantsNumberplannedMaxTimeflightSpeedriskMitigation*exceptionalConditionsMooringinsuranceInformationotherInformationflightPermitApplicationInfo
    • 「参照しない」は「値によって処理・応答が変わらない」という意味であり、「一切読まない」ではない。 本登録・ACCEPTED以降の更新時の入力検証(必須・型・値域・項目間の整合性)は、上記のうち plannedMaxTimeflightSpeedriskMitigationtypesは値域まで)・flightPermitApplicationInfo (発行日と期間の順序、飛行日が期間内か)を読む。ただし値を1度読んで妥当性を確かめるだけで、値の内容で 処理が分岐せず、SQLの述語・結合・集計にも使わない。キーの存在・型をDBで保証する必要も索引も不要で あるため、入力検証で読むことは個別カラム化の理由にしない
    • 帰結: jsonbの中身に対するDB制約は持てず(列に付くのはNOT NULLのみ)、構造・値域を保証するのは 上記の入力検証だけである。検証を持たないotherInformationinsuranceInformationはどの層でも保証が なく、値がAPIの型・コードの値域・OASのrequiredに適合しない場合は詳細取得が当該項目を未設定として 返す(BusinessLogicSpecifications.md5.2節手順2-1)。書き込み時に構造・値域を検証する実体を置くかは followups.mdの台帳(種別実体未作成)に登録済みである。
  • 参照経路のある項目は同じテーブルの個別カラムとして持つ: departurePointdestinationPointflightTypeFlightPlanListItemが必須項目として返し、email(調整連絡先)は競合時の通知先として通知 ドメインが参照する。jsonbのキーのままでは、一覧取得のたびに全行を展開することになり、キーの存在・型を DBで保証できず、索引もNOT NULLも効かない。いずれもDIPS通報項目であるためFLIGHT_PLAN_DIPS_ATTRSから 切り離さず、同テーブルの個別カラムとして持つ。
    • flight_typeはコードの配列だが、FLIGHT_PLAN_DIPS_PURPOSEと違いコードごとに付随する属性(note)を 持たないため、子テーブルにはせず配列型カラムとする。
    • flightAirspaceflightTypeと同じ配列型カラムとする。flightAirspace(人口集中地区上空・150m以上・ 空港周辺)とflightType(夜間・目視外・30m未満など)は特定飛行の類型そのものであり、通報義務の 有無(report_required)を判定する業務ロジックの入力になるためである。一覧の必須項目という理由で 個別化したflightTypeとは根拠が異なるが、いずれも「参照経路がある」という同じ基準に収まる。
    • 値域検証の有無でこの基準は分かれない。 riskMitigation.typesも同じコード配列で本登録時に値域を 検証するが(RiskMitigationType)、値が判定・応答・検索の入力にならないためjsonbに残す。 flight_typeflight_airspaceを昇格させた根拠は値域検証ではなく、上記の参照経路である。
  • 項目単位の対応関係はflight-plan-field-mapping.mdを参照。jsonbへの インライン格納は、収集した他社計画のDIPS_FLIGHT_PLAN.raw_payloadと同じ扱いである。

状態遷移は内容リビジョンと分けてイベントで記録する

Section titled “状態遷移は内容リビジョンと分けてイベントで記録する”

飛行計画の状態が変わったとき(通報・取り下げ・飛行開始・終了・キャンセル)は、新しい FLIGHT_PLAN_REVISIONを作らず、FLIGHT_PLAN_STATE_EVENT(INSERT-only)に1遷移1行を追記する。

  • 履歴は失われない: 「いつ誰がどの操作で状態を変えたか」はFLIGHT_PLAN_STATE_EVENTに、「どの内容を 通報し、DIPSがどの受付番号を返したか」はDIPS_REPORT(1通報1行のappend-only)に残る。 受付番号(dips_receipt_no)は初回の通報成功時にDIPSが発行し、以降の再通報(mode=1)でも同じ値である ため、同一の飛行計画に属する複数の行が同じ受付番号を持つ。したがってdips_receipt_noに単独のUNIQUE制約は 置けない(図2 カラム補足参照)。リビジョンを作らなくてもDIPS通報に関する変更履歴は 完全に追跡できる。
  • リビジョンを作らない理由: リビジョンは不変で、配下のFLIGHT_PLAN_AREAFLIGHT_PLAN_AREA_CIRCLE/_ROUTEFLIGHT_PLAN_PILOT_ASSIGNMENTFLIGHT_PLAN_DIPS_ATTRSdips_report_detailjsonbを含む)・ FLIGHT_PLAN_DIPS_PURPOSEもリビジョンに紐づく。状態遷移ごとにリビジョンを作ると、内容が1文字も変わらないのに これらを毎回複製することになる。さらにREPORTINGWITHDRAWINGはDIPS API呼び出し中の一時状態で、基盤500/502/503の失敗時は 元の状態へ戻るため、通報1回で2〜3個のリビジョンが生まれ、504タイムアウト時は中間状態のリビジョンが最新のまま 残留する。revision_noに技術的な中間状態が混ざり、Operatorに見せる変更履歴として使えなくなる。
  • 概念モデルとの関係: data-model.mdの 全体方針は「イミュータブルデータモデル(リソースとイベントの分離)」であり、IAM・Assetドメインは _EVENTテーブル+導出キャッシュで表現している。飛行計画ドメインの記述だけが「状態遷移も新リビジョン」と なっていたため、全体方針に揃える形で本設計を修正した。
  • 現在値の持ち方: 運航状態はGeoSpatialの領域検索がFlightPlanStatusFilterで絞り込み軸に使うため、 FLIGHT_PLAN.statusに導出キャッシュを置く(listFlightPlansの絞り込み軸はperiodFrom/periodTo/ registeredUserIdのみで運航状態を含まないため、そちらは根拠にならない。デモで提供対象外なのは ページング・ソートであり、絞り込み自体は提供される)。DIPS通報状態は絞り込み軸ではなく、かつDIPS固有の値を飛行計画本体に持たせない 方針のためキャッシュせず最新イベントから導出する。ただしreportStatusは全レスポンスの必須項目であり 一覧の全行で導出が走るため、応答が遅くなった時点でキャッシュ化を再検討する。
  • report_requiredreport_exemption_reasonはリビジョン側に残す: これらはflight_airspaceflight_type・機体重量から判定される 「内容」の属性であり、状態ではないためFLIGHT_PLAN_DIPS_ATTRSに置く。

状態イベントは両軸の結果状態を常に記録する

Section titled “状態イベントは両軸の結果状態を常に記録する”

FLIGHT_PLAN_STATE_EVENTstatus_afterreport_status_afterは、変化しなかった軸も含めて常に そのイベント直後の値を記録する(両方ともNOT NULL)。1行が「この操作の直後、2軸がどうなっていたか」の スナップショットになる。

event_typestatus_afterreport_status_after
REGISTERACCEPTEDUNREPORTED
REPORT_STARTACCEPTEDREPORTING
REPORT_COMPLETEACCEPTEDREPORTED
ACTIVATEACTIVATEDREPORTED

(上表は代表例の抜粋。CREATEUPDATEREPORT_TIMEOUTWITHDRAW_TIMEOUTなど残りのevent_typeも同じ規約に 従い、変化しない軸には直前の値を複写する。REPORT_TIMEOUTWITHDRAW_TIMEOUTは両軸とも変化しないため、 直前の値をそのまま複写した行になる。)

  • 現在値の導出規則が1つで済む: 両軸とも「seqが最大の行の値」で決まる。片方の軸だけをNULL許容の 疎な記録にすると、軸ごとに「その列が非NULLの最新行」を遡る別々の規則が必要になり、導出が2種類に増える。
  • 過去時点の再現が1行で済む: 概念モデルは任意過去時点(occurred_at <= T)の再現を求めている。 結果状態を常に記録していれば該当行を1件読むだけで両軸が定まる。
  • 監査クエリが直接書ける: 「status_after='ACTIVATED'の行でreport_status_afterREPORTEDでない計画」 がそのまま成立する。疎な記録ではACTIVATE行の通報軸が常にNULLになり、この述語は全件に当たってしまう。
  • 書き込み側の規約: イベントを追記する際は、変化しない軸にも必ず直前の値を複写する。これを怠ると NULLが混ざり、現在値の導出が静かに壊れる。実装上はイベント追記を1箇所に集約し、両軸を必ず埋める。
  • これは「同じ事実を複数箇所に符号化しない」という方針(一時保存は専用テーブルで表すDRAFTの扱いなど)とは別の話である。あちらが戒めているのは判定根拠が複数箇所に分散することで、 イベント行がその時点の結果状態を持つのはイベント記録として通常の形である。

一時保存は専用テーブルで表す

Section titled “一時保存は専用テーブルで表す”

飛行計画の登録は仮登録(POST /flight-plans)と本登録(POST /flight-plans/{id}/registration)の2段階に 分かれる。仮登録された内容はFLIGHT_PLAN_DRAFTFLIGHT_PLANと1:0..1)に保持し、本登録時に検証したうえで FLIGHT_PLAN_REVISIONへ確定登録する。

DRAFTかどうかを表すのはFLIGHT_PLAN.statusだけとする。 FLIGHT_PLAN_DRAFTの行の有無や current_revision_id IS NULLでも判定できてしまうと、同じ事実が3箇所に符号化されてDBだけで整合が 取れなくなるため、正となる表現を1つに決める。

  • FLIGHT_PLAN.statusFLIGHT_PLAN_STATE_EVENT由来の導出キャッシュ

  • FLIGHT_PLAN_DRAFTの行 … 一時保存の入力内容を置く場所に過ぎず、状態を表さない

  • current_revision_idstatus=DRAFTの間はNULL。次の2つのCHECK制約で縛る(片方だけでは不足する)

    • CHECK (status <> 'DRAFT' OR current_revision_id IS NULL)DRAFTなのにリビジョンがある状態を防ぐ
    • CHECK (current_revision_id IS NOT NULL OR status = 'DRAFT')ACCEPTED以降なのにリビジョンがない状態を防ぐ。CANCELLEDは許容しない。CANCELLEDへ到達するのはACCEPTED以降からのキャンセルのみで必ず現在リビジョンを持ち、DRAFTからのキャンセルはstatusを変えず論理削除する(status=DRAFTのまま)ため、CANCELLEDかつリビジョンなしという行は発生しない(一時保存は専用テーブルで表すの「DRAFTからのキャンセルは削除と同等に扱う」を参照)
  • FLIGHT_PLAN_DRAFTの行の有無とstatusの整合はテーブルを跨ぐためCHECK制約では表現できない。 statusDRAFT以外へ遷移する際に同一トランザクションで行を削除することをアプリ層で保証し、 必要ならトリガで補強する(物理設計で判断する)。DRAFTのままキャンセル・削除する場合も同様に行を削除する (こちらはstatus遷移を伴わない。status=DRAFTかつFLIGHT_PLAN_DRAFTの行なしという組み合わせは 論理削除済みの行として存在しうるため、「行があればstatus=DRAFT」は成り立つが逆は成り立たない)。

  • 専用テーブルに分ける理由: 一時保存はname以外すべて任意で、空域制限・他の飛行計画との競合判定も行わない。 一方で本登録後(FLIGHT_PLAN_REVISION)はほぼ全項目が必須かつ競合判定・DIPS通報の対象になる。同一テーブルで 両者を表すと、本登録後に守るべき必須制約をDBレベルで表現できなくなり、FLIGHT_PLAN_REVISION ||--|| FLIGHT_PLAN_AREA(必ず1件)やFLIGHT_PLAN_DIPS_ATTRS ||--|{ FLIGHT_PLAN_DIPS_PURPOSE(1件以上必須)も 成立しなくなる。テーブルを分けることで、本登録後の制約を緩めずに一時保存を表現できる。

  • 10月デモでの扱い: フロントエンドは一時保存機能を提供せず、仮登録と本登録のAPIを立て続けに実行する。 仮登録のFlightPlanCreateRequestも「デモでは一時保存機能を提供対象外とするため、飛行計画本登録と同じDIPS 必須項目をすべて必須(null不可)とする」と定義されており、DRAFTは実質的に短命な中間状態になる。 ただしこれは10月デモ限定の運用であり、その先では一時保存機能を提供する前提で本テーブルを用意している。 一時保存を提供する段階になっても、FLIGHT_PLAN_DRAFTの項目を増やす拡張で対応でき、本登録側の制約 (NOT NULL・1件以上必須)を緩めるスキーマ変更は発生しない。

  • draft_fieldsjsonbにまとめる理由: 一時保存ではFLIGHT_PLAN_AREA(+形状別サブテーブル)・ FLIGHT_PLAN_PILOT_ASSIGNMENTFLIGHT_PLAN_DIPS_PURPOSEへの正規化やマスタ参照の整合性検証を行わないため、専用の子テーブルは作らず単一の jsonbで保持する。nameplanned_start_atplanned_end_atのみ、listFlightPlansの期間絞り込みで ACCEPTED以降と統合する必要があるため個別カラムとする。

  • トレードオフ: listFlightPlansはステータスを問わず一覧と期間絞り込みを行うため、FLIGHT_PLAN_DRAFTFLIGHT_PLAN_REVISIONを跨いだ取得が必要になる。この実装コストは受け入れる。

  • ★期間検索の駆動キーは当面置かない: 絞り込み軸のうち期間(periodFrom/periodTo)だけがFLIGHT_PLAN本体に なく別テーブル(FLIGHT_PLAN_DRAFTFLIGHT_PLAN_REVISION)にあるため、(organization_id, status, 期間)の 複合インデックスでクエリを駆動できない。FLIGHT_PLANplanned_start_at/planned_end_atの導出キャッシュを 置けば上記のテーブル跨ぎも同時に解消できるが、性能問題が実際に起きるまでは追加しない方針とする。 10月デモの規模では件数が少なく、導出キャッシュを増やすほど更新時の整合維持の責務が増えるため。 一覧の応答が遅くなった時点で、statusupdated_atと同じ導出キャッシュとして追加する。

  • 一時保存中のキャンセル・削除でもFLIGHT_PLANの行は残す: 物理削除するのはFLIGHT_PLAN_DRAFTの行だけとし、 FLIGHT_PLANdeleted_atによる論理削除にとどめる。行ごと消さないのは、cancelFlightPlandeleteFlightPlanIdempotency-Keyによる同一キーの再送に対して最初の実行結果を返す契約であるため。

  • DRAFTからのキャンセルは削除と同等に扱う: statusDRAFTのままdeleted_atのみを設定し、 FLIGHT_PLAN_DRAFTの行を物理削除する。statusが変わらないためFLIGHT_PLAN_STATE_EVENTは追記しないdeleteFlightPlanが状態遷移イベントを持たないのと同じ扱い。結果としてキャンセルと削除はDB上区別されないが、 扱いを同等とする方針のため区別しない)。CREATECANCELの履歴を残すのはACCEPTED以降からのキャンセルであり、 そちらはstatus=CANCELLEDへの遷移イベントを追記し、論理削除もしない(現在リビジョンを持つため取得できる)。

    • 論理削除する理由: この行はFLIGHT_PLAN_REVISIONFLIGHT_PLAN_DRAFTも持たないため、取得対象に残すと getFlightPlanlistFlightPlansが内容を持たない応答を組むことになり、flightPlan(詳細)・name(一覧)を requiredとするOASを満たせない(検索処理がdeleted_at IS NULLのみを対象とすることはFLIGHT_PLAN.deleted_atの カラム補足のとおり)。
    • 応答はcancelFlightPlanのOAS契約どおりstatus=CANCELLEDを返す。DBのstatusDRAFTのままであり応答値と 一致しないが、論理削除により以後この計画は取得できないため、この不一致が観測される経路はない。
    • この扱いは、冪等キーの再送で返す「最初の実行結果」をADR-020の横断機構が保存・再生し、FLIGHT_PLAN行の 再読取に依存しないことを前提とする(保存先はIssue #132で 設計。行を読み直して応答を組む方式を採る場合は、論理削除済みの行も読める取得経路が別に必要になる)。
  • ★将来の一時保存提供時に決めること: listFlightPlansDRAFTでも返すdeparturePointdestinationPointflightTypepilotInfodraft_fieldsから抽出するか個別カラム化するか。10月デモではDRAFTが一覧に現れないため判断を保留する。

飛行中の内容変更と削除時のDIPS取り下げ

Section titled “飛行中の内容変更と削除時のDIPS取り下げ”
  • 本登録後の内容変更はACCEPTEDACTIVATEDのいずれの状態でも可能で、いずれも新リビジョン (change_type=UPDATE)として記録する。運航状態は変わらないため、その分の状態遷移イベントは追記しない。 DRAFT中の更新はFLIGHT_PLAN_DRAFTの上書きであり、リビジョンもイベントも作らない(履歴を残さない)。 更新の要求を受けても内容が実際に変わらなかった場合は、リビジョンもイベントも作らない (docs/flight-planning/BusinessLogicSpecifications.md5.2節手順3-2。 飛行計画は不変リビジョンの連なりとして管理する参照)。
  • ただし通報済み(report_status=REPORTED)の飛行計画を更新して内容が変わった場合はDIPS通報軸が UNREPORTEDに戻るため、内容変更の新リビジョンとあわせてreport_status_after=UNREPORTEDFLIGHT_PLAN_STATE_EVENTを追記する(再通報が必要になったことを履歴に残す)。内容が変わらなかった 場合はREPORTEDのまま維持され、イベントも追記しない。
  • 削除(DELETE /flight-plans/{flightPlanId})とキャンセル(POST /flight-plans/{id}/cancel)はいずれも、 通報済みの場合にDIPS側の飛行計画削除APIを呼び出す。呼び出し中はreport_status=WITHDRAWING、完了で WITHDRAWNとなる(いずれもFLIGHT_PLAN_STATE_EVENTに追記)。そのうえで、削除ではFLIGHT_PLAN.deleted_atに よる論理削除(リビジョン履歴は保持)を、キャンセルではstatus=CANCELLEDへの遷移イベントを、それぞれ行う (ここでいうキャンセルはACCEPTED以降からのもの。DRAFTからのキャンセルは通報前のため本節の対象外で、 削除と同等にdeleted_atのみを設定する。一時保存は専用テーブルで表すの 「DRAFTからのキャンセルは削除と同等に扱う」を参照)。 DIPS呼び出しが失敗した場合は削除・キャンセルとも操作自体を失敗させる。report_statusは、基盤500/502/503 (送信できていない)ではREPORTEDに戻して再試行可能とし、504(成否不明)ではWITHDRAWINGのまま保持する (DIPS通報の失敗は応答の区分に応じて扱いを分ける参照)。
  • キャンセル・削除を伴わないDIPS通報の取り下げ(飛行計画は有効なまま通報だけ取り下げる)は検討されているが、 10月デモのスコープ外とする。したがってreport_statusWITHDRAWING/WITHDRAWNへ遷移するのは キャンセル・削除経由のみで、取り下げ専用の属性は設けない。将来対応する場合はREPORTED --> UNREPORTED相当の 遷移イベントと専用APIの追加になるが、いずれもFLIGHT_PLAN_STATE_EVENTの値で表現できるためテーブル構成の 変更は不要と見込む。

DIPS通報の失敗は応答の区分に応じて扱いを分ける

Section titled “DIPS通報の失敗は応答の区分に応じて扱いを分ける”

DIPS APIのエラー応答は「入力の誤り(永続的)」と「DIPS側の一時的な事情」に分かれ、UTM側の扱いも変わる (出典は「別紙5_エラー応答一覧」)。区分ごとの扱いを次のとおりとする。

応答意味UTM側の扱い
AP 400入力チェックエラー(一律。{errorMessage}に日本語の理由が入る)永続的な失敗。リトライしても通らないため、report_statusを元の状態へ戻し(通報ならUNREPORTED、取り下げならREPORTED)、応答の理由をそのままOperatorへ返す
AP 500(レコードロック失敗)DIPS内部の排他制御による一時的な失敗。応答本文が「API実行をリトライしてください」と明示する一時的な失敗。リトライする
基盤 500/502/503DIPSの障害・定期メンテナンス。要求がDIPSに届いていない、または処理されていない一時的な失敗。report_statusを元の状態へ戻し(通報ならUNREPORTED、取り下げならREPORTED)、Operatorに再実行を促す
基盤 504(タイムアウト)成否不明。DIPS側で登録・削除が完了している可能性があるreport_statusREPORTINGWITHDRAWINGのまま保持する。元の状態へ戻すと再実行でDIPSへ二重登録・二重削除要求が飛ぶため。あわせてREPORT_TIMEOUTWITHDRAW_TIMEOUTイベントを追記する
  • 通報が失敗した場合DIPS_REPORTに行を作らない: DIPS_REPORTは「DIPSが受け付けた通報」の履歴であり、 dips_receipt_no(DIPSが発行する受付番号)を必須項目として持つ。400で弾かれた場合DIPS側には何も登録されず 受付番号も発行されないため、失敗を表す行は作れない。失敗した事実はFLIGHT_PLAN_STATE_EVENTreport_status_after=UNREPORTEDとして残る(REPORTINGから戻った履歴で失敗が追える)。
  • statusの値域からREJECTEDを外す: 上記のとおり通報の失敗は行として残らない。またDIPSが受け付けた 通報を後から却下する仕組みは、Pub/Sub提供対象イベント(別紙3。飛行計画重複の3種のみ)にも参照APIの 応答項目にも存在しない。したがってREJECTEDに遷移する経路がなく、値域はREPORTED/WITHDRAWNの2値とする。 将来DIPS側から非同期に却下される仕組みが確認された場合は、dips_receipt_noをNULL許容にするのではなく 「受け付けられた通報が後に無効化された」状態として値を追加する。
  • 失敗の理由はテーブルに保持しない: reportFlightPlanは同期APIであり、400のerrorMessageはその応答で Operatorに返せる。後から参照する要件がないため、エラー内容を保持するカラムは設けない。ログには記録する。
  • 504だけ扱いが異なる理由: 504は要求がDIPSに届いて処理された可能性を否定できない。report_statusを 元に戻すとOperatorが再実行でき、その結果DIPSへ二重に登録・削除要求が飛ぶ。呼び出し中を表す REPORTINGWITHDRAWINGのまま保持すれば再実行の導線が閉じる。ただし状態が変わらないため、 何も記録しないと「いつ滞留に入ったか・何回タイムアウトしたか」がデータモデルに残らない。 REPORT_TIMEOUTWITHDRAW_TIMEOUTevent_typeに設け、状態は変えずに事実だけを追記する (両軸の*_afterには直前の値を複写する)。これは flight-planning.yamlreportFlightPlancancelFlightPlandeleteFlightPlanの記述、およびflight-plan-statemachine.mdREPORTING --> REPORTINGWITHDRAWING --> WITHDRAWINGと一致する。保持したまま滞留した場合の状態確認手段は 未整備であり、ADR-022の残課題として引き継ぐ。
  • TX1のコミット後にプロセスが落ちた場合も滞留として扱う: 1をコミットした直後にプロセスが異常終了 すると、DIPSを呼ばないままREPORTINGWITHDRAWINGが残る。504と区別できないため同じ滞留として扱い、 下記の出口で回収する。滞留中はreport_statusREPORTING以外であることを前提とする操作(再通報・ 取り下げ)が弾かれるため、出口がないとその飛行計画に対する以後の操作がすべて拒否される
  • ★滞留の出口を決める必要がある: 概念モデルは「出口のない待機状態(永遠のPENDING)を作らない」ことを 不変則としている。504でREPORTINGWITHDRAWINGに入ったまま復帰できないケースの出口(一定時間経過後に actor_type=SYSTEMのイベントでUNREPORTEDへ戻す、または運用者が確認して手動で確定させる)を、 ADR-022のタイムアウト監視の標準パターンとあわせて決める。10月デモではREPORT_TIMEOUTの記録までを 対象とし、自動復帰は実装しない。
  • 冪等キーは本ドメインのテーブルに持たない: Idempotency-KeyヘッダはreportFlightPlanだけでなく createFlightPlanregisterFlightPlanactivateFlightPlancompleteFlightPlancancelFlightPlanにも 定義され、いずれも「同一キーの再送では最初の実行結果を返す」「同一キーの不正な再利用は422」を要求する (判定軸と応答コードはoperationにより異なる。詳細は下記)。DIPS_REPORTidempotency_keyカラムを置く方式では、行が作られるのは通報成功時 だけであり他の操作もカバーできないため、この要求を満たせない。冪等キーの保存は ADR-020が定める 横断的な機構が担うものとし、本ドメインのテーブルからは外した。ADR-020はキーの有効期間・保存方針を 残課題としているため、保存先の設計はフォローアップとする (OAS・他ドメインへ反映が必要な項目参照)。 本ドメインが横断機構に求める要件は次のとおり。
    • キーの一意性スコープ: 同一キーが別の飛行計画・別の操作に使い回されたことを検出できること。 OASは「同一キーを異なる飛行計画IDに対して再利用した場合は422」と定めるため、キー単体ではなく キーと対象リソースの組で判定できる必要がある。組織を跨いでキーが衝突しうるかは横断機構側で決める。
    • createFlightPlanだけ判定軸と応答が異なる: 対象リソースがまだ存在しない作成系のため、OASは 「同一キーを異なるリクエストボディで再利用したら422」「再送は201で返す」と定めている。 他の5 operationの「キーと対象リソースIDの組で判定」「200で返す」では賄えないため、横断機構は 作成系と更新系で判定軸(リソースIDかリクエストボディか)と応答コードを切り替えられる必要がある。
    • 保存する内容: 対象の操作(どのoperationか)、対象リソースのID、および再送時に返す応答本体。 「同一キーの再送では最初の実行結果を200で返す」ためには、成否だけでなく応答そのものが必要になる。
    • 保存する契機: 状態を変える操作がコミットされる時点。DIPS通報のようにトランザクションを分割する 操作(飛行計画は不変リビジョンの連なりとして管理する参照)では、 どの時点で確定とみなすかを操作ごとに定める必要がある。
    • 有効期間: 本ドメインからの要求はない。ネットワークリトライを吸収できる長さがあれば足りる。
  • エラー応答はJSONに限らない: 基盤(CloudFront)由来のエラーはHTMLを返す(別紙5 No.8〜15、17)。 JSONパースを前提にすると応答の解釈自体に失敗するため、Content-Typeを見て分岐する必要がある。 データモデルには影響しないが、DIPS呼び出し部の実装上の前提として記録しておく。

DIPSから収集した他社飛行計画は独立モデルとして管理する

Section titled “DIPSから収集した他社飛行計画は独立モデルとして管理する”

自組織の飛行計画の周辺エリアをDIPSの飛行計画参照APIで検索して得た飛行計画(他Operator分を含む)は、 UTM内部の飛行計画とは別のテーブルDIPS_FLIGHT_PLANで管理し、「どの飛行計画リビジョンの周辺として収集されたか」は リンクテーブルFLIGHT_PLAN_DIPS_NEARBY_LINK(N:M)で表す。

収集は「飛行計画周辺データ更新」(POST /flight-plans/{flightPlanId}/nearby-data-refresh)により、Operatorの操作を 契機として対象の飛行計画(その最新リビジョン)ごとにオンデマンドで実行する(定期バッチではない)。検索条件は対象計画自身のflyRouteflightPeriodから算出し、飛行領域(ROUTEbuffer_m適用済みの形状)から100mまでのバッファを 「周辺」として扱う。収集結果の閲覧はGeoSpatialのエリア範囲指定API(他Operator計画検索)が本テーブルを 参照して行うため、収集API自体は取得した計画の詳細をレスポンスに含めない。該当するユースケースは UC-PLAN-01-10 飛行計画付近の他の飛行計画を収集するである。

DIPS API呼び出しは1操作あたり、飛行がまたがるJST暦日の数である(後述の検索時間帯の制約による。 日付をまたがない飛行なら1回、またぐ場合は2回、検索できる暦日が無い場合は0回)。自組織の有効な飛行計画すべてを一括更新する処理は持たないため、有効な計画数Nに 比例した呼び出し(O(N))は発生しない。将来これを定期実行に変更する場合はO(N)になるため、DIPS側のレート制限と あわせて呼び出し方式を再検討する。

  • 除外するのは収集起点の組織が通報した計画だけとする: DIPSの飛行計画参照APIは範囲検索であり、自組織の 計画か他社の計画かを区別せずに返す。自組織が通報済みの計画はDIPS側の飛行計画IDを DIPS_REPORT.dips_receipt_noとして保持しているため、収集時に応答のflightPlanId(項番3)と突き合わせ、 一致するものはDIPS_FLIGHT_PLANへ格納しない。
    • 突き合わせは組織で限定する: DIPS_REPORTは同一UTM上の全組織の通報履歴を持つため、テーブル全体と 突き合わせると他組織の計画まで収集対象から落ちる。他組織の飛行計画は運航上まさに把握すべき対象で あるため、DIPS_REPORTFLIGHT_PLAN経由でorganization_id = 収集起点の組織に絞ってから突き合わせる。
    • 突き合わせは収集結果の側からも限定する: DIPS_REPORTは通報が成功するたびに1行増え、組織を絞っても 件数は運用期間に比例して増え続ける。組織の全件をメモリへ展開すると1回の収集の費用が運用期間に比例する ため、収集結果に現れたflightPlanIdを条件に加えて問い合わせ、戻り値を収集結果の部分集合に限る。 収集結果は1回の収集ぶん(DIPS側の返却上限まで)に収まるため、費用が運用期間に依存しなくなる。
    • 認定USPコードでは組織を判別できない: 項番21 certificationUspCodeはUSP単位のコードであり、 同一UTM上のどの組織の計画でも同じ値になる。判別できるのは「自社UTM経由で通報されたか」までで、 組織の区別には使えない。除外の判定には用いない。
    • なお本テーブルは「DIPSから収集した飛行計画」を表すものであり、名前の上で他社限定とはしていない。 上記のとおり同一UTM上の他組織の計画も格納対象になる。
  • 独立モデルとする理由: 収集した飛行計画は外部(DIPS)が管理する実体であり、UTM内部の飛行計画とは ライフサイクル・信頼性・更新契機がまったく異なる。FLIGHT_PLANへのFKを直接持たせると、外部由来のデータが 自社計画のライフサイクルに従属してしまい、同じ計画が複数の自社計画の周辺に現れた場合の重複も避けられない。 dips_flight_plan_id(DIPS側の識別子)を単独のUKとする独立エンティティとする。
  • リンクは飛行計画とリビジョンの両方を持つ: 「周辺」の範囲は対象計画のflyRouteflightPeriodから 算出されるため、リビジョンが変わって飛行領域や時間帯が変われば「周辺」の意味も変わる。どの範囲で収集した 結果かを示すためにflight_plan_revision_idを持つ。一方で主な参照経路は「この飛行計画の周辺にいるDIPS飛行 計画を引く」であり、リビジョン経由のJOINを毎回挟むのは扱いにくいため、flight_plan_idも非正規化して保持する。 両者の食い違いは(flight_plan_id, flight_plan_revision_id)FLIGHT_PLAN_REVISION(flight_plan_id, id)への 複合外部キーとすることでDBレベルで防ぐ(そのためFLIGHT_PLAN_REVISION側にUNIQUE(flight_plan_id, id)を置く)。
  • 保持するのは最新リビジョンの収集結果のみ: リビジョンが新しくなった時点で、旧リビジョンに紐づく FLIGHT_PLAN_DIPS_NEARBY_LINKを残さない。収集結果は「いま周辺に何がいるか」を地図に示すためのデータであり、 過去の周辺状況を遡って参照する要件がないためである。失われるのは「周辺にいたが重複には至らなかった」 情報であり、重複に至った関係は運航調整ドメインのcoordination.conflictionに残る。
    • 残さない手段は、リビジョンが変わっても「周辺」の範囲が変わったかどうかで分ける。飛行領域(flyRoute)・ 飛行予定時刻のいずれかが変わった更新では削除し、どちらも変わらない更新では削除せず flight_plan_revision_idを新リビジョンへ張り替える(収集結果が有効なまま残る)。判定の条件と 根拠はBusinessLogicSpecifications.mdの 「UC-PLAN-01-10 飛行計画付近の他の飛行計画を収集する」1-1を正とする。どちらの手段でも本項の 「最新リビジョンの収集結果のみ」は保たれる。
  • 飛行計画同士の重複は運航調整ドメインが持ち、本ドメインでは扱わない: 重複情報は運航調整側で coordination.conflictionとして既にDDLが定義されており、少なくとも10月デモではDIPSからのPub/Sub受信と 重複情報の作成・更新を運航調整が実施する。したがって収集側(FLIGHT_PLAN_DIPS_NEARBY_LINK)に重複フラグを 持たせず、CONFLICT_DETECTIONにも飛行計画同士の重複(PLAN_VS_PLAN)を含めない。責務は次のとおり。
    • DIPS_FLIGHT_PLAN + FLIGHT_PLAN_DIPS_NEARBY_LINK(本ドメイン) … 周辺に存在する飛行計画の最新の収集結果。 地図表示・状況把握のためのデータ
    • coordination.confliction(運航調整ドメイン) … 飛行計画同士の重複。self_flight_plan_id/ opponent_flight_plan_id(いずれもDIPSの飛行計画ID)、conflict_flag(重複/重複解消)、 opponent_mail_address(相手の連絡先)、運航調整コンテキストへの紐付けを持つ
    • CONFLICT_DETECTION(本ドメイン) … 空域制限との競合
  • emailを収集側に持たせない: 運航調整で必要な相手の連絡先はcoordination.confliction.opponent_mail_addressが 保持する。収集は地図表示のためのデータであり連絡先を必要としないため、FLIGHT_PLAN_DIPS_NEARBY_LINKから emailを外した。これにより個人情報の保持箇所が運航調整ドメインに集約され、収集側は個人情報を持たない。
  • 本ドメインとの対応づけ: coordination.conflictionはDIPSの飛行計画IDで自他の計画を指すため、自組織側は DIPS_REPORT.dips_receipt_no、相手側はDIPS_FLIGHT_PLAN.dips_flight_plan_idと突き合わせる。スキーマが異なる ためFK制約は張らず、アプリケーション側で対応を保証する。
  • 再収集は「DIPS_FLIGHT_PLANはUPSERT、リンクは対象計画分を全削除して再登録」とする: 収集結果は常に 最新状態のみを表すが、テーブルごとに更新方法を変える。
    • DIPS_FLIGHT_PLANは行を一括削除してから再INSERTするのではなく、dips_flight_plan_idをキーにした UPSERTで更新する。理由はDIPS_FLIGHT_PLAN.id(UTMが発行するUUID)の安定性である。GeoSpatialの 他Operator計画検索はこのidflightPlanIdとして返し、かつflightPlanIdでの絞り込み(運航調整で特定の 他Operator計画を直接参照する用途)を提供するため、収集のたびにidが振り直されると外部から参照できない。
    • FLIGHT_PLAN_DIPS_NEARBY_LINKは、収集の起点となった飛行計画(flight_plan_id)に紐づく行を全削除して から、応答に含まれた計画分を現行リビジョンに対して登録し直す。周辺の飛行計画が途中で取り下げられても、 DIPSの応答から消えるだけで取り下げの事実は得られないためである。差分更新では、消えた計画のリンクが 残っているのか応答から漏れただけなのかを区別できず、地図上に存在しない計画が残る。リンクのidは 外部へ公開しておらず(公開するのはDIPS_FLIGHT_PLAN.id)、振り直しても参照経路に影響しない。 削除の範囲をflight_plan_idに限るのは、テーブル全体を消すと他の飛行計画・他組織の収集結果まで 失われるためである。旧リビジョンのリンクもこの操作で同時に消える。
  • リンクが0件になってもDIPS_FLIGHT_PLANの行は残す: リンクの有無で行を削除すると、UPSERTにした目的 (idの安定性)が達成できない。自組織の計画の飛行領域・飛行予定時刻を変える更新1回で旧リビジョンの リンクが全削除され、行が孤児となって消え、次の収集で同じDIPS計画に別のUUIDが振られるためである。DIPS側にはずっと存在して いる計画なのに外部公開IDが変わってしまう。リンクを持たない行は他Operator計画検索の結果に現れないため、 残しておいても影響はない。emailを保持しない構成のため 個人情報が滞留することもない。
    • パージ: リンクを1件も持たずcollected_atが一定期間(例: 7日)を過ぎた行は定期的に削除してよい。 保持期間は物理設計で決める。
  • 「近くにいた飛行計画が消えた場合」の扱い: リンクを消すことで、その計画は他Operator計画検索の結果に 現れなくなる。DIPS_FLIGHT_PLANの行自体は残るが、リンクを持たない行は検索対象にならないため、 参照経路の上では「最新の収集結果のみ」が見えることに変わりはない。過去に周辺にいた事実そのものは 保持せず、重複に至った関係のみ運航調整ドメインのcoordination.conflictionconflict_flagで重複解消も 表現できる)に残る、という役割分担とする。
  • raw_payloadjsonb)に応答原文を保持する理由: DIPS応答の構造(flyRoutecenterradiuscoordinatesでジオメトリタイプごとに切り替わる、duplicateFlightPlanが入れ子になる)はRDBの列にそのまま 写せないため、変換時の解釈違い・DIPS側の項目追加で情報が失われうる。原文を保持しておけば、未使用項目を 後から型付きカラムに昇格させる際に再収集が不要になる。
    • 保持するのは全ユーザー向けに出力される項目だけとする: DIPSの参照APIの応答には、自アカウントの計画に のみ出力される項目(通報者・操縦者・保険・許可承認の連絡先など)が含まれる。これらは個人情報を含み、 かつ他USSの計画では欠落して収集結果の内容が相手によって不揃いになるため、格納前に除去する。 上記の「情報が失われうる」への対処は、全ユーザー向けの項目の範囲に限る。
    • 現状の実装は応答のJSONそのものではない: 収集の実装(RefreshNearbyFlightPlansUseCase)が扱えるのは Portが返す型付きモデルであり、応答本文を持ち回らない。そのため本カラムへ入るのは、項目名と入れ子は DIPS側に合わせつつ日時などを正規化した再構成値で、DIPS側の項目追加を吸収する用途は満たせない (followups.mdの台帳に登録)。
  • あわせてgeometry(PostGIS型)を持つ理由: raw_payloadのままでは自組織のFLIGHT_PLAN_AREA.geometryとの 空間演算(ST_Intersects等)ができず、エリアの重なり判定・地図表示のたびにアプリ側で座標を組み立て直す 必要がある。収集時に幾何型へ変換して保持する。CIRCLE時は中心点(Point)+radius_mの組で持ち (UTM内部でも円を中心点+FLIGHT_PLAN_AREA_CIRCLE.radius_mで持つのと同じ表現)、面としての判定はST_DWithingeographyキャスト)で行う。 地図表示のためにGeoSpatialが返す形状はplan_geometrytop_bottom_3d_geometryとして実体化する (飛行領域はGeoSpatialが返す形状ごとに実体化列を持つ参照)。 空間インデックスをどの列に張るかは物理設計で判断する。
  • エリアを形状ごとに分離しない理由: DIPS応答上も1飛行計画1図形(flyRouteは単一)で、CIRCLEのときだけ radius_mが入る単純な構造にとどまる。UTM内部の飛行領域のように入力検証・制約付けの対象ではないため、 形状別サブテーブルには分けずインライン保持とする。
  • 取得漏れ(DIPS側の返却上限)はデータモデルに持たない: duplicateFlightPlanの返却上限は99件 (100件超は更新日・飛行計画IDの昇順で99件まで)で、項番22 totalCount・項番28 duplicateTotalCountとの 差分で打ち切りを検知できるが、件数はテーブルに保持せず収集処理のログに記録するに留める。デモ規模では 99件を超える状況が想定されないため。打ち切りが実際に発生した場合は、件数の保持とOperatorへの警告表示を 含めて保持方式を再検討する。
  • 自社計画の削除時はリンクを連動して物理削除する: 自社計画が論理削除(FLIGHT_PLAN.deleted_at)された 時点で、その計画の各リビジョンに紐づくFLIGHT_PLAN_DIPS_NEARBY_LINKを物理削除する(DIPS_FLIGHT_PLANは 上記のとおり残す)。収集結果は現況把握のためのデータであり、削除された計画の周辺状況を 保持する意味がないため。飛行計画本体が論理削除であるのに対しリンク側は物理削除となる点に注意する。
    • 飛行終了(ENDED)・取消(CANCELLED)でも同じくリンクを物理削除する: 収集の実行対象は ACCEPTEDACTIVATEDの計画に限る(BusinessLogicSpecifications.mdの 「UC-PLAN-01-10 飛行計画付近の他の飛行計画を収集する」1-1)。更新を止める一方でリンクを残すと、 最新化されない収集結果がGeoSpatialの他Operator計画検索に現れ続ける(同検索はbbox・時刻で 絞り込み、収集起点の計画の状態は条件に持たない)。削除の理由は論理削除時と同じく、現況把握のための データだからである。
  • 他Operator計画のstatusは持たない: DIPSの飛行計画参照APIは運航状態を返さないため、 DIPS_FLIGHT_PLANのカラムとしても導出値としても持たず、GeoSpatialの他Operator計画検索 (OtherFlightPlanAreaProperties)でも設定せず、検索条件としても使わない。DIPSに無い情報を 推定値として作らない。planned_start_at/planned_end_atと参照時刻の比較で導出する仕様は、 OASにstatusがあることに合わせて本設計で作ったものだったが、フロントエンドが不要と判断したため 取り下げた(discussion #6。 PR #219でOASのrequiredからも外した)。
  • 重複計画は項番1の一覧に必ず含まれる前提とする: duplicateFlightPlan(項番23〜25)に挙がる飛行計画は 運航調整の対象であり、その詳細(飛行時間帯・エリア)が揃っていなければ地図上での状況把握ができないため、項番1で 取得する飛行計画の一覧に必ず含まれることを前提とする。これによりDIPS_FLIGHT_PLANの型付きカラムには NOT NULL制約を置ける(重複相手の行が詳細を持たない状態は発生しない)。 ただし前述の99件打ち切りが起きた場合は、重複計画が項番1の一覧から漏れる可能性が残る。10月デモでは 打ち切りをスコープ外とし、必ず含まれるケースのみ対応する。

収集時の検索条件はDIPS参照APIの制約に合わせて変換する

Section titled “収集時の検索条件はDIPS参照APIの制約に合わせて変換する”

DIPSの飛行計画参照API(範囲指定)には検索条件に制約があり、自組織の飛行計画の内容をそのまま検索条件として 渡せない。制約と対応は次のとおり(制約の出典は「別紙2_入力チェック一覧」の2_飛行計画情報参照API_範囲指定、 およびUSP向けガイドライン2.3.5.2のリクエスト仕様)。

DIPS側の制約対応
検索終了時刻は検索開始時刻から24時間以内(別紙2 ID8)検索時間帯をJST暦日単位(0時〜24時)に切り上げ、飛行が日付をまたぐ場合は暦日ごとに1回ずつ呼び出して応答をマージする。所要時間の上限が1440分(OASのFlightPeriod.plannedFlightTimemaximum)で3暦日にまたがらないため、呼び出しは1計画あたり最大2回になる
検索開始時刻はシステム日付の1日前まで(別紙2 ID5)飛行開始が下限より過去の計画は、検索開始時刻をその下限までクランプする。飛行中の長時間計画で発生しうる。クランプするのは検索開始時刻だけで、検索終了時刻はクランプしない(両方を丸めると検索時間帯が飛行日から離れた暦日へ移動する)。暦日全体が下限より過去になる暦日は落とし、検索できる暦日が1つも無い場合は収集結果0件として扱う。下限はJSTの暦日境界ではなくDIPS側のタイムゾーンの暦日で決まるため、下限の算出はそのタイムゾーンを知る層(DipsApiClientの実装)に委ねる(後述の「下限の判定はDIPS側のタイムゾーンで行われる」参照)
時刻形式はyyyyMMdd□HHmm(分単位、□は半角スペース)検索時間帯を暦日境界に取るため分未満の丸めは発生しない。暦日の終端(24時)は翌日の0000として送る
ジオメトリタイプはCircle・Polygonのみ(別紙2 ID10)CIRCLEはそのままCircle(半径に100mを加算)、POLYGONROUTE飛行領域へ100mのバッファを適用したPolygonへ変換して渡す(ROUTEの飛行領域はbuffer_m適用済みのoperational_intent_geometryであり、生の経路へbuffer_m+100mを適用するのではない。本節の「ROUTEは生の経路ではなくバッファ適用後の飛行領域を広げる」を参照)
Polygonは異なる3点以上で、経度・緯度がすべて同一でないこと(別紙2 ID20・ID22・ID23)バッファ適用後のポリゴンは常に3点以上になるため通常は問題にならないが、変換結果の点数を検証してから送信する
Polygonの外周リングは閉環(終点=始点)していなければならない検索範囲は閉環したリングで送る。閉じ点の付与はDipsApiClientの実装が行う(DipsGeometry.Polygonは開いたリングで受け渡すため。登録・更新APIのflyRouteは閉じ点を送らない点と異なる)
  • 変換は必ず外側に丸める: 検索範囲が実際の「周辺」より狭くなると収集漏れになり、地図上に存在すべき他Operator 計画が現れない。時刻のクランプ・丸めも含め、変換による誤差は常に範囲が広くなる方向に取る。収集後の正確な 重なり判定はUTM側のgeometryで行うため、過大に取った分は問題にならない。
  • ROUTEは生の経路ではなくバッファ適用後の飛行領域を広げる: ROUTEの飛行領域は経路(geometryの LINESTRING)そのものではなく、FLIGHT_PLAN_AREA_ROUTE.buffer_mを適用したoperational_intent_geometry である(飛行領域はGeoSpatialが返す形状ごとに実体化列を持つ)。 検索範囲はこの形状をさらに100m外側へ広げたものとし、広げるときのバッファ生成パラメータも飛行領域と 同一('quad_segs=1 endcap=square join=mitre')にして角を丸めず角のまま外へ出す生の経路へbuffer_m+100mを適用してはならない。 経路端・折れ点では飛行領域の角が経路から buffer_mより遠くにあるため、その形の変換は角の外側を取りこぼし、前項の「外側に丸める」に反する (収集漏れになる)。round joinで広げた場合も円弧を内接多角形で近似するぶん角の外側で内側に入る。
  • 帰結として、ROUTEの検索範囲は経路からの距離で見るとbuffer_m+100mより外へ出る: endcap=squareにより経路端の角は経路からbuffer_m×√2まで張り出し、これをjoin=mitreで100m広げると 角はさらに100m×√2外へ出る(mitre joinは辺を距離どおりの平行線に置き、角はその交点まで外へ出す)。 経路からの到達距離は(buffer_m+100m)×√2に相当する。buffer_m=50mでの実測値は FlightPlanRepositoryImplIntegrationTestfindNearbySearchArea_whenAreaIsRoute_expandsBufferedFlightAreaIncludingItsCorners にある。「経路からbuffer_m+100m以内」と解釈すると実際の検索範囲を過小に見積もることになるため、 収集件数を見積もる側・収集結果を地図に表示する側はこの点に注意する。広がる方向の誤差であり、 前項のとおり許容する。
  • 下限の判定はDIPS側のタイムゾーンで行われる: 「システム日付の1日前」をDIPSは自身のタイムゾーンの暦日で 判定するため、本ドメインが使うJSTの暦日境界へ切り上げても足りない。模擬DIPSはUTCで (docs/issues/issue-143-dips-base/open-questions.mdのD-11。本番DIPS2.0はJSTで別仕様であり、 この食い違いはfollowups.mdに種別他ドメイン合意で登録済み)、JSTの0時はUTCでは前日の 15時にあたる。JSTの0時を下限として送ると、ワイヤ上の暦日がシステム日付の1日前より前になり制約を 割り込む。下限は DipsApiClientの実装(DIPS側のタイムゾーンを持つ層)が算出し、検索時間帯の組み立て側はその値へ 切り上げるだけにする。DIPS側のタイムゾーンが変わっても、変更箇所は実装の定数1つに閉じる。
  • 暦日ごとに呼び出しても1回の収集操作として扱う: 各呼び出しの応答はdips_flight_plan_idをキーに マージし、collected_atには呼び出しをまたいで同一の収集時刻を設定する。同じ計画が両方の暦日で 返る場合はUPSERTで後勝ちとなる。
  • 広範囲・長時間の検索は避ける: ガイドラインは範囲指定を広範囲・長時間の条件で頻繁に実行しないよう 求めている。収集は対象計画1件分の飛行領域+100mと、その飛行日(最大2暦日)に限定したオンデマンド実行 であり、この求めに反しない。1回あたりの検索時間帯は暦日単位に広げるが、対象は常に1計画分の飛行日に 限られ、範囲(空間)は変わらない。
  • 構成点数の上限は設けない(参考情報): 参照APIのPolygonについては、ガイドライン・別紙2のいずれにも 構成点数の上限の記載がない(「3点以上」のみ)。参考までに、登録・更新APIのPolygonには3点以上36点以下の 上限がある(別紙2 ID68。上限の出典はflight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」)。参照APIは検索条件を渡すだけで飛行計画として登録されるわけではないため、 仕様に記載のない制約を先回りして課すことはせず、収集側では点数のガードを設けない。実際に点数超過で エラーとなった場合は、通報時と同じく元の領域を包含する側に丸めて点数を削る変換を追加する。

空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する

Section titled “空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する”

CONFLICT_DETECTION空域制限との競合のみを扱う。飛行計画同士の重複は運航調整ドメインが持ち (飛行計画同士の重複は運航調整ドメインが持ち、本ドメインでは扱わない)、 有人機の動態との競合・逸脱・侵入に相当するAPIは現時点で定義されていないため、conflict_typecounterpart_kind といった種別カラムは持たない。

  • 保持する理由: reportFlightPlangetFlightPlanlistFlightPlansは空域制限との競合を再判定せず、 直近のregisterFlightPlanupdateFlightPlan時点の判定結果を返す仕様である。リクエストをまたいで 判定結果を保持する必要があるため、本テーブルに永続化する。
  • DIPSからは通知されない: DIPSのPub/Sub通知は飛行計画重複通知の3イベント(新規重複・重複情報更新・重複解消) のみで、空域制限に関する通知は提供されない(USP向けガイドライン別紙3)。空域制限との競合はUTM内部の判定 (GeoSpatialの空域制限データとの2次元判定)でのみ発生する。
  • 「未チェック」と「チェック済みで競合なし」はstatusから導出する: この区別のための情報を本テーブルに 持つ必要はない。「ACCEPTED以降のリビジョンは必ず空域制限チェック済みである」という不変条件(次項)が 成立するため、statusだけで判定の有無が定まる。応答は状態によらず一様に、本テーブルの行をそのまま返す。
    • status=DRAFTのとき → 行は存在せず空配列
    • ACCEPTED以降 → current_revision_idに紐づく行を返す(競合なしなら空配列)
  • conflictnullにせず常に空配列を返す: 判定の有無は上記のとおりstatusから導出できるため、 conflictnullにして「未チェック」を表す必要はない。同じ事実をstatusconflictの2箇所で 符号化することになり、不整合の余地を残すだけである。conflictは常にオブジェクトを返し、競合が 0件なら空配列とする。これに伴い空配列の意味は「チェック済みで競合なし」から「保持している競合が0件」に 弱まるため、チェック済みかどうかの判断はstatusを参照する。利用側がDRAFTの飛行計画に対して 「競合なし」と表示しないよう、statusで表示を制御する必要がある。
  • 前提となる不変条件: 「ACCEPTED以降のリビジョンは必ず空域制限チェック済みである」。registerFlightPlanACCEPTED以降のupdateFlightPlanが新リビジョンの作成と同時にチェックを実行するため成立する。状態遷移では リビジョンを作らない(状態遷移は内容リビジョンと分けてイベントで記録する)ため、 通報や飛行開始で現在のリビジョンが変わることはなく、記録済みの判定結果はそのまま有効であり続ける。
  • API応答を再構成できるカラムを持つ: AirspaceRestrictionConflictairspaceRestrictionIdairspaceRestrictionTypeの2項目を必須とするため、airspace_restriction_idairspace_restriction_typeとして 保持する。
  • 将来の拡張: 有人機・逸脱・侵入を扱う段階になったら、conflict_typeと対向種別ごとのカラムを追加する形で 拡張する(概念モデルのCONFLICT_DETECTIONはその想定で種別カラムを持っていたが、10月デモでは値域が 空域制限のみになるため落としている)。
  • airspace_restriction_typeを併せて持つ理由: APIのAirspaceRestrictionConflictが種別を必須とする一方、 種別の解決には別ドメインへの参照が必要になる。また検出履歴はイミュータブルであるべきで、空域制限の種別が 後から変わっても検出時点の値を保つべきである。そのため検出時点のスナップショットとして保持する。 スキーマが異なるためairspace_restriction_idにFK制約は張らず、アプリケーション側で対応を保証する (coordination.conflictionとの対応づけと同じ扱い)。

飛行領域は形状ごとにサブテーブルへ分ける

Section titled “飛行領域は形状ごとにサブテーブルへ分ける”

飛行領域の付随パラメータ(円の半径・経路のバッファ幅)はFLIGHT_PLAN_AREAに直接持たせず、形状ごとの サブテーブル(FLIGHT_PLAN_AREA_CIRCLEFLIGHT_PLAN_AREA_ROUTE)に分ける。ASSETASSET_*_ATTRSと同じ クラステーブル継承のパターンである。

  • 理由: area_typeの値によって意味の変わるカラムを作らないため。1つのカラムに「半径」と「バッファ幅」を 兼ねさせると、円にバッファを設定する要件が出た時点で表現できなくなり、また値の意味を読むのにarea_typeを 参照しなければならない。形状ごとに分ければ、各カラムの意味が単独で定まりNOT NULLも課せる。 ただし分けたことで「行が存在しない」という不正状態が新たに生まれる点に注意する。area_type=CIRCLEなら FLIGHT_PLAN_AREA_CIRCLEが必ず1行存在する、という条件付き必須はNOT NULLでは守れない(NOT NULLが守るのは 列の値だけで行の存在ではない)。図の||--o|でも表現できないため、アプリケーション層で保証する。
  • APIとの対応: OASのFlyRouteInputoneOfFlyRouteCircleInputFlyRoutePolygonInputFlyRouteRouteInput)で、circleradiusM必須、routebufferM必須、polygonは付随パラメータなし。 本分割はこの構造と1:1に対応する(POLYGONは付随パラメータがないためサブテーブルを持たない)。
  • 共通側に残すもの: geometry・高度群・altitude_referencealtitude_unitは形状によらず共通のため FLIGHT_PLAN_AREAに残す。
  • DIPSから収集した領域(DIPS_FLIGHT_PLAN)は分割しない: 収集データはDIPS応答のパススルーであり、 CIRCLEのときだけradius_mが入る単純な構造にとどまる。UTM内部の飛行領域のように入力検証・制約付けの 対象ではないため、インライン保持のままとする。

飛行領域はGeoSpatialが返す形状ごとに実体化列を持つ

Section titled “飛行領域はGeoSpatialが返す形状ごとに実体化列を持つ”

GeoSpatialの飛行計画領域検索は、1つの飛行計画につき役割の異なる複数のFeatureを返す契約になっている (geospatial.yamlFlightPlanAreaFeatureOtherFlightPlanAreaFeature)。

dataType内容返却条件保持するカラム
FLIGHT_PLANフロントエンドから入力された元の形状常に存在plan_geometry
OPERATIONAL_INTENTbufferM分を水平方向に膨らませた形状areaType=ROUTEの場合のみoperational_intent_geometry
TOP_BOTTOM_3D3D表示補助用の上面・下面を表すMultiPolygon常に存在top_bottom_3d_geometry

さらに各FeatureがAGLWGS84の2基準ぶん返るため、ROUTEなら3×2=6件、POLYGON/CIRCLEなら2×2=4件になる。 2基準ぶんとも実体化する。上表の各列にはZ値をWGS84楕円体高へ頂点ごとに換算した_wgs84列を対にして持つ (理由は高度は換算値をカラムに保持する参照)。

接尾辞の_wgs84が指すのはZ値の高度基準(WGS84楕円体高)である。座標参照系(CRS)は接尾辞の 有無によらずどの列もSRID 4326(WGS84)であり、_wgs84列だけがWGS84という意味ではない(サブタイプの絞り込みは列ごとに異なる。上記カラム補足のPostGIS型を参照)。 OAS側がminAltitudeMWgs84altitudeReference=WGS84という名前を使っているため、追跡できるよう DB側も同じwgs84の表記に揃えている。

  • 形状は実体化して保持する: OPERATIONAL_INTENTST_Buffer(geometry::geography, buffer_m, 'quad_segs=1 endcap=square join=mitre')::geometryCIRCLEFLIGHT_PLAN形状は中心点+半径の多角形化(同じくST_Buffer)、TOP_BOTTOM_3Dは上下面の組み立てで 得られる。いずれも計算コストが大きく、地図表示のたびに領域検索で呼ばれるため、応答のたびに導出すると コストに見合わない。登録・収集の時点で一度だけ計算してカラムに保持する。
  • 実体化しても不整合が起きない: リビジョンは不変で、FLIGHT_PLAN_AREAの行は作成後に更新されない。 内容が変われば新しいリビジョンの行が作られるため、元形状と実体化列が食い違う状態は生じない。 ただし_wgs84列は外部データ(DEM・ジオイド高)に依存するため、この根拠は及ばない (高度は換算値をカラムに保持する参照)。 DIPS_FLIGHT_PLANは再収集でUPSERTされるが、その際はgeometryとあわせて実体化列も作り直す。
  • geometry(元形状)も残す理由: CIRCLEを中心点+radius_mで持つと真円を頂点列で近似せずに保持でき、 ST_DWithingeographyキャスト)による正確な距離判定ができる。多角形化したplan_geometryは表示用で、近似誤差を含む。 UTM内部の競合判定にplan_geometryは使わず、CIRCLEPOLYGONgeometry側で行う。ROUTEだけは DIPSへ申告した形状と揃えるためoperational_intent_geometry(バッファ適用済み)との交差で判定する (飛行領域の呼び名はUTM内部でROUTEに統一するの 「同じパラメータをoperational_intent_geometryの生成にも適用する」)。
  • dataTypeはカラムとして持たない: 行と1:1に対応せず、1行から複数のdataTypeのFeatureを生成するため。 どの列がどのdataTypeに対応するかは上表で固定する。
  • 空間インデックス: 検索に使う列にGiSTインデックスを張る。どの列を検索対象とするか(元形状か 実体化列か)は物理設計で判断する。

メートル単位の空間演算はgeographyへキャストして行う

Section titled “メートル単位の空間演算はgeographyへキャストして行う”

geometryはすべてSRID 4326(WGS84)で保持するが、SRID 4326のgeometryに対する距離の単位は度であり メートルではないST_Buffer(geometry, 100)と書くと100度のバッファになり、意図した値と桁が数万倍ずれる。 メートルを引数に取る演算はgeographyへキャストして行う。

-- OPERATIONAL_INTENT(ROUTEのバッファ適用形状)
ST_Buffer(geometry::geography, buffer_m, 'quad_segs=1 endcap=square join=mitre')::geometry
-- CIRCLE の多角形化(DIPSへは Circle として通報するため、頂点数を抑えるパラメータは指定せず既定に任せる)
ST_Buffer(geometry::geography, radius_m)::geometry
-- 周辺計画収集の検索範囲(飛行領域を100m外側へ広げる。入力は生の経路ではなく飛行領域であり、
-- パラメータも飛行領域を作ったときと同一にして角を丸めない)
ST_Buffer(COALESCE(operational_intent_geometry, plan_geometry)::geography, 100,
'quad_segs=1 endcap=square join=mitre')::geometry
-- 周辺収集の距離判定
ST_DWithin(a::geography, b::geography, 100)
  • 対象となる箇所: operational_intent_geometryの生成、CIRCLEplan_geometry生成、 ST_DWithinによる距離判定、周辺収集の100mバッファ(入力は飛行領域であり、ROUTEでは operational_intent_geometry収集時の検索条件はDIPS参照APIの制約に合わせて変換するの 「ROUTEは生の経路ではなくバッファ適用後の飛行領域を広げる」)、DIPS通報用Polygonへの変換の5つ。 いずれもメートル値(buffer_mradius_m・100m)を引数に取る。
  • 格納はgeometryのままとする: 型をgeographyに変えると、ST_Intersectsなど単位を持たない 位相演算まで球面計算になり不要なコストがかかる。メートルを扱う演算の直前にキャストする方式に統一する。 実体化列(plan_geometryoperational_intent_geometrytop_bottom_3d_geometry、および対になる_wgs84列)も、 上記の方法で生成した結果をgeometryとして保持する。
  • 投影座標系を明示的に選ばない理由: 平面直角座標系(JGD2011の19系)へ投影すれば国内の精度はより高いが、 対象地点ごとに系を選ぶ必要があり、系の境界をまたぐ運航で破綻する。geographyならアプリ側で系を選ばずに 済む。
    • ただし**geography版のST_Bufferは測地バッファではない**。PostGISは内部で_ST_BestSRIDにより 対象の範囲に適した平面SRSを選んで投影し、平面でバッファを計算してWGS84へ戻している。系の選択が 不要になったのではなく、PostGISが代行しているだけである。ST_DWithinST_Distanceは回転楕円体上で 計算するため、この違いはない。デモ規模の距離(100m〜数km)では実用上の差は生じないが、 バッファ形状の厳密さが問題になる場面では投影系を明示する選択肢を再検討する。
  • 空間インデックス: ST_DWithingeographyで行う場合、対象列にUSING GIST ((geometry::geography))の 式インデックスが必要になる(キャスト式は括弧をもう一段必要とする。USING GIST (geometry::geography)は構文エラー)。どの列に張るかは物理設計で判断する。

高度は換算値をカラムに保持する

Section titled “高度は換算値をカラムに保持する”

FLIGHT_PLAN_AREADIPS_FLIGHT_PLANは、AGLに加えてAMSL・WGS84楕円体高への換算値を保持する。 領域あたりの代表値をスカラー(min/max_altitude_m_amslmin/max_altitude_m_wgs84)で、 頂点ごとに換算したZ値を持つ形状を_wgs84列で持つ。接尾辞のないgeometry系カラムのZ値が どの基準・単位に従うかはaltitude_referencealtitude_unitで自己記述的に持つ。

  • カラムに保持する理由: GeoSpatialの領域検索がmin/maxAltitudeMAmslmin/maxAltitudeMWgs84を Featureのプロパティとして返すため、応答のたびに外部データへ問い合わせずに済ませる必要がある (検索の絞り込み軸はbboxstatuslimitcurrentTime系のみで高度は含まないため、 絞り込みのためではない)。またDIPSから収集した他社計画(DIPS_FLIGHT_PLAN)が同じ換算値カラムを 持つため、自組織側と対称にしておく必要がある。 AGL→AMSLの換算には地表の標高データ、AMSL→WGS84の換算にはジオイド高の参照が必要で外部データへの 問い合わせを伴うため、登録・収集の時点で一度だけ求めて保持する。
    • スカラーの換算値は領域あたりの代表値であり、頂点ごとのZ値は_wgs84列の形状側が持つ。両者は 別の目的で保持する(前者は応答プロパティ、後者はgeometryそのもの)。
  • WGS84基準の形状も実体化して保持する: WGS84楕円体高は地表の起伏に応じて頂点ごとに値が変わるため、 領域あたり1つのスカラーであるmin/max_altitude_m_wgs84からは再構成できない。頂点ごとの換算は 地表標高(DEM)とジオイド高への問い合わせを伴い、頂点数に比例してコストがかかる。一方で換算結果は リビジョンが不変である限り変わらないため、登録・収集の時点で一度だけ換算し、_wgs84列として保持する。 応答のたびに算出するとGeoSpatialが領域検索のたびにDEMへ問い合わせることになり、地図表示の頻度に 見合わない。
    • 対象は実体化した3つの形状(plan_geometryoperational_intent_geometrytop_bottom_3d_geometry)で、それぞれに_wgs84列を持つ。 入力された元形状のgeometryは対象外である(GeoSpatialが返すgeometrydataTypeごとの実体化列 から生成し、geometry列はCIRCLEの中心・半径の復元にのみ使うため、換算値の消費者がいない)。 DIPS_FLIGHT_PLANも同様に対称なカラムを持つ(OPERATIONAL_INTENTはDIPS側に存在しないため2列)。
    • AMSL基準の形状は持たない: GeoSpatialが返すFeatureのaltitudeReferenceAGL/WGS84の2値で、 AMSLはmin/maxAltitudeMAmslというプロパティとしてのみ公開される。AMSLはgeometryのZ値に使われないため、 スカラーのみを保持する。
  • GeoSpatialが返すZ値の規則(Discussion #6で確定): altitudeReference=AGLの Featureはリングごとに全頂点が同一のZ値altitudeReference=WGS84のFeatureは頂点ごとに地表標高を反映した 楕円体高となる(areaTypeによらない)。用いる対地高度はそのリングが天面か床面かで決まり、天面は max_altitude_m_agl、床面はmin_altitude_m_aglである。床面を返すのはdataType=TOP_BOTTOM_3Dの下面リングのみ。 OASも「min/maxAltitudeMWgs84は領域全体の代表値であり個々の頂点のZ値と一致するとは限らない」と明記しており、 本設計が換算値を領域あたりのスカラーで持つ判断はこれと整合する。
  • 床面高度は10月デモでは地表固定とする: 底面の高度指定には対応せず、min_altitude_m_agl0固定である。 したがってデモの範囲ではPOLYGON/CIRCLEでも床面〜天井の高度範囲が可変になることはない。
  • altitude_referencealtitude_unitを持つ理由: 接尾辞のないgeometry系カラムのZ値が何の基準かを 行だけで判別できるようにするためである(現時点では常にAGL・メートル。基準を増やす・変えるときに 既存行の意味が変わらない)。_wgs84列の基準は列名で自明なため本カラムの対象外である。 応答生成時に基準ごとのFeatureを作り分けるためのフラグではない点に注意する(作り分けは 接尾辞のない列と_wgs84列の使い分けで行う)。
  • 形状の実体化と動機が重なる: 飛行領域はGeoSpatialが返す形状ごとに実体化列を持つは 応答のたびのST_Bufferを避けるための実体化で、_wgs84列は応答のたびのDEM問い合わせを避けるための 実体化である。スカラーの換算値だけは応答プロパティの事前算出と収集側との対称性のために持つ。 いずれも「登録・収集の時点で一度だけ求める」点は共通する。
  • elevation.enabled=trueの環境では換算値カラムは常にNOT NULL: 遅延充填は行わない(FLIGHT_PLAN_AREA: この規則は自組織側(FLIGHT_PLAN_AREA、登録・更新)のみに適用される。他Operator計画(DIPS_FLIGHT_PLAN、 周辺収集)は1件が標高データのカバー範囲外でもその計画だけを除外し、収集全体は失敗させない方針のため NULLを許容する(DIPS_FLIGHT_PLANの行参照)。以下、登録・更新の 時点でAltitudeConverterが1頂点でもOptional.empty()を返した場合(DEM・ジオイドのいずれかが 当該地点をカバーしていない場合)、スカラー・_wgs84列を埋めずに保存することはしない。その登録・ 更新自体を拒否する(業務ルールは BusinessLogicSpecifications.mdを正とする)。 そのためelevation.enabled=trueの環境でFLIGHT_PLAN_AREAの行が存在する時点でAMSL・WGS84の 換算値(スカラー・_wgs84列)は必ず全て値を持つ。DEMの段階展開でカバー範囲が広がってもこの規則は 変わらず、単に拒否される確率が下がるだけである。 (検討過程はelevation-service-design.md7.4節「一部頂点だけDEM未整備だった場合の扱い」参照)
  • elevation.enabled=false(既定値・ローカル開発)では拒否せず、NULLのまま保存する: enabled=false の場合AltitudeConverterは常にOptional.empty()を返すNullObjectになる(ElevationConfigの javadoc参照)。上記の拒否ルールを無条件に適用すると、DEM・ジオイドファイルを用意していない開発環境で 飛行計画の登録・更新自体が一切できなくなってしまう。そのためこの列のDB制約はNOT NULLにしないenabled=falseの環境で正当にNULLになるため)。NOT NULLはelevation.enabled=trueの環境でのみ 保たれるアプリケーション層の不変条件であり、DBスキーマ自体はどの環境でも同一のためDBでは強制できない。
  • ただし換算値カラムは形状と違い、後から更新されうる: 形状(plan_geometry等)はAGL基準で外部データに 依存しないため、リビジョンが不変である限り作成後に変わらない。一方AMSL・WGS84の換算値は地表標高 (DEM)とジオイド高という外部データに依存するため、更新が起きる契機がある。 「リビジョンは不変だから不整合が起きない」という根拠は換算値カラムには及ばない。
    • DEM・ジオイドモデルの更新: 元データが更新されても、過去のリビジョンの換算値は再計算しない。 換算値は「そのリビジョンを登録した時点の地表を基準とした参考値」であり、後から書き換えると 通報済みの内容と食い違うためである。再計算が必要になるのは、元データの誤りが判明した場合など 例外的な運用に限る。
    • 更新対象はAMSL・WGS84の換算値(スカラーのmin/max_altitude_m_amsl_wgs84と、 形状の_wgs84列)に限り、AGL基準のgeometry・実体化列・min/max_altitude_m_aglは変更しない。
    • 凍結した換算値とZ値の食い違いは生じない: 頂点ごとのZ値も_wgs84列として同じタイミングで凍結する ため、geometryのZ値の範囲とスカラーのmin/maxAltitudeMWgs84は常に同一のDEM・ジオイドモデルから 導かれる。応答時に再計算しないため、両者が食い違う余地がない。例外的な再計算を行う場合は、 スカラーと_wgs84列を同時に更新する。

飛行領域の呼び名はUTM内部でROUTEに統一する

Section titled “飛行領域の呼び名はUTM内部でROUTEに統一する”

UTM側で作成・管理する飛行計画は、APIとDBで同じ対象を指す。同じ対象に別の呼び名を与えると、 「pathROUTEのことです」という説明・変換・コメントが各所に現れて読み手が混乱するため、 UTM内部(API・DB)ではROUTE/CIRCLE/POLYGONの3語に統一する。呼び名を分ける境界は 「UTM側で作成・管理する飛行計画」と「DIPSから収集した飛行計画」の2つだけとする。

対象呼び名
UTM側で作成・管理する飛行計画(API・DB共通)ROUTE / CIRCLE / POLYGON
DIPSから収集した飛行計画(DIPS_FLIGHT_PLAN.area_typeCIRCLE / POLYGON(DIPSはROUTEに相当する形式を持たない)
  • ROUTEを選んだのは、CIRCLEPOLYGONと同じ「形状」を表す語として揃うためである。pathは形状ではなく 経路という性質を表す語で、他の2語と並びが揃わない。将来ASTM準拠に寄せる場合もROUTEの方が素直である。
  • API側も追随済み: OASのflyRoute.typepath/circle/polygonでこの統一に追随していなかったが、 PR #126pathrouteへ改名した(判別子の値とあわせて スキーマ名もFlyRoutePathInputFlyRouteRouteInputFlyRoutePathFlyRouteRouteに変更)。 API境界でのpathROUTEの読み替えは発生しない。
  • DIPSから収集した飛行領域はUTM内部の飛行計画とは別テーブル(DIPS_FLIGHT_PLAN)で管理するため、同じ area_typeという名前でも取りうる値が異なる。両者を混同しないよう、収集側はCIRCLE/POLYGONの2値に限定する。
  • DIPS通報時はROUTEをPolygonへ変換し、構成点を36点以内に収める: DIPSの飛行計画はCirclePolygon しか扱えず、Polygonの構成点は3点以上36点以下に制限される(別紙2 ID68。flight-plan-field-mapping.mdの 「DIPS側の入力チェックとAPI制約の対応」を単一の正とし、以下は同節の値を根拠として使う)。ROUTEはバッファを適用した ポリゴンへ変換するが、ST_Bufferの結果は容易に36点を超えるため、そのまま送信できない。
    • 変換は外側に丸める: 通報する領域が実際の飛行領域より狭いと、DIPS上で申告した範囲の外を飛ぶことに なる。点数を削る際は必ず元の領域を包含する側に丸める(簡略化で内側に入った分は再度バッファを掛けて補う)。

    • 36点に収まらない場合は外接円でCircleとして通報する: 経路が長く屈曲が多い場合、包含を保ったまま 36点以内にするとポリゴンの形が実質的に失われる。その場合は経路全体を覆う外接円をCircleとして通報する。 領域が大きく過大になりDIPS側で重複と判定される他計画が増えるため、最後の手段として扱う。

    • バッファ生成パラメータを固定する: ST_Bufferの点数は経路の頂点数ではなく、quad_segsendcapjoinに支配される。PostGISの既定(quad_segs=8、round cap/join)では90度の円弧あたり 8セグメントを生成するため、2頂点の直線経路でも両端の半円キャップだけで約34点を消費し、 36点上限をほぼ使い切る。既定のまま運用すると、ほとんどの経路が外接円フォールバックに落ちて 「最後の手段」が常用手段になってしまう。次のパラメータを設計判断として固定する。

      ST_Buffer(geometry::geography, buffer_m, 'quad_segs=1 endcap=square join=mitre')::geometry
      • endcap=square … 両端を各2点で表す。flatは端が切れて範囲が狭くなり「外側に丸める」原則に 反するため採らない。squareは元の領域を包含する。
      • join=mitre … 頂点の外側を尖らせて1点で表す。roundは頂点ごとに円弧を生成して点数が増える。 尖る分だけ外側に出るため包含の原則にも合う(mitre_limitの既定で極端な鋭角は切られる)。
      • 概算の点数は経路の頂点数 N に対して 2N + 4(両側 N 点ずつ+両端の角4点)。36点以内に 収めるには N ≤ 16 となる。
      • ただしこれは上限の保証ではないjoin=mitremitre_limitを超える鋭角では面取りに切り替わり、 1頂点が2点になるため点数が増える。したがって通報前に必ずST_NPointsで実測し、36点を超えた場合に 外接円フォールバックへ落とす判定を入れる。N ≤ 16は入力側のガードとしての目安である。
      • 同じパラメータをoperational_intent_geometryの生成にも適用する。通報用と内部判定用でパラメータが 違うと、DIPSへ申告した形状とUTM内部で競合判定する形状が別物になる。ROUTEの空域制限との競合判定は このoperational_intent_geometryとの交差で行う(BusinessLogicSpecifications.mdの 「UC-PLAN-01 飛行計画を作成する」2-2-3)。経路のgeometryからの距離判定(丸いバッファ)にすると、 角形バッファが外側に出る経路端の分だけ判定領域が狭くなり、申告済みの領域の中にある空域制限を 見落とす。
    • 経路の構成点数の上限をOAS側にも設ける: 上記のとおりNには上限があるが、OASの GeoJsonLineStringは構成点数の上限を持たないため、Operatorが通報できない経路を入力できてしまう。 上限値の設定はOASへの反映事項とする。

  • 項目単位の対応と桁数・値域の制約はflight-plan-field-mapping.mdを参照。

操縦者・機体はマスタ参照と中間テーブルで表す

Section titled “操縦者・機体はマスタ参照と中間テーブルで表す”

飛行計画に紐づく操縦者・機体は、DIPS APIの仕様上いずれも複数指定できるため、リビジョンに紐づく中間テーブル FLIGHT_PLAN_PILOT_ASSIGNMENT(1行=1操縦者×1機体のペア)で管理する。

  • APIはpilotId/aircraftIdsというマスタ参照(UUID)のみを受け取り、氏名・住所・機体諸元などの詳細は DIPS通報時にPilot/Assetマスタから解決する。
  • (flight_plan_revision_id, pilot_id, aircraft_id)にUNIQUE制約を付与し、同一組の重複登録を防ぐ。
  • GeoSpatialの飛行計画領域検索(FlightPlanAreaProperties)はuasId(使用UASのアセットID)を単数・必須で 返す仕様であり、複数機体を割り当てられる本モデルとは整合しない。将来GeoSpatial側で配列化して解消する方針 だが、10月デモの範囲では影響がないため単数のまま据え置く。データモデル側はFLIGHT_PLAN_PILOT_ASSIGNMENTに よるN:M表現を維持する(API側の単数項目に合わせてモデルを縮退させることはしない)。
  • 応答生成時はaircraft_idの昇順で整列する: PilotAssignmentaircraftIdsaircraftNamesが 「同じ順序・同じ件数」で対応することを要求する。順序カラムは設けず、両配列をaircraft_idの昇順で 組み立てることで対応を保証する(Operatorが指定した順序を保つ要件はないため、順序そのものは保持しない)。
  • pilotNameaircraftNamesはマスタの現在値を解決して返す: pilotNamePILOT.nameaircraftNamesASSET.model(型式/名称。DIPS通報の「型式/名称」と同じ値)に対応する。いずれも リビジョンには複製せず、応答生成時にマスタを引く(PilotAssignmentのdescriptionが「マスタを解決した 現在値」と定義しているため)。マスタが論理削除(PILOT.deleted_atASSET.deleted_at)されていても、 過去の飛行計画の表示に必要なため解決対象から除外しない。
  • PILOTUSERのサテライトにせず、ASSETと同様に組織直下の独立マスタとする: 操縦者と機体・飛行計画の 関連付けは飛行計画登録時に中間テーブルで行われ、PILOT自体はUTMの利用者アカウントとは独立に存在しうる。 registered_byが指すのは登録操作をした利用者であり、操縦者本人ではない。外注先の操縦者に運航を委託する ケースでは、その操縦者にUTMの利用者登録は不要で、委託元組織のPILOTレコードとして登録すれば足りる。 操縦者本人をUSERに紐づける設計にすると、外注先の要員まで自組織のメンバーとして登録せざるを得なくなるため 採らない。組織を越えて操縦者を共有する要件(限定的な権限付与を伴うもの)が出てきた場合は、 IAMドメインの認可モデルとあわせて検討する(アクセス権の個別付与は本設計のスコープ外とする)。
  • 割り当てる操縦者・機体は飛行計画と同じ組織のものに限る: FLIGHT_PLAN_PILOT_ASSIGNMENTを通じて PilotAssignmentpilotNameaircraftNamesを返すため、他組織のpilot_id/aircraft_idを割り当てられると 他組織の氏名・機体名が読み取れてしまう。PILOT.organization_idASSET.organization_idFLIGHT_PLAN.organization_idと一致することを登録・更新時に検証し、違反は422とする。
    • DB制約ではなくアプリ層で検証する: 中間テーブルにorganization_idを非正規化して複合外部キーで 縛る方法もあるが、PILOTの組織帰属は将来「組織を越えた共有」へ動きうる(上記)。認可モデルが固まる 前にDB制約で固定すると、その時点で制約自体の変更が必要になる。アクセス権の個別付与をスコープ外とした 判断と揃え、当面はアプリ層で保証する。
    • 制約の対象は組織であって利用者アカウントではないため、外注操縦者を委託元組織のPILOTとして 登録する運用と両立する。
  • PILOT/ASSETの詳細定義はAssetドメインの関心事のため図3に置き、図1には中間テーブルからの関係線のみを示す。

可変属性はイベント由来の導出値として持つ

Section titled “可変属性はイベント由来の導出値として持つ”

ASSETASSET_RELATIONPILOTの可変属性(氏名・住所・技能証明・整備状態・連結状態等)は、 対応する_EVENTテーブル(INSERT-only)を一次データとし、リソース側のカラムはそこから導かれる現在値 キャッシュとして保持する。

  • 理由: 「現在の状態」だけでなく「いつ誰が何を変えたか」の完全な履歴を一次データとして保持し、 任意時点の状態を再構成・報告できるようにするため(概念モデルのイミュータブルデータモデル方針に従う)。
  • ASSET.kindacquired_atASSET_RELATION.first_linked_atは不変属性のためイベント由来ではない。
  • ASSET_RELATIONASSET_RELATION_EVENTLINKED/UNLINKED)により、UASとコントローラー・バッテリー等の 付け替え履歴を保持し、occurred_at <= Tで過去時点の機体構成を再現できる。

機体のDIPS通報用属性は専用テーブルに分離する

Section titled “機体のDIPS通報用属性は専用テーブルに分離する”

機体認証書番号・登録記号・機体認証(第一種/第二種)・DIPS機体種別コードは、一般的な機体属性を持つ ASSET_UAS_ATTRSから分離し、ASSET_UAS_DIPS_ATTRSASSET_UAS_ATTRSと1:0..1)に持たせる。 なおFLIGHT_PLAN_DIPS_ATTRSが1:1であるのに対しこちらを1:0..1とするのは、飛行計画側は本登録の必須項目を 配下に持つため必ず1行必要な一方、機体側はDIPS通報用の属性が未入力の機体も登録しうるためである。

  • 飛行計画ドメインと同じ分離方針に揃える: これらはいずれも日本の航空法に 従うために必要な属性であり、機体そのものの本質的な属性ではない。本体に混在させると、国際化や他ATM/USPとの 整合を取る際に機体テーブルのスキーマ自体を変更せざるを得なくなる。飛行計画ドメインでDIPS通報固有の属性を FLIGHT_PLAN_DIPS_ATTRSへ切り出しているのと同じ理由・同じ形にする (DIPS通報固有の属性は飛行計画本体から分離する)。
  • DIPS_UAS_LINKとは別のテーブルとする: DIPS_UAS_LINKlinked_at/last_synced_atが示すとおり DIPS側との自動連携・同期の状態を持つテーブルである。一方これらの属性はDIPS連携が未確立の段階でも運航者が 手動入力しうる機体属性であり、同期状態とは寿命も更新契機も異なる。「航空法に基づく通報のために必要な属性」と 「DIPS連携の同期状態」を別テーブルに分ける。
  • 行が存在しないことの意味: ASSET_UAS_DIPS_ATTRSの行がないことは、当該機体のDIPS通報用属性が 未入力であることを表す。FLIGHT_PLAN_DIPS_ATTRSは本登録の必須項目を配下に持つため1:1としたが、 機体側にはその制約がないため0側を残す点が異なる。

アクセス権の個別付与は本設計のスコープ外とする

Section titled “アクセス権の個別付与は本設計のスコープ外とする”

概念モデルにあるFLIGHT_PLAN_ACCESS_GRANT(計画単位のアクセス権付与)は、本設計では扱わない。

  • 理由: 現時点で計画単位の権限付与を必要とする機能がなく、IAMドメインの設計が固まる前に飛行計画側だけで 権限モデルを決めると、他リソース(Asset等)と整合しない設計になる。とくに概念モデルのprincipal_typeEVERYONEを含み組織を越境するアクセスを表現するが、リソースは基本的に組織に紐づくため、行レベルセキュリティ (RLS)も組織を軸に書くことになる。この前提が定まらないうちに個別付与を作り込むべきではない。
  • 想定する段階: ①IAMを先に導入する → ②飛行計画などのリソースに組織単位の単純なRLSを入れる → ③組織を越境する共有が必要になった段階で個別付与へ拡張する。
  • 本設計の範囲では、飛行計画へのアクセス可否はFLIGHT_PLAN.organization_idによる組織単位の判定で足りる。

OAS・他ドメインへ反映が必要な項目

Section titled “OAS・他ドメインへ反映が必要な項目”

本設計の結論のうち、既存のOASや他ドメインの仕様と食い違っており、別途反映が必要なものを以下にまとめる。 「対応状況」は次の意味である。

  • PR #126flight-planning.yamlへの反映をPR #126で実施(マージ待ち)
  • 決定済み … OASや他ドキュメントへの反映を要さず、決定として確定させたもの。内容は確定した事項(10月デモ向け)にまとめる
  • 本PR … 本PRで他ドキュメント側を修正
  • Issue #132 / #135 / #136 … 決定・検証が必要なためIssue #132Idempotency-Keyの保存先)・#135uasEntityIdの出所)・#136AIRSPACE_RESTRICTION.idの安定性)で扱う
  • PR #134flight-planning.yamlgeospatial.yamlのdescriptionへの反映をPR #134で実施
  • 未対応 … 反映先・時期が未定
対象内容対応状況経緯
空域制限ドメイン / flight-planning.yaml外部IDを持たないデータソース(sourceKind=GSIなど)の空域制限と競合した場合に、必須項目であるairspaceRestrictionIdへ何を返すかが未決だった。airspaceRestrictionIdをUTMが採番したuuidへ戻すことで解決したairspace-er.md。PR #194)。値の出所が1系統になり、外部IDのないソースのための採番も不要になった決定済み空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する
flight-planning.yamlAirspaceRestrictionConflict.airspaceRestrictionIdはDIPS由来の識別子(例20221105_FISSikou0015)を返す。当初uuidへ変更したが、DIPS由来の識別子をUPSERTのキーとして内部保持する必要があるためレビュー指摘で撤回し、GeoSpatialのrestrictionIdstringに揃えた。この決定はPR #194で覆り、両者ともuuidformat: uuid)へ戻った(1つ上の行を参照。現行OASはuuidPR #126(PR #194で撤回)空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する
flight-planning.yamlAirspaceRestrictionConflictSummaryFullConflictSummarynullable: trueを外し、DRAFTでも空配列を返す。あわせて空配列の意味を「チェック済みで競合なし」から「保持している競合が0件」へ改めるPR #126同上
flight-planning.yamlFullConflictSummary.flightPlanIdsの要素型をFlightPlanId(UTM内部のuuid)からDipsFlightPlanId(DIPS独自形式の文字列)へ変更する。競合相手は他Operatorの計画で、UTMが保持するのはDIPS側のIDのみであり、DIPS_FLIGHT_PLAN.id(UTMの代理キー)を返してもgetFlightPlanで引けないIDになるPR #126DIPSから収集した他社飛行計画は独立モデルとして管理する
flight-planning.yaml変更理由(FLIGHT_PLAN_REVISION.change_reason)を受け取る入力項目をupdateFlightPlanregisterFlightPlanに設けるか判断する。現状はどのAPIにも渡す手段がなく、Operatorが理由を記録できない決定済み飛行計画は不変リビジョンの連なりとして管理する
flight-planning.yamlflyRoute.typepathrouteへ改名する(生成コードの再生成を伴う)PR #126飛行領域の呼び名はUTM内部でROUTEに統一する
geospatial.yamlFlightPlanAreaProperties.uasIdを配列化する。10月デモの範囲では影響がないため据え置き未対応操縦者・機体はマスタ参照と中間テーブルで表す
flight-planning.yamlDIPSの入力チェックより緩い桁数・値域を是正する(内訳はflight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」)PR #126flight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」
flight-planning.yamlFlightPlanFieldsのdescriptionにある「他の項目はDRAFTで未入力のまま存在しうるため、未入力の場合はキー自体を省略する」を、キー省略の対象が任意項目に限る旨へ修正するPR #126一時保存は専用テーブルで表す
geospatial.yamlOtherFlightPlanAreaProperties.minAltitudeMAglのdescription(FlightPlanAreaPropertiesallOfで継承しているため両者に及ぶ)が「areaType=POLYGON/CIRCLEは床面〜天井として異なる値を取りうる」とするが、FlightSpecに最低高度の入力項目がないため現時点ではどの形状でも0にしかならない。実際に取りうる値に合わせて記述を修正するPR #134飛行領域は形状ごとにサブテーブルへ分ける
ADR-020(横断機構)Idempotency-Keyの保存先を設計する。飛行計画APIの6 operation(PR #126deleteFlightPlanを追加すると7)が同ヘッダを使う一方、ADR-020はキーの有効期間・保存方針を残課題としている。テーブル横断でキーの一意性と「最初の実行結果を返す」を担保する置き場所が必要Issue #132DIPS通報の失敗は応答の区分に応じて扱いを分ける
asset.yaml本設計が定義したASSETASSET_UAS_ATTRSASSET_UAS_DIPS_ATTRSPILOTに対応するAPIがない(AircraftidのみのTODO付きスタブ、操縦者スキーマは未定義)。Assetチームと項目を突き合わせる決定済み機体のDIPS通報用属性は専用テーブルに分離する
flight-planning.yamlPilotIdのdescriptionが/api/v1/fp/pilotsを指すが本設計は/api/v1/asset/pilotsとしている。どちらのパスもOASに未定義であり、PILOTの所属ドメインを決めたうえで揃える未対応操縦者・機体はマスタ参照と中間テーブルで表す
flight-planning.yaml操縦者・機体の組織一致違反を422とする方針がOAS側に記載されていない。DB制約を意図的に置かない判断のため、この検証が唯一の担保であり、実装漏れが他組織の氏名・機体名の露出に直結する。FlightPurposeItem.noteと同様にdescriptionへ明記するPR #126操縦者・機体はマスタ参照と中間テーブルで表す
flight-planning.yamlFlyRouteRouteInput(旧FlyRoutePathInput)のdescriptionにある「PostGISのST_Buffer(geometry, bufferM)に相当」は、SRID 4326では距離が度になるため誤り。geographyキャストを前提とした記述へ是正するPR #126メートル単位の空間演算はgeographyへキャストして行う
flight-planning.yamlGeoJsonLineStringflyRoute.type=routeの経路)に構成点数の上限がない。DIPS通報時のPolygon変換が36点以内に収まる上限(バッファ生成パラメータ固定時は16点)を設定するPR #126飛行領域の呼び名はUTM内部でROUTEに統一する
geospatial-api-design.md§5.1のカラム対応が本設計の結論に追随していない。§5.1のカラム対応表を1行ずつ突き合わせた結果、不一致は9件。①statusFLIGHT_PLAN(リビジョンではない)②reportRequiredreportExemptionReasonFLIGHT_PLAN_DIPS_ATTRSreportStatusはカラムを持たずFLIGHT_PLAN_STATE_EVENT.report_status_afterから導出(値数も3値→5値)④bufferMFLIGHT_PLAN_AREA_ROUTE.buffer_mであり、同行の「areaTypeによらず設定可能」も本設計と食い違う(本設計はバッファをROUTEのみが持ち、CIRCLE/POLYGONは持たない)⑤uasIdFLIGHT_PLAN_REVISION.uas_idではなくFLIGHT_PLAN_PILOT_ASSIGNMENT.aircraft_id(本設計はN:M)⑥circleCenterLng/Lat/RadiusMは採らず中心点geometryFLIGHT_PLAN_AREA_CIRCLE.radius_maltitudeReferenceを「対応するカラムなし」とするが本設計は保持する⑧min/maxAltitudeMAmslWgs84も同様に4カラムを新設済み⑨geometryの供給元はFLIGHT_PLAN_AREA.geometryではなくdataTypeごとの実体化列である(geometry列はCIRCLEの中心・半径の復元にのみ使う)なおdataTypeは本設計も持たず一致しており、空域制限側のAIRSPACE_*は本設計のスコープ外であるため、いずれも不一致には数えない本PR本ドキュメント全体
AssetドメインASSET_UAS_DIPS_ATTRS.registration_symbolDIPS_UAS_LINK.dips_registration_no(UK)の関係が未整理。同一の値なのか、DIPS通報時にどちらを正とするかを決める決定済み機体のDIPS通報用属性は専用テーブルに分離する
Assetドメイン / 飛行計画APIカテゴリ判定(ⅡA・Ⅲ)の設計が本設計・OASのいずれにも存在しない。DIPSは「カテゴリ判定の結果がⅡA・Ⅲかつ許可承認情報がnullでない場合、連絡先一式・許可承認番号・許可期間が必須」とするため、判定を実装するか、許可・承認情報が入力された場合は常に必須として扱うかを決める決定済みflight-plan-field-mapping.mdの「項目間の条件付き必須」
geospatial.yamluasEntityIdの出所を確認する。searchFlyingDronePositionsは他Operator機も返しTelemetryPositionPropertiesは本項目を必須とするため、自UTMのASSETに存在しない機体に対して何を返すのかが決まっていないIssue #135flight-plan-field-mapping.mdの「機体IDの対応」
運航調整ドメイン自組織側の飛行速度(flightSpec.speed)を参照するかを確認する。参照するならFLIGHT_PLAN_DIPS_ATTRS.flight_speed_kmhとして個別カラム化し、収集側(DIPS_FLIGHT_PLAN.flight_speed_kmh)と対称にする決定済みflight-plan-field-mapping.mdの注記
flight-plan-sequence.md本ドキュメントは冒頭でトランザクション境界・ロック制御の検討を対象外と宣言しているため、3トランザクション分割そのものは記載不要である。ただし通報・キャンセル・削除のシーケンスにWITHDRAWINGが登場せず(REPORTINGのみ)、取り下げ中の状態がフローに現れていないため、状態の遷移としては追随が必要か確認する本PR飛行計画は不変リビジョンの連なりとして管理する
flight-planning.yaml屋内飛行を申告する入力項目がない。report_exemption_reason=INDOOR_ONLYを設定する根拠をOperatorから受け取る手段がないため、項目の新設を検討する決定済み運航状態とDIPS通報状態を直交2軸で持つ
flight-plan-statemachine.mdDRAFT --> [*](キャンセルで終端)は、本設計がDRAFTからのキャンセルを削除と同等(status=DRAFTのままdeleted_atを設定し取得対象外)とする方針であることと整合するため、図の追随は不要と結論した(PR #138)。あわせて同ドキュメントのCANCELLEDの説明にあった「DRAFTからのキャンセルでも行は削除せずCANCELLEDにする」を本方針へ改めた ②504タイムアウトをREPORTING --> REPORTINGとして自己遷移で表しているが、本設計はREPORT_TIMEOUTWITHDRAW_TIMEOUTイベントとして事実を記録する形にした。イベント名の対応づけを追記する本PR一時保存は専用テーブルで表す / DIPS通報の失敗は応答の区分に応じて扱いを分ける
flight-planning.yamlAirspaceRestrictionConflictairspaceRestrictionIdairspaceRestrictionTypeの2項目しか持たず、CONFLICT_DETECTION.statusDETECTED/RESOLVING/RESOLVED/IGNORED)を返せない。応答をDETECTED系のみに絞る規則をdescriptionに明記するか、状態を返す項目を追加するかを決める。あわせてstatusDETECTED以外へ遷移させるAPIも存在しないため、10月デモでは値域をDETECTEDのみに縮退させる選択肢もあるPR #134空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する
flight-planning.yamldeleteFlightPlanIdempotency-Keyが定義されていない(parameters節そのものがない)。DIPS削除を伴い503/504を返す操作で、OAS自身が「reportStatusREPORTED に戻し、Operatorが本APIを再試行できるようにする」と再送を前提にしているため、冪等キーがないと再送で二重の削除要求が飛ぶPR #126DIPS通報の失敗は応答の区分に応じて扱いを分ける
flight-planning.yamlcreateFlightPlanだけ冪等の判定軸が「異なるリクエストボディ」で応答が201、他5 operationは「異なる飛行計画ID」で200である。横断機構が作成系と更新系を切り替えられる必要があるため、ADR-020側の設計に反映するIssue #132同上
flight-planning.yamlactivateFlightPlanのdescriptionと409の説明に飛行開始条件が書かれていない。本設計と状態遷移図は「status=ACCEPTEDかつ(report_required=falseまたはreport_status=REPORTED)」を条件とし、違反はアプリ層で拒否する。通報義務があり未通報の場合に返すtypeとあわせて明記するPR #126運航状態とDIPS通報状態を直交2軸で持つ
flight-planning.yamldeleteFlightPlanの409の例がACTIVATEDのみで、削除可能な状態の一覧がdescriptionにない。本設計は「削除不可はACTIVATEDENDED」と決めたため、これをdescriptionに明記して確定させる。ENDEDの削除を許すと、飛行済みの計画のDIPS通報を取り下げることになるPR #126図1 カラム補足 deleted_at
flight-planning.yaml一覧の返却順がOAS内部で不一致。listFlightPlansの説明は「登録順(登録日時の昇順)」、FlightPlanListResponse.itemsの説明は「登録日時・更新日時の昇順」でキーの数が違う。デモではソートを提供しないためcreated_at昇順のみが素直PR #126状態遷移は内容リビジョンと分けてイベントで記録する
geospatial.yamlOtherFlightPlanAreaProperties.statusFlightPlanStatus(5値)を返す定義になっているが、他Operatorの飛行計画はDIPSから収集したものであり、本設計のDIPS_FLIGHT_PLANにはstatusに相当するカラムがない。DIPSの飛行計画参照APIは運航状態を返さないため保持もできない。status自体が存在しないため、検索条件としても返却項目としても持たない旨をdescriptionに明記するPR #134 / #219DIPSから収集した他社飛行計画は独立モデルとして管理する
geospatial.yamlFlightPlanStatusFilterFlightPlanStatusDRAFTを含む5値)をそのままitemsにしているが、DRAFTの飛行領域はFLIGHT_PLAN_DRAFT.draft_fieldsjsonb)にありFLIGHT_PLAN_AREAの行が存在しないため、status: [DRAFT]で検索すると常に0件になる。DRAFTを除いた4値の独立enumにするか、descriptionに明記するPR #134一時保存は専用テーブルで表す
運航調整ドメインcoordination.confliction.is_deletedがDDLにのみ存在し内部APIのスキーマに現れない。conflict.flightPlanIdsの導出で参照してよいかを確認する決定済みDIPSから収集した他社飛行計画は独立モデルとして管理する
空域制限ドメイン再同期でAIRSPACE_RESTRICTION.iduuid)を振り直さず、データソース側の元IDで突き合わせて維持することを確認するIssue #136空域制限との競合は判定結果を保持し、未チェックとの区別は状態から導出する

本設計側の判断として確定させた事項を示す。9件のうち7件は上表で「決定済み」としたもの(OASや他ドメインへの 反映を要さないもの)で、残る2件(床面高度の固定、競合は検出のみ)は決定にあわせてOASのdescriptionへも 反映したため、上表では「PR #134」としている。

項目決定
変更理由(change_reasonAPIからの指定は不要。どの操作が行われたかは呼ばれたoperationから導出できるため、入力項目は設けない
屋内飛行10月デモの対象外。デモの飛行場所は屋外で確定しており、report_exemption_reason=INDOOR_ONLYはデモ範囲では設定されない。入力項目も新設しない
床面(底面)の高度指定10月デモでは対応しない。床面高度は地表固定でmin_altitude_m_agl=0とする(高度は換算値をカラムに保持する参照)
空域制限との競合の状態10月デモでは検出(DETECTED)のみを扱う。解決などの過程の表示は以降のリリースで検討する。したがってCONFLICT_DETECTION.statusをAPIで返す必要はない
カテゴリ判定(ⅡA・Ⅲ)制度上は存在するが10月デモでは判定を実施しない。許可・承認の要否判断も同様
Assetドメインの参照API10月デモでは提供しない方針で確定済み。asset.yamlに対応するAPIがない現状は想定どおり
登録記号の二重管理ASSET_UAS_DIPS_ATTRS.registration_symbolDIPS_UAS_LINK.dips_registration_no同一の値。DIPSにおいて機体関係の登録番号とされているものは登録記号を正とする
運航調整ドメインからの飛行速度参照運航調整のOAS・仕様にflightSpec.speedを参照する箇所はないため、FLIGHT_PLAN_DIPS_ATTRS.flight_speed_kmhとしての個別カラム化は行わないFLIGHT_PLAN_DIPS_ATTRS.dips_report_detailのパススルーに含める)
coordination.confliction.is_deleted論理削除済みの行は参照で返却しないconflict.flightPlanIdsの導出では削除済みを除外する

入力制限の是正(flight-planning.yaml

Section titled “入力制限の是正(flight-planning.yaml)”

DIPSの入力チェックより緩かった桁数・値域は14項目あり、PR #126で まとめて是正した。項目ごとの現在値は再掲せずflight-plan-field-mapping.mdの「DIPS側の入力チェックとAPI制約の対応」を単一の正とする。 同節にDIPS側の制約(別紙2のID)・UTM側の制約・一致状況を一覧してある。

未解消はflightSpec.speedの範囲のみで、ガイドライン項番15(1〜100)と別紙2 ID49(1〜999)が食い違うため 上限は緩めていない。DIPSへの照会事項として残っている。

本ドキュメントの構造に関する申し送り

Section titled “本ドキュメントの構造に関する申し送り”

レビューを重ねる中で、同じ結論が複数箇所に書かれていて片側だけ直す追随漏れが複数ラウンドにわたって発生した。 原因は個別の記述ではなく構造にあるため、次の3点を整理対象として挙げていた。2点は解決済みで、3点目は#133のマージで解消する。

課題対応
カーディナリティ・格納形式が、mermaid図・カラム補足・設計方針・命名規約表・field-mappingの5箇所に分散している解決済み。記法と命名規約をデータモデル設計ドキュメントの規約に切り出し、カーディナリティと格納形式は図を単一の正とすることを図を単一の正とするに定めた
加筆時の横断確認が手順化されていない解決済み。Claude Code 向けのルール .claude/rules/design-docs.md に手順を定めた。用語の一括置換が他ドキュメントの逐語引用を書き換える事故が実際に発生したため、引用を置換対象から除外することも含めている
桁数・値域が二重化している。同じ「現在の制約値」が、本ドキュメントの「入力制限の是正」(14項目)とfield-mappingの「DIPS側の入力チェックとAPI制約の対応」(15項目)の2箇所に書かれている#133のマージで1箇所になるPR #139は#133のブランチへマージ済みで、本ドキュメント側の14行の一覧を削除しfield-mapping側を単一の正とする参照に置き換えている。本ブランチは#139を含まないため2箇所のままである。なおfield-mappingの§1〜§7に現在値はなく(maxLengthは全7件が同節内)、当初「三重化」としていた認識は誤りだった。過去に発生した表の数値誤りはすべてこの型だった

実装フェーズで本ドキュメントを参照する際は、図と本文の記述が食い違う場合は図を正として読む。 ただし複合UNIQUE・部分UNIQUEは図に表現できずカラム補足を正とするため、一意制約は必ずカラム補足で確認する (詳細は図を単一の正とする)。