Summary#
Renaming an entity or a property is not the same kind of change as adding one. To your source it is a small edit; to your database it is a table full of rows whose name just changed out from under it. A removal is the same problem seen from the other side — the property is gone from your code, and its data is not.
Neither is guessed at. A deploy that removes something holding data is refused until you say what you meant in a
.migration — and then it does the safe thing: a rename keeps every row exactly where it is, and a removal is
recorded, so the column it leaves behind has an owner instead of becoming an anonymous leftover.
Signature#
migration "what changed" {
from "ast:<hash>";
to "ast:<hash>";
rename entity Job -> Assignment;
rename property Job.Notes -> Job.Remarks;
drop property Job.Spare;
drop entity Ghost;
drop enum JobStatus.Parked;
}Description#
What you see if you forget#
Deploy a removal with nothing to authorize it and the deploy stops, having changed nothing:
This deploy removes something that holds data, and nothing says you meant to. Removing an entity or a property is
not a change the deploy will make on your behalf, because the data outlives the source: the rows stay in the
database after the declaration is gone. Offending change(s): entity 'Job' removed.
Say what you meant in a migration and deploy again:
drop entity Job; // or: rename entity Job -> <NewName>;The reason it asks rather than guessing is that a removal and a rename look identical from the outside — both are just "this name is gone" — and they want opposite handling. Guess wrong in either direction and you lose data: treat a rename as a removal and every row is stranded in a table your app can no longer name; treat a removal as a rename and an unrelated table's rows are silently adopted.
You do not have to write this by hand#
Ask for it, and the file is generated from the difference between what is deployed and what you have now:
$ osy compile --generate-migrationThe generated file carries only the changes that need your word — the additive ones are simply applied. Where it can
tell a rename from a genuine removal it writes the rename for you and says so:
rename entity Job -> Assignment; // INFERRED: 75% of its properties match by name and type. Verify — if these are
// genuinely different entities, replace this with `drop entity Job;`It only writes a rename when there is one obvious answer. If two properties of the same type left and two arrived,
or a removed entity resembles several new ones equally, it will not guess — it lists the candidates as a comment and
leaves the drop in place for you to correct. That reticence is deliberate: a rename you did not mean adopts an
unrelated table's rows, which is worse than the removal it replaced.
Review the file, fix anything it guessed wrong, and deploy with it:
$ osy compile --migration migrations/9f00abcd.migration --new-version # local
$ osyrin app compile --migration migrations/9f00abcd.migration --new-version # a deployed appWhat a rename actually does — which is nothing, physically#
Nothing is copied, nothing is moved, and no table is renamed. Your app's description of its data is versioned; the data itself is shared. So the new name simply points at the same table:
- the version you just deployed knows the entity as
Assignment; - a run still finishing on an older version knows it as
Job; - both read and write the same rows, at the same time, correctly.
A physical rename would be the wrong tool for exactly that reason — it would break every run still asking for the old name. Renaming a property works the same way and for the same reason.
This holds through repeated renames. Job → Assignment → Task still reads and writes the rows you created on day
one; each version just calls them something different.
What a rename saves you from#
Without one, the change reads as a removal plus an addition, because that is all there is to see. You would get a new,
empty Assignment and every existing row left behind in a Job your app can no longer name. Nothing is deleted — but
nothing is reachable either, and the app comes up looking like it forgot everything. That is the outcome the refusal
exists to stop.
What a removal actually does#
The property disappears from your app immediately — it is gone from the model, from queries, from screens. Its column stays, because a run still finishing on an older version may go on reading and writing it, and that is the whole point of versioning.
What is new is that the removal is recorded. The version you just deployed keeps a note saying "this property existed and stopped here". That record is what later lets the column be reclaimed safely, and it travels forward: two deploys later, the record still says the property died in the version where it actually died.
The practical effect is that a leftover column is never confused with a mystery column. When space is eventually
reclaimed, only columns with a recorded removal are taken. Anything else that turns up on a table — the residue of a
hand-run ALTER, a half-finished change — is reported to you and left exactly where it is. Deleting a column nobody
can account for is not a decision worth making automatically.
Removing a required property#
Its column stops being required, because it has to: the version you just deployed does not know about the property, so it has nothing to put there, and rows created from now on simply leave it empty. A run still pinned to an older version will therefore find that value missing on rows created after the removal. Finish moving your readers and writers over before anything depends on it being filled in.
Removing an enum member is the sharpest case#
An enum member is not stored by its name. A column holds the member's position, so removing one re-numbers every
member after it — and no existing row is rewritten. { Queued, Parked, Done } minus Parked is { Queued, Done },
and every row that said Parked now says Done.
That is worse than the stranding above, and quieter. A dropped property leaves data unreachable, which you notice. A dropped enum member leaves data perfectly reachable and meaning something else, which you do not.
So it is treated exactly like the other removals: a deploy that removes a member is refused until a migration says you meant it, and the remedy names the member:
migration "retire the parked state" {
drop enum JobStatus.Parked;
// …or, if the member was really renamed:
rename enum JobStatus.Parked -> JobStatus.Held;
}A rename is the interesting one, and it is why the two are separate verbs. Renaming a member keeps its position, so every stored row goes on meaning what it always meant, under the new name. That is almost always what you wanted.
⚠ While you are developing, the local compile does not refuse — it warns. osy compile edits the version in place
rather than deploying a new one, so it applies the change and tells you what it did to your data:
enum member 'JobStatus.Done' changed its stored value from 2 to 1 — usually because an earlier member was removed
and the rest re-indexed. Existing rows are NOT rewritten, so rows holding 1 now read as 'Done', and rows written
when 'Done' meant 2 no longer do.Read it rather than scrolling past it: the rows in your development database now say something you did not write. The warning appears whenever a member's position moves — a removal, or a reorder — and stays silent when you simply add a member at the end, which moves nothing.
One thing you cannot do#
Changing the stored type, width or precision of a property that already has data is refused. There is no shape a column can take that is simultaneously the old type for a run still using it and the new type for the code you just deployed. Do it additively instead — add a new property, copy the data across, then remove the old one once every reader and writer has moved. See Deploying while workflows are running for the full sequence and its warning.
Examples#
Renaming an entity and one of its properties in the same deploy:
entity Job {
[Required, MaxLength(200)] string Title;
[MaxLength(400)] string? Notes;
}entity Assignment {
[Required, MaxLength(200)] string Title;
[MaxLength(400)] string? Remarks;
}The migration that says so:
migration "job becomes assignment" {
from "ast:<the deployed hash>";
to "ast:<the new hash>";
rename entity Job -> Assignment;
rename property Job.Notes -> Job.Remarks;
}Deploy it and every Job you ever created is an Assignment, with its notes intact under the new name. Runs that
were already in flight go on calling it a Job until they finish.
See also#
- Deploying while workflows are running — why an older version is still reading your data, and how versions are cleaned up
- Stopping runs after a bad deploy — stopping runs that are still executing a version you want rid of
- entity — declaring the entities this is all about
- enum — declaring the enums whose members re-number when one is removed