Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Project

Renaming and removing things that hold data

rename entity Job -> Assignment;

Renaming an entity or property, and removing one, are changes to something that already holds rows. Say what you meant in a migration and the rename keeps your data where it is and the removal is recorded rather than forgotten.

stable2 examples compiled by CIprojectdeploymigration

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-migration

The 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 app

What 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. JobAssignmentTask 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#

Related

Deploying while workflows are running

A workflow run can outlive the deploy that started it. Deploying with --new-version freezes the code and data shape the…

Stopping runs after a bad deploy

A workflow run keeps executing the version it started under, which is exactly what you want until that version is the…

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…