You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(plugins): add `ctx.storage.<collection>.updateIf(id, { where, set?, delta? })` — a predicate-guarded atomic update for plugin storage. The guard and the arithmetic run in a single `UPDATE … WHERE <guard> RETURNING` (no read-then-write), so N concurrent guarded decrements serialize correctly — the no-oversell primitive. `set` writes wholesale field values; `delta` applies integer `inc`/`dec` in-SQL over `COALESCE(base, 0)`. Returns `{ applied: true, data }` or `{ applied: false }` (row absent or guard failed — never inserts). Works on SQLite and Postgres via `json_set`/`jsonb_set` with the numeric-correct guard translation.
7
+
Adds `ctx.storage.<collection>.updateIf(id, { where, set?, delta? })` for atomic conditional updates to existing plugin documents. Use `where` to check stored fields, `set` to replace field values, and `delta` to increment or decrement integer counters. The method returns `{ applied: true, data }` with the updated document, or `{ applied: false }` when the document is absent or the condition fails. It never inserts a document.
8
+
9
+
Malformed update arguments reject without writing. Deltas require safe integer operands and results; missing or `null` counters start at `0`. Invalid stored counters, overflow, and non-object documents return `{ applied: false }` without changing any fields.
10
+
11
+
Available to native plugins and sandboxed plugins on Cloudflare and Workerd, with SQLite, D1, and PostgreSQL support. PostgreSQL serialization failures and deadlocks expose `code: "STORAGE_SERIALIZATION_FAILURE"` and `retryable: true`, including across sandbox transports. Retry standalone calls with bounded backoff, or restart the entire explicit transaction.
Use `updateIf()` to change an existing document only when its stored fields match a condition. The database checks the condition and applies the changes in one atomic operation. This method is available to native plugins and sandboxed plugins on Cloudflare and Workerd.
A successful call returns `{ applied: true, data }` with the complete updated document. It returns `{ applied: false }` if the document is missing or the condition does not match. It never inserts a document.
124
+
125
+
The arguments have the following behavior:
126
+
127
+
-`where` is required and uses the same operators as [query filters](#where-clause-operators). An explicit `where: {}` adds no field conditions. Guard fields do not need declared query indexes because the update targets one document by ID.
128
+
- A range filter needs at least one defined bound. Undefined bounds are ignored when another bound is defined. Numeric operands used by a guard must be finite.
129
+
-`set` replaces each supplied top-level field value and leaves other fields unchanged. Values must be JSON-serializable.
130
+
-`delta` applies exactly one `{ inc: number }` or `{ dec: number }` per field. Each operand must be a safe integer; negative operands are allowed.
131
+
- A field cannot appear in both `set` and `delta`. Top-level `undefined` entries in either object are ignored. At least one defined field must remain.
132
+
133
+
Malformed update arguments reject the promise without changing the document. The arguments object, `set`, `delta`, and each delta operation must be plain objects.
134
+
135
+
### Integer counters
136
+
137
+
A delta starts a missing or `null` counter at `0`. Existing counters and their results must be integers between `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`. A string, boolean, object, array, fractional number, unsafe integer, or out-of-range result causes the entire update to return `{ applied: false }`. A stored document that is not a JSON object also returns `{ applied: false }`. No fields change in either case.
138
+
139
+
Deltas can produce negative values. To keep a counter nonnegative, pair a decrement of `n` with a `where` condition requiring that counter to be at least `n`.
140
+
141
+
### Retry serialization failures
142
+
143
+
PostgreSQL can reject concurrent writes with a serialization failure or deadlock. A deadlock can occur at any isolation level, including READ COMMITTED. In native plugins, these failures throw `StorageSerializationError` with `code: "STORAGE_SERIALIZATION_FAILURE"`, `retryable: true`, and an optional `sqlState` (`40001` or `40P01`). Import the error class from `emdash`.
144
+
145
+
Use bounded retries with backoff for a standalone call. If the call is inside an explicit transaction, restart the entire transaction, including its reads; retrying the write inside the aborted transaction cannot succeed. Handle `{ applied: false }` as an unapplied update rather than a serialization error.
146
+
147
+
Sandbox transports preserve the error name and retry metadata, but do not guarantee `instanceof StorageSerializationError`. Check `code` and `retryable` when handling errors across a sandbox boundary.
148
+
102
149
## Querying
103
150
104
151
`query()` returns paginated results filtered by indexed fields:
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly.
122
+
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly. It is available to native plugins and sandboxed plugins on Cloudflare and Workerd.
123
123
124
124
The following example claims a submission for processing, and succeeds for exactly one caller when several run at once:
125
125
@@ -136,7 +136,7 @@ if (result.applied) {
136
136
}
137
137
```
138
138
139
-
`where` takes the same operators as `query()`. `set` writes whole field values. `delta` applies an integer `inc` or `dec` to a field, counting from `0` when that field is absent.
139
+
`where` takes the same operators as `query()`. `set` writes JSON-serializable whole field values. `delta` applies a safe integer `inc` or `dec` to a field, counting from `0` when that field is absent or `null`.
140
140
141
141
The following example records an attempt while the submission is still open:
142
142
@@ -152,11 +152,18 @@ Behavior to account for:
152
152
153
153
- The call returns `{ applied: true, data }` or `{ applied: false }`. An absent row and a failed guard both return `{ applied: false }`.
154
154
- An absent row is never inserted.
155
-
-`delta` accepts integers. A float throws `TypeError`.
156
-
- Pass at least one of `set` or `delta`. A field cannot appear in both.
155
+
-`where` is required. Explicit `where: {}` adds no field conditions. Guard fields do not require query indexes because the update targets a document ID.
156
+
- Malformed update arguments reject without writing. The arguments object, `set`, `delta`, and each delta operation must be plain objects.
157
+
- Each delta contains exactly one `inc` or `dec` with a safe integer operand. Negative operands are allowed. Fractions and unsafe integer operands reject the call.
158
+
- Pass at least one defined field in `set` or `delta`. Top-level `undefined` entries are ignored. A field cannot appear in both.
159
+
- Existing non-null counters and all results must be safe integers. Invalid types, fractions, unsafe integers, overflow, and non-object documents return `{ applied: false }` without changing any fields.
157
160
- A `dec` drives a field negative when the guard does not cover it. Pair `dec: k` with a `gte: k` guard to keep the field at or above zero.
158
-
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` throws `StorageQueryError` rather than leaving the write unguarded.
159
-
- A losing writer that aborts instead of returning `{ applied: false }` throws `StorageSerializationError`, carrying the Postgres SQLSTATE (`40001` or `40P01`). Retry the call.
161
+
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` rejects the call. Undefined bounds are ignored when another bound is defined. Numeric operands used by a guard must be finite.
162
+
- In native plugins, PostgreSQL serialization failures and deadlocks throw `StorageSerializationError` with `code: "STORAGE_SERIALIZATION_FAILURE"`, `retryable: true`, and optional `sqlState` (`40001` or `40P01`). Deadlocks can occur at READ COMMITTED too. Use bounded retries with backoff; inside an explicit transaction, restart the entire transaction, including its reads.
163
+
164
+
Sandbox transports preserve the error name and retry metadata, but `instanceof StorageSerializationError` is not guaranteed there. Check `code` and `retryable` across sandbox boundaries.
165
+
166
+
Import `NumericDelta`, `UpdateIfArgs`, and `UpdateIfResult` as types from `emdash` or `emdash/plugin`. Native plugins can import the runtime `StorageSerializationError` class from `emdash`; `emdash/plugin` supports type imports only.
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly.
122
+
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly. It is available to native plugins and sandboxed plugins on Cloudflare and Workerd.
123
123
124
124
The following example claims a submission for processing, and succeeds for exactly one caller when several run at once:
125
125
@@ -136,7 +136,7 @@ if (result.applied) {
136
136
}
137
137
```
138
138
139
-
`where` takes the same operators as `query()`. `set` writes whole field values. `delta` applies an integer `inc` or `dec` to a field, counting from `0` when that field is absent.
139
+
`where` takes the same operators as `query()`. `set` writes JSON-serializable whole field values. `delta` applies a safe integer `inc` or `dec` to a field, counting from `0` when that field is absent or `null`.
140
140
141
141
The following example records an attempt while the submission is still open:
142
142
@@ -152,11 +152,18 @@ Behavior to account for:
152
152
153
153
- The call returns `{ applied: true, data }` or `{ applied: false }`. An absent row and a failed guard both return `{ applied: false }`.
154
154
- An absent row is never inserted.
155
-
-`delta` accepts integers. A float throws `TypeError`.
156
-
- Pass at least one of `set` or `delta`. A field cannot appear in both.
155
+
-`where` is required. Explicit `where: {}` adds no field conditions. Guard fields do not require query indexes because the update targets a document ID.
156
+
- Malformed update arguments reject without writing. The arguments object, `set`, `delta`, and each delta operation must be plain objects.
157
+
- Each delta contains exactly one `inc` or `dec` with a safe integer operand. Negative operands are allowed. Fractions and unsafe integer operands reject the call.
158
+
- Pass at least one defined field in `set` or `delta`. Top-level `undefined` entries are ignored. A field cannot appear in both.
159
+
- Existing non-null counters and all results must be safe integers. Invalid types, fractions, unsafe integers, overflow, and non-object documents return `{ applied: false }` without changing any fields.
157
160
- A `dec` drives a field negative when the guard does not cover it. Pair `dec: k` with a `gte: k` guard to keep the field at or above zero.
158
-
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` throws `StorageQueryError` rather than leaving the write unguarded.
159
-
- A losing writer that aborts instead of returning `{ applied: false }` throws `StorageSerializationError`, carrying the Postgres SQLSTATE (`40001` or `40P01`). Retry the call.
161
+
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` rejects the call. Undefined bounds are ignored when another bound is defined. Numeric operands used by a guard must be finite.
162
+
- In native plugins, PostgreSQL serialization failures and deadlocks throw `StorageSerializationError` with `code: "STORAGE_SERIALIZATION_FAILURE"`, `retryable: true`, and optional `sqlState` (`40001` or `40P01`). Deadlocks can occur at READ COMMITTED too. Use bounded retries with backoff; inside an explicit transaction, restart the entire transaction, including its reads.
163
+
164
+
Sandbox transports preserve the error name and retry metadata, but `instanceof StorageSerializationError` is not guaranteed there. Check `code` and `retryable` across sandbox boundaries.
165
+
166
+
Import `NumericDelta`, `UpdateIfArgs`, and `UpdateIfResult` as types from `emdash` or `emdash/plugin`. Native plugins can import the runtime `StorageSerializationError` class from `emdash`; `emdash/plugin` supports type imports only.
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly.
122
+
`updateIf()` applies a write only when a guard matches. The guard and the arithmetic run in a single `UPDATE` statement, so concurrent guarded writes serialize correctly. It is available to native plugins and sandboxed plugins on Cloudflare and Workerd.
123
123
124
124
The following example claims a submission for processing, and succeeds for exactly one caller when several run at once:
125
125
@@ -136,7 +136,7 @@ if (result.applied) {
136
136
}
137
137
```
138
138
139
-
`where` takes the same operators as `query()`. `set` writes whole field values. `delta` applies an integer `inc` or `dec` to a field, counting from `0` when that field is absent.
139
+
`where` takes the same operators as `query()`. `set` writes JSON-serializable whole field values. `delta` applies a safe integer `inc` or `dec` to a field, counting from `0` when that field is absent or `null`.
140
140
141
141
The following example records an attempt while the submission is still open:
142
142
@@ -152,11 +152,18 @@ Behavior to account for:
152
152
153
153
- The call returns `{ applied: true, data }` or `{ applied: false }`. An absent row and a failed guard both return `{ applied: false }`.
154
154
- An absent row is never inserted.
155
-
-`delta` accepts integers. A float throws `TypeError`.
156
-
- Pass at least one of `set` or `delta`. A field cannot appear in both.
155
+
-`where` is required. Explicit `where: {}` adds no field conditions. Guard fields do not require query indexes because the update targets a document ID.
156
+
- Malformed update arguments reject without writing. The arguments object, `set`, `delta`, and each delta operation must be plain objects.
157
+
- Each delta contains exactly one `inc` or `dec` with a safe integer operand. Negative operands are allowed. Fractions and unsafe integer operands reject the call.
158
+
- Pass at least one defined field in `set` or `delta`. Top-level `undefined` entries are ignored. A field cannot appear in both.
159
+
- Existing non-null counters and all results must be safe integers. Invalid types, fractions, unsafe integers, overflow, and non-object documents return `{ applied: false }` without changing any fields.
157
160
- A `dec` drives a field negative when the guard does not cover it. Pair `dec: k` with a `gte: k` guard to keep the field at or above zero.
158
-
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` throws `StorageQueryError` rather than leaving the write unguarded.
159
-
- A losing writer that aborts instead of returning `{ applied: false }` throws `StorageSerializationError`, carrying the Postgres SQLSTATE (`40001` or `40P01`). Retry the call.
161
+
- A range filter in the guard needs at least one defined bound. `{ stock: { gte: undefined } }` rejects the call. Undefined bounds are ignored when another bound is defined. Numeric operands used by a guard must be finite.
162
+
- In native plugins, PostgreSQL serialization failures and deadlocks throw `StorageSerializationError` with `code: "STORAGE_SERIALIZATION_FAILURE"`, `retryable: true`, and optional `sqlState` (`40001` or `40P01`). Deadlocks can occur at READ COMMITTED too. Use bounded retries with backoff; inside an explicit transaction, restart the entire transaction, including its reads.
163
+
164
+
Sandbox transports preserve the error name and retry metadata, but `instanceof StorageSerializationError` is not guaranteed there. Check `code` and `retryable` across sandbox boundaries.
165
+
166
+
Import `NumericDelta`, `UpdateIfArgs`, and `UpdateIfResult` as types from `emdash` or `emdash/plugin`. Native plugins can import the runtime `StorageSerializationError` class from `emdash`; `emdash/plugin` supports type imports only.
0 commit comments