# Schema changes

Source: https://docs.qiick.io/externa/schema-changes

Renaming fields, changing field types, or toggling the translatable flag on collections with existing content requires careful handling to avoid data loss. This page documents renames from externa-core **1.2.1+**, type / translatable locks from **1.3.0+**, the repair command for older drafts, and safe field conversion with dry run from **1.4.0** ([core#220](https://github.com/qiick-io/externa-core/issues/220)).

> Field renames are safe from externa-core **1.2.1** ([#218](https://github.com/qiick-io/externa-core/issues/218), [#223](https://github.com/qiick-io/externa-core/pull/223)). If you renamed fields on an older version, run the repair command in [Repairing older drafts](#repairing-older-drafts) right after upgrading, **before** the scheduler runs or anyone publishes.

---

## Renaming fields

### Current behaviour

Field values are stored internally by **field ID**, not by name (`app/Services/Collections/CollectionItemValuesWriter.php` in externa-core). This means:

- **Rename without pending drafts:** Content is preserved. The published API immediately reflects the new field name.
- **Rename with pending drafts (versioned collections):** From **1.2.1**, pending drafts and older revisions keep the renamed field's value, so publishing (manual or scheduled) and restoring revisions preserve content. On **1.2.0 and earlier**, the value was dropped because drafts and revisions stored a snapshot keyed by the old field name.

Fixed in [v1.2.1](https://github.com/qiick-io/externa-core/releases/tag/v1.2.1) ([#218](https://github.com/qiick-io/externa-core/issues/218), [#223](https://github.com/qiick-io/externa-core/pull/223)).

### Repairing older drafts

Drafts and revisions created before 1.2.1 may still hold a pre-rename field key. Right after deploying 1.2.1, **before** the scheduler runs or anyone publishes:

1. Dry run (default, no writes): `php artisan externa:repair-renamed-field-drafts`
2. Apply: `php artisan externa:repair-renamed-field-drafts --apply`

Mappings come from the CollectionField activity-log rename history. For orphaned keys with no history, pass `--map=collection:old=new` (collection slug or id; repeatable). The command never overwrites a value already stored under the new key, and publish-time behaviour is unchanged. Keys reported as **unmappable** belong to deleted fields and can be ignored.

If values were already lost by publishing on an older version, restore from a database backup taken before the rename, or re-enter them via the admin UI or [Collections API](https://docs.qiick.io/externa/collections-api).

### API breaking change

Renaming a field is a **breaking change** for API consumers. The Public CMS API, Collections API, and GraphQL all use the current field name as the key. Update your frontend or client code after renaming.

---

## Changing field types

### Current behaviour (core 1.3.0+)

Externa rejects `type` changes on a field that already has stored content (published values, `draft_data`, or values on soft-deleted items) — [core#219](https://github.com/qiick-io/externa-core/issues/219) / [PR #265](https://github.com/qiick-io/externa-core/pull/265). The admin form returns a **422** validation error and locks the type control (the field list exposes `has_stored_values`); the AI assistant’s `ManageCollections.updateField` returns an error. Re-submitting the same type, or changing an empty field, still works. Stored values are never converted by the field form. From **1.4.0**, use [field conversion](#field-conversion-core-1-4-0) for the supported pairs, or the manual path below for anything else.

### Safe path

1. **Create a new field** with the desired type.
2. **Migrate content** from the old field to the new field:
   - Export the collection, transform the values, and re-import; or
   - Use the [Collections API](https://docs.qiick.io/externa/collections-api) or [Admin API](https://docs.qiick.io/externa/admin-api) to update each item programmatically.
3. **Delete the old field** after verifying the migration.

---

## Toggling the translatable flag

### Current behaviour (core 1.3.0+)

Externa rejects `translatable` changes on a field that already has stored content (published values, `draft_data`, or values on soft-deleted items) — same lock as type changes ([PR #265](https://github.com/qiick-io/externa-core/pull/265)). The admin form returns a **422** validation error and locks the **Translatable** checkbox (the field list exposes `has_stored_values`); the AI assistant’s `ManageCollections.updateField` returns an error. Re-submitting the same flag, or changing an empty field, still works. Stored values are never converted by the field form. From **1.4.0**, [field conversion](#field-conversion-core-1-4-0) flips the flag with a locale map or a picked locale; otherwise use the manual path below.

### Safe path

Follow the same migration strategy as [type changes](#safe-path) above:

1. Create a new field with the desired `translatable` setting.
2. Migrate and transform values (flatten or expand locale maps as needed).
3. Delete the old field.

---

## Field conversion (core 1.4.0)

From externa-core **1.4.0** ([#220](https://github.com/qiick-io/externa-core/issues/220)), fields with stored content can change type or the translatable flag with a migration of every value, always previewed first.

### Supported conversions

| From                               | To                                 | Translatable                                                                                           |
| ---------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `string` / `textarea` / `markdown` | `string` / `textarea` / `markdown` | unchanged or flipped                                                                                   |
| `string`                           | `select`                           | unchanged or flipped; values map onto options (missing options can be created)                         |
| any supported type                 | same type                          | `false → true` (value moves to the default content locale) or `true → false` (pick the locale to keep) |

Print the matrix from core with `php artisan externa:field-convert --matrix`. Other pairs are rejected — use the [manual safe path](#safe-path).

### Admin

Fields page → field menu → **Change type…** opens the convert dialog. It lists allowed targets (`GET /collections/{collection}/fields/{field}/convert-targets`), runs a **Dry run** with ok / failed / drafts / skipped counts, then **Convert** (`POST …/fields/{field}/convert` with `type`, `translatable`, `locale`, `create_missing_options`, `option_mapping`, `dry_run`). Applying from the dialog queues `ConvertCollectionFieldJob` (a queue worker must run) unless `sync=true` is sent. Needs `can-edit-collections`.

### CLI: `externa:field-convert`

```bash
# Preview (no writes)
php artisan externa:field-convert 42 --to=textarea --dry-run

# Flip to non-translatable, keep the Italian value
php artisan externa:field-convert 42 --translatable=0 --locale=it --dry-run
php artisan externa:field-convert 42 --translatable=0 --locale=it
```

| Option                       | Meaning                                                        |
| ---------------------------- | -------------------------------------------------------------- |
| `field`                      | Field id (required)                                            |
| `--to=`                      | Target type; omit to keep the current type                     |
| `--translatable=`            | `0` / `1`; omit to keep the current flag                       |
| `--locale=`                  | Locale to keep when collapsing translatable → non-translatable |
| `--create-missing-options=1` | Create `select` options from values (`string → select`)        |
| `--chunk=100`                | Items per write batch                                          |
| `--dry-run`                  | Preview only                                                   |
| `--matrix`                   | Print the supported matrix and exit                            |

The summary prints `ok`, `unchanged`, `failed`, `skipped`, and `with_draft` counts. Without `--dry-run`, the command applies the conversion; there is no confirmation prompt, so run the dry run first.

### Guarantees

- **All or nothing:** if any published or draft value cannot be converted, nothing is written (`Conversion aborted: N value(s) cannot be converted. Nothing was written.`).
- Published values **and** pending drafts are converted, in chunked transactions.
- Each changed item gets a revision with `source=convert`, and the change is written to the activity log.
- **Historical revisions stay as they were.** Restoring an old revision adapts its value to the current type, or rejects the restore with a validation error when the value no longer fits.
- The AI assistant can call `ManageCollections` `convert_field`; it is a write action, so it needs the [AI write gate and an approval](https://docs.qiick.io/externa/ai-tool-approvals).

Renaming is still a breaking change for API consumers, and so is a type change: update clients that read the field.

---

## Related pages

- [Collections](https://docs.qiick.io/externa/collections) — Collection and field CRUD
- [Field types](https://docs.qiick.io/externa/field-types) — Field type reference and validation
- [Content versioning](https://docs.qiick.io/externa/content-versioning) — Drafts, approvals, and revisions
- [Collections data model](https://docs.qiick.io/externa/collections-data-model) — EAV storage and field IDs
- [Collections API](https://docs.qiick.io/externa/collections-api) — Programmatic item updates for migration
- [Upgrade](https://docs.qiick.io/externa/upgrade) — Version-specific upgrade notes
