0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MySQL/PostgreSQL/SQLite/SQL Serverの構文差分を構造化データで実装する

0
Posted at

SQL方言(MySQL・PostgreSQL・SQLite・SQL Server)の構文差分を検索できるツールを実装した。59件の比較データ×4DBを1つのTypeScript型に収める設計にした話を書く。

3行まとめ

  • SQL方言(MySQL・PostgreSQL・SQLite・SQL Server)の構文差分を検索できるツールを実装した。59件の比較データ×4DBを1つのTypeScript型定義に収める設計にした
  • sql()/na()という2つのヘルパー関数を経由してデータを作ることで、対応・非対応を同じ型で表現しつつ、非対応の場合は必ず代替手段の説明を書く運用を徹底している
  • 検索は全フィールドの文言を1つの文字列に結合してからincludesで判定するナイーブな実装。59件程度の規模ならこれで十分な速度が出る

なぜ構造化データにしたか

複数のRDBMSを行き来していると、「この構文、SQLiteでも使えたっけ」「PostgreSQLのこの関数、MySQLだと名前が違う」という確認が頻繁に発生する。データ型・DDL・DML・文字列関数・日付関数・集約関数・制御構文・インデックス・制約・トランザクションと、確認したくなる領域は広い。

これを個別ページの手書きHTMLで作ると、DBが1つ増えるだけで全ページの修正が必要になる。そこで、比較データを1つの型定義に収める構造化データとして持たせ、表示・検索・フィルタはそのデータを描画するだけにした。

型定義 — 1エントリに4DB分の情報を持たせる

比較の最小単位(例:「VARCHAR型」「LIMIT句」)を1エントリとして、4DB分の対応状況をまとめて持たせる。

export type DBPlatform = 'mysql' | 'postgresql' | 'sqlite' | 'sqlserver'
export type SQLCategory = 'datatype' | 'ddl' | 'dml' | 'function_string' | 'function_date'
  | 'function_aggregate' | 'function_math' | 'control' | 'index' | 'constraint' | 'transaction'
export type Difficulty = '基本' | '中級' | '応用'

export interface SQLDialect {
  syntax: string
  description: string
  note?: string
  available: boolean
}

export interface SQLDialectEntry {
  id: string
  category: SQLCategory
  difficulty: Difficulty
  purpose: string
  tags: string[]
  mysql: SQLDialect
  postgresql: SQLDialect
  sqlite: SQLDialect
  sqlserver: SQLDialect
  comparisonNote?: string
}

SQLDialectが「構文」「説明」「補足」「対応可否」の4項目を持ち、SQLDialectEntryがそれを4DB分並べる構造。DBが増えたらこのインターフェースにフィールドを1つ足すだけで、既存の59エントリすべてに型エラーとして「未対応DBの情報が抜けている」ことを検出できる。

sql()とna()ヘルパー — 対応/非対応を同じ型で表現する

各エントリのデータは、素の値を直接書かず2つのヘルパー関数を経由して作る。

function sql(syntax: string, description: string, note?: string): SQLDialect {
  return { syntax, description, note, available: true }
}

function na(note?: string): SQLDialect {
  return { syntax: '-', description: '直接対応する構文なし', note, available: false }
}

対応している構文はsql()、対応していない構文はna()で作る。ポイントは、na()の引数noteをほぼ全てのケースで「代替手段」の説明として使っていること。

// 'distinct-on' エントリより
mysql: na('DISTINCT ON は非対応。サブクエリ + ROW_NUMBER() で代替'),
sqlite: na('DISTINCT ON は非対応。GROUP BY + MAX/MIN または ROW_NUMBER() で代替'),
sqlserver: na('DISTINCT ON は非対応。ROW_NUMBER() + サブクエリで代替'),

「非対応です」で終わらせず、必ず「じゃあ代わりに何を使うか」まで書く運用ルールを、コードレベルで強制はしていないものの、59エントリ全体を通して徹底している。読者が本当に知りたいのは「使えない」という事実より「どう書けば同じことができるか」なので、この一貫性がツールの実用性を支えている。

comparisonNoteで「結局何が違うのか」を一言でまとめる

構文ごとの詳細に加えて、エントリ単位で「本質的な違いは何か」を1行にまとめるcomparisonNoteを持たせている。

{
  id: 'varchar',
  category: 'datatype',
  mysql: sql('VARCHAR(n)', '最大 n 文字の可変長文字列', '最大 65,535 バイト。文字コードにより格納可能文字数が変わる'),
  postgresql: sql('VARCHAR(n) / TEXT', 'VARCHAR(n) は最大 n 文字。TEXT は無制限', 'VARCHAR と TEXT のパフォーマンス差はほぼなし'),
  sqlite: sql('TEXT', 'すべての文字列は TEXT 型', 'VARCHAR(n) と書いても内部的には TEXT として扱われる'),
  sqlserver: sql('VARCHAR(n) / NVARCHAR(n)', 'VARCHAR は非Unicode、NVARCHAR はUnicode対応', 'NVARCHAR(MAX) で最大約2GB。日本語には NVARCHAR を推奨'),
  comparisonNote: 'SQL Server で日本語を扱う場合は NVARCHAR を使用する。MySQL・PostgreSQL はデフォルトで Unicode 対応',
}

4DB分のdescription・noteを読み比べれば違いは分かるが、それだと「結局どこに気をつければいいか」の結論にたどり着くまで時間がかかる。comparisonNoteは4項目を読んだ後の結論を先回りして1行に凝縮する役割で、実務で一番よく事故る箇所(この例なら「SQL Serverで日本語カラムをVARCHARにしてしまう」)を名指しする。

検索・フィルタの実装

検索は各エントリの全フィールド(用途・タグ・4DB分の構文/説明/補足・比較メモ)の文言を1つの文字列に結合してからincludesで判定するナイーブな実装。

const filtered = useMemo(() => {
  const q = searchQuery.trim().toLowerCase()
  return sqlDialectEntries.filter((entry) => {
    if (dbFilter === 'mysql' && !entry.mysql.available) return false
    if (dbFilter === 'postgresql' && !entry.postgresql.available) return false
    if (dbFilter === 'sqlite' && !entry.sqlite.available) return false
    if (dbFilter === 'sqlserver' && !entry.sqlserver.available) return false
    if (categoryFilter !== 'all' && entry.category !== categoryFilter) return false
    if (difficultyFilter !== 'all' && entry.difficulty !== difficultyFilter) return false

    if (q) {
      const searchText = [
        entry.purpose, entry.id, ...entry.tags,
        entry.mysql.syntax, entry.mysql.description, entry.mysql.note ?? '',
        entry.postgresql.syntax, entry.postgresql.description, entry.postgresql.note ?? '',
        entry.sqlite.syntax, entry.sqlite.description, entry.sqlite.note ?? '',
        entry.sqlserver.syntax, entry.sqlserver.description, entry.sqlserver.note ?? '',
        entry.comparisonNote ?? '',
      ].join(' ').toLowerCase()
      if (!searchText.includes(q)) return false
    }
    return true
  })
}, [searchQuery, dbFilter, categoryFilter, difficultyFilter])

転置インデックスもFuzzy検索も使わない、配列を毎回joinしてからincludesする力技。59件程度のデータ規模であればuseMemoで依存配列が変わったときだけ再計算すれば体感速度は十分で、検索エンジンを持ち出すほどの規模ではない。dbFilterは「選んだDBで対応している構文だけ表示する」というフィルタで、available: booleanを持たせた設計がここでそのまま活きている。

データ規模

現在は11カテゴリ・59エントリ。内訳はデータ型10、DDL8、DML8、文字列関数8、日付関数7、制御構文4、集約関数4、数値関数4、制約2、インデックス2、トランザクション2。DBごとの対応数(available: trueの件数)もヘッダーに表示していて、「どのDBが一番機能豊富か」がひと目でわかるようにしている。

まとめ

  • 4DB分の比較データを1つの型定義(SQLDialectEntry)に収めることで、DBを1つ追加してもコンパイラが「情報が足りないエントリ」を教えてくれる構成にした
  • sql()/na()ヘルパーで対応・非対応を同じ型として扱いつつ、非対応の場合は必ず代替手段を書く運用を徹底した
  • comparisonNoteで「結局どこに気をつければいいか」を1行に凝縮し、詳細と結論を両方読めるようにした

ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理などの開発者向けツールを 90 個以上公開中。全部無料・登録不要・ブラウザ完結で使える。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?