Skip to content

Dynamic-schema upsert persists new indexed attributes even when the write is refused #3407

Open

Description

@kriszyp

What happens

On a dynamic-schema table (schemaDefined: false), an operations-API write adds its new attributes to the table before the record write is staged. If the write is then refused or fails, the record never lands but the new indexed attributes stay.

Concretely, an insert/upsert/update/put with replicatedConfirmation that is refused before submission (a 400 residency refusal or a 503 feasibility refusal) leaves the table with 0 rows from that request and with every new attribute from the payload added to Table.attributes, written to the attribute descriptors, and indexed (Table.indices[name] is opened). The same leak happens for any later failure in the same loop, such as a validation, permission or source error on a record.

Impact is low: schema and index clutter, no row loss. It is worth tracking because the dynamic-schema attribute list is persistent and each leaked attribute costs an index store.

Where in the code

On origin/main e187b83d2:

  • dataLayer/harperBridge/ResourceBridge.ts:289-311 (upsertRecords): inside transaction(context, ...), if (!Table.schemaDefined) collects new_attributes from the payload and runs await Table.addAttributes(...) (:301) before the record loop that calls Table.create/patch/put.
  • resources/Table.ts:7927 addAttributes goes through declareTable in resources/databases.ts, which changes Table.attributes in place, opens the index store, persists the descriptor, and attaches Table.indices[name], all outside the data DatabaseTransaction.
  • The refusals are raised afterwards: confirmingResidents (400, resources/Table.ts:5444) and DatabaseTransaction.refuseUnconfirmable (503, resources/DatabaseTransaction.ts:1919).

resources/DESIGN.md:982 states that a refused write never lands. That holds for the record (#3148 added the refusal). This issue is about the schema side effect of the same request, which that invariant does not cover.

Repro

qa1868-replication-confirmation-outcome.test.ts (QA candidate P-706), subtests 2a and 2b: send an insert with a new attribute and a confirmation request that is refused, then check describe_table. Measured on harper ea43e5677, RocksDB and LMDB; the ordering was re-read on e187b83d2 and is unchanged. Not re-run.

Suggested fix

There is no clean small fix, which is why this is P3 rather than a quick change:

  • Deferring addAttributes until after the write stages is not possible, because an indexed attribute has to exist before the record can be staged with its index entry.
  • A pre-check for the confirmation refusals would need the residency of the merged record, which is only known at commit.

Reasonable options are to roll back attributes this request added if the enclosing transaction aborts before commit (and nothing else used them in the meantime), or to document that dynamic-schema attribute creation is not transactional and leave it. Tables with schemaDefined: true do not auto-create attributes and are not affected.

Does not prove

Only ResourceBridge.upsertRecords (the operations-API write paths) was tested. Other write paths to dynamic-schema tables were not checked.

🤖 Prepared by finding-triage wave 37 (Claude Sonnet 5.5)

Activity

  1. added theissue type on Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    Priority

    P3

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions