SQL 構文を理解することを主目的として、学習用 RDBMS の最初の実装対象として SQL パーサーを作る。
方針は次の通り。
- 最初は文字列を直接読み進める手書き parser を実装する
- parser は構文解析だけを担当し、テーブル存在確認や型解決はしない
- 将来的に
sqlparser-rsを導入しやすいよう、自前 AST を中心に設計する - トランザクション構文は最初の parser スコープから外し、 単一文の CRUD を優先したうえで後続計画へ切り出す
2026-03-22 時点で、sql crate の hand-written parser は
次の最小 CRUD をサポートしている。
CREATE TABLE users (id INT, name TEXT, active BOOLEAN);
CREATE TABLE metrics (score FLOAT, note NULL);
INSERT INTO users VALUES (1, 'Alice');
INSERT INTO users (id, name, active) VALUES (1, 'Alice', true);
UPDATE users SET name = 'Bob' WHERE id = 1;
UPDATE users SET active = false;
DELETE FROM users WHERE id = 1;
DELETE FROM users;
SELECT id FROM users;
SELECT id, name FROM users;
SELECT * FROM users;
SELECT DISTINCT id FROM users;
SELECT id AS user_id FROM users;
SELECT id user_id FROM users;
SELECT COUNT(id) FROM users;
SELECT COUNT(*) FROM users;
SELECT id + 1 FROM users;
SELECT -1 FROM users;
SELECT id FROM users WHERE id = 1;
SELECT id FROM users WHERE id = (SELECT id FROM users);
SELECT id FROM users WHERE NOT id = 1;
SELECT id FROM users WHERE id = 1 AND active = 1;
SELECT id FROM users WHERE id = 1 OR active = 1;
SELECT id FROM users WHERE (id = 1);
SELECT id FROM users AS u;
SELECT id FROM (SELECT id FROM users) AS u;
SELECT users.id FROM users JOIN orders ON users.id = orders.user_id;
SELECT id FROM users JOIN orders USING (user_id);
SELECT id FROM users GROUP BY id;
SELECT id FROM users GROUP BY id HAVING COUNT(id) > 1;
SELECT id FROM users ORDER BY COUNT(id) DESC;現在の式パーサーは比較式、NOT、unary -、AND、OR、括弧付き条件、
簡単な算術演算、COUNT(...) のような関数呼び出し、
scalar subquery、true / false / NULL の最小リテラルを対象とする。
内部では式パーサーを独立させ、
primary、unary、乗除、加減算、比較、NOT、AND、OR
の順に優先順位を積み上げている。
JOIN ... ON ...、UPDATE ... SET ...、DELETE ... WHERE ...、
INSERT ... VALUES (...) でも同じ式の仕組みを再利用している。
SELECT 項目は * を除き、
単純列、AS 付き列別名、暗黙列別名、関数呼び出し、一般式を
できるだけ同じ考え方で表現する。
FROM と JOIN の参照先も、
通常テーブルと最小の subquery を同じ枠組みで扱う。
INSERT は列リスト省略形と列リスト明示形の単一行 VALUES をサポートする。
UPDATE は単一テーブルに対する SET 句と任意の WHERE をサポートする。
DELETE は単一テーブルに対する任意の WHERE をサポートする。
CREATE TABLE は列名と最小データ型だけを持つ定義をサポートする。
SELECT については、現在の実装状況に列挙した最小形を
第一段階の到達点とする。
JOIN の派生構文、派生表の広い対応、LIMIT / OFFSET などの
追加拡張はこの計画には含めず、後続の
05-select-extensions.md に切り出す。
SELECT 以外については、次の最小形を到達点とする。
CREATE TABLE users (id INT, name TEXT, active BOOLEAN);
INSERT INTO users VALUES (1, 'Alice', true);
INSERT INTO users (id, name, active) VALUES (1, 'Alice', true);
UPDATE users SET name = 'Bob' WHERE id = 1;
DELETE FROM users WHERE id = 1;SELECTの関数呼び出しは最小形のCOUNT(...)と scalar subquery から対応しているが、 一般的な関数群や方言差までは未対応ORDER BY / GROUP BY / HAVING / DISTINCTは最小実装であり、 すべての SQL 方言や派生構文を網羅しているわけではない- subquery は
WHERE id = (SELECT ...)やFROM (SELECT ...) AS aliasのような 最小形に限って対応しており、結合先 subquery や複雑な派生表は未対応 JOINはJOIN/LEFT JOIN/RIGHT JOIN/FULL JOINとON/USINGの最小形に限って対応しており、INNER JOIN、LEFT OUTER JOIN、結合先(SELECT ...) AS aliasの広い派生形は未対応- 行数制御は未対応であり、
LIMIT/OFFSETやそれに類する方言構文はまだ読めない INSERTは単一行VALUESだけに限って対応しており、 複数行VALUESやINSERT ... SELECT ...は未対応UPDATEは単一テーブルのSET ... [WHERE ...]だけに限って対応しており、UPDATE ... FROM ...のような派生構文は未対応DELETEは単一テーブルのDELETE FROM ... [WHERE ...]だけに限って対応しており、USINGを伴う派生構文や結合削除は未対応CREATE TABLEは列名と型名だけの最小形に限って対応しており、 制約、既定値、PRIMARY KEY、NOT NULL、VARCHAR(n)は未対応- データ型は
INT / TEXT / BOOLEAN / FLOAT / NULLから開始し、INTEGERとBOOLは最小の別名として受け付ける BEGIN / COMMIT / ROLLBACKはまだ未対応であり、 後続の03-transaction-control.mdで扱う
文単位は再帰下降で実装する。
現時点では Parser が文字列を直接読み進める方式を採る。
空白スキップ、キーワード読取、識別子読取のような小さい補助処理を組み合わせ、
必要に応じて先読みと消費で句境界を判定する。
FROM 句は単なる文字列ではなく、
基準テーブル、結合先、結合条件を区別できる構造で表現する。
これにより、最小形の FROM users から、
FROM users AS u JOIN orders USING (user_id) のような形まで段階的に拡張できる。
現時点では式パーサーを分離し、
WHERE と JOIN ... ON ... で共通利用している。
論理条件、算術演算、関数呼び出し、scalar subquery までは実装済みで、
UPDATE ... SET ... やさらに広い式構文への展開を今後進める。
最終的に目指す優先順位は次の通り。
- primary: identifier, literal, parenthesized expression, simple function call
- unary:
NOT, unary- - multiplicative:
*,/ - additive:
+,- - comparison:
=,!=,<,<=,>,>= - logical and:
AND - logical or:
OR
既存の AST は土台として使い、現時点では次の方針を採っている。
INSERTは対象テーブル、任意の列リスト、値リストで表現するUPDATEとDELETEを表現できるようにするJOIN条件でONとUSINGの両方を表現できるようにする- 列別名や関数呼び出しを扱うため、
SELECT項目は式ベースを基本形にする *は通常の式とは別扱いにする- 列定義の型は
typescrate のデータ型を使う - リテラル値は共通の値表現を使う
- parser と binder の責務を分けるため、AST では名前解決をしない
query側で使いやすい形に AST と executor の境界を調整するSELECTの将来拡張へ進むか、CRUD を planner / executor へ接続する- 将来の
SELECT拡張
SELECT の将来拡張は、現在の CRUD 実装を一通り通したあとに
05-select-extensions.md の計画として進める。
- AST を CRUD 対応の形に整理する
SELECTを今後の句拡張に耐える形へ寄せるINSERTを実装するUPDATE / DELETEを実装するCREATE TABLEを実装するquery側で使いやすい形に AST と executor の境界を調整する
1 から 5 までは完了し、現在は 6 の段階に入っている。
- SQL 文字列から期待した AST が得られる
SELECT、INSERT、UPDATE、DELETE、CREATE TABLEは 正常系と異常系をテーブルドリブンで検証する- 別名、
JOIN ... ON ...、JOIN ... USING (...)を個別ケースで固定する WHEREはNOT、AND、ORの優先順位と括弧あり式を追加で検証する- 不正な構文で分かりやすいエラーが返る
第一段階の SELECT については、以下がすべてパースできれば完了とする。
SELECT id FROM users;
SELECT id, name FROM users;
SELECT * FROM users;
SELECT id AS user_id FROM users;
SELECT id user_id FROM users;
SELECT id FROM users WHERE id = 1;
SELECT id FROM users WHERE NOT id = 1;
SELECT id FROM users WHERE id = 1 AND active = 1;
SELECT id FROM users WHERE id = 1 OR active = 1;
SELECT id FROM users WHERE (id = 1);
SELECT id FROM users AS u;
SELECT users.id FROM users JOIN orders ON users.id = orders.user_id;
SELECT id FROM users JOIN orders USING (user_id);- まずは自前パーサーで
SQL -> ASTの流れを理解する - その後、必要なら
sqlparser-rsを使って自前 AST へ変換する adapter を追加する - planner, optimizer, transaction はこの段階では広げすぎない