Problem/Motivation

Structure Sync currently only exports and imports the current version of taxonomy terms. If you've been editing a term through multiple revisions, all that history gets lost when you sync to other envs.

This is a problem if you need:
- Audit trails showing who changed what and when
- The full historical context of term changes (not just the latest version)
- Consistent term IDs (TIDs) across environments
- To avoid UUID constraint errors when terms already exist with different names

Steps to reproduce

1. Create a taxonomy term with multiple revisions:
- Initial: "Product Category A"
- Revision 2: "Product Category B"
- Revision 3: "Product Category C" (current)

2. Export taxonomies using Structure Sync

3. Check structure_sync.data.yml

   taxonomies:
     product:
       - vid: product
         tid: '123'
         name: 'Product Category C'
         # Only current version - no revision history

4. Import on other envs using any import mode

5. Result:
- Only "Product Category C" exists
- All revision history (A, B) is lost
- TID may change (becomes a new auto-increment ID)
- Term references may break

Proposed resolution

Add revision support to taxonomy export/import.

Export
- Query all revisions for each term (not just current)
- Export revision metadata: revision_id, timestamp, user, log message
- Export field values for each historical revision
- Pack it all into a revisions array in the YAML

Import

The behavior differs by import mode:

Full Import:
- Preserve the original TID when creating terms
- Import the complete revision history (for both new and updated terms)
- Keep revisions in chronological order

Force Import:
- Same as Full - preserve TID and import all revisions
- Clean slate approach but with full history intact

Safe Import:
- Check both name AND UUID before creating (prevents duplicate errors)
- Skip terms that already exist
- Do NOT import revisions (safe mode = current state only, no history tampering)

Implementation

New helper function importTermRevisions() handles the revision import:
- Imports revisions in chronological order
- Preserves all custom field values
- Restores the current version as the final revision

It's all done through standard Drupal APIs - nothing fancy.

Remaining tasks

The code is working and tested with several fresh DB's, but still needs Automated tests

The patch currently has no test coverage. Would be good to add tests for:
- Terms with multiple revisions
- All three import modes (Full/Safe/Force)
- Custom fields and entity references in revisions
- UUID conflict scenarios

User interface changes

No UI changes.

API changes

New Private Static Method

private static function importTermRevisions($term, array $revision_data_array, array $entity_fields)

Purpose: Import revision history for a taxonomy term

Parameters:
- $term - The term entity to import revisions for
- $revision_data_array - Array of revision data from export
- $entity_fields - Array of custom field definitions

Behavior:
1. Stores current term values
2. Sorts revisions chronologically
3. Creates each historical revision with proper metadata
4. Restores current version as the latest revision

Modified Public Static Methods

exportTaxonomies()

- Now exports revisions array with each term when historical revisions exist.

importTaxonomiesFull()

- Now accepts tid in entity properties
- Calls importTermRevisions() for terms with revision data

importTaxonomiesSafe()

- Now includes UUID verification query
- Prevents duplicate UUID constraint violations

importTaxonomiesForce()

- Now accepts tid in entity properties
- Calls importTermRevisions() for terms with revision data

Data model changes

Export YAML Structure

Terms now include an optional revisions array:

yaml
taxonomies:
  my_vocabulary:
    - vid: my_vocabulary
      tid: '91'
      langcode: en
      name: 'Current Term Name'
      description__value: 'Current description'
      description__format: plain_text
      weight: '0'
      parent: '0'
      uuid: ec3ae37d-a3e6-4420-8d20-5c9f2b46247d
      field_custom_field:
        - value: 'current value'
      revisions:
        - revision_id: '91'
          revision_created: '1765305454'
          revision_user: null
          revision_log_message: null
          name: 'Historical Name 1'
          description__value: 'Old description'
          description__format: plain_text
          weight: '0'
          field_custom_field:
            - value: 'old value'
        - revision_id: '337'
          revision_created: '1772128645'
          revision_user: '1'
          revision_log_message: 'Updated term'
          name: 'Historical Name 2'
          description__value: 'Newer description'
          description__format: plain_text
          weight: '0'
          field_custom_field:
            - value: 'newer value'

Backward Compatibility

Fully backward compatible:
- Terms without revisions array import normally (current behavior)
- Old exports work with new code
- New exports work with old code (revisions array is ignored)
- No database schema changes required

Database Impact

- Uses existing revision tables (taxonomy_term_revision, taxonomy_term_field_revision)
- No new tables or columns required
- Preserves TID in Full and Force modes (uses existing tid column)
- UUID validation uses existing unique constraint

Comments

gonz@meiz created an issue. See original summary.

gonz@meiz’s picture

Status: Active » Needs review
StatusFileSize
new10.43 KB

Attaching patch that adds taxonomy term revision export/import support.

Tested on Drupal 11 with Structure Sync 2.0.8.

gonz@meiz’s picture

The structure_sync-3576231-taxonomy-revisions patch was incomplete — importTermRevisions() was being called in Full and Force modes but was missing in Safe mode. When creating a new term in Safe mode, revisions were silently skipped.
Safe mode now follows the same pattern as Full and Force:

// Before:
Term::create($entity_properties + $entity_fields)->save();

// After:
$new_term = Term::create($entity_properties + $entity_fields);
$new_term->save();
if (!empty($taxonomy['revisions'])) {
  self::importTermRevisions($new_term, $taxonomy['revisions'], $entity_fields);
}