Skip to content

Conditioned relationships: deferred follow-ups (operators/IN, many_to_many, priming) #246

Description

@JJJ

Context

Conditioned relationships shipped in eedada8. A relationship may declare a condition (a fixed column => scalar equality map, e.g. object_type => 'order') that scopes the related rows, so a polymorphic child table (one table with an object_id + object_type pair pointing at different parent types, as EDD's notes/logs/transactions/adjustments do) can be modeled as one relationship instead of hand-coded SQL.

The condition is rendered as AND {remote}.{column} = {value} across every SQL form (get_related traversal, the correlated EXISTS filter, and nested EXISTS), fails closed on any unknown/malformed path, is application-layer only (never a FOREIGN KEY — a discriminator can't be encoded in one), and defaults to the join/EXISTS filter strategy.

This closed the last EDD reunification-readiness "modeling" gap.

v1 scope (intentional) and the deferred follow-ups

Everything below is non-breaking to add later (the value-object shape, the m2m rejection, and the unprimed behavior all extend cleanly, and the relationship API is unreleased) — which is the whole reason they were deferred rather than shipped now. Each has a specific reason beyond "YAGNI":

1. Richer predicates (operators / IN), not just scalar equality

v1 accepts condition => array( 'object_type' => 'order' ) (equality, scalar values). To extend to array( 'status' => array( 'compare' => '!=', 'value' => 'trash' ) ) or IN lists:

  • The filter path already routes through the shared build_condition(), which handles !=, >, IN, casts — so richer predicates there are nearly free.
  • The traversal path (get_related) can only speak query-vars ({col}__in), which express = and IN but not >/!=/LIKE.
  • So doing this halfway creates an asymmetry (a != condition would filter but silently do something else on traversal). The blocker isn't the filter side — it's deciding what traversal does with a non-IN operator (probably: fail closed, or fall back to a per-item query). Spec that first.

2. many_to_many support

v1 rejects a condition on a many_to_many (a get_validation_errors() error; traversal + filter fail closed). Deferred because there's no consumer and a real design fork: on a polymorphic pivot, does the condition scope the pivot row or the target row? Guessing wrong bakes in the wrong answer — resolve the semantics against a real use case first.

3. Cache priming for conditioned relationships

v1 leaves conditioned relationships unprimed (Traits\Query\Cache::prime_* early-returns when has_condition()). This is perf-onlyget_related() still returns correct rows (its own conditioned query; its cache key includes the condition, so an unconditioned primed entry can never be a false hit). Threading the discriminator through the prime primitives (prime_has_many, prime_relationship_tuples) is a deeper change for a with-priming speedup nobody has measured needing yet.

Also worth noting (not a v1 limitation, a requirement)

For the query-var paths (traversal + the in strategy), a condition column must be in => true on the remote (they filter via {col}__in); the join/EXISTS path renders raw SQL and needs no flag. This is enforced (fail closed) and documented on the Relationship::$condition property.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: Database/KernCore database objects in src/Database/Kern.area: Database/ParsersQuery parsers for by, compare, date, in, meta, search, and related clauses.area: Database/TraitsShared database traits and reusable object behavior.object: RelationshipRelationship definitions, foreign keys, and related-data loading.scope: PerformanceRuntime, query, memory, or cache performance.type: ImprovementRefinement to existing behavior, code, or APIs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions