Summary
Add a first-class DataLinq API for atomic conditional updates that executes as a single SQL UPDATE ... WHERE ...
statement and returns the number of affected rows.
This enables compare-and-set and claim-once workflows without dropping down to provider-specific command creation
and raw SQL.
Use case
An application stores a one-time callback token with a nullable consumption timestamp. A callback may proceed only
when it is the first caller to consume the token:
UPDATE callback_sessions
SET consumed_at = @consumedAt
WHERE relay_state = @relayState
AND consumed_at IS NULL;
The application authenticates only when the affected-row count is exactly one.
The normal immutable-model flow cannot safely express this as a read followed by a mutation:
var session = CallbackSession.GetRelayState(relayState);
if (session.ConsumedAt is not null)
return false;
var mutation = session.Mutate();
mutation.ConsumedAt = now;
mutation.Save();
Two concurrent callers can both observe ConsumedAt == null before either write completes. Transactions do not fix
that pattern unless the read obtains an appropriate database lock and the entire operation stays on the same
connection and transaction. A single conditional update is both simpler and the actual concurrency boundary.
The same primitive is useful for:
- consuming one-time bearer tokens;
- claiming queued jobs or callbacks;
- optimistic state transitions such as
Pending -> Processing;
- lease acquisition and renewal;
- compare-and-set updates using a version or current-status predicate.
Desired API characteristics
The exact API shape is open for design. Conceptually, it should allow a model/table update with separate set and
predicate expressions, for example:
var affectedRows = database
.Update<CallbackSession>()
.Set(x => x.ConsumedAt, now)
.Where(x => x.RelayState == relayState && x.ConsumedAt == null)
.Execute();
or an equivalent generated/table-scoped API.
Required semantics:
- Execute one SQL update statement; do not materialize matching rows before the update.
- Parameterize both assigned values and predicate values.
- Return the provider's affected-row count.
- Support execution inside an existing DataLinq transaction as well as directly through a database instance.
- Define cache behavior explicitly. Cached rows affected by the update must be invalidated or updated, or the API
must expose a safe and documented consistency contract.
- Preserve DataLinq's provider-independent expression translation and diagnostics where possible.
Useful follow-up capabilities, if they fit the design, include multiple assignments and expressions based on the
current column value, such as incrementing an attempt counter. They are not required for the initial compare-and-set
primitive.
Acceptance criteria
- A conditional model update is emitted as one
UPDATE ... SET ... WHERE ... statement.
- The API returns the affected-row count without requiring a second query.
- Concurrent attempts to change the same row from the same expected state result in exactly one successful claim.
- Predicate and assignment values are parameterized.
- SQLite, MySQL, and MariaDB provider tests cover the same behavior.
- Transactional and non-transactional execution are covered.
- Cache invalidation or update behavior is documented and tested.
- Unsupported assignment or predicate expressions fail with a focused DataLinq diagnostic rather than silently
falling back to client-side evaluation.
Non-goals
- This does not require bulk materialization or returning updated models.
- This should not silently turn arbitrary LINQ queries into client-side read-modify-write operations.
- A database-specific locking API is not a substitute for the single-statement compare-and-set contract.
Summary
Add a first-class DataLinq API for atomic conditional updates that executes as a single SQL
UPDATE ... WHERE ...statement and returns the number of affected rows.
This enables compare-and-set and claim-once workflows without dropping down to provider-specific command creation
and raw SQL.
Use case
An application stores a one-time callback token with a nullable consumption timestamp. A callback may proceed only
when it is the first caller to consume the token:
The application authenticates only when the affected-row count is exactly one.
The normal immutable-model flow cannot safely express this as a read followed by a mutation:
Two concurrent callers can both observe
ConsumedAt == nullbefore either write completes. Transactions do not fixthat pattern unless the read obtains an appropriate database lock and the entire operation stays on the same
connection and transaction. A single conditional update is both simpler and the actual concurrency boundary.
The same primitive is useful for:
Pending -> Processing;Desired API characteristics
The exact API shape is open for design. Conceptually, it should allow a model/table update with separate set and
predicate expressions, for example:
or an equivalent generated/table-scoped API.
Required semantics:
must expose a safe and documented consistency contract.
Useful follow-up capabilities, if they fit the design, include multiple assignments and expressions based on the
current column value, such as incrementing an attempt counter. They are not required for the initial compare-and-set
primitive.
Acceptance criteria
UPDATE ... SET ... WHERE ...statement.falling back to client-side evaluation.
Non-goals