The extension is in production, the data is in it, and the customer wants a field renamed, a table restructured and a feature retired. How Business Central decides whether your schema change is allowed, how to stage a breaking change over two releases, and how to write the upgrade codeunit that moves the data.
Building an extension is the easy part. Changing it once it has data in production is where developers who learned on a sandbox get into trouble, because Business Central will refuse a schema change that would lose data, and if you force it, you lose the data. This is how to make changes to a live extension safely, every time.
When a new version of an app is installed, the platform compares the new schema with the old and synchronises the database. Additive changes are always fine: new tables, new fields, new keys, widening a text field. Destructive changes are refused by default: removing a table or field, shrinking a field, changing a field's type, changing a primary key. The Add sync mode (the default and the only one on production online) will not do them. ForceSync, which will, is available on sandboxes and on-premise only, and it deletes the data for the changed objects. It is a development convenience, never a deployment strategy.
So every destructive change in production is done by staging, not by force.
Suppose a field "Pallet Config" (Code[10]) needs to become "Pallet Configuration" (Code[30]) on a different table. The sequence:
ObsoleteState = Pending, ObsoleteReason and ObsoleteTag = '29.0'. An upgrade codeunit copies the data from old to new (below). Code now reads and writes the new field; the old field is still there, still has its data, and still compiles for anyone who references it, with a warning.ObsoleteState = Removed. It no longer compiles for anyone; its column stays in the database with its data, inaccessible from AL. Any dependent app that still referenced it has had a release to move.Renames of tables, primary key changes and type changes all follow the same shape: create the new, migrate, obsolete the old. Never reuse an object ID or a field ID for something different; the platform remembers.
A codeunit with Subtype = Upgrade runs when the app is upgraded from a lower version. Its procedures carry [OnUpgradePerCompany] (per company, most data moves) or [OnUpgradePerDatabase]. The shape that works:
Upgrade Tag codeunit: check HasUpgradeTag, do the work, SetUpgradeTag. The upgrade runs once per company, even if the app is upgraded several times, and it can be re-run on a restored database without doing the move twice.[OnInstallAppPerCompany]), so that a fresh company does not try to migrate data it never had.FindSet over the old records, write the new, with SetLoadFields on the read and no per-record commit. For very large tables, batch with periodic commits and a progress log, and test the time on a copy of production.Test it by installing version N-1 in a sandbox with realistic data, then upgrading to N, then checking the counts and spot-checking values. Then do it again from a fresh install to make sure the install path works. The same codeunit is what you write when consolidating several extensions into one: the new app depends on the old, the upgrade copies across, the old is obsoleted.
If another app depends on yours, your obsoleted field is a warning in their build at Pending and an error at Removed. Give them a version. If you depend on an ISV app and they obsolete something, your NextMajor build is where you find out; that is why the pipeline compiles weekly against the latest versions of dependencies, not just the base.
Uninstalling an app does not delete its data; the data is kept, orphaned, until the app is reinstalled (when it reconnects) or someone chooses to delete it (Unpublish with data deletion, or the "delete data" option). In version 29, with extension fields stored on the base table, uninstalling an app with large tables is a heavier operation and should be done in a window. The rule for customers: never choose "delete data" for an app you might want back, and never uninstall an app to tidy up before checking whether anything is reading its fields.
This is routine work once it is habit and a disaster the first time it is not. If you have a schema change you are nervous about, or an extension that will not install because of one, we have done enough of them to make it boring.