Upgrading 3.2.0 -> 3.3.0 could leave a database where approving a device —
or creating any node — failed with `NOT NULL constraint failed: nodes.status`.
The 3.3.0 migration moves the device facts off `nodes` onto `device_inventory`
and then drops the columns. The drop is skipped when a node is still unlinked,
because those columns are the only remaining copy of its facts. But a 3.2.0
`nodes` declares `status`, `services`, `properties` and `show_hardware` NOT NULL
with no server-side default, and the 3.3.0 model no longer writes them — so a
skipped drop bricks every later INSERT.
It skipped because one node killed the whole backfill: the loop shared a single
session, so an IntegrityError raised by autoflush lost every link made before it.
- Give each node its own savepoint. A node whose merge violates a constraint is
skipped and logged on its own; the rest still link. `skipped` joins the stats
reported at boot.
- Relax NOT NULL on the retained legacy columns when the drop is skipped, so the
database stays writable while later boots retry the backfill. Values are kept;
only the constraint goes. Guarded on the PRAGMA notnull flag, so it runs once.
- Share one `_rebuild_nodes` between the drop and the relax: it restores
`PRAGMA foreign_keys = ON` in a finally (the connection returns to the pool)
and clears a `nodes_new` left by a failed attempt.
- Match IEEE addresses case-insensitively, and never write one another inventory
row already owns — `device_inventory.ieee_address` is UNIQUE, and a duplicate
is one way the backfill was raising in the first place.
Tests build the real v3.2.0 schema from the tag: the existing legacy-migration
fixture declares `status` nullable, which is why this reached a release.
Fixes#351
ha-relevant: no