Supporting reference · Working design and evidence, not a promise of complete support.

On this page

Rails scopes and ordering

Status: Implemented for the bounded Predicate and Ordering shapes below. Other shapes are listed gaps.

Each admitted Predicate lowers to a named, chainable Active Record scope, and each admitted Ordering lowers to a named order scope with a matching index. A bounded recursive visitor turns resolved and type-checked Expressions into ordinary where hashes or one checked Arel condition. Separate projections admit relationship-existence scopes and two current-Account scopes once the relationship catalog is fixed. An Entity's implicit order sets implicit_order_column, which affects ordered finders only. Unsupported Predicates and Orderings are listed gaps, and a consumer that selects one keeps a precise consumer gap. The Queries owner states what Predicates, Expressions, and Orderings mean.

On this page

Predicates

The current public Compiler reuses exact resolved and type-checked Expression evidence for one bounded local recursive SQL visitor. Direct emitted-Field comparisons admit scalar literals, current_date, and current_time; literal-set in, optional-Field is_null, Boolean and/or/not, and same-Entity matches_predicate retain one checked Arel condition with complete dependency evidence. The application uses equivalent where/where.not hashes, non-null </<=/>= ranges, and ordered conjunctions. Straightforward Predicate compositions call the final names of actually emitted scopes. A checked dependency without an emitted method remains expanded. Arel stays for disjunction, general negation, strict >, singleton or NULL-containing membership, and shared clock values that independent scope calls would recapture. Singleton hash membership would introduce an equality-derived creation default; NULL hash membership would add an IS NULL alternative. Repeated-attribute conjunctions retain their authored order and all restrictions. Scalar literals and sets remain inert Ruby source. Named scopes stay chainable and preserve SQL true/false/unknown behavior. See the Rails comparison. Each required date or time environment value is captured once per scope evaluation; each root stops lowering and remains listed after 4,096 expanded Expression visits or 262,144 bytes of generated condition source. An unnormalized realized citext Field admits only direct equals or not_equals against a String literal; membership, a normalized citext operand, Field operands, mixed comparison modes, contains, starts-with, and ends-with remain target-support gaps. Unrealized derivation and encryption remain gaps as well.

Broader relationship exists, Association paths inside other Expressions, record evaluators, broader Association consumers, and broader Scaffold Predicate consumers remain unsupported and listed. Admitted Scaffold definitions and admitted collection projections consume their exact selected Predicate entries; an unsupported selection stays an exact consumer gap rather than becoming an unreported unfiltered query. Relation#include? is not a record evaluator.

Direct comparisons, literal-set in, optional-Field is_null, Boolean groups, negation, and same-Entity Predicate reuse compose only when every nested child and referenced Field is lowerable. Required date and time environment values are each captured once per scope evaluation. A root is withheld after 4,096 expanded Expression visits or 262,144 bytes of generated condition source.

Predicate admission still withholds authored query normalization, unrealized derivation and encryption, Field operands, pattern operators, other relationship shapes or current-Account comparisons, and broader Association consumers. Scaffold lowering binds admitted scopes at exact definition and collection-projection use sites; an unsupported selected Predicate remains a precise consumer gap rather than an unreported unfiltered query. An unnormalized realized citext Field admits only direct equals or not_equals with a String literal. Simple named Ordering admission may use an emitted normalized or citext Field because assignment and ordering operate on the stored value; realized comparison composes with the same matching index. An all-Field Ordering may omit explicit terminal id only when its exact term set matches an admitted unconditional uniqueness tuple. Record evaluators and broader generated query indexes remain gaps.

Staged Predicate qualification

Predicate qualification is staged to avoid a dependency cycle. The Compiler first derives local ExpressionSql scopes. DomainReference.catalog may consume only those entries while admitting a predicated Association. After that exact relationship catalog is fixed, a second projection may append root unfiltered relationship-existence scopes. Collection entries reuse exact DomainAssociation rows. One singular entry reuses the exact emitted DomainReference behind its derived forward Association. An appended entry cannot feed back into Association admission. After Domain, relationship, public-index, and exact Account projections are fixed, a third projection may append only the two current-Account relationship scopes. It consumes the same-generation public Account lowering, Field-only registration inputs, Account self, relationship catalog, and typed Expression evidence. Final Predicate diagnostic suppression occurs only after this Account-dependent projection, and the appended entries cannot feed back into Domain, relationship, or Account admission.

Existence scopes

One separate Predicate projection may then admit root unfiltered exists over an emitted cross-Entity direct required immutable collection. A predicated entry reuses the exact Association and calls its emitted child scope, which owns any required clock capture. For example, it emits where(id: Post.published.select(:author_id)). An unpredicated entry uses the full child relation. Both forms select the emitted foreign key in a subquery matched against the owner primary key, so outer records are naturally deduplicated in one SELECT. This is not a SQL EXISTS claim. One separate singular-reference form may retain only an exact same-generation emitted cross-Entity optional immutable non-one-to-one DomainReference whose deletion action is nullify_reference, plus its mechanically derived forward Association. It checks the owner foreign-key slot through Active Record's public where.not hash form. Foreign-key integrity makes a non-null slot equivalent to a present target. Without an admitted inverse for dependent: :nullify, the ordinary non-deferrable foreign key rejects target deletion while the referencing row remains, and the missing authored transition remains an exact gap. This is an outer-row IS NOT NULL check, not SQL EXISTS. Required, mutable, restricted, cascading, one-to-one, self, authored-inverse, indirect, filtered, nested, negated, grouped, reused, and all broader singular existence shapes remain listed gaps. For this standalone relation-level leaf, public where.not preserves the required Relation semantics. Recursive ExpressionSql retains the checked Arel condition and provenance while emitting ordinary Rails filters and calls to admitted named Predicates where equivalent. Grouped disjunction, general negation, singleton/NULL membership, and shared clock values retain condition-level rendering; the expression rendering notes explain those cases and creation defaults.

Once local ExpressionSql scopes and this exact relationship catalog are fixed, a root unfiltered exists may reuse an emitted cross-Entity direct required immutable has_many. The existing predicated representative is user.has_published_posts over user.published_posts. It emits where(id: Post.published.select(:author_id)); the called child scope captures Time.current once and applies the publication filter. The subquery selects the emitted author_id and matches User id. Outer Users are naturally deduplicated. Runtime proof pins exact IDs and one SELECT; it does not claim SQL EXISTS lowering. The unpredicated representative is user.follows_anyone over user.outgoing_follows. It renders the equivalent of where(id: Follow.select(:follower_id)), using the same owner-primary-key/child-foreign-key pattern without a child condition. The appended existence scopes cannot feed back into Association admission.

One separate singular-reference shape is admitted only for a root unfiltered exists over the exact same-generation emitted cross-Entity optional immutable non-one-to-one derived-forward DomainReference whose deletion action is nullify_reference. Photogram's feed_occurrence.sourced_by_follow retains feed_occurrence.source_follow and emits where.not(source_follow_id: nil). Foreign-key integrity makes non-null slot presence equivalent to target presence. The ordinary non-deferrable foreign key rejects target deletion while the referencing row remains because no admitted inverse can carry dependent: :nullify; the authored nullification is an exact target gap. The scope proves stored presence, not that missing deletion transition. This is an outer-row IS NOT NULL condition, not SQL EXISTS. For this unfiltered singular-presence projection, required, mutable, restricted, cascading, one-to-one, self, authored-inverse, indirect, filtered, nested, negated, grouped, reused, and every broader singular shape remain target-support gaps.

Current-Account scopes

Two final relationship scopes require the exact selected valid public Account lowering, its Field-only registration projection, and its successful Account-self surface. Each root has the exact resolved and typed nested comparison current_record == current_account. Photogram's feed_occurrence.belongs_to_current_viewer(current_account:) uses the required immutable viewer Reference and filters viewer_id. post.liked_by_current_account(current_account:) uses the emitted unpredicated direct post.likes collection plus required immutable like.user, then matches Post id against Like.where(user_id: current_account.id).select(:post_id). A nil, wrong-class, unpersisted, or destroyed argument returns none. Generated models read no implicit request, global, or thread-local account state. An emitted index already leads with each filtered Reference column, so no new index is emitted. Other relationship topologies, comparison directions or operands, nesting, Boolean composition, and Predicate reuse remain omitted and listed.

Named Orderings

As the general rule before the two structural patterns below, retain every imported Ordering as an immutable semantic entry, then lower only the first conservative subset. An admitted Ordering has at least two homogeneous-direction terms, carries no null-placement override, and is stable because it either ends explicitly in system id or its all-Field term set exactly matches one admitted unconditional Entity uniqueness tuple. It selects only created_at, id, or required emitted queryable Fields whose normalization, comparison, derivation, and encryption behavior is already realized. Its key must be a safe Active Record scope name and must not collide with an emitted Predicate scope. The model emits scope :name, -> { order(first: :asc, second: :asc) }; the migration and schema emit one ordinary composite B-tree index over the same columns unless an admitted unique index already subsumes the same ordered-column index. PostgreSQL may scan that index backward for a homogeneous descending order, so the index does not encode direction.

Central-capture composition adds two structurally admitted patterns (support inventory row (repository-only)) through ordinary Rails APIs. A required emitted ordinal enum Field descending followed by system created_at and id ascending, without null overrides, uses in_order_of(:priority, ["high", "normal", "low"], filter: false) with the actual Field key and reversed authored enum keys. Its expression index preserves the same rank expression and ascending tie-breaks. An optional emitted datetime Field descending with nulls last, followed by descending system id without a null override, uses typed Arel descending/nulls-last order and a matching composite index. Both render as ordinary Rails t.index declarations. Both require storage without encryption, derivation, normalization, or comparison modifiers; available model scope names; and no Predicate-name collision. Equivalent admitted index claims share one index. Incompatible physical names withhold the affected advanced Orderings, preserving existing ordinary Ordering, implicit-order, Reference, and uniqueness indexes. The current naming path allocates no alternative name. Either pattern may emit without the other. Capture identity still binds each result to its submitted Head and reviewed GapSet. No fixed application SHA, subject UUID, or Oscar name determines support; existing scope and index collision rules still apply. Structural scopes join ordinary scopes in enum-helper allocation and AASM fixed-API admission. Serialization and unrelated metadata do not change support. The independent-Plan qualification (repository-only) covers those dimensions. Direct rendering without a capture does not run the additional composition. Scaffold consumption remains a separate gap with id fallback. Outside these two patterns, mixed direction, nullable Fields, authored null placement, enum rank, Tree values, consumer bindings, and broader combinations remain target-support gaps. Repeated semantic sources are a blocking Plan error rather than a support gap.

Ordering composition

One narrow OrderingComposition derives from the exact CompilationInput::Result used by a Compile; the generation record and RenderingContext receive that same immutable object. Analysis derives the same pure result from its own exact captured input. It admits the two structural patterns described by Ordering lowering: descending required ordinal rank with ascending created_at, id, and descending optional datetime with nulls last and descending id. The leaf results retain exact capture provenance; no particular application SHA, UUID, or readable path controls support. Composition combines their existing entries deterministically after resolving incompatible physical index claims. Model rendering consumes each leaf's existing scope declaration; schema rendering consumes its structured columns, expression, and orders. State Machine fixed-method checks derive these admitted scope names from the same captured input. ScaffoldLowering remains unchanged and does not consume either entry.

Ordered finders

Set the inherited Entity fallback to created_at. Active Record's implicit_order_column API accepts one column or an array. Its ordered-finder implementation appends the query constraints or primary key and deduplicates the result, producing effective created_at, id behavior for this profile. The implemented target Analyzer derives that default when an Entity has no authored rows. It requires every authored Field to be required, unencrypted, and queryable, and it requires an authored order to end with the system id Field. Rails could append that primary key itself, but keeping id explicit makes the Plan's authored sequence independent of hidden framework repair. Captured public Analysis reports these existing semantic errors before GapSet construction. Diagnostic locations use the captured submitted-source index, including after import pruning; legacy raw-input analysis is unchanged. At this boundary, queryable uses the frozen Compiler's Field-kind catalog: every current kind except attachment, image, JSON, and rich text, provided it is not encrypted at rest. Unit coverage pins that complete catalog; the frozen corpus oracle exercises only the positive authored message.sent_at path. The live graph does not yet retain Field derivations, so the Analyzer cannot yet reject a semantically queryable Field whose Rails lowering does not produce a stored queryable column. That eligibility check follows derivation persistence and lowering. The capture-bound Compiler lowers Case Chat's exact message.sent_at, id authored order to self.implicit_order_column = [:sent_at, :id] and emits its ordinary composite index. Generated migration and fresh schema-load proof exercise the resulting finder behavior. This affects ordered finders such as first and last, not ordinary Relation or Association loading. Associations do not carry Ordering in the current Plan. Rails' merged but not yet profile-supported default_order API is not approximated with an ordinary scope order.

Query behavior

Keep Relation pagination, appropriate indexes, useful preloads, and Bullet plus focused query-growth checks on important paths. Ordinary association reads remain available. The verification policy (repository-only) distinguishes detection from query-growth coverage. The Case Chat (repository-only), Photogram (repository-only), and ignore-and-list (repository-only) runtime smokes exercise the representative scopes after schema loading.

Scope rendering and target checks

Expression rendering

The current Rails Compiler lowers the bounded local recursive subset to ordinary named scopes:

scope :active, -> { where(status: "active") }
scope :wants_all_activity, -> { active.where(notification_level: "all_activity") }

The immutable ExpressionSql projection retains a checked Arel condition and its Field, Predicate, and environment dependencies. It separately renders equivalent Rails where and where.not hashes, non-null range bounds for <, <=, and >=, and ordered conjunction chains. Repeated attributes remain separate conditions. An explicit matches_predicate calls the dependency's final named scope only when that scope is emitted. An unavailable public name retains condition-level expansion. Keyword-shaped names and names used by clock locals receive self..

Rails' array handler turns a singleton hash array into equality, which would add a creation default, and turns NULL membership into an IS NULL alternative. Neither is the existing IN behavior. General negation keeps its grouping and cannot negate caller restrictions. The renderer preserves each conjunct's order because Rails takes the last equality for a repeated attribute when deriving creation defaults. It uses neither merge to combine filters nor invert_where to negate a named Predicate.

Each required Date.current or Time.current value is captured once per evaluation. One called child can own the capture. If a called child and another condition need the same value, the parent captures it once and renders the combined Arel condition instead of calling scopes independently. No new public scope arguments are introduced. Scalar literals remain Ruby-dumped data, and SQL three-valued truth is preserved. The immutable lowering consumes the existing same-generation resolution and type evidence; it does not interpret authored Expression JSON again. GapSet removal follows the Predicate subject IDs actually admitted.

The original direct-scope implementation (repository-only) mapped every comparison to Arel, and recursive lowering (repository-only) extended that common representation to Boolean groups and Predicate reuse. Keeping that checked representation inside the Compiler does not require exposing it in every emitted equality. The current renderer uses ordinary Rails syntax where equivalent, without broadening admission or changing the reviewed GapSet. Issue #706 (repository-only) records the preceding 16 paired Oscar Party model/database comparisons, including the NULL-membership difference. Those probes retain their original boundary.

Rails documents the scope-chaining pattern used above. Four preceding Case Chat model/database comparisons preserved SQL, IDs, creation defaults, AASM build behavior, and one SELECT across caller restrictions and contradictory conditions. Separate framework probes confirmed that merge can replace a same-attribute condition and active.invert_where can negate caller filters; Rails warns about the latter. Issue #707 (repository-only) retains those bounded comparisons and the implementation history. Neither comparison turns a condition-level fallback into an unavailable named-scope call.

Predicate reuse by Associations

A no-cardinality referenced-side direct Association over a required immutable non-one-to-one Reference may consume one of those scopes when the Predicate belongs to the result Entity. The qualified Association retains the exact same-generation PredicateScope entry. Its Rails lambda calls that entry's emitted name, for example has_many :unwatched_bookmarks, -> { unwatched }, class_name: "Bookmark". An application owner edits the filter in one place. The Compiler retains checked identity and dependency evidence without emitting a second condition implementation. Every broader Association shape remains independently listed.

The condition has one application definition. Changing the named filter changes its referencing Association, matching the Plan's explicit reuse. The original association implementation (repository-only) inlined a modified condition to suppress creation defaults. ADR 0021 (repository-only) removed that modification and restored ordinary Rails behavior; the later named-scope change removes the duplicated spelling. Three preceding paired Oscar Party model/database comparisons preserved IDs, creation defaults, and one SELECT per side, including a keyword-named scope. Issue #707 (repository-only) retains that evidence. Owner restrictions, writers, AASM protections, preloads, and through traversal remain separate application behavior to preserve when changing a scope.

Existence entries

A root unfiltered exists may become a PredicateScope::CollectionExistence only when it names an admitted cross-Entity direct required immutable has_many. A predicated entry also retains the local Predicate dependency lineage and calls the emitted child scope; an unpredicated entry selects from the full child relation.

The original collection-existence implementation (repository-only) copied the checked child condition, binding its table and required date/time values in the parent scope. Calling Post.published keeps that condition and its time capture in one application definition. Six preceding Photogram model/database comparisons preserved SQL, IDs, creation defaults, one SELECT, and one clock capture across time boundaries, additional parent filtering, and duplicate qualifying children. Issue #707 (repository-only) retains that evidence; it is not a new qualification of the combined application or dependency bundle.

A separate PredicateScope::ReferenceExistence may retain only the exact same-generation emitted cross-Entity optional immutable non-one-to-one DomainReference whose deletion action is nullify_reference, plus its mechanically derived forward Association.

Ordering entries

The named-Ordering lowering similarly consumes captured normalized rows. GapSet removal follows the Ordering subject IDs actually admitted; every other retained root remains independently listed. ScaffoldLowering binds an admitted Ordering entry at its exact index or admitted collection use. When a selected Ordering is unsupported, that consumer retains an exact gap and may use deterministic id fallback without claiming the authored order.

A record-level consumer may also require a Boolean method. A Policy may require record and relation projections. Broader Scaffold and Association consumers of named Orderings remain open.

Target checks

Before emission, target validation checks:

For rails-sketch/2026-09-bookmark-assets, that scope check consumes one frozen 707-name union derived from pinned Rails 8.1.3.1 with the maximal supported Account, Policy, and State Machine dependency set. Predicate and Ordering admission does not reflect whichever Active Record methods happen to be loaded in the First Draft service. The in-place pre-alpha repair advances the Analyzer and Compiler release fence; the profile owner retains extraction provenance and exact digests. This does not claim complete instance-method or capability-local name coverage.

The final item prevents an otherwise valid recursive tree from becoming unexpectedly expensive. Dedicated limit diagnostics plus relationship depth and traversal budgets remain Compiler implementation work.

References marked “repository-only” name implementation or internal material outside this public guide. They are intentionally not links. Publishing a design does not prove its implementation.