Skip to content

Upsert that reports Inserted vs Updated (UpsertObjectAsync / UpsertResult) #32

Description

@michaelstonis

Problem

WriteObjectAsync / WriteObjectsAsync upsert with INSERT OR REPLACE and return only a success bool. A caller cannot tell whether a write created a row or replaced an existing one.

That distinction matters whenever an app keeps a derived, incrementally-maintained view of the store — a badge count, an "N queued" label, an added/removed event stream. Today the only way to get it is a retrieve-then-write pair, and because the two calls are separate connection-gate acquisitions the caller has to wrap them in its own async lock to keep the check-then-act atomic. In PFM.Mobile that outer lock was held across the awaits into Tycho's own gate and wedged the whole TPR queue once in the field (see PFM.Mobile docs/plans/tpr-queue-lock-redesign.md). The interim fix there is an in-memory key set that mirrors the partition; the right fix is for the database to say what it did.

Proposal

Add a single-object upsert that reports the outcome:

public enum UpsertResult { Inserted, Updated }

ValueTask<UpsertResult> UpsertObjectAsync<T>(T obj, string? partition = null, bool withTransaction = true, CancellationToken cancellationToken = default);
ValueTask<UpsertResult> UpsertObjectAsync<T>(T obj, Func<T, object> keySelector, string? partition = null, bool withTransaction = true, CancellationToken cancellationToken = default);
  • Same key rules as WriteObjectAsync (registered id selector or explicit selector, GuardAgainstKeyDivergence, partition scoping on the (Key, FullTypeName, Partition) primary key).
  • Implementation: inside the existing connection block/transaction, INSERT OR IGNORE and read the change count — 1 means Inserted; 0 means the row exists, so UPDATE JsonValue SET Data = json($json) WHERE Key = $key AND FullTypeName = $fullTypeName AND Partition = $partition and return Updated. Two statements only on the update path, one on the insert path, no pre-read, and atomic under the connection gate + serializable transaction. Anything other than exactly one affected row throws TychoException, matching the existing write path's failure behaviour.
  • Row contents after the call are identical to what WriteObjectAsync would have written.

Out of scope for this issue: a batch variant reporting (inserted, updated) counts. It is a natural follow-on (INSERT OR IGNORE batch → change count = inserted; per-row UPDATE for the remainder) but no consumer needs it yet.

Acceptance

  • First write of a key returns Inserted; a second write of the same key (same type, same partition) returns Updated and the stored JSON reflects the second object.
  • Same key in a different partition, or a different registered type, is Inserted.
  • The explicit key-selector overload behaves the same and honours the strict-mode divergence guard.
  • Works in both the default and Encrypted configurations (the suite already runs both).
  • README gets a short section beside the existing write examples; CHANGELOG entry.

Activity

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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions