A Data Bundle is a serialized snapshot of the Wonderful Relations configuration owned by a single Project — Entities, Queries, DataTables, Forms, Form Fields, Linked Forms, Templates, PDF Templates, Services, Buttons, Tasks and the Wonderful Relations links that tie them together. Bundles are how a child plugin ships its baseline configuration to a customer install.

Why bundles instead of initial.sql

A raw SQL dump cannot be replayed safely against a customer database: auto-increment IDs already exist, foreign keys collide, references break. Bundles solve this in three ways:

  • UUID-keyed identity. Every mapped row carries a uuid column. The importer matches by UUID, not by id, so updates land on the correct row even if its numeric id differs between developer and customer install.
  • Project-scoped ownership. Each bundle declares its owning project UUID. The Sync importer refuses to touch rows owned by another project (UNKNOWN/COLLISION/foreign-owner hard stops).
  • FK heal. After the rows are written, a Foreign-Key Heal pass rebuilds wr_link and field references using the resolved UUIDs.

The pipeline

                ┌──────────────┐         ┌────────────────┐
   wp wr        │              │  JSON   │                │   wp wr
 bundle-export  │  Developer   │ ─────▶  │   Customer     │ bundle-sync
 ──────────────▶│  install     │         │   install      │ ──────────▶
                │              │         │                │
                └──────────────┘         └────────────────┘
                       │                          │
                       ▼                          ▼
                Bundle JSON                 Bundle JSON
                (project +                  + UuidResolver
                 hydrated tree)             + Ownership check
                                            + Backup
                                            + Bundle write
                                            + FK heal
                                            + Conflict policy

Three CLI verbs you actually use

# Developer side: export the configuration owned by a project.
wp wr bundle-export \
    --uuid=<project-uuid> \
    --output=/path/to/bundle.json \
    [--fingerprint=ts-2026-05-07]
 
# Customer side: import a single bundle file.
wp wr bundle-import \
    --input=/path/to/bundle.json \
    [--policy=bundle_wins|db_wins] \
    [--strict]
 
# Customer side: sync a folder of bundle files (the typical update flow).
wp wr bundle-sync \
    --dir=/path/to/bundles/ \
    [--policy=bundle_wins|db_wins] \
    [--strict]

The Sync importer is idempotent: running bundle-sync twice in a row does nothing on the second pass. That is the property tests rely on.

Conflict policy

When the same row exists on both sides with different content, the importer falls back to the policy:

  • BundleWins (default) — the bundle’s value replaces what is in DB.
  • DbWins — the DB value is kept; bundle changes are skipped.

Pass --strict to refuse silent fallbacks. Strict mode raises a hard error on UNKNOWN/COLLISION/foreign-owner hits and lists the offending rows.

What gets persisted on the customer side

Three layers cooperate to keep query/data definitions readable across installs:

  1. Database — all WR tables (wp_wr_query, wp_wr_form, …) hold the definitive rows.
  2. Bootstrap JSONlocale_bootstrapped_*.json snapshots in wp-content/wonderful-relations-data/ are the locale-resolved variants used at runtime to skip a DB read on hot paths.
  3. Query cache JSONwr_query_cache.json and wr_project_snapshot.json cache compiled queries.

Never delete or move these JSON files manually. They are persistent runtime state, not throwaway cache. The Bundle importer writes them as part of a successful sync; deleting them between sync runs leaves the system serving stale or missing query SQL.

Bundle file structure

A bundle is a single JSON document with three top-level sections:

{
    "fingerprint": "ts-2026-05-07",
    "project": { "uuid": "…", "identifier": "…", "title": "…" },
    "entries": [ { "kind": "wr_entity",   "uuid": "…", "data": {...} },
                 { "kind": "wr_query",    "uuid": "…", "data": {...} },
                 { "kind": "wr_form",     "uuid": "…", "data": {...} },
  ]
}

The kind enum lists every WR concept the bundle can carry; entries form a logical tree thanks to UUID references inside data.

Multi-project export (wr-bundle-multi-v1)

A plugin can own more than one Project — e.g. yourproject plus its extension yourproject_invoice. A single-project export (--uuid=<project>) only carries that project’s closure, so it can’t represent both. Exporting the set produces a multi-bundle envelope that wraps one wr-bundle-v1 sub-bundle per project:

{
    "format": "wr-bundle-multi-v1",
    "fingerprint": "wr-export-multi-…",
    "bundles": [ { /* project A v1 bundle */ }, { /* project B v1 bundle */ } ]
}

On read the loader flattens a multi-v1 envelope to a single wr-bundle-v1 (BundleEnvelope::flattenMulti()): the sub-bundles are dedup-merged across manifest / entities / links, first-occurrence-wins so a shared row keeps its earliest — dependency-safe — slot (the parent project’s sub-bundle is listed first, before its extension’s). Everything downstream (schema validation, importer, bootstrap) only ever sees a single v1, so a multi-v1 file can be checked in directly as the plugin’s bundle.json.

A bundle.json in wr-bundle-multi-v1 format requires a Wonderful Relations build that ships the multi-v1 loader. A plain single wr-bundle-v1 loads on every version — deploy the matching WR first when switching a child plugin to the multi format.

Backup, rollback and FK heal

bundle-sync always takes a mysqldump backup of the affected tables before writing. Pair this with the safety verbs if a sync goes wrong:

wp wr bootstrap-bundle-dryrun  --dir=/path/to/bundles/   # plan only
wp wr bootstrap-bundle-write   --dir=/path/to/bundles/   # apply with backup
wp wr bootstrap-bundle-rollback --backup=/path/to/dump.sql
wp wr fk-heal                  [--dry-run] [--no-backup]

The legacy verbs are also available:

  • wp wr orphan-sweep — delete rows whose parent UUIDs have vanished.
  • wp wr orphan-link-sweep — same idea for Wonderful Relations links.
  • wp wr health-check — pre-flight integrity checks.
  • wp wr fk-drift-audit — print divergent foreign keys without changing anything.

Legacy migration

Plugins that still ship a JSON export from the old JSONExporter pipeline can convert it once:

use TS\WonderfulRelations\System\DataStorage\Bundle\LegacyJsonToBundleConverter;
 
$bundle = ( new LegacyJsonToBundleConverter() )->convert( $legacy_export_path );
file_put_contents( $output_path, json_encode( $bundle, JSON_PRETTY_PRINT ) );

After conversion, switch the plugin to ship the bundle file and stop producing the legacy export.

Where to put the bundle in your plugin

The convention used by the WR mother and the child plugins is:

your-plugin/
└── src/
    └── Database/
        └── Config/
            ├── config.json        # Update Center declarative plan
            └── bundles/
                └── your-plugin.bundle.json

The Database/Config/config.json file references the bundle so the Update Center can sync it as part of wp_admin → Update Center.