# 0003. Models own their database schema

- **Status:** Accepted
- **Date:** 2025-06-28

## Context

In the stock Yii workflow a table's definition lives in a migration and the model knows nothing about it. The
two drift: a model gains a property, the migration that added the column is three releases back, and the only
way to know a table's current shape is to replay every migration in order.

## Decision

**The model declares its own schema.** `getColumns( Migration $migration )` returns the column definitions and
`getIndexes()` returns the index list; [`SchemaHelper`](../../helpers/SchemaHelper.php) instantiates the model
and creates the table from them.

`app\db\ActiveRecord` contributes the common columns — `id`, `orderNumber`, and conditionally
`createdAt`/`updatedAt`, `slug`, `translations` — driven by the subclass's `$hasTimestamps`,
`$createSlugFrom` and `$multilingualAttributes`.

Migrations never spell out a column:

```php
( new SchemaHelper( $this ) )
    ->create( LocationPropertyGroup::class )
    ->create( LocationProperty::class );
```

## Alternatives

- **Conventional migrations.** Rejected: the current shape of a table is not readable from any single place.
- **Doctrine-style annotations/attributes.** Same benefit, but would mean an ORM the framework does not use.
- **A schema dump kept in the repository.** Solves reading, not writing — the model still would not know.

## Consequences

- **The model is the single source of truth for its table.** One file answers "what columns does this have".
- **Adding a column is a two-step change**: edit `getColumns()`/`getIndexes()` *and* write a migration.
  `getColumns()` alone only affects fresh installs. This is the most common way to get it wrong.
- `getColumns()` composes with `+`, not `array_merge`, so a subclass **cannot** override a base column. A
  redeclared `id` is silently ignored.
- **`SchemaHelper::create()` reflects the model as it is today**, not as it was when the migration was
  written. Migrations are not a record of historical shapes, and re-running the init migration produces the
  current schema.
- FULLTEXT indexes cannot be expressed in `getIndexes()` — see
  [0008](0008-own-full-text-search-index.md).
