Rails Domain storage
Status: Implemented for the bounded Domain slice the Compiler admits. Broader storage is an evidence-backed direction.
Each admitted Entity becomes one PostgreSQL table with a UUIDv7 primary key, created by an ordinary Rails migration.
One immutable schema snapshot feeds the create-table migrations, one post-table foreign-key migration, and the
complete db/schema.rb, so a migrated database and a schema-loaded database agree. Indexes come from References,
uniqueness rules, and admitted Orderings. Migration versions are ranks within one artifact, qualified only against a
fresh database. Proven development data lower to idempotent seeds; reference-data seeding remains a direction. The
Data model owner states what Entities and data records mean. Field lowering
and References and Associations cover columns and relationships.
On this page
- Record identity
- Schema snapshot
- Indexes
- Migrations and columns
- Admitted Domain slice
- Reference and development data
- Code and checks
Record identity
UUIDv7 is the Rails target-profile default for application-owned tables. The Domain renderer emits id: :uuid with
PostgreSQL 18 uuidv7() explicitly in its create-table migration and verifies the generated identity at runtime. This
is not a portable Field or a per-Entity Plan choice. The pinned Core configures ordinary Active Record generators with
primary_key_type: :uuid for new primary and foreign keys. Its shared create-table template adds the uuidv7()
default only to UUID primary keys; model, scaffold, and standalone create-table generators use it. Add-reference
migrations retain native UUID foreign keys without a generated-value default. An explicit --primary-key-type=bigint
retains native bigint keys and references. Mixed-key applications choose reference types to match their target tables.
Already-migrated tables keep their types and defaults. Framework-owned migrations use their native conventions: Active
Storage, Action Text, and Action Mailbox read the generator setting when their migrations run and use Rails'
gen_random_uuid() for UUID primary keys. Core's existing Solid tables remain bigint. These framework migrations do
not use the shared application create-table template. Rails 8.1.3.1 cannot configure the primary-key default itself.
Check the copied template on Rails upgrades and retire it after a suitable native primary_key_default release is
qualified; the upstream proposals are tracked in #688 (repository-only). The
continuation qualification (repository-only) links the local
combined-pin proof and preserves the earlier isolated candidate's history. Independent multi-Entity migrations have
generated-app proof; their versions are artifact-local ranks qualified only on a fresh database. Stable
cross-compilation migration identity and the complete generated Domain remain open. The ordinary Reference slice
avoids table-dependency ordering by adding all admitted foreign keys in one deterministic post-table migration; stable
evolution of that migration across compilations remains open.
Schema snapshot
The profile pins Core's complete schema by version and SHA-256 in the TARGET_CORE_SCHEMA_VERSION and
TARGET_CORE_SCHEMA_SHA256 constants of the
profile (repository-only).
A pure schema snapshot verifies those bytes and supplies one shared Domain projection to the create-table, post-table
foreign-key, and complete-schema renderers. Migrations preserve authored Field and Reference order, while the schema
orders columns, complete rendered index lines, and foreign keys to match Rails 8.1's SchemaDumper; those physical
orders differ without changing column semantics. The projection includes Reference UUID columns, an index that is
unique for one-to-one References, and ordinary database foreign keys without deletion actions. Model rendering
separately assigns an authored deletion outcome to one eligible Active Record inverse Association. It omits a
nonunique Reference index only when a compatible generated composite starts with the same column. The shortest
compatible composite wins, rendered index name breaks a tie deterministically, and the winner retains every omitted
Reference source in its provenance. Unique one-to-one indexes and nonleading Reference indexes remain separate. The
generated-app check independently withholds the emitted schema so Rails 8.1.3.1 and PostgreSQL 18 must apply the
migrations and dump them byte-identically, then schema-loads a fresh database and repeats the runtime checks.
Every model, create-table migration, post-table foreign-key migration, and the complete db/schema.rb are complete
files. One immutable SchemaSnapshot supplies the migration and schema renderers with the same primary keys,
physical columns, nullability, indexes, foreign keys, Account-owned auxiliary deletion actions, timestamps, versions,
and provenance. An
admitted uniqueness declaration contributes one unique index over its authored physical tuple. The index emits
nulls_not_distinct: true only for nulls: "not_distinct"; ordinary distinct keeps PostgreSQL's default.
Identical physical indexes merge every source subject. Because this release requires every tuple column to be
NOT NULL, otherwise-identical distinct and not_distinct rules are equivalent; their shared index keeps
NULLS NOT DISTINCT when either rule requests it.
The target profile pins Core's exact schema version and file digest. SchemaSnapshot rejects a different Core
schema, and the pure schema renderer updates that verified source with the Domain version, tables, and foreign keys
without loading Rails, PostgreSQL, SchemaDumper, or pg_dump. The qualification smoke first applies every
migration and asks the generated app's pinned Rails 8.1.3.1 and PostgreSQL 18 to dump the result. It withholds the
emitted schema during that step because Rails 8.1 otherwise initializes an unmigrated database from db/schema.rb
before db:migrate. The new
dump's bytes must equal the emitted schema. The smoke then creates a fresh database from the emitted schema, runs
db:prepare, and repeats the table, migration-version, column, index, foreign-key, UUIDv7, scalar round-trip,
Reference-behavior, and Validation-behavior checks.
Indexes
An index snapshot has exactly one nonempty column list or one nonempty expression. Column indexes may carry frozen
Rails order options whose keys belong to that list; expression indexes carry no order map. Expression and order are
part of collision identity, and an expression index never suppresses or absorbs a leading-column Reference index.
Both create-table migrations and db/schema.rb render these as ordinary Rails t.index declarations.
After that exact merge, the snapshot omits a nonunique one-column Reference index only when a compatible generated composite index starts with the same column. The strictly wider index preserves the Reference lookup prefix. If several composites qualify, the shortest wins, with the rendered index name as the deterministic tie-break; the winner absorbs every source subject from the omitted index. A unique one-to-one Reference index is never omitted, and neither is a Reference index whose column appears only later in another index.
Migrations and columns
Migration ranks follow authored Entity position with subject UUID as the deterministic tie-break. The one
deterministic post-table migration, <timestamp>_add_foreign_keys.rb with class AddForeignKeys, adds every
Reference foreign key after all Entity tables, so self-References and cycles do not require dependency ordering.
Schema tables, complete rendered index lines, and foreign keys are sorted to reproduce Rails 8.1's SchemaDumper.
Each create-table migration preserves authored Field and Reference order, while the schema renderer sorts columns by
name. Migration-built and schema-loaded databases therefore have different physical column positions but the same
column semantics. Scalar lowering maps boolean, date, datetime, decimal, integer, language_code,
long_text, short_text, time_zone, and url, emits required Fields as null: false, uses PostgreSQL 18
uuidv7() record identity, and adds timestamps exactly once. An emitted short_text Field with case_insensitive
comparison instead uses PostgreSQL citext. One conditional pre-Domain migration enables the extension, and the
shared schema snapshot renders the same extension declaration and t.citext column. Fields without that comparison
remain ordinary string columns. Reference lowering emits belongs_to, adding class_name only when the profile's
pinned inflections do not reproduce the target and optional: true only for optional References. Required References
use the target's Rails 8.1 required-by-default setting. The Compiler emits foreign_key when Rails' reader-based
inference would not reproduce the Reference column. It also emits a UUID column with matching nullability, a useful
leading index, and a database foreign key. An immutable Reference emits attr_readonly for its foreign-key attribute,
so assigning that attribute after persistence raises ActiveRecord::ReadonlyAttributeError under the pinned Rails
profile. This is model-level write-once behavior rather than a database trigger. When one_to_one is true, the index
is unique and the model validates uniqueness on the logical Association with allow_nil: true so requiredness
separately owns missing-target errors. Entity, Field, and Reference subject UUIDs provide task identity and
generated-file provenance; readable names determine paths only through the pinned profile.
Domain migration versions are artifact-local ranks, not stable per-Entity identities across Plan edits. Adding or reordering an Entity can renumber every later migration. The current artifact is qualified only against a fresh database; incrementally upgrading an already migrated generated application would require stable persisted migration identity and a separate evolution design.
Admitted Domain slice
This is a bounded release slice under the ignore-and-list default, not generic model generation. Before task discovery
it requires the complete sealed Project capture. Admitted Domain rows are limited to Entities, plain scalar Fields,
required enum Fields with their ordered FieldValues, the required one-per-Entity Primary Descriptor envelope, ordinary
single-target References, their ordered targets, their exact mechanically derived forward Associations, direct
unqualified referenced-side Associations over mutable References, and direct has_many inverses over required
immutable References. A no-cardinality direct inverse in that last shape may use an exact same-generation admitted
result-Entity PredicateScope condition. Mutable inverses lower to has_one when one_to_one is true and has_many
otherwise. Optional immutable has_many and every immutable has_one are listed and omitted. One first-level
unqualified indirect Association may compose an admitted direct has_many with one mechanically derived forward
belongs_to backed by a mutable or required immutable Reference. The through step may be predicated or unpredicated.
An unpredicated direct through step may instead use an admitted predicated direct has_many source backed by a
required immutable Reference. Its resolved family must be has_many. One further exact collection may traverse that
admitted read-only source shape to a required immutable non-one-to-one derived forward belongs_to. Optional
immutable sources, a predicated direct through step with a collection source, broader nested indirect paths,
referencing-side aliases, cardinality-qualified Associations, and multi-target, defaulted, or realized Reference
shapes remain gaps. The executable first admission
slice lists the admitted Validation declarations. The
public mutation slice admits Association controls for required and optional ordinary References with one_to_one: false and requires every admitted conditional-presence Validation owner in both its create and update input lists.
Scaffold qualification retains that one-to-one rejection even when a broader Domain release admits one-to-one storage.
Rails enforces comparisons; unconditional date bounds also supply inclusive form min and max hints. Everything
outside these admitted shapes follows the ignore-and-list default: an unlowerable Field kind, Reference, Association,
unsupported Predicate, Scaffold surface, Account, Policy, or Application-configuration slice is omitted from the
generated output and listed in the reviewed GapSet, while comparison behavior, encryption, and Field derivation are
ignored and listed per modifier.
Reference and development data
Entity-owned reference_data remains a direction for idempotent db/seeds.rb behavior in every environment. For a
proven nonempty development_data residual, the current Compiler replaces Core's db/seeds/development.rb; Core's
ordinary seed entrypoint loads it only in development. The Compiler first admits only complete records and assignments
whose values, generated required validations, numeric bounds, lookup and uniqueness tuples with target
null-distinctness, Reference dependencies, implicit one-to-one targets, and Account payloads are provably seedable by
this profile. Datetime tuples compare stored instants at PostgreSQL's microsecond precision. Realized citext tuples
compare component by component, proving a result whenever locale-stable ASCII comparison is decisive and leaving
locale-sensitive ASCII or non-ASCII outcomes unproved. Decimal candidates first satisfy the shared canonical semantic
literal predicate, whose admitted spellings are injective across numeric values. Development seeds pass whole decimal
literals to BigDecimal as Ruby Integers and fractional literals as strings, preserving exact values while satisfying
the pinned generated StandardRB rules. This rendering does not change decimal admission or use Float conversion. The
Compiler deterministically retains the first possible duplicate, reruns dependency closure, and lists every resulting
omission rather than letting a valid Plan fail during Compilation or seeding. It then derives creation order from
retained Reference assignments, uses the same admitted lookup tuples with ordinary find_or_create_by!. Readable Ruby
locals support References, Account setup, and State Machine events. Existing ordinary matches return without
assignments or save callbacks. Account seeds retain find_or_initialize_by, mutable setup, creation-only immutable
attributes, and missing-password repair. Generated State Machine events run only when the authored target state has
not been reached. After the complete event path, the seed restores only explicitly authored values overwritten by its
effects, inside that same guard. Unsupported values or dependencies are omitted at the smallest coherent boundary and
remain precise GapSet entries. Seed-file provenance includes each selected enum or State Machine FieldValue used by
emitted source, including a required initial State; an optional nil State assignment emits neither transition source
nor a selected-value claim. Semantic data-record IDs name generated locals or lookup results; they do not become
persisted application columns. Reference-data reconciliation across later Plan revisions remains open.
Code and checks
A committed qualification script imports one Plan containing Movie, Director, Post, Series, and a synthetic Credit
join Entity, all ten supported scalar Field kinds, seven ordinary References that include self-Reference and
cross-Entity cycles, five admitted inverse Associations, two admitted indirect Associations, representative Movie
Validations, exact public-index Scaffolds, a domain, and a selected iOS client. It captures that generation once,
builds one qualified input and
matching rendering context, calls the normal ApplicationManifest coordinator, and materializes the complete Rails
and iOS manifest. Through the emitted application's own
dependency boundary, it installs and checks the locked Ruby bundle with Bundler state outside the artifact root,
installs the locked JavaScript packages with an npm cache outside the artifact root, and builds the generated JS and
CSS.
It creates a uniquely named disposable PostgreSQL test database and verifies that database begins with no tables. It
saves and removes the temporary artifact's emitted schema before db:migrate, forcing Rails to run the complete
migration history, then requires Rails' new schema dump to equal the emitted bytes. After checking the exact
Core-plus-Domain tables, migration versions, authored physical column order, models, representative values, and
UUIDv7 identity, it verifies explicit belongs_to, has_many, and has_one reflection metadata, UUID Reference
columns, nullability, ordinary indexes, the one-to-one unique index, logical :taken feedback, and the database
uniqueness fence. It also verifies database foreign keys, forward and inverse traversal, and ordinary and eager-loaded
Director.series traversal deduplicated across two Movies that reach the same Series. The synthetic Credit path,
whose through and source References are both required immutable, proves a second distinct indirect traversal. Its
runtime checks cover ordinary join creation with both endpoints assigned before the first save and multi-owner eager
loading. For each removal or replacement branch, the proof loads a fresh strict owner with both the indirect target
and through-source chain preloaded. It then proves whole-row deletion or replacement without deleting final targets
and the delete_all(:nullify) branch's NOT NULL failure. Persisted Credit endpoints remain readonly. Unrestricted
mutation is outside this proof. Indirect declarations have no dependent option. Exact direct inverse lifecycle
carriers use conventional Rails dependent: behavior; every Domain foreign key remains an ordinary non-deferrable
integrity backstop.
The script also verifies required-model feedback, unconditional length, active and inactive conditional length,
conditional presence and absence on Field and logical Reference error keys, ordinary foreign keys, and live
Rails-dependent restriction, nullification, destruction, and child destroy callbacks. It also checks ordinary and
uncountable route helpers, true-empty and populated index responses,
Series page 2, and an actionable page 999 response. It then drops and recreates the database, loads the emitted
schema, runs db:prepare, and repeats the same semantic and request checks with SchemaDumper's physical column
order. bin/ci runs this proof. It byte-checks but does not build or run the generated iOS project, and it does not
exercise broader Association, Validation, or Scaffold behavior, another Capability, a production image,
or the future production tool runner.
The current materialization smoke imports its complete candidate Plan once and calls ApplicationManifest from one
captured generation.