Migrating 17 million rows from SAS4

A real 326 MB SAS4 backup imported into a scratch tenant, reconciled row by row, and then the migrated subscribers authenticated again over CHAP.

An importer that works on a demo database proves nothing. The one that matters is the operator's real backup, with fifteen years of soft-deleted rows, invalid bytes in text columns and enum values nobody documented. So the test was a real one.

The input

A production SAS4 v4.56 backup: 326 MB gzipped, about 17.0 million tuples across 89 tables. Raw counts in the dump were 1,131,110 subscriber rows, 1,658,908 cards, 3,072,858 credential rows, 292 network devices, 198 plans and 103 managers.

Before importing anything, the same file was restored to MySQL as it came, so every number the importer produced could be checked against the source rather than against itself.

Mapping

SAS4's users are end subscribers and its own staff table is something else again; X-Radius names them the other way around, so the first half of the mapping is vocabulary. The rest is ordinary: plans, cards and card series, the manager tree with its balances, the money journal, network devices with their shared secrets, and the activation history.

Some things have no equivalent and are not invented. Panel preferences do not come across. A handful of access-control codes had no mapping and were counted rather than guessed — 29 of them in this dump.

Running it

The import is a resumable background job, not a request. It streamed the whole dump in 83 minutes on a development stack, and the worker's memory stayed between 19 and 40 MiB throughout against a 640 MiB limit, because the parser never holds more than a batch.

Every phase writes counters, and the counters are the point. Live subscribers came out at 23,621 — the dump's 1,131,110 rows minus 1,107,489 that were soft-deleted, recounted independently from the source file. Cards came out at 1,658,908, exactly the raw count. Network devices: 292, with no cross-tenant collision. Activations reconciled to the row, with the excluded ones landing in named counters rather than disappearing: 1,107,017 belonging to deleted subscribers, 1,202,827 with no subscriber key at all.

A migration you cannot audit is a migration you have to trust. This one adds up.

The proof that matters

Counts are necessary and not sufficient. The check an operator actually cares about is whether their subscribers can still log in.

A sample of 500 migrated subscribers was compared credential by credential against the source: 484 of 484 that had one matched exactly, checked inside the database so that no credential was ever read out of it. Then 20 imported subscribers were authenticated with real CHAP Access-Requests against FreeRADIUS, with the tenant resolved from an imported device address. They were accepted, with their reply attributes and session correlation intact.

Going back

The exporter writes the same shape back out. Round-tripping the imported tenant returned 23,621 subscribers, 1,658,908 cards, 75 managers, 106 plans and 292 devices — all exact.

That export is also where the isolation rule earns its keep. Exported through the application's own database role, the numbers match. Exported through a superuser connection, they are too high, because a superuser is not subject to the row-level policies and quietly collects other tenants' rows. It is a good reminder that "it returned data" and "it returned the right data" are different tests.

Try it on your own network.

50 subscribers for 7 days. You pay nothing.