Rails Field lowering
Status: Implemented for ten scalar kinds and required enums. Other Field kinds and modifiers are evidence-backed directions.
Each emitted Field becomes one PostgreSQL column plus the model declarations that Rails needs to read, write,
validate, and display it. The Compiler emits ten ordinary scalar kinds and qualifying required enums; other Field
kinds are omitted and listed in the reviewed GapSet. Modifiers lower one by one: immutability becomes
attr_readonly, log redaction filters the attribute, normalization becomes Rails normalizes, and case-insensitive
comparison becomes citext. A modifier the target cannot realize is listed on its own. Encryption, attachments, JSON
values, and derived numbers remain directions. The Field catalog states what each
kind and modifier means, and the support inventory (repository-only) records
which kinds generate.
On this page
- Realized defaults and modifiers
- Required datetime defaults
- Required enum Fields
- Immutable Fields
- Sensitive Fields
- Text normalization
- Text comparison
- JSON values
- Attachments
- Derived numeric values
Realized defaults and modifiers
Defaults are realized only for exact required-datetime current_time on emitted Fields and in-domain literal keys
on admitted required enums. On emitted ordinary scalar and admitted required enum Fields, log redaction adds the
attribute to self.filter_attributes and registers only the model-qualified request parameter. The same key under
another model remains visible; arbitrary application, provider, and external logs are outside this boundary. Authored
normalizations are realized only on emitted scalar Fields. Immutability is realized on emitted scalar Fields and
admitted required enum Fields; the encompassing gap for an unsupported Field shape covers that Field and its authored
modifiers.
Required datetime defaults
Support: see the support inventory row (repository-only).
The current Compiler lowers the exact { "kind": "environment", "name": "current_time" } default on an emitted
required datetime Field to a model-visible attribute :published_at, default: -> { Time.current } declaration.
Omitting a type preserves the schema-derived PostgreSQL timestamp(6) precision. Active Record materializes the
callable on a new model before ordinary presence validation; an explicit nil remains invalid and an explicitly
supplied timestamp remains unchanged. The Domain migration and canonical schema retain NOT NULL with no database
default, so direct and bulk inserts must supply the value. This does not realize current_date, non-enum literal
defaults, or defaults on other Field kinds. The separate enum rule below owns its one literal-key exception. The
callable follows Active Record's new-record attribute lifecycle rather than database-commit time.
Required enum Fields
Support: see the support inventory row (repository-only).
The current Compiler admits a required enum with its canonical nonempty ordered domain. The reviewed Plans contribute
Oscar Party's ordinal bookmark.priority and non-ordinal credit.role; Case Chat's case.status,
participant.role, participant.notification_level, and notification.kind; and Photogram's
feed_occurrence.reason. Each emits one PostgreSQL string column with null: false and an explicit stable-key
Rails enum mapping with validate: true and collision-aware native helper names, plus ordinary presence validation
for the required Field and generated schema-quality check. The target admits that enum only when Rails' pluralized
class mapping method is safe in both the frozen profile and selected dependencies, unique among sibling enums, and
distinct from authored Predicate and Ordering scope names. A collision omits the Field and records the exact
foundation_plan.gap.field_kind.not_generated reason. It emits no native PostgreSQL enum or database CHECK. Direct
SQL and validation-bypassing writes such as update_all therefore remain outside membership proof. Any admitted
required enum accepts a valid in-domain literal-key default. Oscar's bookmark.priority emits default: "normal";
Case Chat's case.status and participant.notification_level emit default: "open" and default: "all_activity" in
the model declaration, migration, and schema. Active Record materializes the model default, while an explicit
compatible key wins. A nonliteral default or a literal outside the closed domain remains a precise
foundation_plan.gap.field_modifier.default record. An enum Field the Domain does not emit keeps its encompassing
Field gap. Optional enums remain ungenerated and yield exact foundation_plan.gap.field_kind.not_generated gaps.
Required enums rejected by the mapping-name rule above use that same encompassing Field gap. Outside the descending
ordinal pattern in Named Orderings, other enum Primary Descriptors, Orderings,
Field-owned enum Validations, and ordinal rank comparisons likewise remain ungenerated and are disclosed through the
reviewed GapSet. Admitted enum Field projections and form labels use the same enums.<model>.<field>.<key> Rails I18n
entries seeded from authored names. Form values remain stable keys in authored order; display labels do not imply
ordinal rank semantics. Native helper naming follows the model helper allocation
rules. Admitted storage feeds the existing generic local ExpressionSql rule, but an ordinal comparison lowers only
when it does not depend on authored rank. An independently admitted predicated direct Association may consume a
non-ordinal scope under the relationship rule's existing gates; the reviewed Plans add no Association consumer.
An admitted required enum also emits an ordinary string column with null: false. Its model uses an explicit
stable-key Rails enum mapping with validate: true and collision-aware native helper names, plus ordinary presence
validation. The explicit presence validator lets the generated schema-quality check recognize the required Field;
active_record_doctor 2.0.1 misses Rails 8.1's string-keyed enum inclusion validator. Admission requires the plural
mapping name to be target-safe, unique among sibling enums, and distinct from authored Predicate and Ordering scope
names. It emits no native PostgreSQL enum or database CHECK; raw SQL and validation-bypassing writes remain outside
the membership proof. Every admitted required enum accepts an in-domain literal-key default. Oscar's
bookmark.priority emits default: "normal"; Case Chat's case.status and participant.notification_level emit
default: "open" and default: "all_activity" in the model declaration, migration, and schema. Active Record
materializes the model default on a new record, while an explicit compatible key wins. Nonliteral and out-of-domain
defaults remain exact modifier gaps. An enum Field the Domain does not emit keeps its encompassing Field gap. This
includes a required enum whose plural mapping name conflicts with the target class namespace, a sibling enum, or an
authored Predicate or Ordering scope. Outside the descending ordinal Ordering pattern, optional enums, Field-owned
enum Validations, descriptors, other Orderings, and ordinal rank comparisons remain unsupported or omitted. A required
non-ordinal enum may participate only as the Field member in Credit's exact admitted Entity-owned uniqueness tuple.
Admitted enum Field projections and form option labels share Rails I18n entries seeded from authored value names.
Forms submit stable keys in authored order. Editable display labels do not change stored keys or imply ordinal rank
semantics. The ordinary attribute may feed the existing generic local Predicate rule and, after independent admission,
the existing predicated direct Association rule. The reviewed Plans add two non-ordinal Predicate scopes and no
Association consumer. Optional enums yield exact foundation_plan.gap.field_kind.not_generated gaps. On an admitted
enum, the retained plural mapping method and collision-aware native value helpers are target realization details and
add no GapSet record.
The model provenance includes the Field and every contributing FieldValue; migration and schema provenance need only the Field because their bytes do not encode membership.
Immutable Fields
An emitted scalar or admitted enum Field with immutable: true is included in the model's attr_readonly
declaration. Creation may assign the value, use a model-visible current_time default, or use an admitted
required-enum literal default carried by both model and column. After persistence, ordinary changed, same-value, and
nil assignments raise ActiveRecord::ReadonlyAttributeError under the pinned Rails 8.1 defaults. This is model-level
write-once behavior, not a database trigger or a fence against direct SQL. Current generated-app proof covers
case.reference_number, case.opened_at, and post.published_at, plus immutable notification.kind and
feed_occurrence.reason; a modifier on an ungenerated Field kind remains a target-support gap.
An emitted scalar or admitted enum Field with immutable: true joins that model's attr_readonly declaration. Its
subject UUID joins the generated model's provenance. Creation still accepts an explicit value, the model-visible
current_time default, or an admitted required-enum literal default carried by model and column.
After persistence, ordinary changed, same-value, and nil assignments raise ActiveRecord::ReadonlyAttributeError.
Like immutable Reference enforcement, this is a model-level write-once boundary rather than a database trigger or
a fence against direct SQL. An immutable modifier on a Field shape the Domain does not emit remains in the reviewed
GapSet.
Sensitive Fields
Use Active Record Encryption as the first direction for encrypted_at_rest. On emitted ordinary scalar and admitted
required enum Fields, redact_from_logs appends the attribute to self.filter_attributes and registers the
model-qualified request parameter. Case Chat filters notification.deduplication_key; the same key under another
model remains visible. This boundary does not encrypt the value or cover arbitrary application, provider, or external
logs. The modifier remains a reviewed gap on every other or unemitted Field. A Predicate, uniqueness rule, or future
lookup that reads an encrypted Field still requires a supported query realization rather than silently enabling
deterministic encryption.
Text normalization
Lower an explicit Field pipeline with Rails normalizes, grouping Fields with identical pipelines
under one readable inline lambda. The Field catalog owns
character semantics and the cleanup-before-blank admission rule. Use the public
StripAttributes.strip(value) formatter for adjacent trim, blank_to_null; trim alone passes
allow_empty: true. The generated dependency contributor pins strip_attributes to 2.0.1 only when a retained
normalization pipeline includes trim. Do not install its model callback. Native Rails value.squish realizes
collapse_whitespace, and Rails value.presence realizes blank_to_null. Append presence only when authored. Ruby
String#downcase supplies the full Unicode lowercase mapping. Preserve operation order and nil, including after an
intermediate blank-to-null result. No type-wide default or application formatter wrapper is added. Loading
StripAttributes adds its strip_attributes method to every model. Reserve that name only when retained trim actually
selects the dependency. If colliding Fields are the only trim selectors, retain their storage and decline each
complete normalization pipeline with the existing modifier gap. Otherwise, independently retained trim makes the
collision real: omit the affected member and dependent consumers through ordinary gaps. Native enum prefixes and AASM
namespaces resolve generated convenience-method collisions where available. The collision
qualification (repository-only) records the library behavior,
selection-cycle rationale, and runtime boundary. An authored Entity whose model constant is StripAttributes makes
the formatter unavailable application-wide. Preserve that Entity and its storage, omit every complete trim-containing
pipeline with a normalization modifier gap, and select no formatter gem. Rails-only pipelines remain eligible. This
handles the actual optional-module collision without reserving the Entity name in applications that do not need the
formatter. Trim removes Unicode whitespace and six additional invisible edge characters while preserving NUL. Collapse
preserves those invisibles but removes edge NUL; both retain interior joiners. These policies intentionally differ.
Ordinary scalar hash equality receives normalization and handles an operand that normalizes to nil. Typed Arel casts
non-null normalized values, but its null operator and hash-array membership are selected before normalization.
Generated Arel, array, untyped, and raw-SQL consumers must call Model.normalize_value_for first and branch on nil.
Text comparison
The current Compiler lowers an emitted short_text Field with case_insensitive comparison to PostgreSQL
citext. One conditional migration installs the extension before every Domain table, and
db/schema.rb declares both the extension and t.citext. Entered case remains stored unless an authored
normalization changes it, while ordinary Active Record hashes, typed Arel equality, ordering, and B-tree indexes
compare case-insensitively. The generated-app qualification verifies that the extension's schema is on the current
search_path. Its pattern operators and several string functions are also case-insensitive; cast to text for a
deliberate exact operation. Pin or verify a supported database LC_CTYPE, or disclose it as an external prerequisite,
because that creation-time setting controls case folding. The bounded uniqueness slice inherits each admitted tuple
member's storage equality, including compound mixtures of citext, exact short_text, date, and foreign-key members.
Entity and Field comparison Validations with Field operands likewise require compatible comparison semantics rather
than relying on PostgreSQL's mixed citext/text resolution. Exclusion must inherit the Field's comparison behavior
instead of Ruby String membership accidentally becoming case-sensitive. Field-to-Field Expressions require the same
compatible comparison semantics before lowering. Nondeterministic ICU collations remain a future profile strategy if
representative multilingual identifiers require more complete Unicode case behavior.
JSON values
Support: see the support inventory row (repository-only).
Lower the semantic json Field to PostgreSQL jsonb, which PostgreSQL recommends for most applications and Active
Record maps to ordinary Ruby values. Do not add a json versus jsonb Plan choice. Keep stable application facts as
Fields, References, or Entities. If exact source bytes matter, store the raw input separately from the parsed JSON
value.
Attachments
Support: see the support inventory row (repository-only).
Active Storage is the first-profile lowering for explicit uploaded Fields and Action Text embeds. Cloudinary is the
intended production storage and image-delivery service; local and test may use an inert local service. Generated image
delivery uses Cloudinary transformations with f_auto/q_auto, not the unsupported Active Storage variant path.
These are profile consequences rather than per-Field choices. Shrine remains deferred until a demonstrated requirement
such as resumable uploads justifies a second profile. See the attachment
direction (repository-only).
Derived numeric values
The Plan can declare sum, average, minimum, and maximum over a named Association and source Field. A derived
Field is not directly writable and must reflect changes to both source values and Association membership. Every
derived Field needs a structured consumer. A detail-only average can remain a fresh Association AVG. An average used
by a Scaffold collection, Predicate, Ordering, or Predicate-filtered Association needs a maintained queryable value.
One isolated Compiler projection records a Counter Culture-maintained sum and count plus their
quotient as a stored PostgreSQL-generated average. It explicitly records
production_wiring_complete: false; neither part is selected profile output or generated-app behavior. Decision 0024
requires the target to compare a conventional Rails or common-gem result-column path and either keep that behavior
there or record a narrow database-owned exception before generation. Counter Culture is not yet a profile pin. The
semantic Field remains one Plan subject and adds no realization choice. A compatible index can follow from actual
Predicate and Ordering use. Nullable sources, scoped membership, bulk writes, repair, and minimum or maximum still
need proof before the Compiler claims support.