Skip to content

Resolve joined table aliases in where() and orderby() at query time - #23

Closed
robinvandernoord wants to merge 4 commits into
masterfrom
ccr-364bb824-5b5xut
Closed

robinvandernoord wants to merge 4 commits into
masterfrom
ccr-364bb824-5b5xut

Conversation

@robinvandernoord

Copy link
Copy Markdown
Member

Summary

This PR enables filtering and ordering on joined relationship tables by resolving their aliases at query execution time. Previously, filtering on a joined table would raise AliasedTableMismatchError because the table reference didn't match the aliased table in the SQL. Now, field references to joined tables are automatically rewritten to point to their aliases, and lambda-based filters can request joined tables by name.

Key Changes

  • Deferred query resolution: Added deferred_queries to QueryBuilder to store where() calls with lambdas that request joined tables. These are resolved when the builder executes, after all joins are known.

  • Lambda argument binding: Implemented _requested_tables() to inspect lambda signatures and extract which joined tables they request by parameter name. Supports nested relationships with __ notation (e.g., articles__comments).

  • Alias rewriting: Added _rewrite_tables() to recursively rewrite field references in queries, expressions, and orderby/groupby clauses to point to their aliased tables instead of the original table names.

  • Join tracking: New _JoinedTable data structure and _joined_tables() method to collect all joined relationships with their aliases and metadata. _alias_rewrites() maps original table names to their single alias (when unambiguous).

  • Ambiguity detection: Enhanced _validate_joins() to detect when the same table is joined multiple times under different aliases and require the user to disambiguate using lambda arguments.

  • Mutation safety: Added _mutation_query() to prevent delete() and update() from using deferred where() lambdas, since mutations ignore joins and would delete/update more rows than intended.

  • Pagination support: Updated _apply_limitby_optimization() to include required left joins in the ID subquery, ensuring pagination works correctly with joined table filters.

  • Cache key stability: Implemented _cache_key_query() to replace per-process alias hashes with relationship paths in cache keys, making caches stable across process restarts.

Notable Implementation Details

  • Field resolution works regardless of whether where() comes before or after join(), since it happens at execution time.
  • Nested relationships are supported with both full path notation (post__writer__articles) and unambiguous short names (articles).
  • The on= relationship type (which joins without an alias) is excluded from rewriting, keeping the original table name.
  • Left joins required by a filter are automatically included in limitby optimization subqueries.
  • Arguments with defaults in lambda signatures are ignored (don't request joined tables).

https://claude.ai/code/session_01FXZN6UgVPuRxvHzMMTShAE

claude and others added 4 commits October 2, 2026 10:20
…ery runs

A builder that is returned and extended later can't use condition_and
anymore, and a plain where() on a joined table used to raise
AliasedTableMismatchError. Fields of a table that is joined under exactly
one alias are now pointed at that alias in where/orderby/groupby/having,
at collect time, so where().join() and join().where() behave the same.

- A table joined more than once still raises; pick the join with a
  lambda whose extra arguments name the relationship
  (`where(lambda article, reviewer: ...)`, nested as `parent__child`).
  These lambdas are resolved at collect time too.
- The limitby id subquery and count() include the left joins the
  predicate filters on, so pagination and counts match the rows.
- The distinct-ids pagination subquery names its id column, which no
  longer collides with an ordered-by id of a joined table.
- delete()/update() refuse relationship lambdas instead of dropping them.
- Cache keys use relationship paths instead of per-process alias hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXZN6UgVPuRxvHzMMTShAE
- A nested relationship to the root table (`Article.join("writer.articles")`)
  renamed the root's own select fields to the nested alias, so collecting
  raised KeyError. It is now aliased upfront like a self-reference.
- Nested relationships lost every row after the first one: once a parent
  instance was seen, later rows (e.g. an article's second comment) were
  skipped before their nested data was read. Rows reached through
  different parents also shared one dedup set keyed by the nested row's
  id. Dedup is now per parent instance, and a seen instance still takes
  nested data from later rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXZN6UgVPuRxvHzMMTShAE
Base automatically changed from release/typedal-v6 to master October 5, 2026 09:31
@robinvandernoord
robinvandernoord deleted the ccr-364bb824-5b5xut branch October 5, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants