Subscribing to a view silently stores empty rows #42
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold#42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while reviewing PR #40 (#32's doctrine correction), reproduced against a live 2.8.2 server. Subscribing to a view silently stores empty rows and reports them as inserts.
The failure
A view arrives over the wire under its own
table_name/table_id— captured with a probe viewflip:table_name: "flip",table_id: 4097— and routes through the samecache.apply_database_updatepath as a table.The generator does not emit views. It walks
.tables[], while a view lives inmisc_exports[].Viewwithreturn_type: {Array: {Ref: 3}}— a shape that walk never visits. So the cache has no definition for it, and then:table_defreturns{}for the unknown relationdef.columnsisnildecode_row(encoded, nil)setscolumns = {}{}Net result: an empty row is stored and counted as an insert, with no error anywhere. Every field of every view row is silently discarded.
Why a client cannot work around it
A view's rows are positional in every message type — not just
TransactionUpdate, butInitialSubscription,SubscribeApplied,SubscribeMultiApplied,UnsubscribeApplied,UnsubscribeMultiAppliedandOneOffQueryResponsetoo. Verified across all seven.And the order is the view's own declared order, not the underlying table's. The probe's table
itemis(id, alpha, beta); its viewflipreturns(beta, alpha, id), and rows arrive as["b1","a1",1]. So a consumer cannot handcache.newthe table's column list and get correct results — they would get every field mapped to the wrong name, which is worse than empty.The view's row type is also dropped from the emitted
typespace, since that is seeded only from tables, reducers and procedures.Why this is worse than "views are unsupported"
Views are the case where column order is most load-bearing — they are positional everywhere, with an order derivable from nothing the SDK currently emits. PR #40 rewrote the repo's doctrine to say exactly that, and the honest conclusion is that the one relation kind whose decoding depends entirely on declared order is the one the generator does not cover.
An unsupported feature that raises is fine. One that silently stores empty rows is the failure class this project has repeatedly gone out of its way to eliminate — #8's dropped header, #17's silent new identity, #21's phantom delete+insert, #26's opaque enums.
Work
cache.apply_database_update(ortable_def) should raise, or emit a distinct event, when atable_namearrives with no definition — rather than storing{}. That is the small change and it converts silent corruption into a clear error, independent of whether views are ever supported.dev/tools/gen-schema.sh, readingmisc_exports[].Viewand resolvingreturn_type'sArray: {Ref: n}to that typespace entry for the column names, order and typesdev/server/index.jsand capture a fixture, so this is proven on the wire rather than arguedAcceptance
A fixture-backed test where subscribing to a view populates the cache with correctly named fields in the view's own order. And, separately, a test that an unknown relation raises rather than storing an empty row — that one matters even if view support is deferred.
Priority
The loud-failure half is a 1.0 blocker: it is silent data loss reachable by an ordinary
SELECT * FROM my_view. Full view support can be deferred with a documented limitation, but not the silent part.