fix(datasource-active-record): normalize demodulized polymorphic types & reuse shared joins#328
Conversation
…s on read [local] Local-only workaround so Forest can resolve the target collection when the app stores a demodulized polymorphic *_type (e.g. "Income" for Api::Income). Not for the has_one:through PR; the real fix lives in feat/support-polymorphic-qonto. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ding a has_one :through Track joins by signature (their ON condition) rather than table name, so a has_one :through whose intermediate table is also joined by a belongs_to of the same association reuses that single join (ActiveRecord dedupes it) instead of falling back to per-hop preload queries. A belongs_to reaching the same table via a different foreign key still bails to preload, since ActiveRecord would alias it and collect_joined_selects cannot reference the alias. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 new issues
|
| rescue StandardError | ||
| next | ||
| end | ||
| hash |
| # Set of tables the subtree adds via JOIN, or nil if any relation in it can't be safely joined. | ||
| def joinable_tables(collection, relation_name, sub_projection, used_tables) | ||
| target = joinable_target(collection, relation_name, used_tables) | ||
| def joinable_joins(collection, relation_name, sub_projection, used_joins) |
| joins = joins.merge(nested) | ||
| end | ||
| tables | ||
| joins |
| return if used_tables.include?(target.model.table_name) # a table joined twice would be aliased by AR | ||
| return if through_tables(collection, relation_name).intersect?(used_tables) | ||
|
|
||
| target |
Two associations joining the same target table with the same FK name from different parents produced identical signatures, so conflicting? treated them as one join and allowed reuse. AR aliases the second, but collect_joined_selects reads the unaliased table.column and got the wrong join's data. Include the source table in the signature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b913911 to
e100c13
Compare
…relations too hash_joined_relation read projected *_type columns straight from the aliased root row, so a JOINed polymorphic belongs_to returned the raw stored value while the preloaded path returned the fully-qualified class name. Normalize the joined hash against the relation's target model (resolved by walking the reflection chain) so both paths agree. Also wrap the join signature string to satisfy Layout/LineLength. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reviewed both fixes (join-signature reuse + polymorphic type normalization). The overall design is sound — 🔴 Blocking — join signature omits the target join key → wrong record served, silently
Reproduced end-to-end on the dummy app (two belongs_to on Suggested fix — fold the target join key into the signature so a differing def signature(reflection)
join_key = Array(reflection.respond_to?(:join_primary_key) ? reflection.join_primary_key : reflection.association_primary_key)
"#{reflection.active_record.table_name}.#{Array(reflection.foreign_key).join(',')}" \
"->#{reflection.klass.table_name}.#{join_key.join(',')}"
end(The same-FK/same-target case without a 🔴 Blocking — polymorphic normalization is untested and not reproducible with current fixtures
Please add a namespaced polymorphic fixture (e.g. 🟠 Silent-failure direction — new rescues fail toward "join anyway / keep raw value" instead of the conservative path, and none log
🟠 Test — conflict/preload-fallback case only asserts SQL shape
🟡 Minor
Happy to pair on the signature fix / the namespaced fixture if useful. |
…re; test polymorphic normalization
Address review (matthv):
- signature: a belongs_to ON clause is target.join_key = source.fk; two
associations with the same source/FK/target but a different :primary_key
had identical signatures and were wrongly reused (AR aliases the 2nd join,
collect_joined_selects reads the unaliased name -> wrong record served).
Encode the target join key so a differing ON yields a differing signature
(-> conflict -> safe preload).
- join_signatures now returns nil on error -> preload, instead of {} which
left the relation joinable but invisible to conflict detection.
- normalize_polymorphic_types: narrow rescue to NameError + log, and memoize
the polymorphic belongs_to lookup per model_class.
- target_model: narrow rescue to NameError.
- Add a namespaced polymorphic fixture (Api::Note/Api::Topic, stored
demodulized) and assert normalization on both the preloaded and JOINed
paths (previously untested / not reproducible with existing fixtures).
- Preload-fallback spec now runs list() and asserts the secondary relation
is served its own row, not the collapsed through row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review @matthv — all addressed in c9caa98. 🔴 Signature omits the target join key — confirmed and fixed. 🔴 Polymorphic normalization untested / not reproducible — added a namespaced fixture: 🟠 Silent-failure direction —
🟠 Conflict/preload-fallback test only asserted SQL shape — it now creates a distinct 🟡 Minor — fixed the stale "Set of tables" comment (now One I left as-is: the |
## [1.35.1](v1.35.0...v1.35.1) (2026-07-03) ### Bug Fixes * **datasource-active-record:** normalize demodulized polymorphic types & reuse shared joins ([#328](#328)) ([503a25a](503a25a))
|
🎉 This PR is included in version 1.35.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Two related fixes in the ActiveRecord datasource:
*_typeis demodulized (e.g.Accountinstead ofModule::Account), resolve it throughpolymorphic_class_forso the serialized value matches the fully-qualified class name Forest expects.has_one :through— track joins by table signature (foreign key → target) rather than by table name only. Joins that share a table with a compatible signature are reused; only genuinely conflicting joins fall back to preload. Filter/sort joins are still never reused.Test plan
join_to_one_optimization_spec.rbupdated and passingNote
Normalize demodulized polymorphic types and reuse shared JOINs in ActiveRecord datasource
normalize_polymorphic_typestoActiveRecordSerializerto resolve demodulized polymorphic type values (e.g. from models withstore_full_class_name = false) to fully qualified class names viapolymorphic_class_for, for both direct and JOIN-hydrated relations.Query#split_relationsto track joins by signature (source_table.foreign_key->target_table.primary_key) instead of table name, allowing identical JOINs to be reused rather than duplicated or fallen back to preload unnecessarily.joinable_joins,conflicting?,join_signatures, andsignaturehelpers to encapsulate the new conflict-detection logic;joinable_targetno longer rejects joins based solely on table reuse.Api::Note,Api::Topic) and migrations to support polymorphic type normalization tests.belongs_totype columns now return the resolved class name rather than the raw DB value; JOIN reuse may change query structure for associations sharing an intermediate table.Macroscope summarized c9caa98.