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 にも同じ内容を投稿しています。