v0.4.2 の時点のガイド(ソース)

dbxray / cli

cli 読解ガイド

DBのカタログ(テーブル・カラム・制約・外部キー等のメタ情報)を読み、人が読むMarkdownのテーブル定義書・ER図と、機械が読むスキーマのスナップショット(JSON Lines)を出力するコマンドラインツールです。--checkを付けると、DBとコミット済みのスナップショットの差分を検知します。このガイドは、入出力と使っている技術の事前知識から始めて、実行の流れ、データの持ち方、中心のロジック、ソースごとの役割と繋がりまでを順に説明します。

  • 約16.7k 行main配下のJava(空行・コメントを含む目安)
  • 4層+2presentation・application・domain・infrastructure と層の外のconfig・shared
  • 3 出力形式Markdown・スナップショット・参考情報(insights)
  • 2 DBMSPostgreSQL・Oracle。SQLはmapper XMLに分離

mainブランチ(0bb6bdd。PR #114まで)を読んで作成し、その後に入った--preview(関数・プロシージャが利用しているテーブルの出力。Issue #134・PR #137)の分を各章に追記しています(9.9節にまとめ)。その後、DB単位のREADMEに載る出力対象の集計(DatabaseMetrics・ExportSink.writeSummary)、関数の個別定義のファイルが重なる場合の警告、テンプレートの共通部品の整理(RowRenderer・MermaidSupport.erDiagramCodeBlock等)を反映し、サンプルDBに足した履歴テーブルemployee_historyに合わせて出力例を採り直しています。コンソールの出力例は、ビルドしたjarを、docs/sample/postgres/ddl.sqlを流し込んだPostgreSQL 16に対して実行して採取したものです(長いパスは短く書き換えています)。Markdown・JSONの例は、コミット済みのベースラインdocs/sample/postgres/outputから引用しています。

先に掴んでおく6つの要点

  1. 一方向の変換器:DBのカタログとサイドカーYAMLを読み、Markdown・スナップショット・参考情報の3形式を書きます。DBへはSELECTしか発行しません。精度を固めている途中の機能は--previewを付けたときだけ動きます(1章・9.9節)。
  2. 層と依存の向き:presentation → application → domain ← infrastructure。ルールはdomainに、DB・ファイル・YAML・JSONとの接点はinfrastructureに、全体の組み立てはエントリーポイントとconfigにあります(3章)。
  3. 検証は入口で、報告はmainの1箇所で:設定・引数・出力先の誤りはDBへ接続する前に検証し、例外はmainだけが捕まえて終了コードに変えます(4章・5章)。
  4. 取得は2つの粒度:軽い情報(テーブル一覧・関連・トリガー等)は対象範囲全体を一括で、重い情報(カラム・インデックス・制約)はスキーマ・チャンク単位で取得して、書いたら捨てます(6章)。
  5. 出力形式ごとにSink:取得の順序は1つで、出力形式ごとのExportSinkがそれぞれの形で書きます。--checkはSinkをスナップショットの1つに減らして一時ディレクトリへ書くだけです(6章・7章)。
  6. テーブルキーで繋ぐ:カラム・関連・トリガー・付帯情報は、テーブルをオブジェクトで持たず「スキーマ名+テーブル名」(TableKey)で指します。別々に取得した情報を後から1テーブル分に組み立てられるのはこのためです(8章)。

1事前知識:このツールの入力と出力

このツールは「DBのカタログ → 文書」の一方向の変換器です。文書に載せる内容の入力が2つ(DBのカタログとサイドカーYAML)、実行のしかたを決める入力が3つあり、出力は3つの形式とコンソールです。

文書に載せる内容 DBのカタログ PostgreSQL 13以上・Oracle 12c以上 発行するのは読み取りのSELECTだけ サイドカーYAML(annotations) 手動付帯情報・論理リレーション・観点 実行のしかたを決める入力 conf/config.yml database:接続情報 (パスワードは環境変数で渡す) target・output・annotations: 出力対象・出力先など8項目 CLI引数 --check・--rm-dist・--preview・--config --help・--version・各設定の上書き cli(このサブプロジェクト) java -jar dbxray.jar Dbxray.main() DBは読むだけ {output.path}/ の下 {DB名}/ … Markdown README・一覧・定義書・ER図・観点ページ 人がGitHub等で読む snapshot/{DB名}/ … JSON Lines database.json・{スキーマ}/tables.jsonl 等 --check・mcp-server・jqが読む insights/{DB名}/ … 参考情報 viewpoints.json … 観点(宣言した場合だけ) {スキーマ}/functionTableUsages.json … 関数の利用テーブル(--previewだけ) コンソール・ログ・終了コード 標準出力 [result]:SUCCESS / FAIL 標準エラー [warn]:(警告) var/log/dbxray.log 終了コード 0・1・2
図1 入力と出力。左上の2つは文書に載せる内容、左下の3つは実行のしかたを決める入力です。後者の誤りはすべて失敗にし、前者のサイドカーYAMLの個々の書き誤りは警告して読み飛ばします(5章)。--checkはスナップショットだけを一時ディレクトリに作り、出力先のsnapshot/と比べます。

通常実行

  • DBから取得し、3つの形式を出力先へ書く
  • 生成するファイルだけを作成・上書きする。DBから消えたテーブルのファイルは残るため、--rm-distで先に出力先を消せる
  • 終了コード:成功0、失敗2

--check

  • 取得は通常実行と同じ。スナップショットだけを一時ディレクトリに作り、出力先のsnapshot/とオブジェクト単位で比べる
  • 出力先には書かず、Markdown・ER図も作らない
  • 終了コード:差分なし0、差分あり1、失敗2。CIで「マイグレーション後の再生成・コミット忘れ」を検知する用途

--preview:固めている途中の機能は、付けたときだけ動く

精度や出力の形を、使った人の声を聞きながら固めている途中の機能は、--previewを付けて実行したときだけ動きます。今あるのはfunctionTableUsageの1つで、関数・プロシージャの定義本体から、利用しているテーブルと操作(C・R・U・D)を機械的に抽出します(9.9節)。結果は関数の定義書の「利用しているテーブル」と、参考情報のfunctionTableUsages.jsonに出ます。

  • 出力・挙動は互換性の対象にしません(CONTRIBUTING.mdの「互換性を約束する範囲」)。外すときは機能追加として扱います。
  • スナップショットは変えません。そのため--checkの結果には影響せず、--checkと同時に指定しても効果はありません(その旨を表示します。7章)。

出力先の構成

出力はすべてoutput.path(未指定なら./output)の下に、DBごとのディレクトリに分けて置かれます。同じ出力先へ複数のDBを出力しても混ざりません。

{output.path}/
├─ {DB名}/                              Markdown(人が読む)
│   ├─ README.md                         一覧へのリンク集と、出力対象の集計
│   ├─ tableList_{DB名}.md               テーブル一覧(常に出力)
│   ├─ erDiagramList_{DB名}.md           スキーマ別ER図の索引
│   ├─ erDiagram_{DB名}_{スキーマ}.md     スキーマ別ER図(大きいと _group{n} に分割)
│   ├─ viewpointList_{DB名}.md・viewpoint_{DB名}_{id}.md   観点
│   ├─ triggerList / functionList / sequenceList / typeList_{DB名}.md
│   └─ {スキーマ}/
│       ├─ table | view | materialized_view/{テーブル}.md  テーブル定義書
│       └─ function | sequence | type/{名前}.md            個別定義(関数は--previewで「利用しているテーブル」の節が付く)
├─ snapshot/{DB名}/                     JSON Lines(機械が読む。--checkの比較対象)
│   ├─ database.json
│   └─ {スキーマ}/tables.jsonl・functions.jsonl・sequences.jsonl・types.jsonl
└─ insights/{DB名}/                     参考情報(--checkの対象外)
    ├─ viewpoints.json                   観点(宣言した場合だけ)
    └─ {スキーマ}/functionTableUsages.json   関数・プロシージャの利用しているテーブル(--previewを付けた場合だけ)

一覧は、テーブル一覧以外は対象が1件以上あるときだけ出力します。どのファイルをどのクラスが書くかは10章にまとめています。

同じ取得結果から3つの形式を作る

Markdownは人が読む最終成果物、スナップショットは機械が読む中間表現です。どちらもDBから1回取得した同じデータ(1テーブル分ならTableDefinitionContent)から作るため、内容は食い違いません。スナップショットは実行のたびに変わる生成日を含まないため、DBが変わらなければ何度実行しても同じ内容になり、--checkが日付で差分を出しません。観点はスキーマの事実ではなく「見せ方」なので、スナップショットには含めず、参考情報として別に出力します。関数・プロシージャが利用しているテーブルも、DBが持つ事実ではなく定義本体から抽出した参考値なので、同じく参考情報として出します。

サイドカーYAML:DBに無い情報を足す

Markdownを手で編集しても、次の生成で上書きされます。DBのコメントに書けない情報は、設定ファイルのannotationsで指定するYAMLに書きます。コード上はこれをサイドカーと呼び(パスはsidecarPath)、性質の違う3種類の情報を持ちます。

YAMLのキー足すものモデル反映先
tables文章(テーブル説明・テーブル備考・カラム備考)Annotations・TableAnnotation定義書の各セル。スナップショットのdescription・remarks
relations構造(DBに外部キー制約が無い関連=論理リレーション)ForeignKeyEntity(RelationType.LOGICAL)定義書の「論理リレーション情報」「被参照情報」、ER図の破線
viewpoints読む単位(業務ドメイン別のテーブルのまとまり=観点)Viewpoints・Viewpoint観点ページ・観点一覧、定義書の「所属する観点」、insights
# docs/sample/postgres/annotations.sample.yml から抜粋
tables:
  sample.department:                 # スキーマ名.物理テーブル名
    remarks: 組織改編時はdepartment_codeを変更せず、department_nameのみ更新すること
    columns:
      department_code: 部署コード。他システム連携時のキーとして使用
relations:
  - table: sample.audit_log          # 参照元(子)
    columns: [record_id]
    parentTable: sample.employee     # 参照先(親)
    parentColumns: [employee_id]
    cardinality: 0..1対多
viewpoints:
  - id: personnel                    # 観点ページのファイル名に使う
    name: 人事管理
    tables:                          # table= と同じ記法
      - sample.employee*
      - "!sample.employee_directory_view"
      - sample.department
      - sample.parking_spot

2事前知識:使っている技術

コードを読むときに前提になる技術は次のとおりです。どれも使い方は限られているので、この章で押さえる範囲を知っていれば読めます。

技術何に使うか主な場所
Java 21record・sealed interface・パターンマッチ・テキストブロックコード全体
MyBatis 3.5SQLをXMLに書き、結果をDTO(record)に詰めるresources/mapper・infrastructure.db
SnakeYAML 2.7サイドカーYAMLの読み込みSidecarYamlRepository
Jackson 2スナップショット・参考情報のJSON化JacksonSnapshotSerializer
MermaidER図の記法。GitHubがMarkdownの中で図に描くErDiagramTemplates・MermaidSupport
log4j2ログファイルと、警告のコンソール出力log4j2.xml
JUnit 6・ArchUnit・Testcontainers単体テスト・構造の規約の検査・Docker上の実DBでの結合テストmodules/cli/src/test・modules/cli/src/integrationTest

2.1 Java 21:record・sealed・パターンマッチ

このコードベースのクラスの多くはrecordです(エンティティ・DTO・スナップショット・request)。recordのコンパクトコンストラクタ(引数リストを書かないコンストラクタ)で、受け取ったリストの複製や値の正規化をしています。

public record NodeLimit(int value) {
  public NodeLimit {
    value = Math.max(value, 0);   // 0以下は「上限なし」(0)へ揃える
  }
}

「描くか省くか」のような場合分けは、sealed interfaceとswitchのパターンマッチで表します。switchは全パターンの網羅をコンパイラが検査するので、場合を増やすと分岐の書き漏れがコンパイルエラーになります。

public sealed interface RenderingPlan {
  ForeignKeyGroup group();
  record Draw(ForeignKeyGroup group) implements RenderingPlan {}
  record Omit(ForeignKeyGroup group, NodeLimit limit) implements RenderingPlan {}
}

// ErDiagramWriter.writeErDiagramPage(要約)
final PagedSection<?> detail = switch (plan) {
  case RenderingPlan.Omit omit -> /* 図の代わりに外部キー一覧を載せる */
  case RenderingPlan.Draw draw -> /* 図に載せたテーブルの一覧を載せる */
};
書き方このリポジトリでの例
recordのコンパクトコンストラクタTableDetail・TriggerEntityがリストをList.copyOfで複製する。Sidecar・TableAnnotationがnullを空へ揃える
sealed interface と switchRenderingPlan(Draw・Omit)、ForeignKeyGroups.PageComposition(Single・Grouped)
レコードパターンErDiagramTemplates.erDiagramのplan instanceof RenderingPlan.Omit(ForeignKeyGroup omitted, NodeLimit limit)
テキストブロック(""")Markdownの表のヘッダー(TableDefinitionTemplates等)
振る舞いを持つenumCardinality(Mermaidの端点とラベル)、SnapshotKind.FUNCTION(識別名に引数を足す)、ExitStatus(終了コード)

2.2 依存関係の組み立て:コンストラクタで受け取り、Componentsが2段階で組み立てる

DI(依存性の注入)は、クラスが使う部品を自分でnewせず、コンストラクタの引数で受け取る書き方です。このツールはDIコンテナ(Guice等)を使わず、誰に何を渡すかをComponents(エントリーポイントと同じパッケージ)がnewで書き下します。押さえる規則は3つです。

DBへ接続して種別を判定したら、第2段で組み立てる 第1段(DBへ接続する前) Components.create() FileRepository → LocalFileRepository SidecarRepository → SidecarYamlRepository OutputPathResolver → DefaultOutputPathResolver SnapshotSerializer → JacksonSnapshotSerializer Clock → Clock.systemDefaultZone() Writer・Sinkの工場・ドメインサービス PagedSectionWriter、TableDefinitionWriter、SnapshotDiff など(第2段で組み立てる) 使い道:outputDirectoryValidator()で出力先を検証 第2段(接続して種別が分かった後) controller(databaseType, sqlSessionFactory) SqlSessionFactory → mainで作った1個 CatalogRepository(switchで選ぶ)→ PostgresCatalogRepository または OracleCatalogRepository 上の部品を渡してnewで組み立てるもの SchemaExportPipeline → CatalogRepository → ExportSchemaUsecase・CheckDocumentDiffUsecase → ExportSchemaController 使い道:mainがコントローラーを受け取って実行
図2 組み立ての2段階。出力先の検証などDB種別に関係ない部品は、DBへ接続する前に第1段から取り出して使います。CatalogRepositoryの実装は接続先がPostgreSQLかOracleかで変わるため、接続して種別が分かってから第2段(controller)で選びます。CatalogRepositoryに依存する3つ(パイプライン・ユースケース・コントローラー)は、このため第2段で組み立てます。
// Dbxray.createController:DBへ接続するのはここだけ
final SqlSessionFactory sqlSessionFactory = MyBatisSqlSessionFactories.create(connectionSettings);
return components.controller(
    DatabaseTypeDetector.detect(sqlSessionFactory),   // ここで接続して種別を判定
    sqlSessionFactory);

コンストラクタの引数の直し漏れ・CatalogRepositoryの実装の選び漏れ(switchがDatabaseTypeを網羅する)はコンパイルエラーになります。両方のDB種別でコントローラーまで組み立てられることは、DBへ接続しないComponentsTestが確かめます。

2.3 MyBatis:SQLはXML、結果はrecord

MyBatisは、SQLをXMLに書き、結果の列をJavaのオブジェクトへ詰めるライブラリです。このツールが発行するのはすべてSELECTで、更新はありません。

SchemaExportPipeline リポジトリの selectTableList(…) AbstractTableDefinition Repository SQLのIDを組み立てて session.selectList(…) mapper XML(DB種別ごと) <select id= "selectTableInfo" resultType="TableDto"> TableDto(record) 列の別名と同じ名前の コンポーネントに入る TableEntity toEntity()で値の形を揃える List<TableEntity> を返す
図3 1回の取得の通り道。リポジトリの12個のメソッドは、どれもこの形で1つ以上のSQLを実行します(selectDatabaseだけは1行を返すselectOne)。DTOからエンティティへの変換で、値が無いことを空文字へ、カンマ区切りの列名をリストへ、という具合に値の形をドメインの表現へ揃えます(8.3節)。

読むときに戸惑いやすい点が4つあります。

// AbstractCatalogRepository(要約)
baseSqlPath = "com.dbxray.domain.repository." + databaseType.id() + ".CatalogRepository.";

try (SqlSession session = sqlSessionFactory.openSession()) {     // 呼ぶたびにプールから接続を借りる
  return session.selectList(baseSqlPath + sqlId, Map.of("schemaList", schemaList, "tableList", tableList));
} catch (PersistenceException e) {
  throw new RuntimeException("Failed to select: " + baseSqlPath + sqlId, e);   // どのSQLかを添えて包む
}
リポジトリのメソッドSQLのIDDTO → エンティティ呼ぶ場所
selectDatabaseselectDatabaseInfoDatabaseDto → DatabaseEntity(メジャーバージョンを含む)fetchTargets
selectTableListselectTableInfoTableDto → TableEntity(区分をenumへ)fetchTargets
selectForeignKeyListselectForeignKeyInfoForeignKeyDto → ForeignKeyEntity(多重度を判定)fetchTargets
selectTriggerListselectTriggerInfoTriggerDto → TriggerEntity(イベントを分解、本体の末尾の空白を除く)fetchTargets(target.objectsにtrigger)
selectPartitionListselectPartitionInfoPartitionDto → PartitionEntityfetchTargets
selectViewReferenceListselectViewReferenceInfoViewReferenceDto → ViewReferenceEntity(区分をenumへ)fetchTargets(出力対象のビューの分へ絞る)
selectFunctionListselectFunctionInfoFunctionDto → FunctionEntity(本体なし)fetchTargets(function)
selectSequenceListselectSequenceInfoSequenceDto → SequenceEntityfetchTargets(sequence)
selectTypeListselectTypeInfoTypeDto → TypeEntityfetchTargets(type)
selectColumnListselectColumnInfoColumnDto → ColumnEntityfetchDiagramBoxes(ER図の箱)
selectFunctionDefListselectFunctionDefInfoFunctionDto → FunctionEntity(本体あり)export(スキーマごと)
selectTableDetailsselectColumnInfo・selectIndexInfo・selectConstraintInfo3種類をテーブルごとに振り分けてTableDetailexport(チャンクごと)

実行したSQL(DEBUG)と取得した行(TRACE)は、MyBatisがmapperの名前空間(このツールのパッケージ配下)のロガーへ出します。既定のレベルはINFOなのでログには残らず、調べるときだけ-Ddbxray.log.level=debugで上げます(2.6節)。Oracle用のmapper(838行)は、LONG型の列(ビューのソース・トリガーの本体等)やDBのバージョン(DBMS_DB_VERSION)を読むためにSQLの中でPL/SQLの関数を定義する(WITH FUNCTION)など、PostgreSQL用(670行)より手が込んでいます。DBごとの違いはSQLで吸収し、同じDTOへ揃えます。

2.4 Mermaid の erDiagram

ER図は、Markdownの中に書くMermaid記法のテキストです。GitHubは```mermaidのコードブロックを図として描画します。サンプルのdepartmentの定義書に出力されたER図をそのまま示します。

```mermaid
erDiagram
    sample_department["department"]
    sample_employee["employee(従業員)"]
    sample_department ||--o{ sample_employee : "employee_department_id_fkey"
    sample_department {
        integer department_id PK
        character_varying department_code
        character_varying department_name
    }
    sample_employee {
        integer department_id FK "所属部署ID"
    }
```
行意味作るメソッド
sample_department["department"]識別子と表示名。識別子は「スキーマ_テーブル」で、記号は_に置き換える。表示名は「テーブル名(論理名)」MermaidSupport.mermaidId・aliasLine・assignLabels
A ||--o{ B : "名前"関連線。親→子の向き。両端の記号が多重度、--が実線(物理の外部キー)、..が破線(論理リレーション)MermaidSupport.relationLine・Cardinality.notation
B { 型 名前 PK, FK "論理名" }箱に表示するカラム。型は桁数を除き、記号を_にするMermaidSupport.attributeBlock・sanitizeType

Mermaidのclick構文はGitHubで無効になるため、図からテーブル定義書へは、図の下に置く「ER図に掲載しているテーブル」の表のリンクで辿ります。

2.5 JSON Lines と Jackson

スナップショットは、1行に1オブジェクトのJSONを並べたJSON Lines(.jsonl)です。1テーブルが1行なので、gitの差分がそのままオブジェクト単位の差分になり、--checkも行をオブジェクトとして突き合わせられます。JSONへの変換はJacksonSnapshotSerializerが行い、次の性質に依存しています。

// snapshot/testdb/database.json
{"formatVersion":1,"name":"testdb","dbms":"PostgreSQL","majorVersion":16}

DBの版はメジャーバージョンだけを持ちます(DatabaseEntity.majorVersion)。マイナー版を更新したときに、--checkが差分を報告しないようにするためです。

domain層はJacksonに依存しません。変換のインタフェースSnapshotSerializerをdomainに置き、Jacksonを使う実装をinfrastructureに置いています。

2.6 log4j2 と ArchUnit

サンプルDBに対して通常実行したときの、既定のレベルのログファイルの全体です(日時とスレッド名を省き、長いパスを短くしています)。

INFO  MyBatisSqlSessionFactories Initializing SqlSessionFactory. [config=mybatis-config.xml]
INFO  MyBatisSqlSessionFactories Initialized SqlSessionFactory.
INFO  DatabaseTypeDetector Connected to the database. [product=PostgreSQL 16.15 (…), driver=PostgreSQL JDBC Driver 42.7.13]
INFO  ExportSchemaController [START] exportSchema
INFO  SidecarYamlRepository Loaded sidecar. [annotations=…/annotations.sample.yml, tableCount=2, relationCount=1, viewpointCount=3]
INFO  SchemaExportPipeline Skipping orphan table annotation check because the output target is filtered.
INFO  SchemaExportPipeline Merged logical relations declared in the sidecar. [relationCount=1]
INFO  SchemaExportPipeline Exported schema objects. [tables=15, functions=11, sequences=9, types=3, triggers=5]
INFO  ExportSchemaController [ END ] exportSchema [elapsedMillis=1395]

メッセージは英語で要旨を先に書き、値は末尾の[key=value, …]にまとめる、というのがこのリポジトリのログの書式です(docs/development/coding-guidelines.mdの「ログ」)。

3全体像と読む順

com.dbxrayの下は、4つの層と、層の外の2つ(config・shared)に分かれます。依存の向きはpresentation → application → domain ← infrastructureで、domainは他のどの層にも依存しません。DB・ファイル・JSONとやり取りするインタフェースをdomainに置き、その実装をinfrastructureに置く(依存関係逆転)ことで、外部に触れるコードをドメインの外へ出しています。

エントリーポイント(直下) main・CLI引数・設定・出力先の検証・Components presentation ExportSchemaController FailureReporter 差分の文面・終了コード application ExportSchemaUsecase CheckDocumentDiffUsecase SchemaExportPipeline config ConfigFile YamlSyntaxErrors InvalidConfigurationException domain domain.model(12パッケージ) table・relation・target・sidecar・viewpoint schemaobject・snapshot・insight・database document・metrics・tableusage(関数の利用テーブル) エンティティ・集合・値オブジェクト・ルール domain.repository(インタフェース) DB・サイドカーYAML・ファイルとのやり取り domain.service export(出力形式ごとのSink)・target(突き合わせ) path(配置の規則)・snapshot(書き込み・比較) insight(参考情報)・UnifiedDiffGenerator tableusage(定義本体から利用テーブルを抽出) writer:Markdownの段取りとテンプレート infrastructure(33) db MyBatis・DTO・接続 file ファイル・サイドカーYAML path 出力先パスの解決 snapshot Jackson(JSON) shared.exception(2) UserCorrectable Exception どの層からも使う
図4 パッケージと依存の向き。矢印は「相手を知っている(importしている)」向きで、domainから外へ向かう矢印はありません。infrastructureはdomainのインタフェース(リポジトリ・OutputPathResolver・SnapshotSerializer)を実装します。エントリーポイントは全体を組み立てる役割(Componentsがinfrastructureの実装をnewする)のため全層に依存でき、configに依存してよいのはエントリーポイントとinfrastructure(ConnectionSettingsがInvalidConfigurationExceptionを使う)だけです。
層・パッケージひとことで
エントリーポイント(直下)main。CLI引数・設定・出力先の検証と、依存関係の組み立て(Components)。--help・--versionの表示
presentation(.dto・.type)ユースケースを呼んで結果を文面にする。失敗の報告、終了コード
application2つのユースケースと、両者が共有する取得・書き出しの段取り
domain.model(12パッケージ)エンティティ・集合・値オブジェクトと、それらが持つルール
domain.repositoryDB・サイドカーYAML・ファイルとのやり取りのインタフェース
domain.service(writer以外)出力形式ごとのSink、突き合わせ、配置の規則(DB由来の名前を安全なパスにする)、スナップショットの書き込み・比較、関数の定義本体から利用しているテーブルを抽出するtableusage(9.9節)
domain.service.writerMarkdownの段取り(Writer)と組み立て(Templates)
infrastructureMyBatis・DTO・接続、ファイル、サイドカーYAML、パス、Jackson
config設定ファイルの読み込み(YAMLの構文誤りの文言を含む)
shared.exception利用者が直せる誤りを表す例外

構造の規約(ArchitectureTestが検査するもの)

次の規約はArchUnitの単体テストで検査され、破るとビルドが失敗します。違反したら、テストを緩めずに依存や置き場所のほうを直す、というのがこのリポジトリの方針です(docs/development/coding-guidelines.md)。

規約内容
レイヤーの依存方向presentationを使えるのはエントリーポイントだけ。applicationはエントリーポイントとpresentationだけ。infrastructureはエントリーポイントとconfigだけ。configはエントリーポイントとinfrastructureだけ
domain.modelの循環概念ごとのパッケージ同士が循環して依存しない(向きはdomain-model.mdの図)
shared例外だけを置き、JDK以外に依存しない
ファイルI/Oapplication・domainはjava.nio.file.Filesを直接使わない(FileRepositoryを通す)
出力先のパスPath.resolve・resolveSiblingを呼ぶのはinfrastructure.pathだけ(DB由来の名前を含むパスを、出力先の外を指さないことを確かめるOutputPathResolverの実装でだけ組み立てる。10章)
現在日時引数なしのnow()を呼ばない(DIで受け取るClockから求める。テストで日付を固定するため)
DIコンテナGuice・jakarta.injectに依存しない(@Injectは付けず、Componentsがnewで組み立てる)
接尾辞と置き場所…Entity→domain.model、…Dto→infrastructure.db.repository.dtoかpresentation.dto、…Templates→domain.service.writer、…Writer→domain.service、…Locations→domain.service.path、…Usecase・…Request→application
リポジトリdomain.repositoryには…Repositoryのインタフェースだけ。実装はinfrastructure..repository
定義本体を読むパッケージdomain.service.tableusage・domain.model.tableusageは正規表現を使わない(java.util.regexと、String.matches・replaceAll・replaceFirst・split・splitWithDelimitersと、java.util.Scanner)。定義本体はDBでCREATEの権限を持つ人が書ける入力で、正規表現はバックトラックで入力の長さに比例しない時間がかかり得る(ReDoS)ため、1文字ずつ読む(9.9節)

クラス名の接尾辞が表す役割

接尾辞役割例
…EntityDBから取得したメタ情報1件のrecordTableEntity・ColumnEntity
複数形エンティティの集合。検索・振り分けのメソッドを持つ(ファーストクラスコレクション)Tables・ForeignKeys・Triggers
…Dto層の境界で受け渡すだけの形(SQLの結果・コントローラーの結果)TableDto・ResultDto
…Repository外部とのやり取り。インタフェースはdomain、実装はinfrastructureCatalogRepository
…Usecase・…Requestユースケースと、その入力をまとめたrecordExportSchemaUsecase
…SinkFactory出力形式ごとのExportSinkを、出力先を決めて作るMarkdownExportSinkFactory
…Writer何を・どの順で・どのファイルに書くかの段取りErDiagramWriter・SchemaSnapshotWriter
…TemplatesMarkdownの行・セクションの組み立て(副作用の無いstaticメソッド)TableDefinitionTemplates
…Locations出力物の配置・ファイル名の規則DocumentLocations・SnapshotLocations
…Snapshotスナップショットの1行(JSONになるrecord)TableSnapshot

クラス名の本体(何についてのクラスか)は、利用者向けのドキュメントの呼び方に揃えた用語集(domain-model.md)に従います。ただしForeignKeyEntity・ForeignKeysは歴史的経緯による名前で、外部キーだけでなく論理リレーションも含む「関連」を表します。

おすすめの読む順

上から順に読むと、外側(起動と入口)から内側(ルール)へ進めます。

  1. Dbxray:main→run→Components.create→createController。処理全体の骨格です(4章)。
  2. ExportSchemaControllerと2つのユースケース:2つのモードの違いが「どのSinkを渡すか」だけだと分かります。
  3. SchemaExportPipeline:取得の順序と、スキーマ・チャンク単位の分割。6章の図と合わせて読むと速いです。
  4. ExportSinkと3つの…SinkFactory:出力形式ごとに「いつ何を書くか」(6章の表)。
  5. domain.model.target:ExportTargets・TableDefinitionContent・TableScope。取得したデータを組み立てる単位です(8章)。
  6. domain.model.table・relation:TableKey・ForeignKeys・ForeignKeyGroups・Cardinality。関連とER図のルールです(9章)。
  7. 関数が利用しているテーブルの抽出(--preview):FunctionTableUsageAnalyzer→FunctionBodyLocator→TableReferenceScanner→SqlLexerの順に、9.9節の図と合わせて読みます。
  8. 気になる出力のWriter→Templates:たとえばTableDefinitionWriter(セクションの順)→TableDefinitionTemplates(各セクションの組み立て)。
  9. infrastructure:AbstractCatalogRepositoryとmapper XML、SidecarYamlRepository。
  10. 入口の検証:CliArguments・DbxrayProperties・ConnectionSettings・FailureReporter(5章)。

4起動から終了までの流れ

Dbxray.mainは、処理全体を1つのtry-catchで囲み、次の順に進みます。前半(1〜6)はDBへ接続せずに入力を検証し、後半(7〜11)で接続して出力します。入力の誤りを接続より前に報告するのは、DBに接続できない環境(接続情報の誤り・DBの停止中)でも、設定の誤りに先に気付けるようにするためです。

CliArguments.parse(args) 引数を最後まで解析する(ここでは例外を投げない) requireKnownArguments() 開始メッセージを出してから、未知の引数を誤りにする DbxrayProperties.of(ConfigFile.load(…), …) conf/config.ymlを1回読み、CLI引数を重ねて検証し、Requestにする Components.create() DB種別に依存しない部品(第1段) outputDirectoryValidator().validate(request) 出力先がファイルでないか、--rm-distで消してよいか ConnectionSettings.merge(…) config.ymlのdatabase+環境変数+CLI引数を重ねて検証 ここまでDBに接続しない(入力の検証) ここから接続する MyBatisSqlSessionFactories.create(…) SqlSessionFactoryを1回だけ作る(まだ接続しない) DatabaseTypeDetector.detect(…) 接続し、製品名から種別を判定する(版をログへ) components.controller(種別, sqlSessionFactory) DB種別に依存する部品を組み立て、コントローラーを返す(第2段) controller.execute(request) / checkDiff(request) ユースケース → SchemaExportPipeline(6章・7章) [result]:SUCCESS を表示 → System.exit(コード) 通常実行は0。--checkは差分なし0・差分あり1 1 2 3 4 5 6 7 8 9 10 11 未知の引数(--chek 等) UserCorrectableException 設定の誤り(見つかった分をまとめて) InvalidConfigurationException 出力先がファイル/--rm-distで危険な場所 UserCorrectableException driver・url未指定、未知のキー等 InvalidConfigurationException DBに接続できない/非対応のDB UserCorrectableException サイドカーYAMLが無い・読めない/SQL・I/O 利用者が直せる誤り/想定外の失敗 main の catch (Throwable) FailureReporter → [result]:FAIL・終了コード2
図5 起動から終了まで。例外を捕まえるのはmainの1箇所だけで、どの段階の失敗も右端を通ってそこへ集まります。CliArguments.parseは未知の引数があっても例外を投げず、モード(--checkか)を決めてから2で誤りにします。そのため--chekと書き誤ると、通常実行の開始メッセージの後に失敗します。
// Dbxray.run(通常実行。runCheckも同じ形)
System.out.println("Starting output of database documents. …");
cliArguments.requireKnownArguments();
final ConfigFile configFile = ConfigFile.load(cliArguments.configPath());   // 設定ファイルは1回だけ読む
final ExportSchemaRequest request =
    DbxrayProperties.of(configFile, cliArguments.settingOverrides())
        .toExportSchemaRequest(cliArguments.isRmDist(), cliArguments.isPreview());
final Components components = Components.create();
components.outputDirectoryValidator().validate(request);
final ConnectionSettings connectionSettings = connectionSettings(configFile, cliArguments);   // ConnectionSettings.merge
final ResultDto resultDto = createController(components, connectionSettings).execute(request);
System.out.println(resultDto.resultMessage());
return ExitStatus.SUCCESS;

入口で文字列を型に変える

設定値は生の文字列のまま深い層へ渡しません。DbxrayPropertiesが空白を除き、整数へ変換し、出力対象の条件をTargetSelection.ofで型(TableScope・OutputObjectTypeの集合)へ変換します。誤りは1件ずつではなく、見つかった分をまとめて報告します。結果はrequestのrecordになり、エントリーポイント→コントローラー→ユースケースの3層を分解・再構築されずに通ります。

record持つもの使う場所
ExportSchemaRequesttargetSelection・sidecarPath・outputPath・chunkSize・erDiagramMaxNodes・erDiagramDistance・rmDist・previewFeatures(有効にするプレビューの機能の集合)通常実行
CheckDocumentDiffRequest上からerDiagramMaxNodes・erDiagramDistance・rmDist・previewFeaturesを除いたもの(Markdownを描かず、出力先も消さず、プレビューの機能も使わないため)--check
TargetSelectionTableScope(スキーマ名+テーブル名パターン)とSet<OutputObjectType>両方のrequestの中

5失敗の扱いと終了コード

失敗は3種類に分けて扱います。どれに当たるかで、表し方と利用者への報告が変わります。

種類例表し方報告
利用者が直せる誤り設定・引数の誤り、出力先が危険、DBに接続できない、非対応のDB、サイドカーYAMLが無い・読めないUserCorrectableException(設定ファイルの誤りは派生のInvalidConfigurationException)。何を直せばよいかをメッセージに書く[result]:FAIL+メッセージ。ログにスタックトレースを残さない
想定外の失敗I/O・SQLの失敗、不具合、JVMのエラー非検査例外のまま伝える[result]:FAIL+メッセージ+ログの場所。ログにスタックトレース
業務上の結果--checkの差分、孤児付帯情報、除外した関連例外にせず値で返す(DiffResult・ConsistencyNotice)差分の報告、警告のログ([warn]:)

捕まえるのは main の1箇所だけ

コントローラー・ユースケースは例外を捕まえません。mainがThrowableまで捕まえてFailureReporterに渡し、終了コード2で終わります(Errorまで捕まえるのは、捕まえないとJVMが終了コード1で終わり、--checkの「差分あり」と区別できなくなるため)。途中の層でcatchしてよいのは、次の3つの場合だけです。

目的実例
検査例外を非検査例外で包むLocalFileRepository(IOException→UncheckedIOException)、AbstractCatalogRepository.select(どのSQLかを添えてRuntimeExceptionで包む)
下位の例外を利用者が直せる誤りへ置き換えるDatabaseTypeDetector(接続の失敗)、SidecarYamlRepository(YAMLの構文誤り)、DbxrayProperties(TargetSelectionのIllegalArgumentException)、ConfigFile(YAMLの構文誤り。文言はYamlSyntaxErrors)
フォールバックするSidecarYamlRepository(誤った観点を、警告して読み飛ばす)

包むときは原因(cause)を渡し、tryの範囲は置き換えたい呼び出しだけに絞ります。例外はYAMLの構文誤りで、SnakeYAMLの例外のメッセージは誤りの行をそのまま引用するため(設定ファイルに誤って書いたパスワードが画面とログに出得る)、YamlSyntaxErrorsで種類と位置だけの文言にし、原因は渡しません。UserCorrectableExceptionを投げるのは、入口(CLI引数・設定・出力先の検証)と、利用者の入力・実行環境に触れるインフラ(DBへの接続・サイドカーYAMLの読み込み)だけで、domain・applicationでは投げません。

FailureReporter の報告の形

[errmsg]に例外のメッセージを、[cause]に原因の連鎖のうち表示に含まれていないものを浅い順に並べます。原因のメッセージを繰り返しているだけの例外(MyBatisの例外等)は省くため、包んだ箇所でDBが返したエラーが失われず、かつ重複もしません。以下は実際の出力です。

$ java -jar dbxray.jar --chek
Starting output of database documents.
Please wait a moment ...

[result]:FAIL
Failed to output database documents.
 [errmsg]:Unknown argument: --chek (values are not shown; specify a value as --name=value) (available arguments: --check, --rm-dist, --preview, --help, --version, --config=<path>, --db-driver=<value>, …, --annotation-path=<value>)
$ java -jar … --chunk-size=abc --output-objects=trigger,view --table='sample.,!'
[result]:FAIL
Failed to output database documents.
 [errmsg]:Invalid configuration in conf/config.yml (overridden by --table, --output-objects, --chunk-size).
  - output.chunkSize must be an integer: abc
  - Invalid table pattern: sample., ! (the table name and the schema name must not be empty)
  - Unknown output object type: view
$ java -jar … --db-url=jdbc:postgresql://localhost:5999/testdb …(DBが待ち受けていないポート)
[result]:FAIL
Failed to output database documents.
 [errmsg]:Could not connect to the database. Check the connection settings (database in the configuration file, --db-* arguments and the DBXRAY_DB_PASSWORD environment variable) and that the database is running.
 [cause]:org.postgresql.util.PSQLException: Connection to localhost:5999 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
 [cause]:java.net.ConnectException: Connection refused

1つ目の(values are not shown; …)は、解釈できない引数の値を伏せている印です(--db-pasword=…のような書き誤りで、パスワードが画面とログファイルに残らないように)。2つ目は、3つの誤りがまとめて1回で報告されている例です。(overridden by …)は、誤った値がCLI引数から来ている可能性を示します。想定外の失敗では、最後にAn unexpected error occurred. See the log file for details. [logFile=./var/log/dbxray.log]が付きます。

警告:処理は続けるが知らせる

サイドカーYAMLの個々の記述の誤りや、DBと付帯情報の乖離は、読み飛ばして警告します(付帯情報の一部の書き誤りで、定義書全体の再生成を止めないため)。警告はWARNのログで、log4j2.xmlがログファイルに加えて標準エラーへ[warn]:付きで出します。以下は、誤りを含むサイドカーYAMLで実行したときの標準エラーです(終了コードは0)。

[warn]:Ignoring unknown key in 'tables' entry of sample.department. [key=descripton, annotations=warn.yml]
[warn]:Ignoring unknown 'cardinality' and falling back to the default. [entry=relations #1, cardinality=多対多, default=1対多, annotations=warn.yml]
[warn]:Ignoring viewpoint. Invalid viewpoint id: '人事' (use only letters, digits, '-' and '_', because it is used as a file name) [entry=viewpoints #1, id=人事, annotations=warn.yml]
[warn]:Annotation exists for a table that was not found (renamed or dropped?). [table=sample.employe]
[warn]:Column annotation exists for a column that was not found (renamed or dropped?). [table=sample.department, column=department_cd]

上の3行はYAMLを読むSidecarYamlRepositoryが直接ログへ出したもの(entry=relations #1は何件目の定義かを示す)、下の2行はExportTargetConsistencyが通知(ConsistencyNotice)として値で返し、SchemaExportPipeline.reportが重要度に応じてログへ出したものです。判定をするドメインサービスは、結果をどこへ出すかを知りません。

出力そのものの食い違いも警告します。関数のオーバーロードの個別定義は{関数名}_{番号}.mdになるため、番号を付けた名前が別の関数の名前と重なる(calcのオーバーロードと関数calc_1)と、後に書いたほうだけが残り、関数一覧のリンクもすべて同じファイルを指します。ExportSchemaUsecaseは書き出しの前にDocumentLocations.functionsSharingDefinitionFileで重なりを求め、Function documents share the same file name, so only the one written last is kept. [file=…, functions=…]を出します(ファイル名は互換性を約束する範囲のため、区切りを変えて避けることはしていません)。

孤児カラム備考の警告は、サイドカーYAMLに書いた順に出ます(TableAnnotationがカラム備考を記述順のまま持ち、orphanColumnNamesは順序のあるListで返す)。同じ入力なら毎回同じ順序になります。

終了コード

コードExitStatus意味
0SUCCESS成功(--checkで差分なしを含む)
1DIFFERENCE_FOUND--checkで差分あり
2FAILURE失敗。仕様上は「2以上」と範囲で定めていて、失敗の種類を分けるときは3以上を足す(POSIXのdiff・cmpと同じ考え方)

--checkで差分があっても、表示の1行目は[result]:SUCCESSです。比較そのものは成功したという意味で、差分の有無は終了コードで判定します。JVMが起動できない場合などmainに入る前の失敗は、Javaの仕様で終了コード1になります。

6取得と書き出しの段取り(SchemaExportPipeline)

通常実行のユースケースは短く、取得と書き出しの段取りをSchemaExportPipelineに任せています。--checkのユースケースも同じパイプラインを使い、違いは渡すExportSinkのリストだけです。

// ExportSchemaUsecase.exportSchema
final Path outputBaseDir = outputPathResolver.resolveOutputBaseDir(request.outputPath());
final ExportTargets targets =
    schemaExportPipeline.fetchTargets(request.targetSelection(), request.sidecarPath());   // 一括取得
final DiagramBoxes diagramBoxes =
    schemaExportPipeline.fetchDiagramBoxes(targets, request.chunkSize());                // ER図の箱
warnFunctionsSharingDefinitionFile(targets.functions());   // 個別定義のファイルが重なる関数を警告(5章)
if (request.rmDist()) {
  removeOutputBaseDir(outputBaseDir);   // 取得に成功してから消す
}
schemaExportPipeline.export(
    targets,
    List.of(
        markdownSinkFactory.create(
            outputBaseDir,
            NodeLimit.of(request.erDiagramMaxNodes()),
            request.erDiagramDistance(),     // テーブル定義書のER図の描画距離(9.7節)
            diagramBoxes,
            targets.foreignKeys()),
        snapshotSinkFactory.create(outputBaseDir),
        insightSinkFactory.create(outputBaseDir)),
    request.chunkSize(),
    request.previewFeatures());   // --previewで有効にした機能。空なら利用テーブルの抽出をしない(9.9節)

軽い情報は一括で、重い情報はチャンクで

対象スキーマ全体を一度にメモリへ載せないよう、取得を2つの粒度に分けています。テーブル一覧・関連・トリガー等は1件が軽く、ER図や被参照情報でスキーマ・チャンクをまたぐ参照を解決するのに全件が要るため、最初に一括で取得して最後まで持ちます。カラム・インデックス・制約はテーブル数に比例して重くなるため、スキーマごと・chunkSize件ごとに取得し、書き出したら捨てます。

メモリに持つもの fetchTargets(targetSelection, sidecarPath) → ExportTargets ① サイドカーYAMLを読む(SidecarRepository.load) ② selectDatabaseInfo → 基本情報。生成日は Clock から ③ selectTableInfo → TableScope.matches で絞り込み → Tables ④ 付帯情報・観点のパターンを、テーブルと突き合わせる(通知) ⑤ selectForeignKeyInfo+論理リレーション → resolveForeignKeys → ForeignKeys ⑥ selectTrigger/Partition/ViewReferenceInfo・selectFunction/Sequence/TypeInfo fetchDiagramBoxes(targets, chunkSize) → DiagramBoxes(通常実行だけ) 関連を持つテーブルのカラムをチャンク単位で取り、関連カラムだけ残す(selectColumnInfo) --rm-dist のときだけ出力先を削除(取得に成功した後。失敗しても既存の出力は残る) export(targets, sinks, chunkSize, previewFeatures) 全Sinkの writeOverview(targets) 一覧・ER図・観点・シーケンス/型の個別定義、database.json 等 スキーマごと(関数があるスキーマ) selectFunctionDefInfo(定義本体)→ 関数ごとに FunctionDefinitionContent (--preview なら FunctionTableUsageAnalyzer.analyze で利用テーブルを抽出) → 全Sinkの writeFunctionDefinitions スキーマごと(tables.bySchema()) 全Sinkの beginSchemaTables(スナップショットは tables.jsonl を空にする) チャンクごと(chunkSize件。0以下ならスキーマ全体) selectTableDetails:selectColumnInfo・selectIndexInfo・selectConstraintInfo テーブルごと:TableDefinitionContent.assemble → 全Sinkの writeTableDefinition(孤児カラム備考も検出)・カラムを集計へ 全Sinkの writeSummary(targets, metrics) DatabaseMetrics(集計)。Markdownは README.md を書く。他の形式は何もしない DatabaseMetrics.Builder スキーマごとの数だけ。最後まで持つ ExportTargets テーブル一覧・関連・トリガー・ パーティション・ビューの参照・ 関数等の一覧・付帯情報・観点 等 最後まで持ち続ける ER図・被参照はスキーマ・チャンクを またぐため、全件が要る DiagramBoxes 関連カラムだけ。最後まで持つ 関数の定義本体・抽出結果 1スキーマ分。書いたら捨てる TableDetail カラム・インデックス・制約 最大 chunkSize 件分 書いたら捨てる
図6 SchemaExportPipelineの段取り。破線の枠はループです。ER図は、テーブル一覧・関連・関連カラム(DiagramBoxes)だけで描けるので、詳細をチャンク単位で取得する前のwriteOverviewで書き出せます。chunkSizeを小さくするとメモリは減り、DBへの問い合わせは増えます。出力の内容は変わりません(結合テストがchunkSizeを変えて同じ出力になることを確かめています)。READMEの集計はカラムの数を含むため、全テーブルを書き出した後のwriteSummaryで書きます。

サンプルDB(ビューを含む15テーブル、schema=sample)で通常実行したときに発行されたSQLの順序です(-Ddbxray.log.level=debugで実行したログファイルから抜き出したもの)。

順段階SQLのID行数
—fetchTargets(サイドカーYAMLを読む。SQLではない)—
1selectDatabaseInfo1
2selectTableInfo15
3selectForeignKeyInfo10
4selectTriggerInfo5
5selectPartitionInfo7
6selectViewReferenceInfo7
7selectFunctionInfo11
8selectSequenceInfo9
9selectTypeInfo3
10fetchDiagramBoxesselectColumnInfo(関連を持つ12テーブル分)63
11export:関数(スキーマごと)selectFunctionDefInfo11
12export:テーブル(チャンクごと)selectColumnInfo75
13selectIndexInfo20
14selectConstraintInfo29

15テーブルが既定のchunkSize(3000)に収まるため、12〜14は1回ずつです。chunkSize=1なら12〜14がテーブルの数だけ繰り返されます。--checkでは10(ER図の箱)を発行しません。6の7行は、サンプルの3つのビューが参照するテーブルの数の合計です(9.8節)。

出力形式ごとのSink

ExportSinkは「取得した情報を1つの出力形式で書き出す」インタフェースで、5つのメソッドを持ちます。パイプラインは取得した順にこれを呼ぶだけで、何をどのファイルへ書くかは実装(…SinkFactoryの内部クラス)が決めます。出力先は、Sinkを作るときに渡します。

呼ばれる時点Markdownスナップショット参考情報
writeOverview(targets)
一括取得の後に1回
テーブル一覧・ER図と索引・トリガー/関数/シーケンス/型の一覧・観点ページと観点一覧・シーケンス/型の個別定義database.json・sequences.jsonl・types.jsonlviewpoints.json(観点があれば)
writeFunctionDefinitions
スキーマごと
関数・プロシージャの個別定義(--previewなら「利用しているテーブル」の節つき)functions.jsonl(利用しているテーブルは含めない){スキーマ}/functionTableUsages.json(抽出した場合だけ。抽出していない実行は前回のファイルを削除)
beginSchemaTables
スキーマごと(既定は空の実装)
何もしないtables.jsonlを空にする(前回の内容へ追記しないため)何もしない
writeTableDefinition(content)
テーブルごと
テーブル定義書tables.jsonlへ1行追記何もしない
writeSummary(targets, metrics)
全テーブルの後に1回(既定は空の実装)
README(出力する一覧へのリンクと、出力対象の集計)何もしない何もしない
使うモード通常実行通常実行・--check(一時ディレクトリへ)通常実行

writeFunctionDefinitionsが受け取るのは、関数・プロシージャ1つ分の出力内容FunctionDefinitionContent(関数+利用しているテーブル)のリストです。パイプラインがスキーマごとに、--previewのときだけFunctionTableUsageAnalyzerで抽出し、そうでなければ「抽出していない」印(FunctionTableUsage.notAnalyzed())を持たせます。Sinkは抽出するかどうかを知らず、Markdownは「利用しているテーブル」の節を出すか、参考情報はファイルを書くか消すかを、この印で決めます(9.9節)。

出力形式を足すときに触る場所

ExportSinkを実装したクラスとそのFactoryを作り、FactoryをComponentsで組み立てて、ユースケースが渡すリストに加えます。SchemaExportPipelineは出力形式を知らないので変えません。--checkの比較対象にしたくない出力は、CheckDocumentDiffUsecaseのリストに加えないだけで、比較から外れます(参考情報がこの方法で外れています)。

突き合わせの通知はログへ

取得の途中で、出力対象のテーブルと、関連・付帯情報・観点をExportTargetConsistency(ドメインサービス)が突き合わせ、気付くべき事柄をConsistencyNoticeの値で返します。パイプラインのreportが重要度に応じて、INFOはログファイルだけへ、WARNはログファイルと標準エラーへ出します。通知の種類は9.2節にまとめています。

7差分検知(--check)

--checkは「DBの今の状態から作ったスナップショット」と「出力先にコミット済みのスナップショット」を比べます。取得は通常実行と同じパイプラインで、Sinkをスナップショットの1つに減らし、書き込み先を一時ディレクトリにするだけです。

// CheckDocumentDiffUsecase.checkDocumentDiff
final Path committedDir = outputPathResolver.resolveOutputBaseDir(request.outputPath());
final Path generatedDir = fileRepository.createTempDirectory(CHECK_TEMP_DIR_PREFIX);
try {
  final ExportTargets targets =
      schemaExportPipeline.fetchTargets(request.targetSelection(), request.sidecarPath());
  schemaExportPipeline.export(
      targets, List.of(snapshotSinkFactory.create(generatedDir)), request.chunkSize(),
      Set.of());   // プレビューの機能はスナップショットを変えないため、差分検知では有効にしない
  return snapshotDiff.compare(
      outputPathResolver.resolveSnapshotDirectory(generatedDir),
      outputPathResolver.resolveSnapshotDirectory(committedDir));
} finally {
  fileRepository.deleteDirectory(generatedDir);
}
DB 今の状態 SchemaExportPipeline fetchTargets → export Sinkはスナップショットだけ 一時ディレクトリ/snapshot/ 生成側(generated) finallyで必ず削除 {output.path}/snapshot/ コミット側(committed)。読むだけ SnapshotDiff.compare 行をオブジェクトとして 突き合わせ、違いには unified diffを付ける DiffResult 生成側だけ・コミット側だけ・内容が違う(unified diff付き) DiffReportFormatter.format diffは1件200行・全体2000行まで載せる 差分が1件でもあれば終了コード1(DiffCheckResultDto.exitStatus)
図7 --checkの流れ。比較対象の出力先には書き込みません。比較はsnapshot/の下だけなので、兄弟のinsights/やMarkdownは構造的に比較から外れます。

行をオブジェクトとして突き合わせる

SnapshotDiffは両方のディレクトリのファイルを読み、1行ごとに「どのオブジェクトの行か」の表示名を付けて突き合わせます。表示名はSnapshotKind.identifyが作り、同じ表示名でも別のファイル(別のDB)なら別物として扱います(内部のrecord Targetはファイルと表示名の組)。

ファイル突き合わせの単位表示名の例
{スキーマ}/tables.jsonl1行=1テーブル(ビューを含む)table sample.employee
{スキーマ}/functions.jsonl1行=1関数・プロシージャ。同名は引数で区別function sample.calculate_bonus(p_salary numeric)
{スキーマ}/sequences.jsonl・types.jsonl1行=1オブジェクトsequence sample.invoice_no_seq
上記以外(database.json)ファイル全体testdb/database.json

内容が違うものは、両側の行をSnapshotSerializer.formatForDiffで「1項目1行・配列は1要素1行」に整形してから、UnifiedDiffGenerator(Myers法の自前実装。前後3行の文脈)でunified diffにします。1行のJSONのままだと、差分が「丸ごと削除+丸ごと追加」にしかならないためです。整形後の行番号は、ファイル上の行番号とは対応しません。

サンプルDBにsample.employee.nicknameカラムとsample.tmp_new_tableテーブルを足してから--checkした実際の出力です(終了コード1)。

[result]:SUCCESS
Difference detected between the generated document and the committed document.
Only in generated document (possibly missing commit):
 - table sample.tmp_new_table
Content differs:
 - table sample.employee
--- committed/testdb/sample/tables.jsonl (table sample.employee)
+++ generated/testdb/sample/tables.jsonl (table sample.employee)
@@ -15,6 +15,7 @@
     {"name":"profile","logicalName":"プロフィール(JSON)","type":"jsonb","primaryKey":false,"notNull":true,"defaultValue":"'{}'::jsonb"}
     {"name":"hired_date","logicalName":"入社日","type":"date","primaryKey":false,"notNull":true,"defaultValue":"CURRENT_DATE"}
     {"name":"updated_at","logicalName":"更新日時","type":"timestamp without time zone","primaryKey":false,"notNull":true,"defaultValue":"now()"}
+    {"name":"nickname","type":"text","primaryKey":false,"notNull":false}
   ]
   "indexes": [
     {"name":"employee_employee_code_key","method":"btree","unique":true,"primary":false,"definition":"CREATE UNIQUE INDEX employee_employee_code_key ON sample.employee USING btree (employee_code)"}

足したカラムにはコメントが無いためlogicalNameの項目がありません(空は出力しない、2.5節)。

検知しないもの

8データの持ち方

8.1 テーブルキーで繋ぐ

このツールはDBを読むだけなので、更新の整合性を守るための集約や状態遷移はありません。代わりに設計の軸になっているのがテーブルキー(TableKey=スキーマ名+物理テーブル名)です。カラム・制約・トリガー・関連・付帯情報は、所属するテーブルをオブジェクトの参照ではなくテーブルキーで指します。だから、別々のSQLで、別々の時点で取得した情報を、後からテーブルキーで引き合わせて1テーブル分に組み立てられます。

TableKey schema + table sample.employee TableEntity TableKey.of(table) Tables contains(key)・find(key) ForeignKeyEntity tableKey() = 参照元(子) referenceTableKey() = 参照先(親) Column・Index・Constraint SchemaTableKeyed.tableKey() TableAnnotation Annotationsのキー(YAMLの sample.x) Trigger・PartitionEntity 所属テーブル(パーティションは根) AbstractEntities.index Map<TableKey, List<E>> で引く Viewpoint TableNamePatterns.matches(key)
図8 テーブルキーで結び付くもの。ColumnEntityはTableEntityへの参照を持たず、スキーマ名・テーブル名だけを持ちます。集合のクラス(Triggers・Partitions・ForeignKeys・ViewReferences等)はAbstractEntitiesを継承し、テーブルキーで引ける索引を作ってbelongingTo(table)で返します。関数・シーケンス・型はテーブルに属さないため、テーブルキーを持ちません。

ForeignKeysとViewReferencesは索引を2つ持ちます。ForeignKeysは、参照元のテーブルキーで引く索引(自分が参照している関連)と、参照先のテーブルキーで引く索引(自分を参照している関連=被参照)です。被参照の索引からは自己参照を除きます(定義書の「外部キー情報」と「被参照情報」に二重に載らないように)。ViewReferencesも同じ形で、ビューのテーブルキーで引く索引(belongingTo。ビューが参照するテーブル)と、参照されるテーブルのテーブルキーで引く索引(referencingTo。そのテーブルを参照しているビュー)を持ちます(9.8節)。

8.2 取得の単位と、1テーブル分の組み立て

一括取得したExportTargetsと、チャンク単位で取得したTableDetailから、テーブルごとにTableDefinitionContent.assembleで「1テーブル分の出力内容」を組み立てます。これがテーブル定義書1ファイル、スナップショット1行に対応する書き出しの単位です。

ExportTargets(一括取得) baseInfo・foreignKeys・triggers partitions・viewReferences annotations・viewpoints 対象範囲全体の集合 TableDetail(チャンク単位) table・columns・indexes・constraints 1テーブル分 TableDefinitionContent .assemble(…) 全体の集合から このテーブルの分を抜き出して 1つのrecordに束ねる テーブル定義書(Markdown) TableDefinitionWriter tables.jsonl の1行 TableSnapshot.of(content)
図9 1テーブル分の組み立て。TableDefinitionContentは「何を出力するか」だけを持ち、出力先は持ちません。同じrecordを、Markdownとスナップショットの両方のSinkが受け取ります。
TableDefinitionContentの項目どこから求めるか使う場所(Markdown)
table・columns・indexes・constraintsTableDetail(チャンク単位で取得)テーブル情報・カラム情報・インデックス情報・制約情報
foreignKeysForeignKeys.physicalBelongingTo(table)外部キー情報(DBに実在する制約だけ)
logicalRelationsForeignKeys.logicalBelongingTo(table)論理リレーション情報
incomingRelationsForeignKeys.referencingTo(table)(物理・論理の両方。自己参照を除く)被参照情報
referencedTablesViewReferences.belongingTo(table)(ビューのときだけ)参照するテーブル
referencingViewsViewReferences.referencingTo(table)(出力対象のビューだけ)参照しているビュー
triggersTriggers.belongingTo(table)トリガー情報
partitionsPartitions.belongingTo(table)(パーティション表のときだけ)パーティション情報
annotationAnnotations.belongingTo(table)(無ければTableAnnotation.EMPTY)テーブル説明・備考・カラム備考
viewpointsViewpoints.containing(table)(宣言順)所属する観点
baseInfoExportTargets.baseInfo()(DB名・DBMS種別・メジャーバージョン・生成日)基本情報

テーブル定義書のER図は、この項目の関連からではなく、MarkdownExportSinkが対象範囲全体のForeignKeysから求めるDiagramNeighborhoodで描きます(9.7節)。描画距離が2以上だと、関連先のテーブルが持つ関連まで要るためです。TableDefinitionContentはスナップショットと共有するため、Markdownだけが使う図の範囲は持ちません。

関数・プロシージャはテーブルに属さないため、テーブルキーでは束ねません。関数1つ分をFunctionDefinitionContent(function+tableUsage)として持ち、スキーマ単位で取得した定義本体から作ります。tableUsageは、--previewのときだけ定義本体から抽出し、そうでなければFunctionTableUsage.notAnalyzed()です。これが、関数の定義書1ファイル、functions.jsonlの1行(tableUsageは含めない)、functionTableUsages.jsonの1件に対応する書き出しの単位です(9.9節)。

8.3 「値が無い」の表し方

値が無いことの表し方は、段階ごとに決まっています。これを知っていると、nullチェックがほとんど無い理由と、どこで変換すべきかが分かります。

SQLの結果 NULL・''・'a,b' true/1(Oracle) DTO(record) toEntity() で DtoValues.text・split エンティティ 値が無い = 空文字 "" List・boolean・enum nullを持たない スナップショット(JSON) SnapshotValues.text:"" → null NON_EMPTY:null・空は省略 リストは配列。falseも出力する Markdown(テンプレート) escapeTableCell・escapePipe marker():true → ○ "" は空のセル。リストは a,b
図10 値の形の約束。DBによる違い(Oracleは空文字をNULLで返す、真偽値の型が無く1/0で返す)はSQLとDTOの変換で吸収し、エンティティでは「空文字=値が無い」に揃えます。表示用の連結・エスケープ・○はテンプレートだけが行います。SQLでエスケープすると、スナップショットにまでMarkdownの記法が混ざるためです。
値SQLの結果エンティティスナップショットMarkdown
コメントの無いカラムの論理名''(PostgreSQLはcoalesce)/NULL(Oracle)""項目ごと省略空のセル
複合外部キーの列'warehouse_code,zone_code'List.of("warehouse_code", "zone_code")["warehouse_code","zone_code"]warehouse_code,zone_code
トリガーのイベント'INSERT/UPDATE'List.of("INSERT", "UPDATE")["INSERT","UPDATE"]INSERT/UPDATE
トリガーの本体PL/SQLの文字列(Oracle。LONG型をlong_valueで読む)/''(PostgreSQL。本体は実行する関数が持つ)末尾の改行・空白を除いた文字列(stripTrailing)"body":"…"(PostgreSQLは項目ごと省略)表の後に### トリガー名と```sqlのコードブロック(空なら出さない)
NOT NULLtrue(Oracleは1)true"notNull":true(falseも出す)○(偽は空)
区分'materialized_view'TableType.MATERIALIZED_VIEW(未知なら例外)"type":"materialized_view"materialized_view
カラム備考(SQLには無い。サイドカーYAML)TableAnnotation.columnRemark(名前)"remarks":"…"改行・|をエスケープして備考のセル

8.4 サイドカーの3種類の流れ先

SidecarYamlRepositoryがYAMLをSidecar(record)へ読み込み、fetchTargetsの中で3つに分かれて流れます。YAMLの読み込みは型の変換に専念し、ルールはドメインのメソッドに任せています。

中身ルールを持つ場所流れ先
Annotations(手動付帯情報)キーの解析TableKey.parse、孤児の検出TableAnnotation.orphanColumnNamesExportTargets.annotations → TableDefinitionContent.annotation
論理リレーション(List<ForeignKeyEntity>)関連名の自動生成ForeignKeyEntity.resolveLogicalRelationName、多重度の既定値Cardinality.DEFAULT_FOR_LOGICAL_RELATIONExportTargetConsistency.resolveForeignKeysで物理外部キーと同じForeignKeysへ合流
Viewpoints(観点)識別子・パターンの検証Viewpoint.of、所属の判定Viewpoint.contains観点ページ(ViewpointWriter)、参考情報(InsightWriter)、定義書の「所属する観点」

8.5 不変と null の約束

9ドメインの中心ロジック

9.1 出力対象の絞り込み(TableScope・TableNamePatterns)

設定のschema・tableから入口で1回だけTableScopeを組み立て、テーブルごとにmatchesで判定します。スキーマの絞り込みはSQLのINで、テーブル名パターンの判定はJavaで行います(ワイルドカード・除外はSQLの完全一致では表せないため)。

schematable対象になるテーブル
未指定未指定すべて(PostgreSQLはpg_catalog・information_schemaを除く、OracleはOracleが管理するスキーマを除く)
指定未指定スキーマ名が一致するもの
未指定指定テーブル名パターンが一致するもの
指定指定両方が一致するもの

テーブル名パターン(TableNamePatterns)の規則は、出力対象の範囲と観点の所属テーブルで共通です。

パターンsample.employeesample.employee_profilesample.employee_directory_view
employee○
employee*○○○
employee*,!*_view○○
other.employee*

TableScope.isFiltered()(スキーマかテーブルが指定されている)は、突き合わせの通知を抑えるかの判断に使います。絞り込みで外れたのか、リネーム・削除で乖離したのかを区別できないためです(次節)。

9.2 関連の合流と突き合わせ(ExportTargetConsistency)

外部キー制約を張らないDBでは、カタログの外部キーだけではER図がほとんど空になります。そこでサイドカーで宣言した論理リレーションを、物理の外部キーと同じ集合(ForeignKeys)へ合流させます。合流させることで、ER図のグループ分割(連結成分)、スキーマ跨ぎの関連の抽出(crossSchema)、被参照(referencingTo)に、追加の実装なしで反映されます。読み手が「DBに制約がある」と誤読しないよう、区別は2箇所で付けます。

合流の前に、参照元・参照先の両方が出力対象のテーブルにある関連だけを残します(片側だけ残すと、定義書の無いテーブルがER図にだけ箱として現れるため)。除いたことを知らせるかは由来で違います。突き合わせの通知の一覧です。

ConsistencyNotice.Kind重要度出す条件検出するメソッド
UNRESOLVED_FOREIGN_KEYWARN物理外部キーの片側が出力対象に無い。絞り込んでいるときは意図した除外なので出さないresolveForeignKeys
UNRESOLVED_LOGICAL_RELATIONWARN論理リレーションの片側が無い。絞り込みか乖離かを区別できないため常に出すresolveForeignKeys
LOGICAL_RELATIONS_MERGEDINFO論理リレーションを宣言していれば、合流した件数を報告resolveForeignKeys
ORPHAN_TABLE_ANNOTATIONWARN実在しないテーブルへの付帯情報(絞り込んでいないとき)findOrphanTableAnnotations
ORPHAN_TABLE_ANNOTATION_CHECK_SKIPPEDINFO絞り込んでいるため上の検出をしなかったfindOrphanTableAnnotations
ORPHAN_COLUMN_ANNOTATIONWARN実在しないカラムへのカラム備考(出力対象のテーブルについて、チャンクごと)findOrphanColumnAnnotations
UNMATCHED_VIEWPOINT_PATTERNWARNどのテーブルにも一致しない観点のパターン(絞り込んでいないとき)findUnmatchedViewpointPatterns

9.3 多重度の判定(Cardinality)

外部キーそのものは「関連がある」ことしか示しません。多重度は、参照元(子)の外部キー列に付いた制約から機械的に決めます。SQLが2つの真偽値を返し、ForeignKeyDto.toEntityがCardinality.of(unique, mandatory)で多重度にします。

外部キー列が一意すべてNOT NULLCardinality表記サンプルの例
×○ONE_TO_MANY(1対多)||--o{employee.department_id → department
××OPTIONAL_ONE_TO_MANY(0..1対多)|o--o{employee.manager_id → employee(自己参照)
○○ONE_TO_ONE(1対1)||--o|employee_profile.employee_id → employee
○×OPTIONAL_ONE_TO_ONE(0..1対1)|o--o|employee.parking_spot_id → parking_spot

9.4 ER図の出し分け(ForeignKeyGroups・RenderingPlan)

Mermaidは大きすぎる図を描けないため、スキーマ別ER図1枚のテーブル数に上限(erDiagramMaxNodes、既定80、0以下は上限なし)があります。「ノード」は関連を持つテーブルで、関連の無いテーブルは図に描きません(全テーブルはテーブル一覧に載る)。ページ構成はForeignKeyGroups.composeが1回で決め、各ページで描くか省くかはForeignKeyGroup.planRenderingが決めます。Writer・テンプレートはその結果に従うだけです。

スキーマに関わる関連 ForeignKeys.groupBySchema()。スキーマ跨ぎは両方に入る ノード数(関連を持つテーブル数)≦ 上限? Single(1枚) planRendering → Draw connectedComponents → pack 連結成分を求め、上限に収まるだけまとめ直す まとめた結果が2グループ以上? Single(1枚) planRendering → Omit Grouped(グループごとに1枚) スキーマのページはグループ一覧になる グループごとに planRendering → Draw / Omit はい いいえ いいえ はい サンプルで上限を5に 関連を持つテーブル 12 12 > 5 なので分ける 連結成分は2つ ・employeeを含む 10 ・shipmentを含む 2 10は単独で上限を超える → 1つのグループ 2は10と合わせると12 → 別のグループ Grouped(2グループ) group1:10 > 5 → Omit(外部キー一覧) group2:2 ≦ 5 → Draw(図を描く)
図11 スキーマ別ER図のページ構成と描き方。右は、サンプルDBを--er-diagram-max-nodes=5で出力した実際の結果です。3つのビュー(マテリアライズドビューを含む)は関連を持たないのでノードに数えません。
// 上限5で出力したスキーマ別ER図(erDiagram_testdb_sample.md)の本文
## ER図

ER図に描画するテーブル数が12件となり、上限(erDiagramMaxNodes = 5件)を超えるため、
外部キーで繋がったテーブルのまとまりごとに2個のグループへ分割しました。

## グループ一覧

| No. | テーブル数 | 外部キー数 | 主なテーブル | Link |
|:---|:---|:---|:---|:---|
|1|10|10|sample.employee|[■](./erDiagram_testdb_sample_group1.md)|
|2|2|1|sample.shipment|[■](./erDiagram_testdb_sample_group2.md)|

「主なテーブル」はForeignKeyGroup.mainTableで、最も多くの関連が接続するテーブルです。group1の外部キー数10には、論理リレーションaudit_log.record_idも含まれます(合流しているため)。

9.5 ER図の箱に何を表示するか(DiagramBoxes)

ER図はチャンクより前(writeOverview)に書きますが、箱にはカラムを表示します。そこでfetchDiagramBoxesが、関連を持つテーブルのカラムだけをスキーマ・チャンク単位で別に取得し、DiagramBoxes.Builderが関連カラム(関連の参照元・参照先として使われるカラム)だけを残して他を捨てます。

9.6 長い表のページ分割(PagedSectionWriter)

GitHubで表として表示できる行数には限りがあるため、行が多くなり得る一覧はすべてPagedSectionWriterを通して書きます。3,000行以下なら本体ページに表を埋め込み、超えるなら{本体のファイル名}_{n}.mdへ分けて本体にはリンクだけを載せます。行の文字列はページ単位で作り、全行分を同時にメモリに持ちません。対象は、テーブル一覧、ER図の掲載テーブル・外部キー一覧、スキーマ跨ぎの外部キー、トリガー/関数/シーケンス/型の一覧、観点の所属テーブル・観点一覧です。

9.7 テーブル定義書のER図の描画距離(DiagramNeighborhood)

テーブル定義書のER図は、既定では自テーブルの関連だけを描きます。設定のoutput.erDiagramDistance(--er-diagram-distance。1〜3、既定1)を上げると、関連先のテーブルが持つ関連まで描きます。どこまで描くかはForeignKeys.neighborhoodOfが決め、結果をDiagramNeighborhood(描く関連・描いた距離・指定された距離)で返します。

  1. たどる:自テーブルから関連を向きを問わず幅優先でたどります(参照先の索引と被参照の索引の両方を見る)。距離nなら、n−1段目までに到達したテーブルが持つ関連を、近い順に集めます。距離1は自テーブルの関連だけで、従来の出力と同じです。
  2. 縮める:図のテーブル数がerDiagramMaxNodes(NodeLimit)を超えるなら、超えなくなるまで距離を1つずつ縮めます。1でも超える場合は1で描きます(自テーブルの関連は省かない)。
  3. 知らせる:縮めた場合(isShortened)は、TableDefinitionTemplates.erDiagramが図の前にその旨を書きます。

サンプルのdepartmentを距離2で出力すると、関連先のemployeeが持つ関連(自己参照・attendance・employee_history・employee_profile等)まで描かれ、図のテーブルは2から8に増えます。距離3と上限5を指定すると、上限を超えるため1段で描き、次の1文が付きます。

## ER図

関連するテーブルが多く、3段先までを描画するとテーブル数が上限(erDiagramMaxNodes)を超えるため、1段先までを描画しています。

```mermaid
erDiagram
    sample_department["department"]
    sample_employee["employee(従業員)"]
    sample_department ||--o{ sample_employee : "employee_department_id_fkey"
    …

上限を3にしているのは、関連の多いDBでは段数を増やすと図がすぐに大きくなり読めなくなるためです(DbxrayProperties.MAX_ER_DIAGRAM_DISTANCE)。範囲外の値は既定値に置き換えず、設定の誤りにします。図の関連線は、どの段の関連も「参照先 → 参照元」の向きで同じMermaidSupport.relationLineで描きます。

9.8 ビューが参照するテーブル(ViewReferences)

ビュー・マテリアライズドビューが参照するテーブル(ビューを含む)は、ビューのSQLを解析せず、DBが保持する依存関係から取得します(PostgreSQLはpg_depend・pg_rewrite、OracleはALL_DEPENDENCIES)。1件がViewReferenceEntity(参照する側のビューと、参照されるテーブル)で、集合がViewReferencesです。

// view/attendance_monthly_view.md の「参照するテーブル」
| No. | 参照先 | 区分 |
|:---|:---|:---|
|1|sample.attendance|table|
|2|sample.employee_directory_view|view|
|3|sample_archive.department|table|

9.9 関数・プロシージャが利用しているテーブルの抽出(--preview)

ビューが参照するテーブル(9.8節)と違い、DBは関数・プロシージャの依存関係を保持しません。そこで、定義本体(FunctionEntity.definition)のSQLを読んで、利用している出力対象のテーブルと操作(C・R・U・D)を機械的に拾います。DBが持つ事実ではない参考値で、精度にリスクがあるため、--previewを付けたときだけ動くプレビューの機能(PreviewFeature.FUNCTION_TABLE_USAGE。互換性の対象外)として入れています。

設計の軸は2つです。

各段が返す値 FunctionEntity.definition + language スキーマ単位で取得した定義本体(書いたら捨てる) analyze(--previewのときだけ。スキーマごとに1つ作る) ① SqlLexer.lex 1文字ずつ読み、コメント・文字列を区別して字句に分ける 規則はDBごと(Dbms)。閉じていない要素は例外にせず印を立てる ② FunctionBodyLocator.locate 見出しを除き、解析する本体の字句を切り出す PostgreSQL:ドル引用符の中身 / Oracle:サブプログラムの範囲 ③ TableReferenceScanner.scan INSERT INTO・FROM・JOINの後の名前と操作を拾う 文ごとの状態。「;」と最上位のLOOPで作り直す ④ FunctionTableUsageAnalyzer.resolve 出力対象のテーブル(Tables)と照らして残す スキーマは定義から決まる場合だけ決め、決まらなければ候補を示す SqlLexResult 空白・コメントを除いた字句(SqlToken)の列 incomplete:閉じていない要素があったか FunctionBody 解析しない理由 or 本体の字句(segments) PostgreSQL:searchPath/Oracle:invokerRights ScanResult references:スキーマ・名前・操作(C/R/U/D) dynamicSql:動的SQLの種類 FunctionTableUsage status・tables(スキーマ決定 or 候補・操作) dynamicSql・incomplete・overloadsMerged 定義書「利用しているテーブル」 FunctionTableUsageTemplates .section(Markdown) functionTableUsages.json InsightWriter. writeFunctionTableUsages mcp-server:get_function の tableUsage 定義本体のハッシュ(definitionHash)が スナップショットと一致するときだけ返す
図12 利用しているテーブルの抽出。点線の枠がFunctionTableUsageAnalyzer.analyzeの中で、右の列は各段が次の段へ渡す値です。どの段も、解析できない場合は例外ではなく、FunctionBodyの解析しない結果(NotAnalyzable・UnsupportedLanguage)・FunctionTableUsageのstatus(解析しなかった理由)で返します。抽出しない実行(--previewなし・--check)では、この枠の中を一切通りません。
  1. 字句に分ける(SqlLexer):先頭から1文字ずつ読み、今いる場所が文字列・コメント・識別子のどれかを状態として持ちます。コメントと文字列は互いの中で効かない(文字列の中の--はコメントにならず、コメントの中の'は文字列を始めない)ため、同時に左から読まないと区別できません。規則はDBで違います(PostgreSQLはブロックコメントの入れ子・ドル引用符・E'…'、Oracleはq'[…]'・条件付きコンパイル)。そのため、DBMS種別を文字列ではなくDbmsのenumにして、switchで網羅的に切り替えます(DBMSを足すとコンパイルエラーで気付ける)。
  2. 本体を切り出す(FunctionBodyLocator):引数・戻り値の型の見出しは解析しません(RETURNS SETOF t・p t%ROWTYPEをテーブルの利用と誤らないため)。実装はDBMSごとです。
    PostgreSQL(PostgresBodyLocator):pg_get_functiondefは本体を常にドル引用符で囲むため、見出しの後の最上位のドル引用符の中身を字句に分け直します。SQL標準の本体(BEGIN ATOMIC … END・RETURN 式)は、最上位のBEGIN ATOMIC・RETURNから終わりまでです。
    Oracle(OracleBodyLocator):単独の関数・プロシージャは見出しのIS・ASの後ろ全体です。パッケージのサブプログラムは、どれも定義にパッケージ全体が入っているため、PackageSubprogramsがBEGIN・CASEとENDの対応を数えて、パッケージ本体の最上位のサブプログラムの範囲を求めます。オーバーロードは引数名で対応付け、区別できなければすべての本体からまとめて抽出します(overloadsMerged)。
  3. 拾う(TableReferenceScanner):字句を先頭から1回なめ、きっかけのキーワードの後ろの名前と操作をTableReferenceにします。文の状態(StatementState)は;と最上位のLOOPで丸ごと作り直すため、括弧の数が合わない文があっても次の文から読み直せます。動的SQL(EXECUTE・EXECUTE IMMEDIATE・DBMS_SQL等)は、含むことだけをDynamicSqlKindで示します。
  4. 照らす(FunctionTableUsageAnalyzer):拾った名前のうち、出力対象のテーブル(ビュー・マテリアライズドビューを含む)と一致したものだけを残します。CTEの名前・変数・DUAL等や、出力対象外のスキーマ・テーブルはここで落ちます。テーブルごとに操作をまとめ、スキーマが決まったものをスキーマ名・テーブル名の順に、決まらないものをその後に並べます。

操作とそのきっかけ。操作はCrudOperationです。細かい規則(括弧で囲んだJOIN・インラインビューへのDML・MERGEの各WHEN等)は、TableReferenceScannerTestのケースの一覧がそのまま仕様になっています。

操作テーブルの名前の前にあるきっかけ
C(作成)INSERT INTO(OracleのINSERT ALL/FIRSTの各INTO、MERGEのWHEN NOT MATCHED THEN INSERTを含む)
R(読み取り)FROM(カンマ区切りの並び)・JOIN・APPLY(Oracle)・USING(DELETE・MERGE)。括弧で囲んだFROM (a JOIN b ON …)の先頭や、SELECT … FOR UPDATEもR
U(更新)UPDATE(MERGEのTHEN UPDATE、ON CONFLICT DO UPDATEを含む)
D(削除)DELETE(MERGEのTHEN DELETEを含む)・TRUNCATE

権限・トリガー・行ロックの構文にもINSERT・UPDATE・DELETE・TRUNCATE・EXECUTEの語が現れます(GRANT SELECT, TRUNCATE ON a, b TO r・CREATE TRIGGER … BEFORE TRUNCATE ON t・GRANT EXECUTE等)。直前の語(GRANT・ON・BEFORE等)でこれらを文として読まず、後ろのカンマ区切りの名前に操作を付けないようにしています。

スキーマ修飾の無い名前。スキーマは、定義から決まる場合だけ決めます。決まらない場合は、同じ名前の出力対象のテーブルがあるスキーマをすべて候補として持ち、候補が1つでもそのスキーマに決めつけません(SchemaResolution・TableUsage.schemaCandidates)。

DBMS定義から決まる場合決まらない場合(SchemaResolution)
PostgreSQL関数の属性としてSET search_pathが指定されていれば(FROM CURRENTは作成時の値に展開された形)、その並びの先頭から、出力対象のテーブルがあるスキーマにする($user・pg_catalog・pg_tempは除く)。空('')なら出力対象のスキーマが無く、DEFAULTは指定なしと同じ。本文中のSET文・set_config・EXECUTEで組み立てたSET文(変数でスキーマを渡す場合を含む)は読まない属性の指定が無ければ(本文で変更していても)実行時のsearch_pathで決まる(search_path)。指定がある場合に並びのどこにも無い名前は、出力対象外のものとして載せない
Oracle定義者権限(既定)は、所有者のスキーマにする定義者権限で所有者のスキーマに無い名前は、シノニム経由の可能性がある(synonym。シノニムの参照先は照らさない)。実行者権限(AUTHID CURRENT_USER)は実行するユーザーで決まる(current_user)

解析しなかった・限界がある場合。状態はTableUsageStatusで表し、定義書では表の代わりに(または表の前に)その旨を書きます。

状態・印意味
NOT_ANALYZED抽出を行わない実行(--previewなし・--check)。定義書の節も参考情報も出さない
ANALYZED解析した(利用しているテーブルが無い場合を含む)。dynamicSql・incomplete(定義本体を最後まで読めなかった)・overloadsMergedがあれば、表の前に注記する
UNSUPPORTED_LANGUAGE言語がSQL・PL/pgSQL・PL/SQL以外(C・plpython3u・Javaの呼び出し仕様等)
NO_DEFINITION
WRAPPED
定義本体を取得できなかった(権限が無い等)/wrap(難読化)されている(Oracle)
SUBPROGRAM_NOT_FOUNDパッケージ本体からサブプログラムの本体を見つけられなかった(Oracle)

サンプルのclose_project(プロシージャ。--previewで出力した関数の定義書から抜粋)と、スキーマが決まらない例のcount_rows(functionTableUsages.jsonから抜粋)です。

// function/close_project.md
## 利用しているテーブル

定義本体から機械的に抽出した参考値です。呼び出している関数・プロシージャの中の参照と、動的SQLの中の参照は含みません。

| No. | テーブル | 区分 | C | R | U | D |
|:---|:---|:---|:---|:---|:---|:---|
|1|sample.audit_log|table|○|○|||
|2|sample.employee|table|||○||
|3|sample.project|table||○|||
|4|sample.project_assignment|table||○||○|
// insights/testdb/sample/functionTableUsages.json の functions のうち count_rows(整形)
{"name":"count_rows", "arguments":"p_table_name text", "definitionHash":"a600475c…",
 "status":"analyzed",
 "tables":[{"name":"employee", "schemaResolution":"search_path",
            "schemaCandidates":["sample"], "operations":["R"]}],
 "dynamicSql":["EXECUTE"]}

参考情報は、古い結果を残さない・返さない

InsightWriter.writeFunctionTableUsagesは、抽出した実行(FunctionTableUsage.isAttempted())ではスキーマごとにfunctionTableUsages.jsonを書き、抽出していない実行では同じファイルを削除します。残すと、定義書には節が無いのにMCPサーバーが前回の結果を返し、テーブルや定義が変わった後の結果を最新のものとして答えてしまうためです。このファイルの削除は、cliでFileRepository.deleteFileを呼ぶ唯一の箇所で、消すのはOutputPathResolverで解決した1ファイルだけです(ディレクトリの削除は--rm-distの出力先に限ります)。

それでも、--rm-distを付けずに対象のスキーマを減らした場合などは、そのスキーマの参考情報だけが前回のまま残ります。そこで参考情報は、抽出元の定義本体のSHA-256(definitionHash)を持ち、MCPサーバーはスナップショットの定義本体のハッシュと一致するときだけ返します(mcp-server 読解ガイドの7.10節)。DB名・スキーマ名は、ファイル名に使うために置き換えられるため、ファイルの中のdatabase・schemaが実際の名前です。

精度をどう確かめているか。精度にリスクがある機能なので、テストの種類を分けています。サンプル(PostgreSQLの関数11件・Oracleのサブプログラム6件)は、全件がDDLから見て正しいことを確かめてあり、OracleはALL_DEPENDENCIESの参照先とも一致しました(PR #137の手元の確認)。実際の業務DBでの精度の確認は、プレビューを外す前に行う予定です(Issue #134)。

テスト確かめること
TableReferenceScannerTest
FunctionBodyLocatorTest
判定・切り出しの規則を、表駆動のケースの一覧にしている。一覧を読むと規則を仕様として追える
TableUsageMetamorphicTest
SqlTransforms
全ケースを、改行・空白・コメント・大文字小文字で変形しても、コメント・文字列の中にDMLを足しても、結果が変わらない
GeneratedPackagesTest文法から生成した妥当なOracleのパッケージ3,000個で、サブプログラムごとの結果を正解と照らす。変形テストは字句の分け方に依存するため、範囲の切り出し(PackageSubprograms)の誤りはこちらで検知する
SqlLexerPropertyTest
FunctionTableUsageAnalyzerRobustnessTest
ランダム・壊れた・悪意のある入力でも、例外を投げず、入力の長さに比例する時間で終える
RealisticFunctionBodiesTestレガシーなシステムにありがちな長い本体(日本語のコメント・コメントアウトした古いコード・入れ子のループ・多数のサブプログラム)
FunctionTableUsageTemplatesTest
MarkdownInjectionTest
節の組み立てと、テーブル名・関数名がMarkdownのエスケープを通ること
SampleDatabaseExportIT
OracleSampleDatabaseExportIT
実DBのサンプルの関数に対する結果を、差分のベースライン(docs/sample/*/output-preview)と比べる

10出力物とソースの対応

出力する各ファイルについて、書き出すメソッドと、中身を組み立てるクラスの対応です。Markdownは「Writerが段取りし、Templatesが文字列を組み立てる」、スナップショットは「Writerがrecordを作ってJSONにする」という分担です。

出力ファイル書き出すメソッド組み立てるクラス書く時点(Sink)
Markdown(MarkdownExportSink。出力先は {output.path}/{DB名}/)
README.mdReadmeWriter.writeReadmeReadmeTemplates(集計の表はmetrics)writeSummary
tableList_{DB名}.mdTableDefinitionWriter.writeTableDefinitionListTableDefinitionListTemplateswriteOverview
{スキーマ}/{区分}/{テーブル}.mdTableDefinitionWriter.writeTableDefinitionTableDefinitionTemplates・MermaidSupportwriteTableDefinition
erDiagramList_{DB名}.mdErDiagramWriter.writeErDiagram(索引)ErDiagramTemplateswriteOverview
erDiagram_{DB名}_{スキーマ}.md・_group{n}.mdErDiagramWriter.writeErDiagram(スキーマごと)ErDiagramTemplates・MermaidSupportwriteOverview
viewpointList_{DB名}.md・viewpoint_{DB名}_{id}.mdViewpointWriter.writeViewpointsViewpointTemplates(図と表はErDiagramTemplatesを再利用)writeOverview
triggerList・functionList・sequenceList・typeList_{DB名}.mdObjectListWriter.write…ListObjectListTemplateswriteOverview
{スキーマ}/function/{名前}.mdObjectListWriter.writeFunctionDefinitionObjectDefinitionTemplates.functionFile・FunctionTableUsageTemplates(「利用しているテーブル」の節。--previewのとき)writeFunctionDefinitions
{スキーマ}/sequence/{名前}.md・type/{名前}.mdObjectListWriter.writeSequenceDefinition・writeTypeDefinitionObjectDefinitionTemplateswriteOverview
…_{n}.md(分割ページ)PagedSectionWriter.writePagedSectionPagedSectionTemplates各一覧と同時
スナップショット(SnapshotExportSink。出力先は {output.path}/snapshot/{DB名}/)
database.jsonSchemaSnapshotWriter.writeDatabaseDatabaseSnapshotwriteOverview
{スキーマ}/tables.jsonlSchemaSnapshotWriter.initTableFile・appendTableTableSnapshotbeginSchemaTables・writeTableDefinition
{スキーマ}/functions.jsonlSchemaSnapshotWriter.writeFunctionsFunctionSnapshotwriteFunctionDefinitions
{スキーマ}/sequences.jsonl・types.jsonlSchemaSnapshotWriter.writeSequences・writeTypesSequenceSnapshot・TypeSnapshotwriteOverview
参考情報(InsightExportSink。出力先は {output.path}/insights/{DB名}/)
viewpoints.jsonInsightWriter.writeViewpointsViewpointsInsight・ViewpointInsightwriteOverview
{スキーマ}/functionTableUsages.jsonInsightWriter.writeFunctionTableUsages(--previewのとき。抽出していない実行は前回のファイルを削除)FunctionTableUsagesInsightwriteFunctionDefinitions

どの一覧を出すかはMarkdownExportSinkFactory.listDocumentsが1箇所で決めます。テーブル一覧は常に、ER図一覧はテーブルがあれば、関数・シーケンス・型・トリガーの一覧は対象が1件以上あれば、観点一覧は観点を宣言していれば出します。READMEのリンクとテーブル一覧の「関連ドキュメント」もこの結果を使うため、空の一覧を出したり、存在しない一覧へリンクしたりしません。

テーブル定義書のセクション

TableDefinitionWriter.writeTableDefinitionは、テンプレートのメソッドが返す文字列を次の順に並べて1ファイルにします。空文字を返したセクションは出力されません。

順セクションTableDefinitionTemplatesのメソッド出す条件
1見出し「テーブル名(論理名)」fileHeader常に
2基本情報(RDBMSとメジャーバージョン・DB名・作成日)baseInfo常に
3テーブル説明tableExplanation常に。本文はサイドカーに説明があるときだけ
4テーブル情報tableInfo常に(備考はサイドカー)
5所属する観点viewpoints所属する観点があるとき
6カラム情報columns常に(備考はサイドカー)
7パーティション情報partitionsパーティション表のとき
8ソースviewビュー・マテリアライズドビューのとき
9参照するテーブルreferencedTablesビューが参照するテーブルがあるとき
10インデックス情報indexes常に(0件なら見出しと表の頭だけ)
11制約情報constraints常に(同上)
12外部キー情報(物理)foreignKeys常に(同上)
13論理リレーション情報logicalRelations論理リレーションがあるとき
14被参照情報incomingRelations参照されているとき
15参照しているビューreferencingViews出力対象のビューから参照されているとき
16トリガー情報triggers常に(同上。target.objectsでtriggerを外すと行が無くなる)。本体を持つトリガー(Oracle)は、表の後にトリガーごとの見出しと本体のコードブロック
17ER図erDiagram常に(関連が無ければ「関連するテーブルはありません。」。描画距離は9.7節)
18フッター(テーブル一覧へ)footer常に

関数・プロシージャの定義書のセクション

ObjectDefinitionTemplates.functionFileが、次の順に並べて1ファイルにします。「利用しているテーブル」は、--previewで抽出した実行だけの節で、FunctionTableUsageTemplates.sectionが空文字を返す(NOT_ANALYZED)と出力されません。関数・プロシージャ一覧(functionList_{DB名}.md)には列を足しません。

順セクション組み立てるメソッド出す条件
1見出し(関数名)functionFile常に
2基本情報baseInfo常に
3利用しているテーブルFunctionTableUsageTemplates.section--previewで抽出したとき。表(No.・テーブル・区分・C・R・U・D)の前に、動的SQL・読み切れなかった等の注記を置く。解析しなかった場合は、表の代わりにその理由。スキーマが決まらない名前は、表の後に候補を並べる
4定義functionFile常に(定義本体をcodeFenceで囲む)
5フッター(関数・プロシージャ一覧へ)functionFile常に

Writer と Templates の分担

ファイル名と相対リンクの規則

ファイル名・パス・リンクを、テンプレートやWriterがString.format等で直接組み立てることはしません。出力先の絶対パスとドキュメント間の相対リンクが同じ規則を参照するため、ファイル名を変えてもパスとリンクが食い違いません。

クラス決めること例
DocumentLocationsMarkdownのファイル名と、{DB名}/からの相対パス。ドキュメント間の相対リンク。関数のオーバーロードは{名前}_{番号}、観点ページは識別子から。DB由来の名前はPathSegments.encodeでパスの1要素にする。番号を付けた名前が別の関数と重なる場合(calcのオーバーロードと関数calc_1)はfunctionsSharingDefinitionFileで求めて警告する(ファイル名は互換性を約束する範囲のため、上書き自体は変えない)sample/table/department.md、calculate_bonus_1.md
OutputPathResolver(実装DefaultOutputPathResolver)出力先の絶対パス。未指定なら./output、{DB名}/を付ける。分割ページのパス。解決したパスが起点のディレクトリの外を指さないことを確かめる(within)。--rm-distで消してよいか(ルート、カレント・ホームとその上位のディレクトリは拒否)./output/testdb/sample/table/department.md
SnapshotLocationssnapshot/の下の配置と、ファイル名から種別への逆引き(kindOf。比較で使う)snapshot/testdb/sample/tables.jsonl
InsightLocationsinsights/の下の配置(snapshot/の兄弟)。関数の利用テーブルはスキーマごとinsights/testdb/viewpoints.json、insights/testdb/sample/functionTableUsages.json
ListDocumentType一覧の種類ごとのファイル名の接頭辞とタイトルFUNCTION → functionList_testdb.md「関数・プロシージャ一覧」

相対リンクは、リンク元が{DB名}/直下ならlinkFromDatabaseRoot(./を付ける)、{スキーマ}/{区分}/の下の個別定義書ならlinkFromDefinition(../../を付ける)で作ります。サンプルのdepartment.mdには[人事管理](../../viewpoint_testdb_personnel.md)・[テーブル一覧へ](../../tableList_testdb.md)が出力されています。

DB由来の文字列を信頼しない

DB名・スキーマ名・テーブル名・コメント・関数やビューの定義は、ツールを実行する人とは別の人(DBでCREATE・COMMENTの権限を持つ人)が書ける入力です。引用符で囲んだ識別子には/・..・改行・HTMLも書けるため、出力・パスに使う箇所はすべて決まった部品を通します(docs/development/coding-guidelines.mdの「セキュリティ」)。サイドカーYAMLは利用者自身が書くため、説明文はMarkdownとしてそのまま出力します。

使い道通す部品防ぐこと検査するテスト
ファイル名・ディレクトリ名PathSegments.encode(/ \ : * ? " < > |・~・制御文字を~+16進2桁に、.だけの名前と空の名前も置き換える。例:a/b → a~2Fb)と、DefaultOutputPathResolverのwithin出力先の外への書き込み(パストラバーサル)OutputPathContainmentTest(OutputPathResolverの全メソッドを危険な名前で呼ぶ)、ArchitectureTest(Path.resolveの呼び出し場所)
表の行row(表の行はすべてこのメソッドで組み立てる)。改行を<br>に、それ以外の<を&lt;に。\で終わるセルの後には空白を1つ置く(GFMは\|を区切りではなく文字の|と読むため)生のHTMLの描画、行の分断、セルの結合による列のずれMarkdownInjectionTest(各テンプレートにHTML・```・見出しを混ぜた文字列を渡す)
見出し・リストの項目escapeInline(改行を空白に)次の行がMarkdownの構文になること
関数・ビューの定義、トリガーの本体、パーティションキーcodeFence・codeSpan(中身の最長のバッククォートより長い囲み)コードブロックが途中で閉じること
Mermaidの図MermaidSupport(識別子は記号を_に、表示名・関連名の二重引用符と改行を置き換える)図の構文の破壊、コードブロックを閉じる行MermaidSupportTest
ログlog4j2.xmlの%enc{%m}{CRLF}改行による偽のログ行—

名前に上の文字を含まない限り、ファイル名は名前のままです。DB由来の文字列を受け取るテンプレートを足したら、MarkdownInjectionTestのrenderedDocumentsにも足します。関数の「利用しているテーブル」も、テーブル名・スキーマ名はescapeTableCell、候補の一覧と解析しなかった言語の名前はescapeInlineを通し、危険な文字列を入れた入力をMarkdownInjectionTestで確かめています。

11ソースごとの役割

main配下のクラスと、リソース・テストの一覧です。似た小さなクラスは1行にまとめ、package-infoは最後に1行でまとめています。「使う相手・使われる相手」の→は使う相手、←は使われる相手です。

ファイル役割使う相手・使われる相手
com.dbxray(直下):エントリーポイント
Dbxraymain。処理全体を1つのtry-catchで囲み、通常実行(run)か差分検知(runCheck)を呼ぶ。依存関係をComponentsで2段階に組み立て、失敗はFailureReporterで報告して終了コードに変える→ CliArguments・DbxrayProperties・OutputDirectoryValidator・ConnectionSettings・MyBatisSqlSessionFactories・DatabaseTypeDetector・Components・Controller・FailureReporter
CliArgumentsパッケージプライベートCLI引数の解析(--check・--rm-dist・--preview・--help・--version、接続情報と実行時設定の上書き値)。未知の引数は集めておき、requireKnownArgumentsで誤りにする(値は伏せて報告)。設定の引数名はDbxrayProperties.SETTINGSから、接続情報の引数名はConnectionSettings.CONNECTION_KEYSから引き、上書きできる引数の一覧OVERRIDE_ARGSにまとめる← Dbxray/→ DbxrayProperties・ConnectionSettings
DbxrayPropertiesパッケージプライベートconf/config.ymlのうちdatabase以外の仕様(8項目の位置・CLI引数名・既定値・形式・範囲)と検証。CLI引数で上書きしてから検証し、誤りをまとめてInvalidConfigurationExceptionにする。2種類のRequestへ変換(--previewはpreviewFeatures=PreviewFeatureの全部へ)← Dbxray/→ ConfigFile・TargetSelection
OutputDirectoryValidatorパッケージプライベート出力先の検証(既存のファイルを指していないか、--rm-distで消してよいか)。DBへの接続前に、Componentsから取り出して使う← Dbxray/→ OutputPathResolver・FileRepository
Components依存関係をnewで組み立てる(composition root)。DB種別に依存しない部品は接続前に、CatalogRepositoryとそれに依存するユースケースはcontroller(種別, sqlSessionFactory)で接続後に組み立てる← Dbxray/→ 全レイヤーの具象クラス
CliUsageパッケージプライベート--help・--versionの文言。版はjarのマニフェスト(Implementation-Version)から← Dbxray
presentation(.dto・.type):コントローラーと報告
ExportSchemaControllerユースケースを呼び、結果をResultDto・DiffCheckResultDtoにする。例外は捕まえない← Dbxray/→ 2つのUsecase・DiffReportFormatter
FailureReporter失敗の報告。[errmsg]と、表示に含まれない原因を[cause]で並べる。想定外の失敗はスタックトレースをログへ出し、画面にログの場所を示す← Dbxray.main/→ ProcessResult
DiffReportFormatterパッケージプライベート--checkの結果を文面にする。3区分の一覧と、unified diff(1件200行・全体2000行まで)← Controller/→ DiffResult・ContentDiff
ResultDto
DiffCheckResultDto
処理結果のrecord。DiffCheckResultDtoは差分の有無から終了状態(exitStatus)を返す← Controller・Dbxray
ProcessResultSUCCESS・FAIL。[result]:の行を組み立てる← 2つのDto・FailureReporter
ExitStatus終了状態と終了コード(0成功・1差分あり・2失敗)← Dbxray・DiffCheckResultDto
application:ユースケース
ExportSchemaUsecase通常実行。一括取得→ER図の箱→個別定義のファイルが重なる関数の警告→(--rm-distなら削除)→3つのSinkで書き出し。有効にしたプレビューの機能をINFOでログへ← Controller/→ SchemaExportPipeline・3つの…SinkFactory
CheckDocumentDiffUsecase--check。スナップショットのSinkだけで一時ディレクトリへ書き、SnapshotDiffで比べる。プレビューの機能は渡さない(Set.of())。一時ディレクトリは必ず消す← Controller/→ SchemaExportPipeline・SnapshotExportSinkFactory・SnapshotDiff
SchemaExportPipeline両ユースケースが共有する取得と書き出しの段取り(fetchTargets・fetchDiagramBoxes・export)。通知と件数をログへ出し、生成日をClockから求める。関数は、スキーマごとにFunctionDefinitionContentを作る(exportFunctionDefinitions。--previewならFunctionTableUsageAnalyzerで抽出し、テーブル名の索引は全スキーマで1回だけ求める)。テーブルの詳細を書くたびにカラムをDatabaseMetrics.Builderへ数え、最後にwriteSummaryを呼ぶ← 2つのUsecase/→ CatalogRepository・SidecarRepository・ExportTargetConsistency・ExportSink・DatabaseMetrics
ExportSchemaRequest
CheckDocumentDiffRequest
ユースケースへの入力のrecord。3層を同じrecordのまま通る。通常実行用は有効にするプレビューの機能previewFeaturesも持つ。--check用はerDiagramMaxNodes・erDiagramDistance・rmDist・previewFeaturesを持たない← DbxrayProperties → Controller → Usecase
PreviewFeature--previewで有効にするプレビューの機能のenum(今はFUNCTION_TABLE_USAGE=functionTableUsage)。出力は互換性の対象外で、スナップショットは変えない← DbxrayProperties・ExportSchemaUsecase・SchemaExportPipeline
TargetSelection出力対象の絞り込み条件(TableScope+OutputObjectTypeの集合)。ofで文字列を型に変え、2種類の誤りをまとめる← DbxrayProperties・SchemaExportPipeline
domain.model.target:出力対象
ExportTargets一括取得した出力対象の組(基本情報・テーブル・関連・トリガー・パーティション・ビューの参照・関数/シーケンス/型・付帯情報・観点)← SchemaExportPipeline → 各Sink
TableDefinitionContent1テーブル分の出力内容。assembleで組み立てる。定義書1ファイル・スナップショット1行に対応← SchemaExportPipeline/→ TableDefinitionWriter・TableSnapshot
FunctionDefinitionContent関数・プロシージャ1つ分の出力内容(function+tableUsage)。定義書1ファイル・スナップショットと参考情報の1件に対応。withoutTableUsageは抽出しない実行用← SchemaExportPipeline/→ 各Sink・FunctionTableUsagesInsight
TableScope出力対象の範囲(スキーマ名リスト+テーブル名パターン)。matches・isFiltered・schemaNames← TargetSelection・SchemaExportPipeline/→ TableNamePatterns
OutputObjectTypetrigger・function・sequence・type。parse(空なら全種別、未知の名前は例外)← TargetSelection・SchemaExportPipeline
ConsistencyNotice突き合わせの通知(種類・文言)。重要度(INFO・WARN)は種類が決める← ExportTargetConsistency → SchemaExportPipeline.report
domain.model.table:テーブルとその付属物
TableEntityテーブル1件(table・view・materialized_view)。論理名・ビューの定義・パーティションキー。headerName・isPartitioned・isViewどこでも
TableKeyスキーマ名+テーブル名。テーブルの識別子で、カラム・関連・付帯情報とテーブルを結ぶ。parseで「スキーマ.テーブル」を解析どこでも
TableTypeテーブルの区分のenum。未知の区分は読み込み時に例外← TableDto・TableEntity
Tables出力対象のテーブルの集合。contains・find(テーブルキー)、bySchema・byTableName(呼ぶたびに作る。後者は利用テーブルの抽出でスキーマ修飾の無い名前を引く)← SchemaExportPipeline・ExportTargetConsistency・FunctionTableUsageAnalyzer・Writer等
TableDetail1テーブル分のカラム・インデックス・制約。assembleAllで、まとめて取得した結果をテーブルごとに振り分ける← AbstractCatalogRepository・TableDefinitionContent
ColumnEntity
IndexEntity
ConstraintEntity
カラム・インデックス・制約1件← TableDetail・テンプレート・TableSnapshot
Columns
Indexes
Constraintsパッケージプライベート
TableDetail.assembleAll専用の、テーブルキーで引ける集合← TableDetail
TriggerEntity
Triggers
トリガー1件と集合。イベントはリスト。定義はOracleでは宣言部だけで、本体(body)を別に持つ(PostgreSQLは空文字)← SchemaExportPipeline・TableDefinitionContent・ObjectListWriter
PartitionEntity
Partitions
パーティション表の下位のパーティション1件と集合。根のテーブルキーで引く。名前は根のスキーマからの相対(displayName)← TableDefinitionContent・TableDefinitionTemplates
ViewReferenceEntity
ViewReferences
ビューが参照するテーブル1件と集合。ビューと参照されるテーブルの両方のテーブルキーで引く(belongingTo・referencingTo)← SchemaExportPipeline・TableDefinitionContent・TableSnapshot
SchemaTableKeyedスキーマ名・テーブル名を持つエンティティのインタフェース。tableKey()の既定実装← 各Entity・AbstractEntities
AbstractEntitiesエンティティのリストをテーブルキーで索引する集合の基底。belongingTo(table)← Triggers・Partitions・ForeignKeys・Columns等
TableNamePatternsテーブル名パターン(ワイルドカード・除外・スキーマ修飾)。出力対象の範囲と観点の所属テーブルで共有← TableScope・Viewpoint
domain.model.relation:関連とER図のまとまり
ForeignKeyEntity関連1件(物理の外部キーと論理リレーションの総称)。参照元・参照先・列・多重度・由来。logical・resolveLogicalRelationNameどこでも
ForeignKeys関連の集合。参照元・参照先の2つの索引。physicalBelongingTo・logicalBelongingTo・referencingTo・groupBySchema・crossSchema・withinTables・crossingTableSetBoundary、テーブル定義書のER図の範囲(neighborhoodOf)← ExportTargetConsistency・TableDefinitionContent・MarkdownExportSinkFactory・ErDiagramWriter・Viewpoint
RelationType由来。PHYSICAL(実線--・物理)とLOGICAL(破線..・論理)← ForeignKeyEntity・Cardinality
Cardinality多重度。of(unique, mandatory)での判定、Mermaidの端点、日本語のラベル、論理リレーションの既定値← ForeignKeyDto・SidecarYamlRepository・TableSnapshot
ForeignKeyGroup1枚のER図に描く関連の集まり。ノード(スキーマ名・テーブル名順)、上限との比較、planRendering、mainTable(外部キーが無いグループでは例外。グループ一覧に載るグループは必ず外部キーを持つ)← ErDiagramWriter・Viewpoint
ForeignKeyGroupsページ構成の決定(compose)。連結成分(Union-Find)と、上限内でのまとめ直し(pack)。PageComposition(Single・Grouped)← ErDiagramWriter
NodeLimit1枚の図のノード数の上限。0以下は上限なしに揃える← MarkdownExportSinkFactory・ForeignKeyGroup等
RenderingPlan描く(Draw)か省く(Omit)かの計画(sealed interface)← ErDiagramWriter・ErDiagramTemplates・ViewpointWriter
DiagramBoxesER図の箱の表示内容(論理テーブル名・関連カラム)。Builderが取得したカラムから関連カラムだけを残す← SchemaExportPipeline・ER図のWriter・テンプレート
DiagramColumn箱に表示するカラムと、FKか← DiagramBoxes・MermaidSupport・TableDefinitionTemplates
DiagramNeighborhoodテーブル定義書のER図に描く関連と、描いた距離・指定された距離。isShortenedで上限のため縮めたかを判定← ForeignKeys.neighborhoodOf → TableDefinitionTemplates.erDiagram
domain.model.sidecar・viewpoint:サイドカーと観点
SidecarサイドカーYAMLの内容(付帯情報・論理リレーション・観点)。nullを空へ揃える← SidecarYamlRepository → SchemaExportPipeline
Annotationsテーブルキーごとの手動付帯情報。無ければTableAnnotation.EMPTYを返す← SchemaExportPipeline・ExportTargetConsistency・TableDefinitionContent
TableAnnotation1テーブル分の説明・備考・カラム備考(記述順を保つ)。orphanColumnNames(記述順のList)← TableDefinitionTemplates・TableSnapshot
Viewpoint観点。of(識別子・パターンの検証)、contains、unmatchedPatterns、resolve(所属テーブル・内側の関連・境界の関連)← SidecarYamlRepository・ViewpointWriter・InsightExportSinkFactory
Viewpoints観点の集合(宣言順)。containing(table)でテーブルから逆引き← Sidecar・TableDefinitionContent・ViewpointWriter
ViewpointContent1観点分の出力内容(所属テーブル・ForeignKeyGroup・観点外との関連)← Viewpoint.resolve → ViewpointWriter・ViewpointsInsight
domain.model.schemaobject・database・document
FunctionEntity
Functions
関数・プロシージャ1件と集合。オーバーロードの番号・数、定義本体(一覧の取得では空)。schemaNamesは定義本体を取る単位← SchemaExportPipeline・ObjectListWriter・FunctionSnapshot・DocumentLocations
SequenceEntity
Sequences
シーケンス1件と集合。数値は桁あふれしないよう文字列のまま← ObjectListWriter・SchemaSnapshotWriter
TypeEntity
Types
ユーザー定義型1件と集合← ObjectListWriter・SchemaSnapshotWriter
Dbms接続先のDBMS種別のenum(POSTGRESQL・ORACLE)。表示名(PostgreSQL・Oracle)を基本情報・スナップショットに出し、種別そのものは定義本体の字句の規則(9.9節)の切り替えに使う。fromDisplayNameで、mapperが返す表示名から求める← DatabaseDto・DatabaseEntity/→ SqlLexer・各BodyLocator・TableReferenceScanner
DatabaseEntity
BaseInfoEntity
DB名・DBMS種別(Dbms)・メジャーバージョン(カタログから)と、それに生成日を足した基本情報。BaseInfoEntity.dbmsNameは表示名← AbstractCatalogRepository・SchemaExportPipeline/→ テンプレート
ListDocumentType一覧ドキュメントの種類(7つ)。ファイル名の接頭辞とタイトル← MarkdownExportSinkFactory・DocumentLocations・テンプレート
domain.model.metrics:出力対象の集計(DB単位のREADME)
DatabaseMetricsスキーマごとの集計。カラム以外は一括取得したExportTargetsからbuilderの時点で数え、カラムはチャンクを書くたびにBuilder.collectColumnsで足す。集計用のSQLは使わない(テーブルの絞り込みは取得後に行うため、SQLで数えると出力したドキュメントと数が合わない)。isCountedで取得した種別かを判定する← SchemaExportPipeline・ExportSink.writeSummary・ReadmeTemplates
SchemaMetrics1スキーマ分の数(テーブル・パーティション表・ビュー・マテリアライズドビュー・カラム・関数・プロシージャ・シーケンス・型・トリガー・論理名を持つテーブルとカラム・外部キー・論理リレーション・関連を持たないテーブル・観点に所属するテーブル・説明を補ったテーブル)。plusで合計する。割合・閾値による良し悪しの判定は付けない← DatabaseMetrics・ReadmeTemplates
domain.model.tableusage:関数・プロシージャの利用テーブル(--preview)
FunctionTableUsage1関数分の抽出結果(状態・解析しなかった言語・tables・dynamicSql・incomplete・overloadsMerged)。notAnalyzed(抽出しない実行)・analyzed等のファクトリ、isAttempted(節・参考情報を出すか)、schemaUndeterminedTables(決まり方ごとの行)← FunctionTableUsageAnalyzer・FunctionDefinitionContent/→ FunctionTableUsageTemplates・FunctionTableUsagesInsight
TableUsage利用しているテーブル1件。同じ名前の候補のテーブル・スキーマの決まり方・操作の集合。schemaDetermined・has・orderedOperations・tableType← FunctionTableUsageAnalyzer/→ FunctionTableUsage
CrudOperation
DynamicSqlKind
SchemaResolution
TableUsageStatus
操作(C・R・U・D)/動的SQLの種類(EXECUTE・EXECUTE_IMMEDIATE・DBMS_SQL・OPEN_FOR・DBLINK)/スキーマの決まり方(DETERMINED・SEARCH_PATH・CURRENT_USER・SYNONYM)/抽出の状態(NOT_ANALYZED・ANALYZED・UNSUPPORTED_LANGUAGE・NO_DEFINITION・WRAPPED・SUBPROGRAM_NOT_FOUND)← TableReferenceScanner・FunctionTableUsage・テンプレート
domain.model.snapshot・insight:機械が読む形
DatabaseSnapshotdatabase.json(形式バージョン・DB名・DBMS・メジャーバージョン)。FORMAT_VERSIONはmcp-serverとの契約← SchemaSnapshotWriter
TableSnapshottables.jsonlの1行。カラム・インデックス・制約・外部キー・論理リレーション・トリガー(Oracleは本体を含む)・付帯情報・ビューが参照するテーブル(referencedTables。項目の並びを変えないよう末尾)を項目として持つ。被参照と参照しているビューは持たない(参照元から導ける)← SchemaSnapshotWriter
FunctionSnapshot
SequenceSnapshot
TypeSnapshot
functions・sequences・types.jsonlの1行← SchemaSnapshotWriter
SnapshotValuesパッケージプライベート空文字をnullにする(JSONで項目ごと省略させる)← 各Snapshot
SnapshotKindtables・functions・sequences・types。identifyで行の表示名(関数は引数を含む)← SnapshotLocations・SnapshotDiff
DiffResult
ContentDiff
比較結果(生成側だけ・コミット側だけ・内容不一致)と、1件分のunified diff← SnapshotDiff → DiffReportFormatter
ViewpointsInsight
ViewpointInsight
viewpoints.json(形式バージョン・観点・所属テーブル)。ViewpointContentから作る← InsightWriter
FunctionTableUsagesInsightfunctionTableUsages.json(1スキーマ分。形式バージョン・DB名・スキーマ名・関数ごとの状態・利用テーブル・動的SQL・抽出元の定義本体のSHA-256)。FunctionDefinitionContentから作る。抽出していない関数は含めない← InsightWriter/mcp-serverが読む
domain.repository:外部とのやり取りのインタフェース
CatalogRepositoryDBからの取得(12メソッド)。実装はDB種別ごと← SchemaExportPipeline/実装 AbstractCatalogRepository
SidecarRepositoryサイドカーYAMLの読み込み。パスが空なら空のSidecar← SchemaExportPipeline/実装 SidecarYamlRepository
FileRepositoryファイル操作(書く・追記・ディレクトリ作成・存在・一覧・読む・一時ディレクトリ・再帰削除・ファイル1つの削除)。ファイル1つの削除(deleteFile)を呼ぶのはInsightWriterだけ← Writer・Usecase・SnapshotDiff・OutputDirectoryValidator/実装 LocalFileRepository
domain.service(export・target・snapshot・insight・path・直下)
ExportSink1つの出力形式で書き出すインタフェース(writeOverview・writeFunctionDefinitions(関数1つ分の出力内容FunctionDefinitionContentのリストを受ける)・beginSchemaTables・writeTableDefinition・writeSummary(集計DatabaseMetricsを受ける))← SchemaExportPipeline
MarkdownExportSinkFactoryMarkdownのSinkを作る。出力する一覧の決定(listDocuments)、テーブル定義書のER図の範囲(neighborhoodOf)の決定と、5つのWriterの呼び出し。READMEはwriteSummaryで集計とともに書く← ExportSchemaUsecase/→ 5つのWriter
SnapshotExportSinkFactoryスナップショットのSinkを作る← 2つのUsecase/→ SchemaSnapshotWriter
InsightExportSinkFactory参考情報のSinkを作る。writeOverviewで観点を、writeFunctionDefinitionsでスキーマごとの関数の利用テーブルを書く← ExportSchemaUsecase/→ InsightWriter
ExportTargetConsistency出力対象と関連・付帯情報・観点の突き合わせ。関連の合流(resolveForeignKeys)と、孤児・不一致の検出。結果は通知で返す← SchemaExportPipeline
SchemaSnapshotWriterスナップショットの書き込み。テーブルはinitTableFileで空にしてから1行ずつ追記← SnapshotExportSinkFactory/→ SnapshotSerializer・FileRepository
SnapshotDiff2つのsnapshot/をオブジェクト単位で比べ、内容の違いにunified diffを付ける← CheckDocumentDiffUsecase/→ UnifiedDiffGenerator
SnapshotSerializerrecordとJSONの変換のインタフェース(serialize・deserialize・formatForDiff)実装 JacksonSnapshotSerializer
UnifiedDiffGenerator2つの行リストからunified diffを作る(Myers法、前後3行、外部ライブラリに依存しない)← SnapshotDiff
InsightWriterviewpoints.jsonの書き込み(観点が無ければ書かない)と、functionTableUsages.json(スキーマごと)の書き込み。抽出していない実行では、後者の前回のファイルをdeleteFileで削除する← InsightExportSinkFactory
OutputPathResolver出力先パスのインタフェース(ベース・{DB名}/・各ドキュメント・分割ページ・snapshot/・insights/・参考情報の各ファイル)。--rm-distで消してよいか実装 DefaultOutputPathResolver
DocumentLocationsMarkdownのファイル名・{DB名}/からの相対パス・相対リンクの規則。名前はPathSegmentsを通す。個別定義のファイルが重なる関数を求める(functionsSharingDefinitionFile)← DefaultOutputPathResolver・各テンプレート・ExportSchemaUsecase
SnapshotLocations
InsightLocations
snapshot/・insights/の配置。ファイル名から種別を逆引き(kindOf)← DefaultOutputPathResolver
PathSegmentsDB由来の名前をパスの1要素として安全な形にする(encode。区切り・Windowsで使えない文字・制御文字・~を~+16進2桁に)← DocumentLocations・SnapshotLocations・InsightLocations・DefaultOutputPathResolver
OutputRoot出力先ベースディレクトリと基本情報の組。常に対で使うため1つの値にしている← 各Sink・Writer・OutputPathResolver
domain.service.tableusage:関数・プロシージャの定義本体から利用テーブルを抽出(9.9節。正規表現を使わない)
FunctionTableUsageAnalyzer抽出の入口。of(tables, tablesByName, dbms)でスキーマごとに作り(テーブル名の索引tablesByNameは全スキーマで1回だけ求めて渡す)、analyze(function)で本体を切り出し→テーブルを拾い→出力対象のテーブルと照らす。スキーマが決まる条件(SET search_path・Oracleの所有者)と候補の示し方を持つ← SchemaExportPipeline/→ FunctionBodyLocator・TableReferenceScanner・Tables
SqlLexer定義本体を字句に分ける。1文字ずつ読み、コメント・文字列を同時に区別する。規則はDBごと(PostgreSQLの入れ子のブロックコメント・ドル引用符・E'…'、Oracleのq'[…]'・条件付きコンパイル)。閉じていない要素は例外にせずincomplete← 各BodyLocator/→ Dbms・SqlToken
SqlToken
SqlTokenKind
SqlLexResult
字句1つ(元の文字列・位置・行番号・ドル引用符の中身の範囲)/字句の種類/字句の列とincomplete← SqlLexer・TableReferenceScanner
SqlTokensパッケージプライベート字句の列と、位置を指定した判定・名前の読み取り。範囲外の位置は判定が偽・取得がnullで、前後を覗く処理で位置の検査を繰り返さない← TableReferenceScanner・PackageSubprograms・各BodyLocator
SqlNames
AsciiCase
字句からカタログ上の名前(引用符を外し、引用符の無い名前はASCIIの英字だけ畳み込む)を求める/ASCIIの英字だけを対象にした大文字小文字の比較・変換(ıがIになる等を避ける)← TableReferenceScanner・各BodyLocator
FunctionBodyLocatorパッケージプライベート
PostgresBodyLocator
OracleBodyLocator
定義から解析する本体を切り出すインタフェース(of(dbms)で実装を選ぶ)と、DBMSごとの実装。PostgreSQLはドル引用符の中身・SQL標準の本体、Oracleは単独の関数は見出しのIS・ASの後ろ、パッケージのサブプログラムはPackageSubprogramsで求めた範囲。言語が対象外・wrap・定義が無い場合は理由を持つ結果を返す← FunctionTableUsageAnalyzer/→ SqlLexer・PackageSubprograms
PackageSubprogramsパッケージプライベートOracleのパッケージ本体から、最上位のサブプログラムの範囲を求める。BEGIN・CASEとENDの対応をスタックで数え(CASEは文と式を区別)、条件付きコンパイルは分岐ごとに読む。範囲がずれると後ろのサブプログラムすべてに波及するため、テストが厚い← OracleBodyLocator
FunctionBodyパッケージプライベート切り出した結果。解析しない場合(NotAnalyzable・UnsupportedLanguage)と、DBMSごとの解析する本体(Postgres:字句・incomplete・searchPath/Oracle:字句・incomplete・overloadsMerged・invokerRights)を型で分ける(sealed interface)← 各BodyLocator/→ FunctionTableUsageAnalyzer
TableReferenceScannerパッケージプライベート字句から、テーブルを指す位置の名前と操作(C・R・U・D)・動的SQLを拾う。きっかけの語と前後の字句だけで判定し、完全な構文解析はしない。;と最上位のLOOPで文の状態を作り直す← FunctionTableUsageAnalyzer/→ SqlTokens
TableReference
ScanResultパッケージプライベート
拾った名前1件(スキーマ・テーブル・操作。出力対象と照らす前)/拾った結果(名前の列と動的SQLの種類)← TableReferenceScanner/→ FunctionTableUsageAnalyzer
domain.service.writer:Markdownの段取りとテンプレート
PagedSectionWriter3,000行を超える表を別ページへ分ける← 4つのWriter
TableDefinitionWriterテーブル一覧と定義書の段取り(セクションの順)← MarkdownExportSinkFactory
TableDefinitionTemplates定義書の各セクション(エスケープ・○・参照するテーブル/参照しているビュー・トリガーの本体・描画距離に応じたテーブル単位のER図を含む)← TableDefinitionWriter/→ MermaidSupport
TableDefinitionListTemplatesテーブル一覧(関連ドキュメントへのリンクを含む)← TableDefinitionWriter
ErDiagramWriterスキーマ別ER図・グループ別ER図・索引の段取り。composeの結果で振り分け、planRenderingに従う← MarkdownExportSinkFactory/→ ForeignKeyGroups
ErDiagramTemplatesスキーマ別ER図のページ(図の本体はMermaidSupport.erDiagramCodeBlock)、省略・分割の文言、グループ一覧(groupIndex)、掲載テーブル・外部キー一覧の表← ErDiagramWriter・ViewpointTemplates
ViewpointWriter観点ページと観点一覧の段取り。観点ごとにresolveを1回だけ← MarkdownExportSinkFactory
ViewpointTemplates観点ページの各セクション。図と表はErDiagramTemplatesを再利用← ViewpointWriter
ObjectListWriterトリガー・関数・シーケンス・型の一覧と個別定義← MarkdownExportSinkFactory
ObjectListTemplates
ObjectDefinitionTemplates
一覧の表/個別定義のページ(関数はFunctionDefinitionContentから作り、「利用しているテーブル」の節を差し込む)← ObjectListWriter
FunctionTableUsageTemplates関数の個別定義の「利用しているテーブル」の節。表の前に限界(該当なし・動的SQL・読み切れなかった・オーバーロードをまとめた)を示す文、スキーマが決まらない名前は(search_path)等のプレースホルダーと候補。解析しなかった場合は表の代わりに理由。テーブル名・候補は既存のエスケープを通す← ObjectDefinitionTemplates/→ MarkdownTemplateSupport
ReadmeWriter
ReadmeTemplates
{DB名}/README.md(出力する一覧へのリンクと、出力対象の集計。集計の列は、target.objectsで外した種別を0ではなく列ごと省く)← MarkdownExportSinkFactory(writeSummary)/→ DatabaseMetrics
MarkdownTemplateSupport共通部品(基本情報・ファイルの見出しfileHeader・表の行row・リンクのセル・一覧へのリンクの箇条書きlistLinks・一覧への相対リンク・エスケープ・HTMLの無害化・コードブロック/コードスパンの囲み・SQLのコードブロックsqlCodeBlock・○)← 全テンプレート
MermaidSupportMermaid記法の部品(図の本体erDiagramCodeBlock・識別子の重複回避assignNodeIds・表示名・別名・属性・関連線)。二重引用符・改行を置き換える← ErDiagramTemplates・TableDefinitionTemplates
RowRenderer表の要素1つを1行にする関数(render(rowNumber, row)。行番号は1始まりで「No.」列に出す)。BiFunction<Integer, T, String>では行番号だと読めないため名前を付けている← PagedSectionWriter・ObjectListWriter・TableDefinitionTemplates
PagedSectionTemplates見出し・分割ページへのリンク・ページ送り・戻るリンク← PagedSectionWriter・テンプレート
infrastructure.db:DBへの接続と取得
ConnectionSettings検証済みの接続情報。config.ymlのdatabase+環境変数のパスワード+CLI引数。driver・url必須、ファイルのpasswordとurlへのパスワードの埋め込みは誤り。接続情報のキー(CONNECTION_KEYS)と、上書きするCLI引数の名前の規則(--db-キー。cliArgumentName)もここで1か所に定義する← Dbxray・CliArguments
MyBatisSqlSessionFactoriesmybatis-config.xmlからSqlSessionFactoryを作る(接続はしない)← Dbxray
DatabaseTypeDetector接続して製品名からDatabaseTypeを判定し、DBMS・ドライバの版をログへ残す。接続できない・非対応のDBはUserCorrectableException← Dbxray
DatabaseTypepostgresql・oracleと、ドメインのDBMS種別(Dbms)の対応。名前はSQLの名前空間にも使う← Components・AbstractCatalogRepository
AbstractCatalogRepositoryCatalogRepositoryの共通実装。SQLのIDを組み立てて実行し、DTO→エンティティ。SQLの失敗はどのSQLかを添えて包む← 2つの実装/→ 12のDTO
PostgresCatalogRepository
OracleCatalogRepository
DB種別を渡すだけの実装← DatabaseType
TableDto 等 12個のDTOTableDto・ColumnDto・IndexDto・ConstraintDto・ForeignKeyDto・TriggerDto・PartitionDto・ViewReferenceDto・FunctionDto・SequenceDto・TypeDto・DatabaseDto。SQLの結果の受け皿(record)で、toEntity()で値の形を揃える。ForeignKeyDtoは多重度を判定し、TriggerDtoはイベントを分解して本体の末尾の空白を除く← AbstractCatalogRepository
DtoValuesパッケージプライベートnull・空白を空文字へ、区切り文字で連結された値をリストへ← 各DTO
infrastructure.file・path・snapshot
LocalFileRepositoryFileRepositoryの実装(UTF-8で書く・追記・再帰削除・ファイル1つの削除等)← Components
SidecarYamlRepositoryサイドカーYAMLの読み込み(SnakeYAMLのSafeConstructor)。ファイルが無い・YAMLとして読めないのは誤り(文言はYamlSyntaxErrors)、個々の記述の誤りは何件目の定義かを添えて警告し、読み飛ばす← Components/→ TableKey.parse・Viewpoint.of・Cardinality
DefaultOutputPathResolverOutputPathResolverの実装。既定の出力先./output。解決したパスが起点の外を指さないことをwithinで確かめる。--rm-distでルート、カレント・ホームとその上位を拒否→ DocumentLocations・SnapshotLocations・InsightLocations
JacksonSnapshotSerializerSnapshotSerializerの実装(NON_EMPTY、差分表示用の整形)← Components
config・shared.exception
ConfigFile設定ファイル(--config、無ければconf/config.yml)をYAMLとして読む。仕様と検証は値を使う側が持つ← Dbxray・DbxrayProperties・CliArguments
YamlSyntaxErrorsSnakeYAMLの構文誤りを、ファイルの内容を引用しない文言(種類と位置)へ変える← ConfigFile・SidecarYamlRepository
InvalidConfigurationException設定の誤り(UserCorrectableExceptionの派生)← ConfigFile・DbxrayProperties・ConnectionSettings
UserCorrectableException利用者が直せる誤り。投げてよいのは入口と、入力・実行環境に触れるインフラだけ← CliArguments・OutputDirectoryValidator・DatabaseTypeDetector・SidecarYamlRepository・FailureReporter
package-info各パッケージの要旨。shared.exceptionだけは置いてよいものの規約も書く—
リソース(modules/cli/src/main/resources)
mapper/postgresql/
catalogMapper.xml
PostgreSQLのカタログ(pg_catalog)を読む13のSELECT。パーティションの子・親から複製された外部キー・トリガーを除く← AbstractCatalogRepository(名前空間…postgresql.CatalogRepository)
mapper/oracle/
catalogMapper.xml
Oracleのディクショナリ(ALL_*)を読む13のSELECT。LONG型の列(ビューのソース・トリガーの本体等)とDBのバージョンはWITH FUNCTIONで読む← 同上(…oracle.CatalogRepository)
mybatis-config.xmlDTOの型の別名、接続(POOLED。${driver}等)、2つのmapper← MyBatisSqlSessionFactories
log4j2.xmlログファイル(既定はこのツールがINFO・ライブラリがWARN。dbxray.log.levelで上げる)と、このツールのWARNの標準エラー出力。メッセージの改行を置き換える—
conf/config.yml配布する設定(全項目が未指定。各項目の例をコメントで示す)← ConfigFile
テスト(modules/cli/src/test・modules/cli/src/integrationTest)
ArchitectureTest構造の規約(3章の表)をArchUnitで検査→ main配下の全クラス
ComponentsTest両方のDB種別で、コントローラーまで組み立てられるか(DBには接続しない)→ Components
ExportSchemaUsecaseTest通常実行・--checkの流れ全体(--previewの有無による関数の出力の違いを含む)。Writerは本物、ファイルはメモリ上(InMemoryFileRepository)、DBは呼び出しを記録するスタブ→ 2つのUsecase
tableusageのテスト(domain.service.tableusage・domain.model.tableusage)字句(SqlLexerTest・SqlLexerPropertyTest)、本体の切り出し(FunctionBodyLocatorTest。パッケージのサブプログラムの範囲)、テーブルの判定(TableReferenceScannerTest。ScanCase・ExpectedNotationで期待値を書く)、名前の解決(FunctionTableUsageAnalyzerTest)、結果の型(TableUsageTest)、精度(TableUsageMetamorphicTest・GeneratedPackagesTest・RealisticFunctionBodiesTest・FunctionTableUsageAnalyzerRobustnessTest。9.9節)。本番のDBを使わず、src/test/resources/tableusageに長い定義本体を置く→ domain.service.tableusage
入口のテストCliArgumentsTest・CliUsageTest・DbxrayPropertiesTest・OutputDirectoryValidatorTest・TargetSelectionTest→ 入口の各クラス
presentationのテストController・FailureReporter(原因の併記)・DiffReportFormatter(行数の上限)・ExitStatus→ presentation
domain.modelのテスト多重度・グループ分割・関連の集合(描画距離を含む)・ビューの参照・テーブル名パターン・TableScope・観点・TableDefinitionContent・TableSnapshot等→ domain.model
domain.serviceのテスト突き合わせ・スナップショットの書き込みと比較・unified diff・配置の規則(PathSegmentsTest)・全Writer・全テンプレート(TableDefinitionTemplatesTestは911行)・DB由来の文字列の注入(MarkdownInjectionTest)→ domain.service
infrastructureのテスト接続情報・DB種別・DTOの変換・ファイル・サイドカーYAML・パス(出力先の外を指さないことの網羅的な検査OutputPathContainmentTestを含む)・JSON→ infrastructure
ConfigFileTest
testsupport
設定ファイルの読み込みと、テスト用の部品(EntityFixtures・ForeignKeyFixtures・DiagramBoxesFixtures・MarkdownAssert・ログの警告を確かめるCapturedLogs)← 各テスト
SampleDatabaseExportIT
OracleSampleDatabaseExportIT
結合テスト。サンプルDBに対して全体を通し、出力をベースライン(docs/sample/*/output)とファイル単位で完全一致させる。--previewの出力は、既定のベースラインに差分のベースライン(docs/sample/*/output-preview)を重ねたものと比べる。--checkで差分が無いことも確かめる→ ExportBaseline
PostgresCatalogRepositoryIT
OracleCatalogRepositoryIT
mapperの各SQLの取得結果(パーティション・多重度・オーバーロード・ビューの参照・メジャーバージョン・トリガーの本体等)→ 2つのmapper
ExportBaseline
SampleDatabase
OracleSampleDatabase
Testcontainersでddl.sqlを流したDBを起動し、本番と同じ2段階でコントローラーを作る(時計は固定、作成日は比較の前に置き換える)← 結合テスト

./gradlew buildは、domain配下(com/dbxray/domain/**)の単体テストのカバレッジがline 95%・branch 85%を下回ると失敗します。結合テストはDockerが必要なため、別のタスク(integrationTest・oracleIntegrationTest)に分かれています。

12変更するときの道しるべ

よくある変更ごとに、触る場所と確かめ方をまとめます。規約の原文はdocs/development/coding-guidelines.mdの「変更の種類ごとに必要な作業」にあります。

設定項目を1つ足すとき

  1. DbxrayPropertiesのSETTINGSに項目(設定ファイルでの位置・CLI引数名・値の形式)を足し、既定値と変換を書く。上書きのCLI引数はCliArgumentsがこの表から引く。
  2. 値をrequest(ExportSchemaRequest・必要ならCheckDocumentDiffRequest)に足し、使う層まで同じrecordで運ぶ。文字列のまま深い層へ渡さず、入口で型にする。
  3. 配布するconf/config.yml(全項目が未指定)に項目を並べ、docs/usage/cli.mdの設定の表(CLI引数の列を含む)を更新する。
  4. DbxrayPropertiesTest・CliArgumentsTestに、未指定・誤った値・上書きのテストを足す。

DBから取得する項目を足すとき

  1. PostgreSQL用とOracle用の両方のmapper XMLに列を足す。別名はDTOのコンポーネント名と同じにする。
  2. DTO(record)にコンポーネントを足し、toEntity()で値の形を揃える(無い値はDtoValues.textで空文字、連結された値はsplitでリスト)。業務ルール(既定値の決定・名前の生成等)はDTOやリポジトリに書かず、ドメインのメソッドに持たせる。
  3. エンティティに項目を足し、表示はテンプレートで(連結・エスケープ・○)。機械が読む必要があれば…Snapshotにも足す(値の形は8.3節、「空は出力しない」は2.5節)。
  4. ./gradlew integrationTestを実行し、PostgresCatalogRepositoryITに取得結果のテストを足す。Oracle側を変えたら./gradlew oracleIntegrationTestとOracleCatalogRepositoryIT。出力が変わるなら、verifyスキルの手順でベースラインを出し直す。

テーブル定義書にセクションを足すとき

出力形式を足すとき

新しいドメインサービス・リポジトリを足すとき

プレビューの機能を足す・外すとき

関数の定義本体を読むコードを足す・変えるとき

スナップショットの形式を変えるとき

確かめ方の早見表

変更実行するもの
どの変更でも./gradlew build(単体テスト・ArchitectureTest・Spotless・domainのカバレッジ基準)。Javadocの書式は./gradlew javadoc(doclint)
mapper XML・ドメイン層・テンプレート./gradlew integrationTest(Docker上のPostgreSQL。出力全体をベースラインと比較)
Oracle用mapper、Oracleの出力が変わりうる変更./gradlew oracleIntegrationTest(Oracle Database Free。メモリ2GB程度)
スナップショットの形式./gradlew :mcp-server:test
出力を意図して変えたverifyスキル(.claude/skills/verify/SKILL.md)の手順でdocs/sample/*/outputを出し直す。--previewの出力が変わるなら、差分のベースラインoutput-previewも作り直す
参考情報の形式(functionTableUsages.json等)./gradlew :mcp-server:test(SampleFunctionTableUsagesContractTestが差分のベースラインを読む)。互換性の無い変更ならFunctionTableUsagesInsight.FORMAT_VERSIONとmcp-serverのInsightsDirectoryReader.SUPPORTED_FORMAT_VERSIONを揃えて上げる

やってはいけないこと

13用語集

利用者向けのドキュメントでの呼び方とコード上の名前の対応です。全体はdomain-model.mdの用語集にあります。

テーブルキー
TableKey。スキーマ名+物理テーブル名。テーブルの識別子で、カラム・関連・付帯情報とテーブルを結ぶ。
テーブル
TableEntity。テーブル・ビュー・マテリアライズドビューの総称(区分はTableType)。
詳細情報
TableDetail。1テーブル分のカラム・インデックス・制約。チャンク単位で取得する。
出力対象
ExportTargets。一括取得した軽い情報の組(条件ではなくデータ)。何を出力するかの条件は「出力対象の絞り込み条件」(TargetSelection)と呼んで区別する。
出力対象の範囲
TableScope。設定のschema・table。
テーブル名パターン
TableNamePatterns。table=の記法(ワイルドカード・除外・スキーマ修飾)。観点の所属テーブルの指定でも使う。
出力対象オブジェクト種別
OutputObjectType。設定のtarget.objects(trigger・function・sequence・type)。
1テーブル分の出力内容
TableDefinitionContent。テーブル定義書1ファイル・スナップショット1行分。
関数1つ分の出力内容
FunctionDefinitionContent。関数・プロシージャの定義書1ファイル・スナップショットと参考情報の1件分(関数+利用しているテーブル)。
プレビューの機能
PreviewFeature(--preview)。精度・出力の形を固めている途中で、付けたときだけ動く機能。互換性の対象にせず、スナップショットは変えない。
利用しているテーブル
FunctionTableUsage・TableUsage・FunctionTableUsageAnalyzer。関数・プロシージャの定義本体から機械的に抽出した、利用している出力対象のテーブルと操作。呼び出し先の関数・動的SQLの中は含まない参考値。
操作(C・R・U・D)
CrudOperation。利用しているテーブルへの作成・読み取り・更新・削除。TRUNCATEはD、MERGEはWHEN句の操作。
スキーマが決まらない名前
SchemaResolution・TableUsage.schemaCandidates。スキーマ修飾が無く、定義からは決まらない名前(search_path・current_user・synonym)。同じ名前の出力対象のテーブルがあるスキーマを候補として示し、決めつけない。
動的SQL
DynamicSqlKind。実行時に組み立てるSQL(EXECUTE・EXECUTE IMMEDIATE等)。中の参照は抽出できないため、含むことだけを示す。
字句(トークン)
SqlToken。定義本体を、空白・コメントを除いて分けた最小の単位(SqlLexerが作る)。
DBMS種別
Dbms。接続先のDBMS(PostgreSQL・Oracle)。基本情報に出す表示名と、定義本体の字句の規則の切り替えに使う。
関連
ForeignKeyEntity・ForeignKeys。外部キー(物理、RelationType.PHYSICAL)と論理リレーション(論理、LOGICAL)の総称。クラス名は歴史的経緯でForeignKey。
論理リレーション
DBに制約が無く、サイドカーYAMLのrelationsで宣言した関連。ER図では破線。
被参照の関連
incomingRelations・ForeignKeys.referencingTo。自分を参照している関連(自己参照を除く)。
多重度
Cardinality。参照元の外部キー列の一意性とNOT NULLから決まる、関連の両端の件数の関係。
描画距離
output.erDiagramDistance・DiagramNeighborhood。テーブル定義書のER図に描く関連の段数。1は自テーブルの関連だけ。図が上限を超える場合は縮める。
ビューが参照するテーブル/参照しているビュー
ViewReferenceEntity・ViewReferences。ビュー・マテリアライズドビューが参照するテーブル(referencedTables)と、その逆向き(referencingViews。MCPではreferencedByViews)。DBの依存関係から取得する。
ノード
ER図に描くテーブル。関連を持つテーブルだけを数える。
グループ
ForeignKeyGroup。1枚のER図に描く関連のまとまり。上限を超えると連結成分ごとにまとめ直す(ForeignKeyGroups)。
関連カラム
関連の参照元・参照先として使われるカラム。ER図の箱(DiagramBoxes)に表示する。
サイドカー
Sidecar。DBに無い情報を書くYAML(設定のキーはannotations、コード上はsidecarPath)。手動付帯情報・論理リレーション・観点を持つ。
手動付帯情報
Annotations・TableAnnotation。テーブル説明・テーブル備考・カラム備考。
集計(metrics)
DB単位のREADMEに載せる、出力対象のスキーマごとの数(オブジェクト・論理名の記述状況・関連と読み解きの状況)。出力したドキュメントと数が合うよう、SQLでは数えず取得した結果から数える。
孤児付帯情報
実在しないテーブル・カラムに対する付帯情報。リネーム・削除の見落としとして警告する。
観点
Viewpoint。業務ドメイン別にテーブルをまとめる切り口。観点ページと観点一覧になり、スナップショットには含めない。
突き合わせの通知
ConsistencyNotice。出力対象と関連・付帯情報・観点を突き合わせた結果。重要度に応じてログへ出す。
基本情報
BaseInfoEntity。各ドキュメントの先頭に載せるRDBMS(メジャーバージョンを添える)・DB名・作成日。作成日はDBではなくClockから。
チャンク
詳細情報をまとめて取得・出力するテーブルの塊。大きさは設定のchunkSize(既定3000、0以下でスキーマ全体)。
Sink
ExportSink。取得した情報を1つの出力形式で書き出すもの。Markdown・スナップショット・参考情報の3つ。
スナップショット
domain.model.snapshot。取得結果を構造化したJSON Lines。生成日を含まず、--checkの比較対象で、mcp-serverの入力。
参考情報(insights)
domain.model.insight。スナップショットの事実とは別にAIへ渡す情報(観点と、--previewのときの関数の利用しているテーブル)。--checkの対象外。
利用者が直せる誤り
UserCorrectableException。設定・入力・実行環境を見直せば解消する失敗。報告にスタックトレースを含めない。