# MIGRATION_SCHEMA_TABLES.md

**The 16 missing migration destination tables, authored and created.** 2026-08-25.

**D7 unmodified. No business data migrated — all 16 tables verified empty.** No schema was written
for a Commerce 3 entity-storage table, the four revision migrations were not touched, and the two
retired tax migrations were left alone.

---

## 1. Why these tables had to be authored

`CeTable::import()` (`Drupal\ce_migrate\Plugin\migrate\destination\CeTable`) writes with `merge()`
and **never creates a table**. Every `ce_table` destination must therefore exist before its
migration runs, or the migration fails on its first row. 16 destinations were defined nowhere.

## 2. Method — generated, not transcribed

Every schema was derived **column-for-column from the live D7 `information_schema`**:

1. `.logs/task49-schema-dump.sh` / `task49b` captured columns, types, nullability, defaults,
   primary keys and indexes for all 16 D7 source tables → `.logs/d7-schemas.txt`.
2. `.logs/task51-gen-schema.php` mapped MySQL types to Drupal Schema API specs and rendered the
   field arrays → `.logs/generated-schemas.txt`.
3. Those arrays were assembled into `hook_schema()` implementations.

Nothing was typed by hand, so no type, default or index could be mistranscribed. Two generator bugs
were caught and fixed during this: a varchar `'0'` default being cast to integer `0`, and
`information_schema`'s literal string `'NULL'` (which it returns for a nullable column with no
default) being emitted as a default value of `'NULL'`.

### Two deliberate deviations from D7 — both forced, not chosen

1. **A TEXT/BLOB column cannot carry a `DEFAULT` in MySQL.** Where D7 marks such a column
   `NOT NULL` with no default, it is declared **nullable** here. Affects `confirmation`, `value`,
   `extra`, `excluded_components`.
2. **Columns the migration does not currently write** but which D7 declares `NOT NULL` with no
   default are given an explicit default, so a partial insert cannot fail. **The column is still
   created** — the schema mirrors D7 in full, so resolving the outstanding unmapped-column question
   (**W-4**) needs no schema change later. Affects `question_nid` / `question_vid` on
   `quiz_multichoice_answers`.

## 3. One rename

`webform_emails` → **`ce_webform_emails`** and `webform_roles` → **`ce_webform_roles`**
(destination only; **the D7 source names are unchanged**).

Both were unprefixed D7 names sitting inside the `webform_*` namespace that `drupal/webform` 6.x
governs — it owns 12 tables there, including `webform` itself. It does not currently define these
two, so there was no live collision, but their four sibling archive destinations were already
`ce_webform_*`. All six are now consistent and out of a namespace another module controls.
Affects 0 and 2 rows respectively.

## 4. The 16 tables

### `ce_quiz` — 5 tables

| D10 destination | D7 source | rows | purpose | verified |
|---|---|---:|---|---|
| `quiz_multichoice_answers` | `quiz_multichoice_answers` | 391 | Answer options per question, with score for choosing / not choosing | ✅ 12/12 cols, pk `id` |
| `quiz_node_result_options` | `quiz_node_result_options` | 0 | Per-quiz result feedback bands. **Empty in D7** — created so the migration has a destination | ✅ 8/8, pk `option_id` |
| `quiz_question_properties` | `quiz_question_properties` | 128 | Per-question, per-revision max score and feedback | ✅ 6/6, pk `qqp_id` |
| `quiz_terms` | `quiz_terms` | 0 | Taxonomy-driven question selection. **Empty in D7** | ✅ 6/6, pk `vid,tid` |
| `quiz_node_results_answers` | `quiz_node_results_answers` | 5,129 | Per-answer detail behind each of the 3,973 attempts | ✅ 12/12, pk `result_answer_id` |

### `ce_wistia` — 2 tables *(new `.install`)*

| D10 destination | D7 source | rows | purpose | verified |
|---|---|---:|---|---|
| `wistia_media` | `wistia_media` | 32,737 | Per-user video completion. `completed` is a **0/1 flag**, not a timestamp | ✅ 4/4, pk `id_wistia_media` |
| `wistia_media_track` | `wistia_media_track` | 62,137 | Playback samples. `time` is a **position in seconds**, not a timestamp | ✅ 4/4, pk `id_wistia_media_track` |

`uid` is `varchar(100)` in both — preserved as VARCHAR, never cast (decision D3-6).

### `ce_referral` — 2 tables *(new `.install`)*

| D10 destination | D7 source | rows | purpose | verified |
|---|---|---:|---|---|
| `ce_referral_discount` | `ce_referral_discount` | 361 | The referral credit ledger. `referrer_disc_created` is the **FIFO key** that decides burn order | ✅ 15/15, pk `id` |
| `ce_referral_admin_discount` | `ce_referral_admin_discount` | 24 | Admin-granted credit, separate from earned credit | ✅ 6/6, pk `id` |

All 15 ledger columns are present — including the two expiry columns the migration does not
currently map (W-4), so that decision needs no schema change.

### `ce_reports` — 1 table *(new `.install`)*

| D10 destination | D7 source | rows | purpose | verified |
|---|---|---:|---|---|
| `report_history` | `report_history` | 779 | Saved report date ranges per user | ✅ 4/4, pk `h_id` |

### `ce_migrate` — 6 tables (the webform archive)

| D10 destination | D7 source | rows | purpose | verified |
|---|---|---:|---|---|
| `ce_webform_settings` | `webform` | 1 | Archived settings for node 27. `submit_notice = 0` against a D7 default of 1 | ✅ 15/15, pk `nid` |
| `ce_webform_component_archive` | `webform_component` | 28 | ⭐ **Archival record of the numeric `cid` → `form_key` pairing** — the durable evidence behind `data[30]` | ✅ 10/10, pk `nid,cid` |
| `ce_webform_submissions` | `webform_submissions` | 21,064 | Submission parents. D7 `submitted` feeds **both** `created` and `completed` | ✅ 7/7, pk `sid` |
| `ce_webform_submitted_data` | `webform_submitted_data` | 441,970 | Submitted values. `cid`→`element_name`, `no`→`delta`, `data`→`value` | ✅ 5/5, pk `sid,element_name,delta` |
| `ce_webform_emails` | `webform_emails` | 0 | Email handlers. **Empty in D7** | ✅ 10/10, pk `nid,eid` |
| `ce_webform_roles` | `webform_roles` | 2 | Roles permitted to submit (node 27, rids 2 and 3) | ✅ 2/2, pk `nid,rid` |

## 5. Verification results

| check | result |
|---|---|
| `php -l` on all 5 `.install` files | ✅ no syntax errors |
| Schema API validation — tables actually created | ✅ **16 created, 0 errors** |
| All 16 exist in D10 | ✅ 16/16 |
| All 16 **empty** (no business data imported) | ✅ 0 rows in every table |
| Structure vs D7 (columns, types, nullability, defaults, PK) | ✅ every table matches on column count and primary key |
| Unexplained structural differences | ✅ **0** — all 8 diffs are the two documented deviations plus the intended `cid`→`element_name` type change |
| Collision with Commerce 3 entity storage | ✅ none of the 16 is a Commerce storage table |
| `drush cr` | ✅ clean |
| `drush migrate:status` | ✅ all 19 affected migrations report real source counts |
| `drush config:status` | ✅ clean after export (`core.extension` — `ce_quiz` enabled) |
| D7 unchanged | ✅ `course_actions.module` md5 `f9913eda…`; users 13,250 · node 620 · field_data_body 543 · commerce_order 87,640; 338 tables |

**Verification script:** `ddev drush php:script /var/www/html/.logs/task54-structure-verify.php`

### Incidentally fixed

Creating these also installed **five tables that had never been installed** despite their schemas
existing: `ce_certificate`'s `certificate_snapshots`, `certificate_node`,
`certificate_node_settings`, and `ce_migrate`'s own `ce_migrate_orphan_log` and
`ce_migrate_reconcile_run`. All were missing for the same reason — the modules were enabled while
still bare skeletons, and `hook_schema()` only runs at install time. **The orphan logger and the
reconciliation runner had no tables to write to.**

`ce_quiz` was enabled (it is on the runbook's Stage 1 list), creating its 3 pre-existing table
definitions alongside the 5 new ones.

## 6. Destination coverage now

| | count |
|---|---|
| `ce_table` destinations declared | 36 |
| **exist in D10** | **22** |
| still missing | 14 |

The 14 break down as:

- **4 belong to contrib already in the codebase** — `acl_node`, `acl_user` (`drupal/acl`),
  `content_access` (`drupal/content_access`), `flag_counts` (`drupal/flag`). Created when those
  modules are enabled. **Not blocked.**
- **10 are Commerce** — pending **W-3**: the authoritative register maps these to Commerce 3
  *entities*, not raw tables, so no schema should be written for them. Includes
  `commerce_coupon_usage_transaction`, which W-2b repoints.

---

*Read-only against D7. No migration executed; every table created empty.*
