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.
Problem
WriteObjectAsync/WriteObjectsAsyncupsert withINSERT OR REPLACEand return only a successbool. 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:
WriteObjectAsync(registered id selector or explicit selector,GuardAgainstKeyDivergence, partition scoping on the(Key, FullTypeName, Partition)primary key).INSERT OR IGNOREand read the change count — 1 meansInserted; 0 means the row exists, soUPDATE JsonValue SET Data = json($json) WHERE Key = $key AND FullTypeName = $fullTypeName AND Partition = $partitionand returnUpdated. 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 throwsTychoException, matching the existing write path's failure behaviour.WriteObjectAsyncwould have written.Out of scope for this issue: a batch variant reporting
(inserted, updated)counts. It is a natural follow-on (INSERT OR IGNOREbatch → change count = inserted; per-rowUPDATEfor the remainder) but no consumer needs it yet.Acceptance
Inserted; a second write of the same key (same type, same partition) returnsUpdatedand the stored JSON reflects the second object.Inserted.Encryptedconfigurations (the suite already runs both).