Add support for FINAL when joining tables - #261
Open
cbisnett wants to merge 3 commits into
Open
Conversation
ClickHouse's `final` modifier only wrapped the query's primary FROM source, so `.final.joins(:assoc)` emitted a plain `INNER JOIN <table>` against the un-merged table. Code had to work around this with hand-written `INNER JOIN (SELECT * FROM <table> FINAL) alias ON ...`. Add `joins_final(*associations)`, which adds the join(s) like `joins` and renders `INNER JOIN <table> FINAL ON ...` via the ClickHouse Arel visitor. Implemented with a small `Arel::Nodes::FinalTable` marker that wraps the join's table source; because every join visitor emits its table via `visit o.left`, one visitor method covers all join types. Joins are matched by table name and finalized before the FROM-level FINAL node is applied. Supports chaining, dedup with existing joins, and `unscope(:joins_final)`. Add gem specs covering generated SQL and real execution against ClickHouse (FINAL requires an engine that supports it, e.g. ReplacingMergeTree). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SeQx7AkCPiyk5yqj7m3koX
joins_final only emits INNER JOIN ... FINAL. left_joins_final is the LEFT OUTER counterpart: it adds the association via left_outer_joins and renders FINAL on the joined table, for optional/catalog joins whose rows must be preserved when there is no match while still merging the joined table with FINAL. It shares joins_final_values, so the existing final-join marking and unscope(:joins_final) handling apply unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uhz69nyLNfuciaTh8UhSa
Addresses review feedback: cover the edge case where the same association is added via both joins_final and left_joins_final. The shared joins_final_values store dedups via |= and ActiveRecord collapses an association present in both joins and left_outer_joins into a single INNER JOIN, so FINAL is applied exactly once. Behavior verified consistent across ActiveRecord 7.2.3, 8.0.1, and 8.1.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014uhz69nyLNfuciaTh8UhSa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ClickHouse's
finalmodifier only wraps the query's primaryFROMsource, so.final.joins(:assoc)emits a plainINNER JOIN <table>against the un-merged joined table (wrong row mapping, duplicates). This adds two query methods that renderFINALon the joined table:joins_finaladds the association likejoins(INNER JOIN);left_joins_finaladds it likeleft_outer_joins(LEFT OUTER JOIN) for optional joins whose rows must be preserved when there is no match — while still merging the joined table withFINAL.Implementation
Arel::Nodes::FinalTable— a newUnarymarker node wrapping a join's table source. Because every Arel join visitor (InnerJoin,OuterJoin, …) emits its table viavisit o.left, a singlevisit_Arel_Nodes_FinalTablecovers all join types.Relation#joins_final/#left_joins_final(+!variants) add the join(s), record the requested association names injoins_final_values, and — inbuild_arel, before the FROM-levelFINALis applied — wrap matching join sources inFinalTable.Arel::TableandTableAlias). If the same physical table is joined more than once, every join of that table receivesFINAL(documented on the methods).Model.joins_final), chaining, dedup with an existing.joins, andunscope(:joins_final)are all supported.ActiveRecord::ActiveRecordErroron non-ClickHouse connections, consistent with the existingfinal/settingsclauses.Changes
lib/arel/nodes/final_table.rb— new marker nodelib/arel/visitors/clickhouse.rb—visit_Arel_Nodes_FinalTablelib/active_record/connection_adapters/clickhouse_adapter.rb— require the node, delegate the new methodslib/core_extensions/active_record/relation.rb— the query methods, join-marking, andunscopesupportREADME.md,CHANGELOG.md— docsspec/single/model_spec.rb— specs covering generated SQL, FINAL combination with.final, dedup,unscope, INNER-vs-LEFT, mixed same-association use, and real execution against ClickHouse (FINALrequires a supporting engine, so the execution test self-joins thesampletable)