Rails data integrity
Status: Implemented for the bounded Validation slice below. Other Validation shapes are listed gaps.
Plan Validations lower to Rails model validations, and storage invariants add database constraints where they are
the clearest backstop. Required scalar Fields derive presence, inclusion, or numericality declarations plus
NOT NULL. The admitted slice adds comparison, length, format, absence, conditional presence, and uniqueness, and
uniqueness receives a matching unique index, partial when the rule is conditional. A uniqueness rule the target
leaves out keeps its unique index when every tuple member is an indexable column. Every declaration targets an
explicit Field or logical Reference key, so forms show each error beside its input. An unsupported Validation keeps
its bytes and is listed in the reviewed GapSet, while the admitted declarations still render. When its model is
emitted, the model also declares validate :method with an empty method for it, and a failing model example pins
its error, as left-out Validations describes. The Validations owner
states what each rule means and when database enforcement fits.
On this page
- Direction
- Admitted slice
- Qualification
- Requiredness and numeric input
- Reference comparison
- Format
- Length limits
- Uniqueness
- Realization layers
- Executable first admission slice
- Ordinary numeric validation
- Ordinary Reference comparison
- Reusing enum predicates
Direction
Support: see the support inventory row (repository-only).
Choose model feedback and database enforcement according to their purposes; follow the validation
guidance when deciding whether a failure is
correctable input or broken application code. A constraint does not automatically require a matching model validation.
An Entity Validation's Field error_target uses its Field binding and a Reference target uses its logical local-slot
error key. Models and forms share those semantic bindings. Plan Validations require an explicit input target;
Rails-native lifecycle errors may still use errors[:base] and appear in form summaries. Begin with Rails' built-in
presence, absence, comparison, length, format, exclusion, and uniqueness families where they preserve the Plan
meaning. Closed when Expressions and dynamic comparison operands become Compiler-generated predicates, same-record
attribute symbols, or callables rather than authored Ruby. Optional Fields derive nil handling; Boolean presence means
non-null rather than truthy, and empty JSON objects or arrays remain present values. Attachments, rich text, and
References use their semantic existence APIs. Numeric Field types derive raw-input numericality validation before
Active Record coercion; integer-only behavior follows from the Field type rather than another Plan option. A target
must prove admitted format patterns safe for its pinned runtime. This profile starts with one runtime-independent
whole-value printable-ASCII character-class grammar and retains the framework Regexp timeout as defense in depth.
Every source outside that grammar stays a reviewed gap. The current admitted uniqueness slice always receives matching
structural enforcement, and a left-out uniqueness keeps the index its tuple names while the GapSet lists the
missing model validation. Domain tables get no literal-bound CHECK constraint, so literal comparisons stay
model-only, as Realization layers explains. There is no blanket trigger or RLS preference.
Admitted slice
The current profile's executable admission slice is narrower than that direction.
For each of the ten admitted stored scalar Field kinds, required: true derives a model validation as well as
database NOT NULL. Date, datetime, language-code, long-text, short-text, time-zone, and URL Fields use presence: true. For a realized required current_date or current_time default or a literal column default, Active Record
materializes the default before this check; explicit nil and later nil assignments remain invalid. Boolean Fields use
inclusion in [true, false], preserving false as present. Integer and decimal Fields use native numericality's
default nil rejection; optional numeric Fields add allow_nil: true. Required numeric Fields receive no duplicate
presence validation. Optional Fields receive no derived requiredness declaration. Every URL Field also derives url: true from a generated UrlValidator; URLs describes it and how it skips blank values. Every
image Field derives image: true from a generated ImageValidator, which Image content
type describes. Each derived declaration is immutable, follows Field order, and adds
that Field's UUID to generated-model provenance.
The slice also resolves the declarations that the
executable first admission slice lists; the
support inventory (repository-only) records each family's
probe. Format accepts \A and \z around printable-ASCII character-class atoms with
optional ?, *, or
+, permits an optional case-insensitive flag, derives allow_nil: true, and emits inert source through
Regexp.new with a one-second instance timeout. A Field error target emits Rails' built-in uniqueness validator.
A Reference error target requires a composite tuple and uses the logical association name in the same native
validator. Rails binds it to the foreign key, applies the remaining physical columns as the scope, excludes the
persisted record, and adds :taken to the logical Reference key. Both forms emit one
unique index over the authored physical tuple;
nulls: "not_distinct" adds NULLS NOT DISTINCT, while distinct omits that option. citext tuple members keep
their column-defined equality, so generated validators do not add case_sensitive: false. A conditional rule adds
the query condition Rails compares within and a partial index over the same records, as
Conditional uniqueness describes.
Decimal and money comparisons take exact BigDecimal bounds from the Plan's canonical decimal literals, written as
an Integer when whole, such as numericality: {greater_than_or_equal_to: 0}. Date comparisons emit ordinary Rails
comparison validators with Date.new(year, month, day) bounds. Only canonical Plan dates from 1582-10-15
through 9999-12-31 lower: earlier literals remain source-addressed target gaps because the Plan uses proleptic
Gregorian dates while Rails 8.1.4 casts through Ruby's historical-calendar
Date.new. This bound
restriction preserves authored meaning without replacing Rails casting. Rails casts an invalid calendar submission
such as 2025-02-30 to nil; required presence then owns the error, while an optional Field keeps Rails' nil
behavior. This does not add a raw-date validator or change admission of authored date values. Dynamic dates,
relationship-path operands, and datetime literal comparisons remain gaps; same-record Field operands follow the
admission slice. Development-data selection evaluates date clauses with the
Plan's Gregorian date values and omits a record only when a clause is proven broken. A seed date before 1582-10-15
governed by one of these clauses is still omitted with a gap, because Rails would cast it to another day.
A development record can leave a required current_date or current_time default unassigned. That Field then takes
the clock's value when the seed runs, which the Compiler does not know. For example, a record that sets only
checked_out_at is compared with whatever checked_in_at receives at seeding. Selection cannot decide a literal or
same-record comparison that reads such a Field, so it keeps the record, and seeding raises if the clock breaks the
rule. Authored development values are literals, so
the seed keeps them as written instead of computing a relative value such as 1.day.from_now. A record that assigns
both operands compares them as literals.
Comparison and length declarations derive allow_nil because optional owners accept nil while required scalars
report missingness separately.
Reference inequality with a participating Reference error target uses native Rails comparison and normal
association loading. It compares records, including shared unsaved objects, and leaves missingness to required
Reference validation. Reference errors use the logical
Reference key. Native comparison's default message interpolates the other record's to_s, normally an object
reference such as #<Person:0x...>. Application wording and localization can use ordinary Rails error configuration.
Every source
outside the bounded grammar—including line anchors,
\Z, backreferences, and subexpression calls—plus negative or conditional format and long_text format remains a
listed gap. Every other Validation shape is skipped and listed. Compiler
input
requires every owner, structured condition operand and provenance record, uniqueness tuple column, normalization,
and comparison behavior to be emitted from the same generation.
The complete-file model renderer emits relationship declarations first, then required scalar declarations,
native numericality, state declarations where present, and authored Validation declarations, then any private
condition predicates. Integer numericality keeps integer syntax
and the effective intersection of signed four-byte storage bounds and unconditional authored bounds. Conditional
comparisons remain separate; an input-owned numeric error suppresses a residual comparison only for the same
attribute and actual native error signature. Date clauses remain separate calls.
Decimal inputs use native Rails numericality. Missing required numeric values receive :not_a_number; zero stays
valid. Ordinary malformed strings are rejected, and precise decimal attributes retain their stored precision.
Native parsing also rejects the old custom parser's "1D2" spelling; ordinary scientific "1e2" remains valid.
Money numericality adds its column's storage bound, so an amount of 10^10 or more in magnitude, NaN, or infinity
receives a validation error before Active Record saves. For an unbounded decimal Field, PostgreSQL's extreme
integral/fractional overflow may instead raise during persistence, and Ruby Float/BigDecimal NaN and infinity may
pass validation and persist. The Compiler adds no finite-value callback, digit parser, database check, or exception
translation. Foundation Plan literal and development-data admission remain unchanged.
This release emits no database CHECK for the admitted comparison
slice, so Rails owns enforcement. Generated-app verification exercises required Boolean, numeric,
and text behavior, optional scalar behavior, requiredness composed with an authored comparison, both scalar
integer and date comparison boundaries, safe positive format with an ordinary :invalid Field error, logical Field
and Reference
keys, and conditional behavior and numeric lexical/storage boundaries after migration and schema loading.
Photogram additionally proves create and update failure for follow.not_self, conditional requiredness of
feed_occurrence.source_follow while reason == "following", conditional absence otherwise, and the corresponding
successful writes after schema loading. This
remains the complete
Validation boundary for the current release. Correctable duplicate input reaches the ordinary Rails :taken and
422 form path; the matching index remains the atomic backstop for concurrent or validation-bypassing writes.
Qualification
Normal qualification reconstructs the Validation, Expression, Association-method, and Reference-storage evidence
from that one sealed graph. It runs structural and semantic Validation analysis, applies the target declaration
resolver, and aggregates every prerequisite diagnostic before task discovery. The valid result carries one
same-generation declaration set and verifies that each declaration's Entity and condition Fields belong to the
admitted Domain catalog. Bounded uniqueness additionally verifies every tuple column, normalization, comparison,
and ordinary Reference realization against emitted Domain storage. An unsupported Validation keeps its exact bytes;
it is skipped rather than
lowered, its blocked identity joins the reviewed AnalysisRun GapSet, and the admitted declarations still render.
Generated condition Ruby, ordered null-test attribute names, and subject provenance derive together from one closed
Compiler-created condition value. Ruby source is output, not input to a second parser. CompilationInput still
cross-checks those names and subject UUIDs against the admitted Domain as an independent provenance boundary.
Requiredness and numeric input
Scalar Field requiredness now derives one model declaration before authored Validations. Required Booleans use
inclusion in [true, false], preserving false; integers and decimals use native numericality, and the remaining
admitted scalar kinds use presence. All report invalid values on the logical Field key during valid?.
Field defaults describes how a realized default reaches a new record before these checks; an
explicit nil still reports the model error. Optional scalar Fields derive no
requiredness declaration. Authored comparison and length declarations skip
nil so this derived declaration remains the single owner of a missing required scalar error. Database NOT NULL
remains the durable storage backstop for every required scalar. Every emitted integer, decimal, and money Field also
derives a raw-input validation before authored comparisons. Native integer numericality requires integer syntax and
the intersection of signed four-byte storage bounds and unconditional authored bounds. Money numericality keeps the
range its decimal(12, 2) column stores, and an unconditional authored bound replaces the storage bound it
tightens. Other decimal Fields use native numericality with only their authored bounds. Required numbers report :not_a_number for missing values;
optional numbers allow nil. Unbounded decimal validation may accept Ruby non-finite values or leave extreme storage
overflow to PostgreSQL. No custom parser or finite-value callback is emitted. Input-owned numeric errors suppress
only duplicate residual comparison errors for the same attribute and actual native signature. The
Validation owner records the tradeoffs.
Reference requiredness uses the target's Rails 8.1 required-by-default belongs_to and matching database nullability;
optional References emit optional: true. The generated model therefore reports a missing required target before
the database rejects it.
Generated-file provenance includes every contributing Association, Reference, Predicate, Ordering, Validation, and
condition Field subject UUID. A Validation is omitted and listed when its owner or a condition Field is not emitted;
generated models never retain a declaration or predicate that calls absent storage. Compiler input cross-checks each
condition's structured null-test or enum-equality metadata against the exact emitted Field and provenance instead of
reparsing generated Ruby. A direct enum equality or its immediate negation in a presence, absence, length, or
uniqueness condition uses if: or unless: with the actual allocated enum predicate; compound conditions and State
Machine values retain private helpers. Straightforward unconditional integer, decimal, and money bounds join the
Field's native numericality declaration while preserving original Validation declarations and provenance.
Conditional literal comparisons retain separate Rails comparison: calls.
Native numericality owns required numeric input errors; optional numeric Fields allow nil.
Reference comparison
The bounded Entity comparison handles unconditional not_equals between two distinct required ordinary References
with the same record type. A participating Reference error target emits native comparison: {other_than: ...}
and normal association loading. Validation error targets must name an explicit Field or Reference; ordinary Rails
lifecycle errors may still use :base.
Missing References retain their requiredness errors. The native comparison message interpolates the other record's
to_s, normally an object reference such as #<Person:0x...>; ordinary Rails error configuration can customize it.
These comparison shapes remain model-only; the schema renderer derives no database CHECK constraint.
Format
The format slice emits the built-in Rails validator on one stored short_text Field. Its pure target parser admits
only \A and \z around one or more printable-ASCII character-class atoms, each optionally followed by ?, *,
or +. Class-syntax bytes remain reserved, and ranges stay ascending within a-z, A-Z, or 0-9. Every source
outside that grammar—including line anchors, \Z, backreferences, and subexpression calls—remains a reviewed
target gap; the Compiler does not ask the First Draft service's Regexp engine to interpret it. Generated Ruby calls
Regexp.new with a quoted source String, the profile-owned option integer, and a one-second instance timeout; Plan
bytes never enter a Regexp literal or executable interpolation. allow_nil: true keeps requiredness as the sole
missing-value error owner. Negative, conditional, and long_text format remain reviewed gaps.
Length limits
The same immutable Validation declarations feed the shared schema snapshot. For a short_text Field realized as a
Rails string, the snapshot chooses the tightest upper bound from admitted unconditional maximum and
exact_length declarations and supplies one column limit to both the create migration and emitted schema. It emits
no limit for zero, conditional or minimum-only rules, long_text, citext, or a bound outside PostgreSQL's supported
range from 1 through 10,485,760 characters. Only declarations whose bound equals the emitted tightest limit join the
migration and schema provenance. The Rails declaration retains the complete rule; the database limit is only the
validation-bypass upper backstop for non-space excess. PostgreSQL may truncate excess trailing spaces to the declared
varchar length, so the model declaration remains the complete rule for the authored input. A maximum without an
equal column limit, such as one on long_text, stays model-only; the generated .active_record_doctor.rb exempts
that attribute from incorrect_length_validation, which reads only column limits.
Uniqueness
The uniqueness slice uses Rails' built-in validator for both Field and logical Reference error targets; every other
tuple member becomes a physical Active Record scope, including _id for an ordinary Reference. Rails binds a
logical Reference target to its foreign key, excludes the persisted record, and adds :taken to the logical
Reference key. It may load the Association through its ordinary reader. Neither path adds
case_sensitive: false: citext or the ordinary column type owns each Field's equality. The same declaration
supplies a unique schema index, so normal record and form paths report :taken before persistence while PostgreSQL
rejects a concurrent or validation-bypassing duplicate.
The executable first admission slice lists the admitted tuple shapes. Every other tuple, and tuple meaning whose Field lowering is incomplete, remains a reviewed gap.
A left-out uniqueness rule still gets the unique index its tuple names when every member is a Field column or an
ordinary Reference foreign key, so PostgreSQL rejects a duplicate while the model validation waits to be written.
Optional members and nulls: "distinct" keep their ordinary index semantics there. A member that is long text, JSON,
encrypted, or multi-target leaves the rule without an index, and so does a condition, which needs a partial index the
target cannot derive. The GapSet entry says which case applies.
Conditional uniqueness
A uniqueness rule whose when is a conjunction of same-record null tests and equality or membership of a required
enum or State Machine Field lowers to the Rails validator with two conditions. if: checks only a record the
condition selects, and conditions: compares it only with other selected records:
validates :venue, uniqueness: {conditions: -> { where(kind: "vip") }}, if: :vip?
The matching unique index is partial over the same records and is named for the rule, because the same columns can carry another index:
t.index ["venue_id"], name: "bookings_one_vip_booking_per_venue", unique: true,
where: "((kind)::text = 'vip'::text)"
The predicate is written in a form PostgreSQL 18 prints back byte for byte, so migration and schema dump agree: a
column named by a non-unreserved keyword, such as limit, is double-quoted, and a membership test compares with an
array of text literals. A conditional rule over a single Reference is admitted too, because unlike an
unconditional one it is not a one-to-one Reference. An or, a negated value test, or a path through an Association
leaves the rule a reviewed gap.
Realization layers
A complete Rails realization should normally generate all applicable layers:
- an Active Model or Active Record validation for immediate record and form feedback;
- a database constraint or index where the same rule has safe structural enforcement;
- form attributes, hints, and error placement derived from the same meaning;
- localized error keys; and
- model, database, request, and form tests appropriate to the claimed rule.
Those layers can arrive incrementally, as the code-worth-keeping requirement allows. The Compiler may keep an ordinary Rails model validation or form hint while the GapSet discloses a missing request, localization, or interaction layer. It must not call the Validation completely realized until the layers required by its semantics and claim are present.
Domain tables get no CHECK constraint for now, as register row DB8 (repository-only)
records, except the one that backs an exclusive arc's one-target
validation. ADR 0024 (repository-only) treats writes that skip
validations as nonclaims, so a literal bound enforced only in the model is not a GapSet entry. It belongs in the
generated application's documents instead, which do not disclose it yet;
deriving those documents from the project graph (repository-only) is tracked
separately. The option to
back literal bounds with CHECK constraints (repository-only) remains open.
The first lowering direction is:
- conditional
presenceorabsencestarts with a built-in validator and generated predicate; Boolean presence and JSON containers use type-specific non-null semantics where Rails blankness differs; comparisonstarts with built-in comparison options or a generated closed cross-value check;lengthstarts with the built-in validator, while unconditional bounds can inform form hints andmaxlength;formatstarts with the built-in validator and a Regexp derived from a target-proven safe source grammar;exclusionstarts with the built-in validator when its equality matches the Field and otherwise receives comparison-aware lowering; anduniquenessstarts with the built-in validator when one target can anchor it and receives matching structural enforcement for a complete atomic-uniqueness claim.
Executable first admission slice
To add a declaration to this slice, follow the support-admission procedure (repository-only). It lists the code consumers, inventory row, and public Explorer text that move with an admission.
The rails-sketch/2026-10 target currently resolves only these Rails declarations:
- an unconditional or conditional Field-owned
comparisonon a storedinteger,decimal,money, ordateField, with one or more matching literalgreater_than,greater_than_or_equal_to,less_than, orless_than_or_equal_toclauses; date literals currently start at1582-10-15because earlier Gregorian bounds differ from ordinary Rails casting (see the Rails target). The declaration derivesallow_nil: trueso requiredness separately owns a missing-value error; - an unconditional same-record Field comparison between two stored
dateFields or two storeddatetimeFields, using ordered operators. A Field-owned rule targets its own Field; an Entity-owned rule targets the Field itserror_targetnames, and a sole clause whose error target is the right-hand Field is rewritten with the converse operator. Each clause emits Railscomparisonwith the other attribute's symbol,allow_nil: true, andif: :other?, so requiredness owns missing values on either side. Stay Exchange'svalidates :check_out, comparison: {greater_than: :check_in}, allow_nil: true, if: :check_in?is the representative; - an unconditional Entity-owned
comparisonwhose sole clause isnot_equalsbetween two distinct required ordinary References with the same record type. A participating Reference target uses Railscomparisonwithother_than: :other_referenceandallow_nil: true. Normal association loading and record equality preserve saved and shared-unsaved target behavior. Missing References retain their required-association errors. The native error message interpolates the other record'sto_s, normally an object reference such as#<Person:0x...>; application wording can use ordinary Rails error configuration; - a
lengthValidation on ashort_textorlong_textField, withminimum,maximum, orexact_lengthplus an optional admitted condition; the declaration derives the same nil handling; - an unconditional Field-owned
formaton a storedshort_textField, using positivematchesin the bounded whole-value printable-ASCII character-class grammar with an optional Booleancase_insensitiveflag; the declaration derivesallow_nil: true, compiles with a one-second instance timeout, and leaves missing-value errors to requiredness; - conditional
presenceorabsenceon a text, URL, language-code, time-zone, integer, decimal, money, date, or datetime Field. Rails'presence: trueandabsence: truereadblank?, which agrees with the Plan's presence for these kinds: blank text is absent, while zero and every date are present; - conditional
presenceorabsenceon an ordinary single-target Reference with exact foreign-key storage; and - Entity-owned
uniquenessover any number of required storedshort_text,integer,decimal,money,date,datetime, non-ordinalenum,url,language_code,time_zone, orsecure_tokenFields and ordinary one-column References, with a Field error target or a Reference error target. An unconditional Reference error target needs a composite tuple; Oscar'scredit.movie+credit.person+credit.roleis the representative. A rule may carry a condition that conditional uniqueness admits.
An admitted condition contains direct same-record Field null tests and equality or membership (in) between one
total direct required non-ordinal enum or State Machine Field and literals in its closed domain. Either atom may
participate in not and and or or groups. The condition must always produce a Boolean, and every value binding
must be total; a path that can become unknown through an optional Association is rejected. The resolver retains
compiler-owned structured metadata from that closed grammar. A single direct enum equality reuses the actual emitted
enum predicate with if:; its direct negation uses unless:. An allocated enum prefix or suffix becomes part of that
same predicate name. Other admitted conditions, including State Machine values and membership such as
status.in?(["pending", "accepted"]), retain an inspectable predicate method. Its source parenthesizes only where
precedence requires it or an && sits inside ||, as in
!starts_at.nil? && (status == "cancelled" || status == "requested"). A nested group with the same operator joins its
parent, and a repeated operand appears once, so a && (a && b) reads a && b. The generated app's StandardRB then
accepts it. Compiler input checks the metadata against emitted Field storage and provenance rather than reparsing the
Ruby source. It also keeps a Reference Validation's error key on the logical Reference reader rather than its
physical _id column.
Other Entity-owned rules, other Field comparison owners, unsupported equality operators, conditional Field-to-Field
comparisons, dynamic or relationship-path comparison operands, Boolean, JSON, and attachment presence or absence,
nonordinary Reference storage, broader pattern source, negative or conditional format, long_text format,
exclusion, optional, Boolean, or long-text uniqueness members, conditional uniqueness outside a conjunction, and every
other condition shape are skipped with deterministic source-addressed warnings and kept as
left-out Validations where their model is emitted. Focused tests
prove nil behavior, range
boundaries, conditional behavior, format safety and error keys, uniqueness error keys, and exact physical tuple
indexes.
Compiler qualification requires the declaration, Entity, owner, structured condition operands, condition Fields,
Reference-comparison columns, and uniqueness tuple storage to belong to one admitted generation. Uniqueness
additionally requires every Field's normalization and comparison
behavior to be realized. Missing meaning produces a source-addressed target-support gap instead of dangling code.
One complete-file model renderer composes the remaining declarations with forward and inverse Associations, then
emits ordinary validates calls. One trailing private boundary contains necessary condition predicates.
Straightforward unconditional integer,
decimal, and money bounds join the Field's native numericality declaration; tighter authored bounds replace redundant
storage bounds. Conditional literal rules remain separate. Date comparisons retain one Rails comparison call per authored
clause. Comparisons emit no
database CHECK constraints. Unconditional emitted date clauses additionally supply the form's tightest inclusive
min and max: a strict lower bound advances one calendar day and a strict upper bound retreats one day. Bounds
outside the control's four-digit year range, conditional rules, and unsupported rules do not constrain the calendar.
The generated-app smoke proves both admitted integer and date range boundaries and Rails
comparison error kinds, unconditional comparison, active and inactive conditional comparison and length behavior,
conditional Field and Reference behavior, and positive format behavior after normalization. A conditional length
bound does not become an unconditional form maxlength.
For a short_text Field whose realized column type is Rails string and PostgreSQL varchar, the shared schema
snapshot also projects the tightest upper bound from its admitted unconditional length declarations. exact_length
contributes its exact value and maximum contributes its maximum. The migration and emitted schema receive the same
limit only when that value is in PostgreSQL's supported range from 1 through 10,485,760 characters. This projection
deliberately excludes zero, conditional rules, minimum-only rules, long_text, and case-insensitive
short_text stored as citext, whose PostgreSQL type rejects a length modifier. A larger admitted model bound also
stays model-only rather than producing an invalid migration. The full exact-length rule remains in Rails; PostgreSQL
rejects non-space excess above its upper half, while a shorter validation-bypassing write remains possible by design.
PostgreSQL also truncates excess trailing spaces to the declared varchar length, so the Rails validator remains the
complete correctable-error boundary for the authored input.
Runtime values and relationship paths in a comparison become Compiler-generated callables. For example,
{ "kind": "environment", "name": "current_time" } lowers to a callable returning Time.current.
Rails already resolves a same-record attribute symbol, so
{ "target": { "field": "follow.last_requested_at" } } can lower to
greater_than: :last_requested_at without a generated method. The author never writes Ruby expressions in the
Plan.
Model validation alone is not sufficient to claim atomic uniqueness because concurrent requests can pass the query before either write commits. The current complete lowering adds a matching unique index.
An emitted all-Field named Ordering with at least two terms may also use an exact subject-set match to one of these unconditional uniqueness declarations as its stability proof. The semantic set match does not reorder either declaration: when the Ordering and Validation author their columns in different orders, the schema retains the ordinary query index and the unique enforcement index separately. Single-term Orderings remain outside this target slice.
The model declaration remains the primary correctable-error path. Field and Reference error targets use Rails'
built-in uniqueness validator. A Reference target supplies its logical association name; Rails binds the association
to its foreign key, scopes the remaining tuple members, and excludes the persisted record itself. It adds :taken
to the logical Reference key. Ordinary generated forms can therefore return 422 against movie, person,
user, followed, or post rather than an implementation _id column. The database index derives from the same
rule and remains the final atomic backstop for concurrent or validation-bypassing writes; it does not become a
second home for business behavior.
When the error target lowers to one validator-compatible Active Record column and the rest of the tuple can be
expressed as a Rails scope, the Compiler uses the built-in uniqueness validator. Rails 8.1.4 also
resolves ordinary belongs-to association names to their foreign keys.
This preserves the logical error target without a private query method or error-remapping callback. The earlier
private query existed to put feedback on that logical input; the native validator already provides that behavior.
For example, Case Chat Participant uses validates :user, uniqueness: { scope: :conversation_id } alongside the
matching unique index. The Reference-uniqueness issue (repository-only)
retains the preceding
Participant, Follow, and Credit model comparisons. This does not add optional or conditional uniqueness.
The first profile lowers every case_insensitive Field to PostgreSQL citext, so
ordinary equality on both the anchor and scope columns retains each Field's comparison behavior. The generated
validator should not pass case_sensitive: false: the column already supplies the semantics. Omitting the option
also lets Rails skip a redundant persisted-record validation when a matching unique index covers an unchanged
tuple.
The database receives a unique index over the complete physical tuple. nulls: "distinct" uses ordinary PostgreSQL
unique-index semantics; nulls: "not_distinct" emits NULLS NOT DISTINCT. Within the bounded executable slice,
citext members compare case-insensitively, while exact short_text, date, and foreign-key members compare
normally. This gives mixed-comparison tuples one race-safe enforcement layer without expression-index SQL.
When that tuple starts with a column that would otherwise receive a nonunique one-column Reference index, the
Compiler keeps only the composite index and transfers the Reference source to its provenance. A unique one-to-one
Reference index remains separate, as does a Reference index whose column is not the composite's leading member.
For example, if a Movie title were case-insensitive while its release date remained exact, the index could remain ordinary:
add_index :movies, [:title, :released_on], unique: true
That lowering is not universal for other reasons. A closed multi-target Reference may expand into several physical columns, and a polymorphic Reference includes both an ID and a type. The Compiler must generate a closed uniqueness check over the complete physical tuple whenever Rails' built-in validator cannot represent the semantic tuple safely. Such a lowering needs a pinned-framework comparison before adding custom code, and routes the error through the Rails profile's derived binding for the selected Field or Reference.
A Reference error target maps its logical local slot to the corresponding Rails error key. bookmark.movie
therefore maps to movie. The live Project graph supplies the mechanically maintained same-key, unqualified
forward Association used by ordinary forms and readers. The Compiler can lower Rails polymorphism directly; an
exclusive arc needs a generated reader over its physical foreign keys. Localization and validation errors remain
keyed to the semantic Reference rather than its storage strategy.
Not every application-level rule can or should become a database constraint. The target profile should disclose the enforcement boundary rather than pretend the layers are equivalent.
Ordinary numeric validation
The Compiler uses Rails numericality for decimal Fields. A required decimal uses
validates :rank_score, numericality: true; an optional one adds allow_nil: true.
Numericality already rejects nil by default, so required numeric Fields do not need a separate presence validator.
Their missing-value error intentionally becomes :not_a_number instead of :blank. Preserve integer-only
enforcement, the effective storage and business bounds, authored rules, requiredness metadata, and database
NOT NULL.
A money Field is stored as decimal precision: 12, scale: 2, which holds magnitudes below 10^10. Its numericality
carries that storage bound, as integer numericality carries the four-byte range:
validates :price, numericality: {greater_than: -10**10, less_than: 10**10}
Without the bound, an 11-digit amount passed validation and then raised ActiveRecord::RangeError on save, so the
form showed an error page. Active Record's numericality validator rounds Float and BigDecimal
input to the column's scale before comparing. It parses a form string as a Float and rounds that. Input such as
9999999999.995, which the column also rounds to 10^10, is therefore rejected, while 9999999999.99 saves. The
match with the column has two known edges. Float parsing rejects some long strings that the column would round down,
such as 9999999999.99499999. A Rational is compared unrounded, so Rational(19_999_999_999_999, 2000) passes
validation and then overflows. Generated forms, seeds, and factories produce neither input.
For straightforward unconditional integer bounds, combine the checks into one numericality declaration:
validates :score, numericality: {
only_integer: true,
greater_than_or_equal_to: 1,
less_than_or_equal_to: 5
}
Earlier separate comparison declarations inspected numeric errors to suppress misleading range messages. Rails numericality already stops before range checks when input is not numeric or integral, so the consolidated bounds need no extra guard. A tighter business range can subsume the storage range without admitting more values; extreme integers then receive the business-bound error. The change preserves authored conditions, error targets, and admission.
A 29-input comparison of the emitted Oscar Party Rating and two same-table model copies retained every acceptance
decision. All eight valid inputs saved and reloaded correctly. Consolidation alone changed only the two tested
storage-overflow messages to the 1–5 business bounds. A second copy also applied the approved requiredness cleanup
and changed nil/empty/whitespace errors to :not_a_number. This is model/persistence evidence using the emitted
migrations, not fresh Compilation or request qualification.
The removed decimal block parsed raw input with BigDecimal, rejected nonfinite values, and checked PostgreSQL's
numeric storage limits. For an unbounded decimal Field, its replacement accepts ordinary Rails behavior: storage
overflow may raise a database exception, and programmatically supplied Float/BigDecimal NaN or infinity may validate
and persist. The usual form strings "NaN" and "Infinity" still failed validation in the probe. That ruling
weighed PostgreSQL's extreme limits for unbounded numeric, which ordinary input does not reach. Money's 12-digit
precision puts its limit within reach of a typo, so the owner reopened the ruling for money in the
round-2 review decisions (repository-only) and chose the native numericality
bound above. Do not replace the removed block with another generated finite-value or digit-count guard.
The earlier source-only audit retained this block partly because Rails converts values while validating them.
That conversion does not replace the model's stored decimal value. A fresh 22-input emitted-model comparison
rejected "12oops" with both validators and saved/reloaded a 29-significant-digit decimal exactly. It isolated the
accepted overflow and nonfinite differences. Those model copies still retained presence validation; a separate
pinned validator probe confirmed the requiredness simplification for nil, empty, whitespace, zero, and an ordinary
decimal. The native-numericality issue (repository-only) retains those original
comparisons; they
are not new qualification of the combined application.
This changes emitted runtime policy, not the Plan's JSON literal admission or development-data checks needed for seedable output. Preserve ordinary gaps for incomplete authored behavior. Potential application-specific follow-up is collected in the owner-guidance notes (repository-only).
Ordinary Reference comparison
The Compiler uses Rails' comparison validator when a participating Reference owns the error. For example, a transfer whose destination must differ from its source can use:
validates :destination_account,
comparison: {other_than: :source_account},
allow_nil: true
Ordinary association readers and Rails record equality replace the former helper's association-cache and foreign-key bookkeeping. Accept normal association loading; the application's agent can optimize an observed performance problem. A local 20-case model comparison preserved acceptance and unsaved-record persistence behavior, with two additional SELECTs for unloaded persisted References. It does not qualify a newly compiled application or its request/form behavior.
Use ordinary Rails error behavior without requiring new generated message overrides. Comparing records can put an inspected Ruby object in the default message; an application's agent can customize that copy through Rails I18n when needed. Keep authored labels and the existing locale infrastructure. Candidate continuation guidance is collected in the owner-guidance notes (repository-only).
This is a generic Reference comparison, not a self-follow-specific feature. The explicit example migrations and removal of record-level Validation targets are described under Entity Validation. The record-wide-target issue (repository-only) retains the preceding model comparisons and decision.
Reusing enum predicates
A presence, absence, length, or uniqueness rule conditioned on one required direct non-ordinal enum value reuses that enum's Rails predicate:
validates :source_follow, presence: true, if: :following?
validates :source_follow, absence: true, unless: :following?
Use the predicate's actual emitted name, including an allocated prefix or suffix. The earlier Compiler emitted two private comparison methods through its general structured-condition machinery while the enum API was disabled. Restoring native helpers makes those wrappers unnecessary; the Compiler still retains structured provenance.
This reuse covers a single supported enum equality and its direct negation. A Field rule reads the same way, such
as validates :title, presence: true, if: :group?. Comparisons and other admitted conditions retain their private
predicate.
The enum-predicate issue (repository-only)
records the preceding 21-case model comparison of acceptance, error keys/types, full messages, and valid persistence.
Those model copies remain historical evidence, separate from fresh Compiler and application qualification.