内部結合、外部結合、ユニオン型にも対応できるようにして。
以下、内部結合(INNER JOIN)/外部結合(LEFT/RIGHT/FULL OUTER JOIN)/UNION をサポートするための ステップバイステップ 拡張方法です。すべて プライベートメソッドは使わず、単一責任・関心分離・依存性逆転を守ります。
⸻
ステップ1:AST ノードの拡張
まず、core/parser/ast/queries.rb に新たに以下の2つを追加します。
# core/parser/ast/queries.rb
module AST
# …既存の Query/InsertQuery/SelectQuery/UpdateQuery/DeleteQuery…
# JOIN を表す AST ノード
class JoinQuery < Query
attr_reader :left_table, :right_table, :type, :condition_ast
# left_table/right_table: String, type: "INNER"/"LEFT"/"RIGHT"/"FULL", condition_ast: AST::Condition
def initialize(left_table, right_table, type, condition_ast)
@left_table, @right_table, @type, @condition_ast =
left_table, right_table, type, condition_ast
end
end
# UNION を表す AST ノード
class UnionQuery < Query
attr_reader :left_query, :right_query, :distinct
# distinct: true なら重複排除(UNION)、false なら全件含む(UNION ALL)
def initialize(left_query, right_query, distinct: true)
@left_query, @right_query, @distinct = left_query, right_query, distinct
end
end
end
⸻
ステップ2:QueryParser の拡張
core/parser/QueryParser.rb に、JOIN と UNION のパターンを追加します。以下は一例です。
# core/parser/QueryParser.rb
require_relative 'ast/queries'
require_relative 'ConditionParser'
class QueryParser
def parse(sql)
text = sql.strip
# --- UNION ---
if text =~ /\A(.+)\s+UNION\s+(.+)\z/i
left_sql, right_sql = $1, $2
left_ast = parse(left_sql)
right_ast = parse(right_sql)
return AST::UnionQuery.new(left_ast, right_ast, distinct: true)
end
# --- SELECT ... JOIN ... ON ... ---
if text =~ /\ASELECT \* FROM (\w+)\s+
(INNER|LEFT|RIGHT|FULL)?\s*JOIN\s+(\w+)\s+
ON\s+(.+)\z/ix
left_table = $1
join_type = ($2 || 'INNER').upcase
right_table = $3
cond_str = $4
cond_ast = ConditionParser.new.parse(cond_str)
return AST::JoinQuery.new(left_table, right_table, join_type, cond_ast)
end
# --- 既存の INSERT/SELECT/UPDATE/DELETE 処理 ---
# …(省略)…
end
end
⸻
ステップ3:QueryDispatcher にハンドラを登録
core/engine/QueryDispatcher.rb に、新しいハンドラを読み込み&振り分けを追加します。
# core/engine/QueryDispatcher.rb
require_relative 'InsertHandler'
require_relative 'SelectHandler'
require_relative 'UpdateHandler'
require_relative 'DeleteHandler'
require_relative 'JoinHandler'
require_relative 'UnionHandler'
class QueryDispatcher
def initialize(storage)
@ih = InsertHandler.new(storage)
@sh = SelectHandler.new(storage)
@uh = UpdateHandler.new(storage)
@dh = DeleteHandler.new(storage)
@jh = JoinHandler.new(storage)
@uh2 = UnionHandler.new(storage)
end
def dispatch(ast)
case ast
when AST::InsertQuery then @ih.handle(ast)
when AST::SelectQuery then @sh.handle(ast)
when AST::UpdateQuery then @uh.handle(ast)
when AST::DeleteQuery then @dh.handle(ast)
when AST::JoinQuery then @jh.handle(ast)
when AST::UnionQuery then @uh2.handle(ast)
else
raise "未対応の AST: #{ast.class}"
end
end
end
⸻
ステップ4:JoinHandler の実装
core/engine/JoinHandler.rb を作成します。以下はシンプルな実装例です。
# core/engine/JoinHandler.rb
class JoinHandler
def initialize(storage)
@storage = storage
end
def handle(ast)
left_rows = @storage.select(ast.left_table, nil)
right_rows = @storage.select(ast.right_table, nil)
case ast.type
when 'INNER' then inner_join(left_rows, right_rows, ast)
when 'LEFT' then left_outer_join(left_rows, right_rows, ast)
when 'RIGHT' then right_outer_join(left_rows, right_rows, ast)
when 'FULL' then full_outer_join(left_rows, right_rows, ast)
else
raise "未対応のJOINタイプ: #{ast.type}"
end
end
def inner_join(lrows, rrows, ast)
combine(lrows, rrows, ast.condition_ast, include_left: false, include_right: false)
end
def left_outer_join(lrows, rrows, ast)
combine(lrows, rrows, ast.condition_ast, include_left: true, include_right: false)
end
def right_outer_join(lrows, rrows, ast)
combine(lrows, rrows, ast.condition_ast, include_left: false, include_right: true)
end
def full_outer_join(lrows, rrows, ast)
combine(lrows, rrows, ast.condition_ast, include_left: true, include_right: true)
end
# 行の結合ロジック
def combine(lrows, rrows, cond_ast, include_left:, include_right:)
result = []
# 内部結合・左外部結合用
lrows.each do |lr|
matched = false
rrows.each do |rr|
combined = lr + rr
if AST::ConditionEvaluator.evaluate(cond_ast, combined)
result << combined
matched = true
end
end
result << (lr + [nil]*rrows.first.size) if include_left && !matched
end
# 右外部結合(FULLで右側未マッチ行も含める)
if include_right
rrows.each do |rr|
matched = lrows.any? {|lr| AST::ConditionEvaluator.evaluate(cond_ast, lr+rr) }
result << ([nil]*lrows.first.size + rr) unless matched
end
end
result
end
end
注意: AST::ConditionEvaluator.evaluate は、結合後の「配列+配列」での条件評価に対応するよう拡張してください。
⸻
ステップ5:UnionHandler の実装
core/engine/UnionHandler.rb を作成します。
# core/engine/UnionHandler.rb
class UnionHandler
def initialize(storage)
@dispatcher = QueryDispatcher.new(storage)
end
def handle(ast)
left_res = @dispatcher.dispatch(ast.left_query)
right_res = @dispatcher.dispatch(ast.right_query)
combined = left_res + right_res
ast.distinct ? combined.uniq : combined
end
end
⸻
ステップ6:ConditionEvaluator の拡張
結合時には「左右のテーブルのカラム数合計」を扱うので、core/model/ConditionEvaluator.rb のシグネチャを拡張します。
# core/model/ConditionEvaluator.rb
class ConditionEvaluator
# cond_ast: AST::Comparison/And/Or
# row: 結合後の配列
# columns: 結合後のカラム名配列(["a.id","a.name","b.id",...])
def self.evaluate(cond_ast, row, columns)
# 既存の比較 AND OR 処理を利用しつつ、
# ComparisonEvaluator に columns, row も渡すよう変更
# 例:
if cond_ast.is_a?(AST::Comparison)
ComparisonEvaluator.evaluate(columns, row,
"#{cond_ast.left_table}.#{cond_ast.field}",
cond_ast.op, cond_ast.value)
elsif cond_ast.is_a?(AST::And)
evaluate(cond_ast.left, row, columns) &&
evaluate(cond_ast.right, row, columns)
...
end
end
end
⸻
ステップ7:全体の流れ確認
- SQL 文 を QueryParser が AST に変換
- BasicQueryEngine が QueryValidator で構文/名前を検証
- QueryDispatcher が AST タイプを見て、
- JoinQuery → JoinHandler
- UnionQuery → UnionHandler
- その他 → 既存の CRUD Handler
- Handler が FileStorage を通してデータを取得し、
- JoinHandler → 2つのテーブルを結合
- UnionHandler → 2つの SELECT 結果を結合
- それぞれ「配列の集合演算」で返却
- REPL が結果を表示
⸻
動作例
-- users と orders を内部結合
SELECT * FROM users INNER JOIN orders ON users.id = orders.user_id;
-- users の全行 + orders 結合(LEFT OUTER JOIN)
SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id;
-- 2つの SELECT を重複排除して合成
SELECT * FROM users WHERE age > 20
UNION
SELECT * FROM users WHERE age < 30;
このステップを順に実装すれば、**集合演算(JOIN/UNION)**を活かした高度なクエリが 純粋な Ruby+JSON ファイル 環境で動作するようになります。ぜひお試しください!