Skip to content

Support atomic conditional updates with affected-row counts #65

Description

@bazer

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.

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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions