Skip to content

Instantly share code, notes, and snippets.

@beginerbeginer
Last active April 19, 2025 04:48
Show Gist options
  • Save beginerbeginer/a1c591455d5efe576c9154d96b7adbcb to your computer and use it in GitHub Desktop.
Save beginerbeginer/a1c591455d5efe576c9154d96b7adbcb to your computer and use it in GitHub Desktop.
rubyでdbを実装する

質問

内部結合、外部結合、ユニオン型にも対応できるようにして。


以下、内部結合(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:全体の流れ確認

  1. SQL 文 を QueryParser が AST に変換
  2. BasicQueryEngine が QueryValidator で構文/名前を検証
  3. QueryDispatcher が AST タイプを見て、
  • JoinQuery → JoinHandler
  • UnionQuery → UnionHandler
  • その他 → 既存の CRUD Handler
  1. Handler が FileStorage を通してデータを取得し、
  • JoinHandler → 2つのテーブルを結合
  • UnionHandler → 2つの SELECT 結果を結合
  • それぞれ「配列の集合演算」で返却
  1. 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 ファイル 環境で動作するようになります。ぜひお試しください!

以下、ステップバイステップでフォルダ構成と各ファイルの役割を解説します。

ステップ1:プロジェクトルート

ruby-json-db/
├── Gemfile
└── main.rb
1.	Gemfile
•	このプロジェクトで使うRubyのライブラリ(gem)を列挙
•	bundle install でRSpecなど必要なライブラリを一括インストール
2.	main.rb
•	アプリ全体の組み立て(Composition Root)
•	FileStorage/QueryParser/BasicQueryEngine/QueryREPL の具体実装を注入
•	REPLを起動し、ユーザーの入力からクエリ実行までを制御

ステップ2:core フォルダ

ruby-json-db/core/
├── interface/
├── storage/
├── parser/
├── model/
├── engine/
├── repl/
└── security/
•	core/

アプリケーション本体のコードを機能ごとに分割するトップフォルダ

ステップ3:抽象インターフェース層 (core/interface)

core/interface/IStorage.rb

•	IStorage.rb
•	「テーブル作成/存在確認/CRUD」などストレージが提供すべきAPIを定義
•	上位(ハンドラやエンジン)はここに依存し、具体実装(FileStorage)を気にしない

ステップ4:永続化ストレージ層 (core/storage)

core/storage/
├── FileManager.rb
└── FileStorage.rb
1.	FileManager.rb
•	JSONファイルの読み書き・ディレクトリ管理だけを担当
•	table_path(tableName) → ファイルパス
•	read_table(name) / write_table(name, data) メソッド
2.	FileStorage.rb
•	IStorageインターフェースを具体実装
•	FileManagerを呼び出してテーブル定義・行追加・検索・更新・削除を実現
•	NameValidatorによるテーブル名/カラム名チェックもここで行う

ステップ5:パーサ層 (core/parser)

core/parser/
├── ast/
│   ├── queries.rb
│   └── conditions.rb
├── ValueParser.rb
├── ConditionParser.rb
└── QueryParser.rb
1.	ast/queries.rb
•	SQL文(INSERT, SELECT, UPDATE, DELETE)ごとのASTノード定義
•	各ノードはテーブル名や値、条件ASTをプロパティとして保持
2.	ast/conditions.rb
•	WHERE句の比較条件(Comparison, And, Or)ASTノード定義
3.	ValueParser.rb
•	リテラル(文字列リテラル/数値)をRubyの型に変換
4.	ConditionParser.rb
•	WHERE句の文字列を ast/conditions.rb のASTに変換
5.	QueryParser.rb
•	SQL文字列を受け取り、ast/queries.rb のASTノードに組み立て
•	ValueParser と ConditionParser を組み合わせて使用

ステップ6:モデル層 (core/model)

core/model/
├── ConditionEvaluator.rb
└── RowFilter.rb
1.	ConditionEvaluator.rb
•	ASTノード(Comparison, And, Or)を受けて、実際の比較/AND/ORを評価
•	評価ロジックだけに責務を絞る
2.	RowFilter.rb
•	テーブルのカラム配列と行配列、条件ASTを受け取り
•	ConditionEvaluatorを使って「該当する行リスト」を返す

ステップ7:実行エンジン層 (core/engine)

core/engine/
├── IQueryEngine.rb
├── QueryDispatcher.rb
├── BasicQueryEngine.rb
├── InsertHandler.rb
├── SelectHandler.rb
├── UpdateHandler.rb
└── DeleteHandler.rb
1.	IQueryEngine.rb
•	execute(ast) メソッドだけを定義する抽象インターフェース
2.	QueryDispatcher.rb
•	ASTの種類に応じて、InsertHandler〜DeleteHandlerのいずれかに振り分け
3.	BasicQueryEngine.rb
•	IQueryEngineを実装し、executeの中で
1.	QueryValidatorで安全性チェック
2.	QueryDispatcherに処理を委譲
4.	Insert/Select/Update/DeleteHandler.rb
•	各クラスとも、handle(ast) でstorage.insertやstorage.select等を呼ぶだけ
•	単一責任かつメソッドは小さい(ABCサイズ ≤9)

ステップ8:REPL層 (core/repl)

core/repl/QueryREPL.rb

•	QueryREPL.rb
•	コマンドラインでのプロンプト表示・入力受付・AST解析・エンジン実行・結果表示
•	UI/IOロジックのみを担う

ステップ9:セキュリティ/バリデーション層 (core/security)

core/security/
├── NameValidator.rb
└── QueryValidator.rb
1.	NameValidator.rb
•	テーブル名/カラム名が正規表現に合致するかチェック
•	実際に storage.table_exists? / storage.columns を使って存在を保証
2.	QueryValidator.rb
•	ASTレベルで「存在しないテーブル参照」「存在しないカラム参照」などを検証
•	SQLインジェクション的な誤った入力を物理的に排除

以上で、フォルダごとの役割と各ファイルの責務をステップバイステップで整理しました。 この設計に沿ってコードを参照すれば、各層の責任範囲が明確に理解できるはずです。

以下が、これまでのスクリプトをすべて1つのリポジトリにまとめた構成例です。プライベートメソッドは一切使わず、単一責任・関心分離・抽象依存を徹底しています。

ruby-json-db/
├── Gemfile
├── main.rb
└── core
    ├── interface
    │   └── IStorage.rb
    ├── storage
    │   ├── FileManager.rb
    │   └── FileStorage.rb
    ├── parser
    │   ├── ast
    │   │   ├── queries.rb
    │   │   └── conditions.rb
    │   ├── ValueParser.rb
    │   ├── ConditionParser.rb
    │   └── QueryParser.rb
    ├── model
    │   ├── ConditionEvaluator.rb
    │   └── RowFilter.rb
    ├── engine
    │   ├── IQueryEngine.rb
    │   ├── QueryDispatcher.rb
    │   ├── BasicQueryEngine.rb
    │   ├── InsertHandler.rb
    │   ├── SelectHandler.rb
    │   ├── UpdateHandler.rb
    │   └── DeleteHandler.rb
    ├── repl
    │   └── QueryREPL.rb
    └── security
        ├── NameValidator.rb
        └── QueryValidator.rb

Gemfile

# Gemfile
source 'https://rubygems.org'
gem 'rspec', '~> 3.0'

main.rb

エントリポイント。初期化→REPL 起動

# main.rb
require_relative 'core/interface/IStorage'
require_relative 'core/storage/FileStorage'
require_relative 'core/parser/QueryParser'
require_relative 'core/engine/BasicQueryEngine'
require_relative 'core/repl/QueryREPL'

storage = FileStorage.new('data')
unless storage.table_exists?('users')
  storage.create_table('users', %w[id name age])
end

parser = QueryParser.new
engine = BasicQueryEngine.new(storage)
QueryREPL.new(parser, engine).start

core/interface/IStorage.rb

ストレージ操作の抽象

# core/interface/IStorage.rb
module IStorage
  def create_table(name, columns)      ; raise NotImplementedError; end
  def table_exists?(name)             ; raise NotImplementedError; end
  def columns(name)                   ; raise NotImplementedError; end
  def insert(table_name, row_hash)    ; raise NotImplementedError; end
  def select(table_name, condition)   ; raise NotImplementedError; end
  def update(table_name, updates, condition); raise NotImplementedError; end
  def delete(table_name, condition)   ; raise NotImplementedError; end
end

core/storage/FileManager.rb

JSONファイル読み書き

# core/storage/FileManager.rb
require 'json'

class FileManager
  def initialize(data_dir)
    @data_dir = data_dir
    Dir.mkdir(@data_dir) unless Dir.exist?(@data_dir)
  end

  def table_path(table_name)
    File.join(@data_dir, "#{table_name}.json")
  end

  def read_table(table_name)
    JSON.parse(File.read(table_path(table_name)))
  end

  def write_table(table_name, data)
    File.write(table_path(table_name), JSON.pretty_generate(data))
  end
end

core/storage/FileStorage.rb

IStorage 実装:CRUD とスキーマ取得

# core/storage/FileStorage.rb
require_relative '../interface/IStorage'
require_relative 'FileManager'
require_relative '../model/RowFilter'
require_relative '../security/NameValidator'

class FileStorage
  include IStorage

  def initialize(data_dir = 'data')
    @fm = FileManager.new(data_dir)
  end

  def create_table(name, columns)
    unless table_exists?(name)
      @fm.write_table(name, { 'columns' => columns, 'rows' => [] })
    end
  end

  def table_exists?(name)
    File.exist?(@fm.table_path(name))
  end

  def columns(name)
    @fm.read_table(name)['columns']
  end

  def insert(table_name, row_hash)
    NameValidator.validate_table(table_name, self)
    NameValidator.validate_columns(table_name, row_hash.keys, self)
    table = @fm.read_table(table_name)
    cols  = table['columns']
    row   = cols.map { |c| row_hash[c] }
    raise '列数不一致' unless row.size == cols.size
    table['rows'] << row
    @fm.write_table(table_name, table)
  end

  def select(table_name, condition_ast)
    NameValidator.validate_table(table_name, self)
    table = @fm.read_table(table_name)
    return table['rows'] if condition_ast.nil?
    RowFilter.new(table['columns'], table['rows']).filter(condition_ast)
  end

  def update(table_name, updates, condition_ast)
    NameValidator.validate_table(table_name, self)
    NameValidator.validate_columns(table_name, updates.keys, self)
    table   = @fm.read_table(table_name)
    cols    = table['columns']
    rows    = table['rows']
    filtered = RowFilter.new(cols, rows).filter(condition_ast)
    count = 0
    filtered.each do |row|
      updates.each do |col, val|
        row[cols.index(col)] = val
      end
      count += 1
    end
    @fm.write_table(table_name, table)
    count
  end

  def delete(table_name, condition_ast)
    NameValidator.validate_table(table_name, self)
    table = @fm.read_table(table_name)
    cols  = table['columns']
    rows  = table['rows']
    to_del = RowFilter.new(cols, rows).filter(condition_ast)
    before = rows.size
    rows.reject! { |r| to_del.include?(r) }
    @fm.write_table(table_name, table)
    before - rows.size
  end
end

core/parser/ast/queries.rb

SQL AST ノード

# core/parser/ast/queries.rb
module AST
  class Query; end

  class InsertQuery < Query
    attr_reader :table, :values
    def initialize(table, values); @table, @values = table, values; end
  end

  class SelectQuery < Query
    attr_reader :table, :condition
    def initialize(table, condition = nil); @table, @condition = table, condition; end
  end

  class UpdateQuery < Query
    attr_reader :table, :updates, :condition
    def initialize(table, updates, condition)
      @table, @updates, @condition = table, updates, condition
    end
  end

  class DeleteQuery < Query
    attr_reader :table, :condition
    def initialize(table, condition); @table, @condition = table, condition; end
  end
end

core/parser/ast/conditions.rb

条件 AST ノード

# core/parser/ast/conditions.rb
module AST
  class Condition; end

  class Comparison < Condition
    attr_reader :field, :op, :value
    def initialize(field, op, value); @field, @op, @value = field, op, value; end
  end

  class And < Condition
    attr_reader :left, :right
    def initialize(left, right); @left, @right = left, right; end
  end

  class Or < Condition
    attr_reader :left, :right
    def initialize(left, right); @left, @right = left, right; end
  end
end

core/parser/ValueParser.rb

リテラル解析

# core/parser/ValueParser.rb
class ValueParser
  def self.parse(v)
    return $1      if v =~ /\A'(.*)'\z/m
    return v.to_i  if v =~ /\A\d+\z/
    v
  end
end

core/parser/ConditionParser.rb

WHERE 文字列 → 条件 AST

# core/parser/ConditionParser.rb
require_relative 'ast/conditions'

class ConditionParser
  def parse(str)
    if str.include?(' AND ')
      l, r = str.split(' AND ', 2)
      AST::And.new(parse(l), parse(r))
    elsif str.include?(' OR ')
      l, r = str.split(' OR ', 2)
      AST::Or.new(parse(l),  parse(r))
    elsif str =~ /\A(\w+)\s*(=|<|>)\s*(.+)\z/
      AST::Comparison.new($1, $2, $3.strip)
    else
      raise "条件文解析失敗: #{str}"
    end
  end
end

core/parser/QueryParser.rb

SQL 文字列 → 各種 AST

# core/parser/QueryParser.rb
require_relative 'ast/queries'
require_relative 'ConditionParser'
require_relative 'ValueParser'

class QueryParser
  def parse(sql)
    case sql.strip
    when /\AINSERT INTO (\w+) \((.+)\) VALUES \((.+)\)\z/i
      table = $1
      cols  = $2.split(',').map(&:strip)
      vals  = $3.split(',').map(&:strip).map { |v| ValueParser.parse(v) }
      AST::InsertQuery.new(table, Hash[cols.zip(vals)])

    when /\ASELECT \* FROM (\w+)(?: WHERE (.+))?\z/i
      table = $1
      cond  = $2 ? ConditionParser.new.parse($2) : nil
      AST::SelectQuery.new(table, cond)

    when /\AUPDATE (\w+) SET (.+) WHERE (.+)\z/i
      table   = $1
      assigns = $2.split(',').map(&:strip)
      updates = assigns.map { |a|
        col, val = a.split('=',2).map(&:strip)
        [col, ValueParser.parse(val)]
      }.to_h
      cond = ConditionParser.new.parse($3)
      AST::UpdateQuery.new(table, updates, cond)

    when /\ADELETE FROM (\w+)(?: WHERE (.+))?\z/i
      table = $1
      cond  = $2 ? ConditionParser.new.parse($2) : nil
      AST::DeleteQuery.new(table, cond)

    else
      raise "未対応のクエリ: #{sql}"
    end
  end
end

core/model/ConditionEvaluator.rb

条件評価

# core/model/ConditionEvaluator.rb
require_relative '../parser/ast/conditions'

class ComparisonEvaluator
  def self.evaluate(columns, row, field, op, value)
    idx  = columns.index(field) or raise "列なし: #{field}"
    cell = row[idx]
    case op
    when '=' then cell.to_s == value.to_s
    when '>' then cell.to_i > value.to_i
    when '<' then cell.to_i < value.to_i
    else raise "演算子未対応: #{op}"
    end
  end
end

class AndEvaluator
  def self.evaluate(ast, columns, row)
    ComparisonEvaluator.evaluate(columns, row, ast.left.field, ast.left.op, ast.left.value) &&
      ComparisonEvaluator.evaluate(columns, row, ast.right.field, ast.right.op, ast.right.value)
  end
end

class OrEvaluator
  def self.evaluate(ast, columns, row)
    ComparisonEvaluator.evaluate(columns, row, ast.left.field, ast.left.op, ast.left.value) ||
      ComparisonEvaluator.evaluate(columns, row, ast.right.field, ast.right.op, ast.right.value)
  end
end

class ConditionEvaluator
  def self.evaluate(ast, columns, row)
    case ast
    when AST::Comparison then ComparisonEvaluator.evaluate(columns, row, ast.field, ast.op, ast.value)
    when AST::And        then AndEvaluator.evaluate(ast, columns, row)
    when AST::Or         then OrEvaluator.evaluate(ast, columns, row)
    else true
    end
  end
end

core/model/RowFilter.rb

行フィルタ

# core/model/RowFilter.rb
require_relative 'ConditionEvaluator'

class RowFilter
  def initialize(columns, rows)
    @columns = columns
    @rows    = rows
  end

  def filter(condition_ast)
    @rows.select { |row| ConditionEvaluator.evaluate(condition_ast, @columns, row) }
  end
end

core/engine/IQueryEngine.rb

エンジン抽象

# core/engine/IQueryEngine.rb
module IQueryEngine
  def execute(ast) ; raise NotImplementedError; end
end

core/engine/QueryDispatcher.rb

ハンドラ振り分け

# core/engine/QueryDispatcher.rb
require_relative 'InsertHandler'
require_relative 'SelectHandler'
require_relative 'UpdateHandler'
require_relative 'DeleteHandler'

class QueryDispatcher
  def initialize(storage)
    @ih = InsertHandler.new(storage)
    @sh = SelectHandler.new(storage)
    @uh = UpdateHandler.new(storage)
    @dh = DeleteHandler.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)
    else raise "未対応 AST: #{ast.class}"
    end
  end
end

core/engine/BasicQueryEngine.rb

バリデータ挟み込み

# core/engine/BasicQueryEngine.rb
require_relative 'IQueryEngine'
require_relative 'QueryDispatcher'
require_relative '../security/QueryValidator'

class BasicQueryEngine
  include IQueryEngine

  def initialize(storage)
    @storage    = storage
    @dispatcher = QueryDispatcher.new(storage)
  end

  def execute(ast)
    QueryValidator.validate(ast, @storage)
    @dispatcher.dispatch(ast)
  end
end

core/engine/InsertHandler.rb

# core/engine/InsertHandler.rb
class InsertHandler
  def initialize(storage); @s = storage; end
  def handle(q)       ; @s.insert(q.table,  q.values); end
end

core/engine/SelectHandler.rb

# core/engine/SelectHandler.rb
class SelectHandler
  def initialize(storage); @s = storage; end
  def handle(q)       ; @s.select(q.table,  q.condition); end
end

core/engine/UpdateHandler.rb

# core/engine/UpdateHandler.rb
class UpdateHandler
  def initialize(storage); @s = storage; end
  def handle(q)       ; @s.update(q.table,  q.updates, q.condition); end
end

core/engine/DeleteHandler.rb

# core/engine/DeleteHandler.rb
class DeleteHandler
  def initialize(storage); @s = storage; end
  def handle(q)       ; @s.delete(q.table,  q.condition); end
end

core/repl/QueryREPL.rb

対話式 REPL

# core/repl/QueryREPL.rb
class QueryREPL
  def initialize(parser, engine)
    @parser, @engine = parser, engine
  end

  def start
    puts "Ruby‑JSONDB REPL (exitで終了)"
    loop do
      print "db> "
      line = gets&.strip
      break if line.nil? || line.downcase == 'exit'
      next if line.empty?
      begin
        ast    = @parser.parse(line)
        result = @engine.execute(ast)
        p result
      rescue => e
        puts "ERROR: #{e.message}"
      end
    end
  end
end

core/security/NameValidator.rb

識別子/テーブル/カラム名チェック

# core/security/NameValidator.rb
module NameValidator
  def self.validate_identifier(name)
    raise "不正な識別子: #{name}" unless name =~ /\A[A-Za-z_][A-Za-z0-9_]*\z/
  end

  def self.validate_table(table, storage)
    validate_identifier(table)
    raise "テーブルが存在しません: #{table}" unless storage.table_exists?(table)
  end

  def self.validate_columns(table, columns, storage)
    validate_table(table, storage)
    cols = storage.columns(table)
    columns.each do |c|
      validate_identifier(c)
      raise "列が存在しません: #{c}" unless cols.include?(c)
    end
  end
end

core/security/QueryValidator.rb

ASTレベルでSQL妥当性チェック

# core/security/QueryValidator.rb
require_relative 'NameValidator'
require_relative '../parser/ast/queries'

module QueryValidator
  def self.validate(ast, storage)
    case ast
    when AST::InsertQuery
      NameValidator.validate_columns(ast.table, ast.values.keys, storage)
    when AST::SelectQuery
      NameValidator.validate_table(ast.table, storage)
      validate_condition(ast.condition, ast.table, storage) if ast.condition
    when AST::UpdateQuery
      NameValidator.validate_columns(ast.table, ast.updates.keys, storage)
      validate_condition(ast.condition, ast.table, storage)
    when AST::DeleteQuery
      NameValidator.validate_table(ast.table, storage)
      validate_condition(ast.condition, ast.table, storage) if ast.condition
    else
      raise "未知のクエリAST: #{ast.class}"
    end
  end

  def self.validate_condition(cond, table, storage)
    case cond
    when AST::Comparison
      NameValidator.validate_columns(table, [cond.field], storage)
    when AST::And, AST::Or
      validate_condition(cond.left,  table, storage)
      validate_condition(cond.right, table, storage)
    else
      raise "不明な条件AST: #{cond.class}"
    end
  end
end

以上が完全版リポジトリです。

•	main.rb で起動→REPL
•	core/interface~core/security 以下で機能を細分化
•	プライベートメソッドは一切なし
•	SQLインジェクション相当を防ぐバリデーション層あり

このまま GitHub にプッシュして、ruby main.rb で動作確認、bundle exec rspec でテストを実行できます。

以下の観点でモデル図をチェックしてみました。

  1. 高レベルモジュール(ビジネスロジック)が低レベルモジュールに依存していないか • InsertHandler/SelectHandler/UpdateHandler/DeleteHandler はすべて IStorage(抽象)に依存しています。 • BasicQueryEngine も同様に IQueryEngine(抽象)& QueryDispatcher 経由でハンドラを呼び出しており、具体的な FileStorage には依存していません。 • QueryREPL は IQueryEngine(抽象)に依存し、具体実装(BasicQueryEngine)は外から注入されます。

→ いずれも「高レベルモジュールは抽象に依存し、低レベルモジュールが抽象を実装する」という依存性逆転の原則(DIP)に沿っています。

  1. 低レベルモジュール(インフラ)が高レベルモジュールへ露骨に依存していないか • FileStorage は IStorage を実装する立場であり、上位のビジネスロジック層(ハンドラやエンジン)には依存せず、あくまでファイル操作/JSON永続化・バリデーションに集中しています。 • FileManager, NameValidator もストレージ実装の一部であり、上位層には依存していません。

→ 低レベル側は自分より上の層に依存せず、責務がきれいに分かれています。

  1. Composition Root(Main)はどこに concrete を依存しているか • Main クラス=アプリの起動処理(Composition Root)だけが FileStorage や QueryParser、BasicQueryEngine などの具体実装を知っています。 • Composition Root は例外的に具体クラスへ依存してよく、アプリ全体の組み立てを担います。

→ ここも適切です。

  1. その他、改善を検討できるポイント • NameValidator は静的メソッドだけですが、もし差し替えテストや拡張を考えるなら INameValidator 抽象を置く手もあります。ただ現状の用途なら過剰かもしれません。 • 同様に ValueParser/ConditionParser/QueryParser もインターフェースを持たせると、Parserだけ別実装に切り替え可能になります。こちらも必要に応じて。

結論:

 依存性逆転の原則は正しく適用できていますので、Rubyスクリプト自体のリファクタリングは不要です。 (もしさらに Parser や Validator も抽象化したい場合は、そのときに IQueryParser や INameValidator を追加し、Composition Root で組み立てる形を採れば完璧です。)

まとめ 1. 高レベル層はすべて IStorage/IQueryEngine などのインターフェース依存 2. 低レベル層はファイル操作+JSON永続化に専念 3. Composition Root(main.rb)が具体実装を組み立て

…という構成で、DIP は達成されています。ご安心ください!

classDiagram
    %% Interface
    class IStorage {
        <<interface>>
        +createTable(name, columns)
        +tableExists(name)
        +insert(tableName, row)
        +select(tableName, conditions)
        +update(tableName, conditions, values)
        +delete(tableName, conditions)
    }

    %% Storage
    class FileManager {
        -buildPath(tableName)
        +readTable(tableName)
        +writeTable(tableName, data)
    }

    class FileStorage {
        -fileManager
        -nameValidator
        +createTable(name, columns)
        +tableExists(name)
        +insert(tableName, row)
        +select(tableName, conditions)
        +update(tableName, conditions, values)
        +delete(tableName, conditions)
    }

    %% Parser
    class QueryParser {
        -valueParser
        -conditionParser
        +parse(sqlString)
    }

    class ValueParser {
        +parseValue(valueString)
    }

    class ConditionParser {
        +parseCondition(conditionString)
    }

    %% AST Classes
    class InsertQuery {
        +tableName
        +values
    }

    class SelectQuery {
        +tableName
        +conditions
    }

    class UpdateQuery {
        +tableName
        +conditions
        +values
    }

    class DeleteQuery {
        +tableName
        +conditions
    }

    class ComparisonCondition {
        +column
        +operator
        +value
        +evaluate(row, columns)
    }

    class AndCondition {
        +left
        +right
        +evaluate(row, columns)
    }

    class OrCondition {
        +left
        +right
        +evaluate(row, columns)
    }

    %% Model
    class ConditionEvaluator {
        +evaluate(condition, row, columns)
    }

    class RowFilter {
        -conditionEvaluator
        +filter(rows, columns, condition)
    }

    %% Engine
    class IQueryEngine {
        <<interface>>
        +execute(ast)
    }

    class BasicQueryEngine {
        -storage
        -queryValidator
        -queryDispatcher
        +execute(ast)
    }

    class QueryDispatcher {
        -insertHandler
        -selectHandler
        -updateHandler
        -deleteHandler
        +dispatch(ast)
    }

    class InsertHandler {
        -storage
        +handle(ast)
    }

    class SelectHandler {
        -storage
        +handle(ast)
    }

    class UpdateHandler {
        -storage
        +handle(ast)
    }

    class DeleteHandler {
        -storage
        +handle(ast)
    }

    %% REPL
    class QueryREPL {
        -queryParser
        -queryEngine
        +start()
        -prompt()
        -displayResult(result)
    }

    %% Security
    class NameValidator {
        +validateTableName(name)
        +validateColumnName(name)
        +tableExists(storage, name)
        +columnExists(storage, tableName, columnName)
    }

    class QueryValidator {
        -nameValidator
        -storage
        +validate(ast)
    }

    %% Main Application
    class Main {
        +initialize()
        +run()
    }

    %% Relationships
    IStorage <|.. FileStorage
    FileStorage --> FileManager
    FileStorage --> NameValidator
    
    QueryParser --> ValueParser
    QueryParser --> ConditionParser
    QueryParser ..> InsertQuery
    QueryParser ..> SelectQuery
    QueryParser ..> UpdateQuery
    QueryParser ..> DeleteQuery
    
    RowFilter --> ConditionEvaluator
    ConditionEvaluator ..> ComparisonCondition
    ConditionEvaluator ..> AndCondition
    ConditionEvaluator ..> OrCondition
    
    IQueryEngine <|.. BasicQueryEngine
    BasicQueryEngine --> QueryValidator
    BasicQueryEngine --> QueryDispatcher
    QueryDispatcher --> InsertHandler
    QueryDispatcher --> SelectHandler
    QueryDispatcher --> UpdateHandler
    QueryDispatcher --> DeleteHandler
    InsertHandler --> IStorage
    SelectHandler --> IStorage
    UpdateHandler --> IStorage
    DeleteHandler --> IStorage
    
    QueryREPL --> QueryParser
    QueryREPL --> IQueryEngine
    
    QueryValidator --> NameValidator
    QueryValidator --> IStorage
    
    Main --> FileStorage
    Main --> QueryParser
    Main --> BasicQueryEngine
    Main --> QueryREPL
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment