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 whose scopes raise until written.

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. An unsupported Predicate or Ordering is a listed gap that still gets its named scope: an unwritten scope that raises NotImplementedError, which every Scaffold consumer that selects it calls. The Queries owner states what Predicates, Expressions, and Orderings mean.

On this page

Predicates

A Predicate or Ordering key names its scope. A key that Active Record or a selected library already defines as a class method, such as public or order, would break the model at class load, so its scope becomes the key plus the table name, such as Profile.public_profiles. Every generated caller, including Scaffold pages, predicated Associations, and composed Predicates, uses that name, and the Analyzer warns with foundation_plan.rails_target.scope.renamed so the owner can rename the key. When the renamed name is taken too, by another authored key or an enum mapping method, the subject keeps the ordinary omission.

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 calls its unwritten scope and keeps an exact consumer gap, never 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 calls its unwritten scope and keeps a precise consumer gap. 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. Deleting the Follow nulls the slot through Follow's inferred has_many :feed_occurrences, dependent: :nullify. 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 scope name, renamed as Predicates describes when its key is reserved, 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, each an unwritten scope. 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. When an authored order names no id, the Analyzer appends the system id Field, the tie-breaker Rails' ordered finders append anyway, so the emitted tuple and its index state the order the finders run. It rejects an authored id that another term follows, since that term could not break any tie. Captured public Analysis reports these 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, or message.sent_at alone, 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.

Unwritten scopes

Under the stub decision (repository-only), the Compiler emits a named scope for each Predicate and Ordering it cannot lower. The scope raises NotImplementedError with a message that names its subject and the request specs whose examples request its pages, or else the model spec to add. It calls Kernel.raise because the scope body runs on a Relation, where a scope named raise would answer a bare raise:

scope :highest_rated, -> {
  Kernel.raise NotImplementedError,
    "movie.highest_rated is not written yet. README.md describes it; spec/requests/movies_spec.rb covers it."
}

ScopeStubs derives these entries from the exact CompilationInput and, for a captured Compile, its Ordering composition, so Analysis and Compile agree and an Ordering that a structural pattern admits is never stubbed. Direct rendering without a capture runs no composition, so the two structural patterns are unwritten scopes there. A scope is unwritten only when its generated name is free on the model, so a reserved key's stub takes the renamed name. A reserved key whose renamed name is taken too, a Predicate and an Ordering that share a key, and an AASM fixed class method on an Entity with a State Machine keep the ordinary omission. An unwritten scope reserves its name in enum-helper allocation as an admitted scope does.

Every admitted Scaffold index and collection projection that selects the subject calls the unwritten scope, so the page raises until the owner writes it, and a Home that shows that index raises too. A gated collection, or a collection on a profile whose read Policy may deny an account, calls it only for the accounts that Policy admits; the README names that Policy. An unwritten Ordering emits no index; its GapSet consequence and the README say so. A cursor-paginated consumer serializes the timestamps that the authored terms name. Predicate-filtered Associations and Counters, associated creation on a Predicate-filtered collection, and a Validation whose condition cannot be lowered stay omitted, because a raising scope there would break preloads, counts, or every write. The generated tests owner describes the README entries and specs.

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. The simple-query-syntax issue (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. The scope-reuse issue (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. The scope-reuse issue (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. The scope-reuse issue (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 calls its unwritten scope and retains an exact gap. It keeps deterministic id fallback, without claiming the authored order, only when no unwritten scope can take the name or a structural pattern emits a scope it does not consume.

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-10, that scope check consumes one frozen 711-name union derived from pinned Rails 8.1.4 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.