ArelのINSERT/UPDATE/DELETEにRETURNING句のサポートを追加
Arelのステートメントクラスおよびマネージャークラスに returning メソッドが追加され、INSERT/UPDATE/DELETE 文で RETURNING 句を構築できるようになりました。これにより、データの変更と取得を1つのクエリで表現する複雑なSQL構造をArelで組み立てられます。
背景
PostgreSQLとSQLiteは INSERT/UPDATE/DELETE 文に対して RETURNING 句をサポートしていますが、ArelにはこれをAST(抽象構文木)として表現する仕組みがありませんでした。これまで RETURNING を使ったクエリを構築するには、生SQLを文字列として埋め込むしかなく、Arelの型安全な構文構築の恩恵を受けられませんでした。
ActiverRecordレベルでの RETURNING サポートについては #39968 や #42955 で議論されてきた経緯があります。本PRはActiveRecord層への統合は行わず、Arelのクエリビルダー層のみを対象として RETURNING 句の構造を表現できるようにしています。
RETURNING の典型的なユースケースとして、CTE(Common Table Expression)との組み合わせが挙げられます。例えば、カウンターキューテーブルの全レコードを削除しながらその集計値を取得するクエリは以下のように構築できます:
counter_queue = Arel::Table.new(:counter_queue)
flush = Arel::Table.new(:flush)
delete_manager = Arel::DeleteManager.new
.from(counter_queue)
.returning([counter_queue[:post_id], counter_queue[:delta]])
select_manager = Arel::SelectManager.new
.with(Arel::Nodes::TableAlias.new(Arel::Nodes::Grouping.new(delete_manager.ast), flush.name))
.from(flush)
.project(flush[:post_id], flush[:delta].sum)
.group(flush[:post_id])
これにより生成されるSQLは次のようになります:
WITH "flush" AS (
DELETE FROM "counter_queue"
RETURNING "counter_queue"."post_id", "counter_queue"."delta"
)
SELECT "flush"."post_id", SUM("flush"."delta")
FROM "flush"
GROUP BY "flush"."post_id"
技術的な変更
変更は、ノードクラス・マネージャークラス・Visitorクラスの3層にわたり、互いに対応した形で加えられています。
ノードクラスへの returning 属性の追加として、DeleteStatement・InsertStatement・UpdateStatement の3クラスに attr_accessor :returning が追加され、コンストラクタで @returning = [] に初期化されます。また、initialize_copy・hash・eql? のそれぞれも returning を含むように更新されており、クローンや同一性比較が正確に機能します。
# 変更前
attr_accessor :relation, :wheres, :groups, :havings, :orders, :limit, :offset, :comment, :key
# 変更後
attr_accessor :relation, :wheres, :groups, :havings, :orders, :limit, :offset, :comment, :key, :returning
マネージャークラスへの returning メソッドの追加として、DeleteManager・InsertManager・UpdateManager にそれぞれ以下のようなメソッドが追加されています。メソッドチェーンを維持するため self を返す設計です:
def returning(returning)
@ast.returning << returning
self
end
引数には単一の値と配列の両方を渡せます。Arel.star(*)や [users[:id], users[:name]] のようなカラム指定が可能で、複数回呼び出すことで RETURNING リストに項目を追加できます。
Visitorクラスの更新として、to_sql.rb の visit_Arel_Nodes_DeleteStatement・visit_Arel_Nodes_UpdateStatement・visit_Arel_Nodes_InsertStatement に returning の出力ロジックが追加されています:
if o.returning.empty?
collector
else
collector << " RETURNING "
visit o.returning, collector
end
returning が空配列の場合は RETURNING 句を出力しないため、既存のクエリへの影響はありません。dot.rb の各 visit_edge にも "returning" が追加され、ASTの可視化にも対応しています。
設計判断
returning をArrayとして保持し、<< で追記する方式が採用されています。これにより returning メソッドを複数回呼び出すことで返却カラムを段階的に追加できます。一方、RETURNING 句はSQL標準ではなくPostgreSQLとSQLite固有の構文であるため、MySQLでは利用できないことに注意が必要です。本PRはArelレベルのみの変更であり、アダプターレベルでのデータベース互換性チェックは行っていません。
ActiveRecordの INSERT/UPDATE/DELETE との統合は今回の変更には含まれていません。これは意図的な判断であり、Arelを使った複雑なクエリを組み立てる用途に絞ることで、ActiveRecord層の変更を伴わずに機能を提供しています。
まとめ
本PRはArelのAST表現を拡張し、RETURNING 句をファーストクラスの構文要素として扱えるようにした変更です。ノード・マネージャー・Visitorの3層に一貫して returning のサポートを追加することで、CTEと組み合わせた高度なデータ操作クエリをArelの構文で安全に構築できる基盤が整いました。