PS: This issue has the help of GPT 5.4 xhigh, I identified the problem, tried to fix it myself, gave the steps to reproduce and it did the rest. The bug is 100% real, the solution may be hallucination but it's there to try to help this issue to be resolved faster. I'm not a Rust developer
Description of the error
In a JS-hosted module on SpacetimeDB 2.1.0, spacetime dev / pre_publish can report:
Removed view: my_chats
Created view: my_chats
even when the view source is unchanged.
I reproduced this with a stronger check than "same source twice": I published one dist/bundle.js to a fresh local database, then called pre_publish again against that same database using the exact same bundle bytes. The planner still reported RemoveView + AddView for my_chats.
Expected behavior:
the view should be treated as unchanged or at most UpdateView / recompute-only.
Actual behavior:
the planner treats the view as incompatible and emits a breaking remove/create plan.
Table and view used to reproduce
import { type Infer, t, table } from "spacetimedb/server";
export const MessageRow = t.row("Message", { // same error with t.object and t.row
id: t.string(),
role: t.string(),
content: t.string(),
});
export const Chat = t.row("Chat", {
id: t.u64().primaryKey().autoInc(),
identity: t.identity().index(),
messages: t.array(MessageRow),
});
export const chatsTable = table(
{
name: "chats",
public: false,
indexes: [] as const,
},
Chat
);
export type Chat = Infer<typeof chatsTable.rowType>;
export type Message = Infer<typeof MessageRow>;
import spacetimedb from "../../module";
import { chats } from "../../schemas";
import { t } from "spacetimedb/server";
export const chatsView = spacetimedb.view(
{ name: "my_chats", public: true },
t.array(chats.rowType),
(ctx) => {
return Array.from(ctx.db.chats.identity.filter(ctx.sender));
}
);
Steps to reproduce
- Create a JS SpacetimeDB module with the table and view above.
- Build it:
- Publish it once to a local DB:
spacetime publish view-repro --server local --yes --js-path dist/bundle.js --no-config
- Without changing any source code or the generated bundle, call
pre_publish again on the same DB using the same bundle:
curl -X POST \
"http://127.0.0.1:3000/v1/database/<db_identity>/pre_publish?host_type=Js&pretty_print_style=NoColor" \
-H "Authorization: Bearer <token>" \
--data-binary @dist/bundle.js
- Observe that the migration plan still contains:
Removed view: my_chats
Created view: my_chats
Optional sanity check:
spacetime describe --json shows the same public view definition before and after. In my repro, my_chats matched on name, index, is_public, is_anonymous, params, and return_type.
Why it is probably failing
This looks like a false positive in view compatibility checking.
ModuleDefLookup for ViewColumnDef and ViewParamDef already avoids using ColId and matches by (view_name, column_name) instead, which suggests ColId is not intended to be portable across migrations.
However, auto_migrate_view appears to still treat old.col_id != new.col_id as a breaking incompatibility for view params / return columns.
If internal view col_id values are recomputed differently between:
- the currently running module definition, and
- the freshly extracted JS module definition
then the planner can emit AddView + RemoveView even though the public ABI of the view did not change.
That matches what I saw:
- same bundle bytes
- same described public schema
- still a breaking remove/create plan
Likely code location
The issue is probably in:
crates/schema/src/auto_migrate.rs
crates/schema/src/def.rs
impl ModuleDefLookup for ViewColumnDef
impl ModuleDefLookup for ViewParamDef
There is also a related UX problem in:
crates/schema/src/auto_migrate/formatter.rs
because UpdateView is intentionally omitted from the formatted plan, so a false incompatibility only shows up as Removed view / Created view, which makes the problem look like a real destructive schema change.
Suggested fix (Suggested by GPT 5.4 xhigh. May not be the real fix, I'm not a Rust developer)
In auto_migrate_view, do not treat raw view col_id equality as part of the public compatibility contract.
Instead, consider a view breaking only when one of these changes:
- a public param or return column is added or removed
- the public order of params or return columns changes
- a param or return column type changes
is_anonymous changes
Otherwise, the planner should emit UpdateView instead of AddView + RemoveView.
A good regression test would be:
- create two otherwise identical view defs
- change only
ViewColumnDef.col_id or ViewParamDef.col_id
- assert the plan is
UpdateView
- assert it does not contain
AddView / RemoveView
Extra evidence
I reproduced this with the exact same published bundle against the exact same database, so this does not require a real schema change in user code.
PS: This issue has the help of GPT 5.4 xhigh, I identified the problem, tried to fix it myself, gave the steps to reproduce and it did the rest. The bug is 100% real, the solution may be hallucination but it's there to try to help this issue to be resolved faster. I'm not a Rust developer
Description of the error
In a JS-hosted module on SpacetimeDB 2.1.0,
spacetime dev/pre_publishcan report:Removed view: my_chatsCreated view: my_chatseven when the view source is unchanged.
I reproduced this with a stronger check than "same source twice": I published one
dist/bundle.jsto a fresh local database, then calledpre_publishagain against that same database using the exact same bundle bytes. The planner still reportedRemoveView+AddViewformy_chats.Expected behavior:
the view should be treated as unchanged or at most
UpdateView/ recompute-only.Actual behavior:
the planner treats the view as incompatible and emits a breaking remove/create plan.
Table and view used to reproduce
Steps to reproduce
spacetime publish view-repro --server local --yes --js-path dist/bundle.js --no-configpre_publishagain on the same DB using the same bundle:Removed view: my_chatsCreated view: my_chatsOptional sanity check:
spacetime describe --jsonshows the same public view definition before and after. In my repro,my_chatsmatched onname,index,is_public,is_anonymous,params, andreturn_type.Why it is probably failing
This looks like a false positive in view compatibility checking.
ModuleDefLookupforViewColumnDefandViewParamDefalready avoids usingColIdand matches by(view_name, column_name)instead, which suggestsColIdis not intended to be portable across migrations.However,
auto_migrate_viewappears to still treatold.col_id != new.col_idas a breaking incompatibility for view params / return columns.If internal view
col_idvalues are recomputed differently between:then the planner can emit
AddView+RemoveVieweven though the public ABI of the view did not change.That matches what I saw:
Likely code location
The issue is probably in:
crates/schema/src/auto_migrate.rsauto_migrate_view(...)crates/schema/src/def.rsimpl ModuleDefLookup for ViewColumnDefimpl ModuleDefLookup for ViewParamDefThere is also a related UX problem in:
crates/schema/src/auto_migrate/formatter.rsbecause
UpdateViewis intentionally omitted from the formatted plan, so a false incompatibility only shows up asRemoved view/Created view, which makes the problem look like a real destructive schema change.Suggested fix (Suggested by GPT 5.4 xhigh. May not be the real fix, I'm not a Rust developer)
In
auto_migrate_view, do not treat raw viewcol_idequality as part of the public compatibility contract.Instead, consider a view breaking only when one of these changes:
is_anonymouschangesOtherwise, the planner should emit
UpdateViewinstead ofAddView+RemoveView.A good regression test would be:
ViewColumnDef.col_idorViewParamDef.col_idUpdateViewAddView/RemoveViewExtra evidence
I reproduced this with the exact same published bundle against the exact same database, so this does not require a real schema change in user code.