価格

アーキテクチャ概要(全体像)

関連ソースファイル

このページの内容は以下のソースファイルに基づいて生成されています:

tbls はデータベーススキーマの解析、ドキュメント生成、および整合性チェックを行うための CLI ツールである。Cobra フレームワークをベースとしたコマンドラインインターフェースを持ち、多様なデータソース(RDBMS、NoSQL、クラウドデータベースサービス)に対応するドライバーアーキテクチャを採用している。本セクションでは、システム全体のアーキテクチャ、主要モジュールの責務と実装詳細、およびデータフローについて詳述する。

システムアーキテクチャ概要

tbls は、関心の分離原則に基づき、CLI層、データソース分析層、スキーマモデル層、出力処理層の4つの主要層で構成される。各層は明確なインターフェースを通じて通信し、疎結合な設計となっている。

正在加载图表渲染器...

アーキテクチャ図の要点:

  1. CLI層main.go:21-36 から開始し、Cobra ベースのコマンドツリーエントリーポイントとして機能する。外部サブコマンドの動的検出機能を持つ。
  2. データソース層datasource/datasource.go:45-90 でDSN解析を行い、プロトコルプレフィックスに基づいて適切な分析関数へ振り分ける。
  3. スキーマモデル層schema/schema.go:39-113 で定義される構造体群により、データベースメタデータを統一的に表現する。
  4. 出力層output/output.go:22-112 のテンプレート関数を用いて、Markdown や Mermaid 形式でのドキュメント生成を行う。
  5. 各層間の依存は単方向であり、下位層は上位層の実装詳細に依存しない。

エントリーポイントとCLI構造

プログラム起動フロー

tbls の実行は main.go:21-36 から開始される。main パッケージは、各種データベースドライバー(MySQL、PostgreSQL、SQLite、MSSQL、Snowflake、Databricks)をブランクインポートし、cmd.Execute() を呼び出す。

go
1import (
2    _ "github.com/databricks/databricks-sql-go"
3    _ "github.com/go-sql-driver/mysql"
4    _ "github.com/lib/pq"
5    _ "github.com/mattn/go-sqlite3"
6    _ "github.com/microsoft/go-mssqldb"
7    _ "github.com/snowflakedb/gosnowflake"
8    "github.com/k1LoW/tbls/cmd"
9)
10
11func main() {
12    cmd.Execute()
13}

この設計により、データベースドライバーの初期化(init() 関数による sql.Register 呼び出し)が main 関数実行前に完了する。

ルートコマンドとフラグ定義

cmd/root.go:183-203Execute() 関数が定義されている。この関数は、まず getExtSubCmds("tbls") を呼び出して PATH 環境変数から tbls-* プレフィックスを持つ外部サブコマンドを検出し、その後 rootCmd.Execute() を実行する。

go
1func Execute() {
2    var err error
3    subCmds, err = getExtSubCmds("tbls")
4    if err != nil {
5        printError(err)
6        os.Exit(1)
7    }
8    if err := rootCmd.Execute(); err != nil {
9        printError(err)
10        os.Exit(1)
11    }
12}
13
14func init() {
15    rootCmd.SetUsageTemplate(rootUsageTemplate)
16    rootCmd.Flags().StringVarP(&when, "when", "", "", "command execute condition")
17    rootCmd.Flags().StringVarP(&configPath, "config", "c", "", "config file path")
18    rootCmd.Flags().StringVarP(&dsn, "dsn", "", "", "data source name")
19}

init() 関数では、グローバルフラグ(--when--config--dsn)が定義される。これらはすべてのサブコマンドで継承される。

外部サブコマンド検出メカニズム

cmd/root.go:205-245 には、外部サブコマンドの検出とシェル補完機能が実装されている。getExtSubCmds() 関数は PATH 環境変数を走査し、tbls-* プレフィックスを持つ実行可能ファイルを検出する。

go
1func getExtSubCmds(prefix string) ([]string, error) {
2    subCmds := []string{}
3    paths := lo.Uniq(filepath.SplitList(os.Getenv("PATH")))
4    for _, p := range paths {
5        if strings.TrimSpace(p) == "" {
6            continue
7        }
8        // ... ファイルシステム走査ロジック
9    }
10    return subCmds, nil
11}

genValidArgsFunc() は、シェル補完時に外部サブコマンドの __complete サブコマンドを実行し、動的な補完候補を取得する。これにより、外部サブコマンドが独自の補完ロジックを提供できる。

責務境界:

  • 行うこと: CLI引数の解析、外部サブコマンドの検出と実行、シェル補完の提供
  • 行わないこと: ビジネスロジックの実行、データベース接続の管理

エラー処理:

  • 外部サブコマンド検出エラー時は printError() でエラーメッセージを表示し、os.Exit(1) で終了
  • コマンド実行エラー時も同様にエラー表示と終了コード1を返す

データソース分析とドライバーアーキテクチャ

DSN解析と振り分けロジック

datasource/datasource.go:45-90Analyze() 関数は、DSN(Data Source Name)のプロトコルプレフィックスを解析し、適切な分析関数へ処理を委譲する。

go
1func Analyze(dsn config.DSN) (_ *schema.Schema, err error) {
2    defer func() {
3        err = errors.WithStack(err)
4    }()
5    urlstr := dsn.URL
6    if strings.HasPrefix(urlstr, "https://") || strings.HasPrefix(urlstr, "http://") {
7        return AnalyzeHTTPResource(dsn)
8    }
9    if strings.HasPrefix(urlstr, "github://") {
10        return AnalyzeGitHubContent(dsn)
11    }
12    if strings.HasPrefix(urlstr, "json://") {
13        return AnalyzeJSON(urlstr)
14    }
15    if strings.HasPrefix(urlstr, "bq://") || strings.HasPrefix(urlstr, "bigquery://") {
16        return AnalyzeBigquery(urlstr)
17    }
18    if strings.HasPrefix(urlstr, "span://") || strings.HasPrefix(urlstr, "spanner://") {
19        return AnalyzeSpanner(urlstr)
20    }
21    if strings.HasPrefix(urlstr, "dynamodb://") || strings.HasPrefix(urlstr, "dynamo://") {
22        return AnalyzeDynamodb(urlstr)
23    }
24    if strings.HasPrefix(urlstr, "mongodb://") || strings.HasPrefix(urlstr, "mongo://") {
25        return AnalyzeMongodb(urlstr)
26    }
27    if strings.HasPrefix(urlstr, "databricks://") {
28        return AnalyzeDatabricks(urlstr)
29    }
30    // ... RDBMS処理
31}

この設計により、新しいデータソースタイプの追加が容易になる。プロトコルプレフィックスのチェック順序は、より具体的なプレフィックス(例: bigquery://)を一般的なプレフィックス(例: bq://)より前に判定する。

HTTP/GitHubリソース分析

datasource/datasource.go:166-226 では、HTTPリソースおよびGitHubコンテンツからのスキーマ分析が実装されている。

AnalyzeHTTPResource() は、HTTP GET リクエストを発行し、レスポンスボディを JSON としてデコードする。カスタムヘッダーの設定が可能であり、タイムアウトは10秒に設定されている。

go
1func AnalyzeHTTPResource(dsn config.DSN) (_ *schema.Schema, err error) {
2    defer func() {
3        err = errors.WithStack(err)
4    }()
5    s := &schema.Schema{}
6    req, err := http.NewRequest("GET", dsn.URL, nil)
7    if err != nil {
8        return nil, err
9    }
10    for k, v := range dsn.Headers {
11        req.Header.Add(k, v)
12    }
13    client := &http.Client{Timeout: time.Duration(10) * time.Second}
14    resp, err := client.Do(req)
15    if err != nil {
16        return nil, err
17    }
18    defer resp.Body.Close()
19    dec := json.NewDecoder(resp.Body)
20    if err := dec.Decode(s); err != nil {
21        return nil, err
22    }
23    if err := s.Repair(); err != nil {
24        return nil, err
25    }
26    return s, nil
27}

AnalyzeGitHubContent() は、github://owner/repo/path/to/schema.json 形式のDSNを解析し、GitHub API を通じてファイルコンテンツを取得する。ghfs ライブラリを使用してGitHubファイルシステムをマウントする。

Driverインターフェースによる抽象化

drivers/drivers.go:8-14Driver インターフェースが定義されている。

go
1type Driver interface {
2    Analyze(*schema.Schema) error
3    Info() (*schema.Driver, error)
4}
5
6// Option is the type for change Config.
7type Option func(Driver) error

このインターフェースは、すべてのデータベースドライバーが実装すべき契約を定義する:

  • Analyze(*schema.Schema) error: 渡されたスキーマオブジェクトにテーブル、カラム、インデックス等のメタデータを設定する
  • Info() (*schema.Driver, error): ドライバー情報(名前、バージョン等)を返す

Option 型は関数型オプションパターンを実装しており、ドライバーの設定を柔軟にカスタマイズできる。

責務境界:

  • 行うこと: DSN解析、プロトコル判定、適切な分析関数への委譲、スキーマオブジェクトの構築
  • 行わないこと: ドキュメント生成、設定ファイルの読み込み

エラー処理:

  • すべての分析関数は defererrors.WithStack() を使用し、スタックトレースを付加
  • HTTPリクエスト失敗時は即座にエラーを返す
  • JSONデコード失敗時はエラーをラップして返す

スキーマ構造とメタデータ管理

ラベルとビューポイントによる整理

schema/schema.go:39-83 では、スキーマ要素の整理と分類のための構造体が定義されている。

Label はテーブルやカラムにタグ付けするための構造体であり、Virtual フィールドで自動付与されたラベルを識別する。

go
1func (labels Labels) Merge(name string) Labels {
2    if labels.Contains(name) {
3        return labels
4    }
5    return append(labels, &Label{Name: name, Virtual: true})
6}
7
8func (labels Labels) Contains(name string) bool {
9    return lo.ContainsBy(labels, func(item *Label) bool {
10        return item.Name == name
11    })
12}

Viewpoint は、関連するテーブルをグループ化して表示するための構造体である。ラベルベースまたはテーブル名リストベースでグループを構成でき、Distance フィールドで関連テーブルを再帰的に含める深さを指定できる。

go
1type Viewpoint struct {
2    Name     string            `json:"name"`
3    Desc     string            `json:"desc"`
4    Labels   []string          `json:"labels,omitempty"`
5    Tables   []string          `json:"tables,omitempty"`
6    Distance int               `json:"distance,omitempty"`
7    Groups   []*ViewpointGroup `json:"groups,omitempty"`
8    Schema   *Schema           `json:"-"`
9}

Viewpoints.Merge() メソッドは、同名のビューポイントが存在する場合は上書き、存在しない場合は追加するセマンティクスを持つ。

インデックス、制約、トリガー定義

schema/schema.go:85-113 では、データベースオブジェクトのメタデータ構造体が定義されている。

go
1type Index struct {
2    Name    string   `json:"name"`
3    Def     string   `json:"def"`
4    Table   *string  `json:"table"`
5    Columns []string `json:"columns"`
6    Comment string   `json:"comment,omitempty"`
7}
8
9type Constraint struct {
10    Name              string   `json:"name"`
11    Type              string   `json:"type"`
12    Def               string   `json:"def"`
13    Table             *string  `json:"table"`
14    ReferencedTable   *string  `json:"referenced_table,omitempty" yaml:"referencedTable,omitempty"`
15    Columns           []string `json:"columns,omitempty"`
16    ReferencedColumns []string `json:"referenced_columns,omitempty" yaml:"referencedColumns,omitempty"`
17    Comment           string   `json:"comment,omitempty"`
18}
19
20type Trigger struct {
21    Name    string `json:"name"`
22    Def     string `json:"def"`
23    Comment string `json:"comment,omitempty"`
24}

設計上の特徴:

  • Table フィールドはポインタ型(*string)であり、テーブル名が未設定の場合と空文字列の場合を区別
  • ReferencedTableReferencedColumns は外部キー制約専用のフィールド
  • YAML タグでスネークケースのエイリアスを定義し、設定ファイルでの可読性を向上

責務境界:

  • 行うこと: データベースメタデータの統一的な表現、JSON/YAML シリアライゼーション、ラベル/ビューポイントによる整理
  • 行わないこと: メタデータの取得(ドライバーの責務)、ドキュメント生成(出力層の責務)

出力処理とテンプレート機能

カスタムテンプレート関数

output/output.go:22-60 で、テンプレートエンジン用のカスタム関数が定義されている。

go
1func Funcs(d *dict.Dict) map[string]interface{} {
2    return template.FuncMap{
3        "nl2br": func(text string) string {
4            r := strings.NewReplacer("\r\n", "<br />", "\n", "<br />", "\r", "<br />")
5            return r.Replace(text)
6        },
7        "nl2br_slash": func(text string) string {
8            r := strings.NewReplacer("\r\n", "<br />", "\n", "<br />", "\r", "<br />")
9            return r.Replace(text)
10        },
11        "nl2mdnl": func(text string) string {
12            r := strings.NewReplacer("\r\n", "  \n", "\n", "  \n", "\r", "  \n")
13            return r.Replace(text)
14        },
15        "nl2space": func(text string) string {
16            r := strings.NewReplacer("\r\n", " ", "\n", " ", "\r", " ")
17            return r.Replace(text)
18        },
19        "escape_nl": func(text string) string {
20            r := strings.NewReplacer("\r\n", "\\n", "\n", "\\n", "\r", "\\n")
21            return r.Replace(text)
22        },
23        "escape_double_quote": func(text string) string {
24            return strings.ReplaceAll(text, "\"", "#quot;")
25        },
26        "show_only_first_paragraph": ShowOnlyFirstParagraph,
27        "lookup": func(text string) string {
28            return d.Lookup(text)
29        },
30        "label_join": LabelJoin,
31        "escape": func(text string) string {
32            return mdurl.Encode(text)
33        },
34        "escape_mermaid": func(text string) string {
35            return escapeMermaidRe.ReplaceAllString(text, "_")
36        },
37        // ... カーディナリティ変換関数
38    }
39}

これらの関数は、Markdown、HTML、Mermaid 図などの出力フォーマットに合わせてテキストを変換する:

  • nl2br 系: 改行コードを HTML の <br /> に変換
  • escape_* 系: 特殊文字のエスケープ処理
  • lookup: 辞書による用語翻訳
  • escape_mermaid: Mermaid 図で使用できない文字をアンダースコアに置換

テキスト処理ユーティリティ

output/output.go:90-112 には、テキスト処理のユーティリティ関数が定義されている。

go
1func ShowOnlyFirstParagraph(text string) string {
2    if strings.Contains(text, "\r\n\r\n") {
3        splitted := strings.SplitN(text, "\r\n\r\n", 2)
4        return splitted[0]
5    }
6    if strings.Contains(text, "\r\r") {
7        splitted := strings.SplitN(text, "\r\r", 2)
8        return splitted[0]
9    }
10    splitted := strings.SplitN(text, "\n\n", 2)
11    return splitted[0]
12}
13
14func LabelJoin(labels schema.Labels) string {
15    if len(labels) == 0 {
16        return ""
17    }
18    m := []string{}
19    for _, l := range labels {
20        m = append(m, l.Name)
21    }
22    return fmt.Sprintf("`%s`", strings.Join(m, "` `"))
23}

ShowOnlyFirstParagraph() は、長いコメントテキストの概要表示に使用される。異なる改行コード形式(CRLF、CR、LF)に対応している。

責務境界:

  • 行うこと: テンプレート関数の提供、テキスト変換処理、フォーマット固有のエスケープ処理
  • 行わないこと: スキーマデータの取得、ファイル I/O 操作

主要コマンドの実装パターン

スキーマ取得の共通フロー

cmd/cmd.go:10-30 で、複数のコマンドから使用されるスキーマ取得の共通処理が定義されている。

go
1func getSchemaFromJSONorDSN(c *config.Config) (*schema.Schema, error) {
2    if _, err := os.Stat(c.SchemaFilePath()); err == nil {
3        s, err := datasource.AnalyzeJSONStringOrFile(c.SchemaFilePath())
4        if err != nil {
5            return nil, err
6        }
7        if err := c.FilterTables(s); err != nil {
8            return nil, err
9        }
10        return s, nil
11    }
12    s, err := datasource.Analyze(c.DSN)
13    if err != nil {
14        return nil, err
15    }
16    if err := c.ModifySchema(s); err != nil {
17        return nil, err
18    }
19    return s, nil
20}

この関数は以下の優先順位でスキーマを取得する:

  1. 設定ファイルで指定されたスキーマファイルパス(schema.json)が存在する場合、JSON から読み込み
  2. 存在しない場合、DSN からデータベースに接続して分析

FilterTables() はインクルード/除外パターンに基づいてテーブルをフィルタリングし、ModifySchema() は設定ファイルのメタデータでスキーマを拡張する。

docコマンドの実装

cmd/doc.go:115-162 では、ドキュメント生成コマンドの引数読み込みと設定オプション構築が行われる。

go
1func loadDocArgs(args []string) ([]config.Option, error) {
2    options := []config.Option{}
3    if len(args) > 2 {
4        return options, errors.WithStack(errors.New("too many arguments"))
5    }
6    if adjust {
7        options = append(options, config.Adjust(adjust))
8    }
9    if sort {
10        options = append(options, config.Sort(sort))
11    }
12    options = append(options, config.ERFormat(erFormat))
13    if withoutER {
14        options = append(options, config.ERSkip(withoutER))
15    }
16    options = append(options, config.BaseURL(baseURL))
17    options = append(options, config.Include(append(tables, includes...)))
18    options = append(options, config.Exclude(excludes))
19    options = append(options, config.IncludeLabels(labels))
20    if len(args) == 2 {
21        options = append(options, config.DSNURL(args[0]))
22        options = append(options, config.DocPath(args[1]))
23    }
24    if len(args) == 1 {
25        options = append(options, config.DSNURL(args[0]))
26    }
27    if dsn != "" {
28        options = append(options, config.DSNURL(dsn))
29    }
30    return options, nil
31}

docコマンドは、ER図フォーマット(--er-format)、テーブル幅調整(--adjust-table)、ベースURL(--base-url)など、ドキュメント生成に特化した多数のオプションを持つ。

diffコマンドによるスキーマ比較

cmd/diff.go:36-95 では、スキーマ比較ロジックが実装されている。

go
1var diffCmd = &cobra.Command{
2    Use:   "diff [DSN] [DSN_OR_DOC_PATH]",
3    Short: "diff database and ( document or database )",
4    Long:  `'tbls diff' shows the difference between database schema and ( generated document or other database schema ).`,
5    Args:  cobra.MaximumNArgs(2),
6    RunE: func(_ *cobra.Command, args []string) error {
7        // ... 条件チェックと設定読み込み
8        
9        switch len(args) {
10        case 2:
11            if _, err := os.Lstat(args[1]); err == nil {
12                // a:dsn and b:path
13                if err := c.Load(configPath, append(options, config.DSNURL(args[0]))...); err != nil {
14                    return err
15                }
16                c2 = nil
17                docPath = args[1]
18            } else if !strings.Contains(args[1], "://") {
19                // a:dsn and b:path
20                // ... 同上
21            } else {
22                // a:dsn and b:dsn
23                if err := c.Load(configPath, append(options, config.DSNURL(args[0]))...); err != nil {
24                    return err
25                }
26                if err := c2.Load(configPath, append(options, config.DSNURL(args[1]))...); err != nil {
27                    return err
28                }
29                docPath = ""
30            }
31        // ...
32        }
33        
34        switch {
35        case docPath != "":
36            diff, err = md.DiffSchemaAndDocs(docPath, s, c)
37        case s2 != nil:
38            diff, err = md.DiffSchemas(s, s2, c, c2)
39        default:
40            diff, err = md.DiffSchemaAndDocs(c.DocPath, s, c)
41        }
42        // ...
43    },
44}

diffコマンドは3つの比較モードをサポートする:

  1. DSN vs ドキュメントパス: データベーススキーマと生成済みドキュメントを比較
  2. DSN vs DSN: 2つのデータベーススキーマを比較
  3. デフォルト: 設定ファイルのDSNとDocPathを使用

差分がある場合、終了コード1を返すため、CI/CD パイプラインでの自動チェックに適している。

データフローとエンドツーエンド処理

以下のシーケンス図は、tbls doc コマンド実行時のエンドツーエンドのデータフローを示す。

正在加载图表渲染器...

データフローの要点:

  1. コマンド解析: CLI層は cmd/doc.go:115-185 で引数を解析し、設定オプションを構築
  2. 設定読み込み: 設定ファイル(.tbls.yml)が存在する場合、DSN、除外テーブル、メタデータ等を読み込む
  3. スキーマ分析: datasource/datasource.go:45-90 でDSNを解析し、適切なドライバーを選択
  4. メタデータ取得: ドライバーはデータベースに接続し、情報スキーマクエリを実行してメタデータを取得
  5. スキーマ構築: 取得したメタデータを schema/schema.go:39-113 の構造体にマッピング
  6. フィルタリングと拡張: 設定に基づいてテーブルをフィルタリングし、追加メタデータでスキーマを拡張
  7. ドキュメント生成: output/output.go:22-112 のテンプレート関数を使用してMarkdown ファイルと ER 図を生成

モジュール依存関係

正在加载图表渲染器...

依存関係の要点:

  1. 単方向依存: すべての依存は上から下へ流れ、循環依存は存在しない
  2. schema パッケージの中核性: ほぼすべてのパッケージが schema パッケージに依存し、データモデルを共有
  3. drivers の分離: drivers パッケージは datasource からのみ参照され、他のパッケージはドライバー実装詳細を認識しない
  4. dict の独立性: 辞書パッケージは出力層でのみ使用され、国際化機能を提供

技術選定と設計判断

技術要素用途選定理由代替案
CobraCLI フレームワーク豊富な機能(サブコマンド、フラグ、シェル補完)、Go コミュニティでのデファクトスタンダードurfave/cli, Kingpin
database/sql + 各種ドライバーRDBMS 接続標準インターフェース、広範なデータベースサポートGORM, sqlx
dburlDSN 解析複数データベース形式の統一的な解析手動解析
goccy/go-yamlYAML 処理高性能、JSON 互換 APIgopkg.in/yaml.v3
goccy/go-graphvizER 図生成プログラム的なグラフ生成、複数フォーマット出力Graphviz CLI 呼び出し
MermaidER 図フォーマットMarkdown 内埋め込み可能、GitHub 標準サポートPlantUML, Draw.io
samber/loユーティリティ関数Lodash 風の関数型ユーティリティ、ジェネリクス対応自前実装
k1LoW/errorsエラーハンドリングスタックトレース付与、エラーラッピングpkg/errors

設計上の判断と制約:

  1. 関数型オプションパターン: ドライバー設定と設定オプションで採用。コンストラクタ引数の増加を防ぎ、後方互換性を維持
  2. インターフェース分離: Driver インターフェースは最小限のメソッドのみ定義。ドライバー実装の複雑性を隠蔽
  3. DSN プレフィックスベースの振り分け: 拡張性が高いが、プレフィックスの重複リスクがある。具体的なプレフィックスを先に判定することで回避
  4. JSON スキーマサポート: scripts/jsonschema/main.go:1-36 で JSON Schema 生成を自動化。設定ファイルのバリデーションとエディタ支援を実現
  5. 外部サブコマンド機構: PATH ベースの動的検出により、tbls 本体の変更なしに機能拡張が可能。ただし、セキュリティリスク(悪意ある実行ファイルの配置)に注意が必要

既知の制約:

  • SQLite3、MySQL、PostgreSQL ドライバーは CGO を必要とし、クロスコンパイル時に複雑性が増す
  • 大規模スキーマ(数千テーブル)の場合、メモリ使用量が増大する可能性がある
  • 外部サブコマンドのエラーハンドリングは、サブコマンドの実装に依存する