---
title: Update Operators
description: Reference for the OMGDB update operators — field, array, and positional operators with dotted paths, applied through immediate updates or the agent-safe mutation flow.
---

Update operators describe *how* to mutate matched documents. They mirror MongoDB's
update syntax: an update spec is a JSON object whose top-level keys are operators
(`$set`, `$inc`, `$push`, …), and each operator's value is an object mapping field
paths to arguments. Operators are applied in the order they appear in the spec.

There are two ways to apply an update spec, and both use the operators on this page:

- **Immediate updates** — [`update-one`](/docs/cli/#update-one), [`update-many`](/docs/cli/#update-many),
  [`find-one-and-update`](/docs/cli/#find-one-and-update), and [`bulk-write`](/docs/cli/#bulk-write)
  apply the spec atomically and fail-closed: a many-document update commits its complete selected
  set or appends nothing, and every command is bounded by explicit scan/mutation limits.
- **The agent-safe mutation flow** — *plan* a change (a dry run that writes nothing), *apply* it by
  token in a single transaction, then optionally *roll it back* to the recorded before-state. See
  [agent mutations](/docs/agent-mutations/) for the `plan` / `apply` / `rollback` protocol.

Either way, a change is appended to the op-log as new records — history, never an overwrite. For
selecting *which* documents an update touches, see [query operators](/docs/query-operators/).

## Field operators

| Operator | Description | Example |
| --- | --- | --- |
| `$set` | Sets each named field to the given value, inserting it if absent. | `{"$set":{"role":"admin"}}` |
| `$unset` | Removes each named field. The value side of each entry is ignored. | `{"$unset":{"tmp":""}}` |
| `$inc` | Adds a numeric delta to the field's current value. | `{"$inc":{"age":1}}` |
| `$mul` | Multiplies the field's current numeric value by a factor. | `{"$mul":{"n":3}}` |
| `$bit` | Bitwise `and`/`or`/`xor` of an integer field with a mask. | `{"$bit":{"flags":{"or":4}}}` |
| `$min` | Sets the field only if the given value is *less* than the current value. | `{"$min":{"lo":2}}` |
| `$max` | Sets the field only if the given value is *greater* than the current value. | `{"$max":{"hi":9}}` |
| `$rename` | Removes a field and re-inserts its value under a new name. | `{"$rename":{"old":"new"}}` |
| `$currentDate` | Sets the field to the current timestamp as a date value. | `{"$currentDate":{"updatedAt":true}}` |

### $set

Inserts or overwrites each named field with the supplied value.

```json
{"$set":{"role":"admin"}}
```

### $unset

Removes each named field from the document. Removing an absent field is a no-op,
and the value side of each entry (`""` below) is ignored.

```json
{"$unset":{"tmp":""}}
```

### $inc

Adds the delta to the field's current numeric value. An absent field is treated as
`0`, so `$inc` of `1` on a missing field results in `1`.

```json
{"$inc":{"age":1}}
```

Integer arithmetic uses checked addition: an `i64` overflow (for example
`i64::MAX + 1`) returns a `Malformed` error — `$inc would overflow i64: x + y` —
rather than panicking or silently wrapping a corrupted value into the store. Mixed
integer/float operands are promoted to `f64`. A non-numeric current value or delta
yields `$inc requires numeric values`.

```text
$inc on a field holding i64::MAX with delta 1  →  Malformed error (not wrapped)
```

### $mul

Multiplies the field's current numeric value by the factor.

```json
{"$mul":{"n":3}}
```

Like `$inc`, integer multiplication is checked: an `i64` overflow returns a
`Malformed` error (`$mul would overflow i64: x * y`) and never wraps. Mixed
integer/float operands promote to `f64`.

> **Note:** Because an absent field defaults to `0`, `$mul` on a *missing* field
> produces `0` — it does not skip the field or fall back to the factor.

### $bit

Applies one bitwise operation — exactly one of `and`, `or`, or `xor` — to an
existing integer field with an integer mask.

```json
{"$bit":{"flags":{"or":4}}}
```

Unlike `$inc`/`$mul`, `$bit` does not auto-create the field: a missing field fails
with ``$bit requires an existing integer field `flags` ``, and a non-integer value
is rejected. A misspelled operation gets a did-you-mean hint (e.g. `xr` suggests
`xor`).

### $min and $max

`$min` writes the supplied value only when it is strictly less than the current
value (or the field is absent). `$max` writes only when the supplied value is
strictly greater (or the field is absent). Comparison uses the same ordering as the
query engine.

```json
{"$min":{"lo":2}, "$max":{"hi":9}}
```

If the existing value is already on the right side of the bound, it is kept
unchanged. For example, `$min` of `99` against an existing `5` leaves `5` in place;
`$min` of `2` against `5` lowers it to `2`.

### $rename

Removes the source field and re-inserts its value under the target name.

```json
{"$rename":{"old":"new"}}
```

The target name must be a string, otherwise the update fails with
`$rename target must be a string`. If the source field is absent, `$rename` is a
no-op. It does not check whether the target already exists — an existing target is
overwritten. Both source and target may be dotted paths (`"a.b"` → `"a.c"`), but
they must differ, must not be parent/child of one another, and must not contain
positional array segments (`$`, `$[]`, `$[id]`).

### $currentDate

Sets each named field to the current timestamp as a date value. The per-field spec
is `true` or `{"$type":"date"}`:

```json
{"$currentDate":{"updatedAt":true}}
{"$currentDate":{"updatedAt":{"$type":"date"}}}
```

MongoDB's `{"$type":"timestamp"}` form is rejected with an explicit error
(``$currentDate $type `timestamp` is not supported; use `date` ``) — OMGDB has one
date/timestamp type.

## Array operators

| Operator | Description | Example |
| --- | --- | --- |
| `$push` | Appends value(s) to an array field, creating it if absent. Supports the `$each`, `$position`, `$slice`, and `$sort` modifiers. | `{"$push":{"tags":{"$each":["c","d"]}}}` |
| `$addToSet` | Adds value(s) only if not already present (set semantics). Supports `$each`. | `{"$addToSet":{"tags":"a"}}` |
| `$pull` | Removes every element matching a value or a query condition. | `{"$pull":{"scores":{"$gt":80}}}` |
| `$pullAll` | Removes every element equal to any value in the given array. | `{"$pullAll":{"tags":["a","b"]}}` |
| `$pop` | Removes one element: `1` removes the last, `-1` removes the first. | `{"$pop":{"tags":1}}` |

### $push

Appends value(s) to the array field, creating the array if the field is absent.

```json
{"$push":{"tags":{"$each":["c","d"]}}}
```

Without a modifier, the single spec value is appended as one element. With
modifiers, `$each` is required and the others adjust the result:

| Modifier | Effect |
| --- | --- |
| `$each` | Appends every element of the given array, in order. |
| `$position` | Inserts the `$each` elements at an index instead of the end (negative counts from the end). |
| `$slice` | Truncates the resulting array: positive keeps the head, negative keeps the tail, `0` empties it. |
| `$sort` | Sorts the resulting array: `1`/`-1` for whole-element sorts, or `{"field":1}` to sort object elements by a field. |

Pushing onto a field that exists but is **not** an array fails with
``array update on `field` which is not an array``. An unknown modifier is rejected
with a did-you-mean hint.

```text
$push {"$each":["c","d"]} onto ["a","b"]  →  ["a","b","c","d"]
$push {"xs":1} onto {}                    →  {"xs":[1]}
$push {"n":1} onto {"n":5}                →  Malformed error (n is not an array)
```

### $addToSet

Adds value(s) to the array only if not already present. Deduplication is by full
value equality.

```json
{"$addToSet":{"tags":"a"}}
```

An already-present value leaves the array unchanged. With `$each`, each candidate is
checked individually against the current array (`$each` is the only modifier
`$addToSet` accepts). Like `$push`, it creates the array if absent and errors on a
non-array field.

```text
$addToSet "a" onto ["a","b"]  →  ["a","b"]   (no-op, "a" already present)
```

### $pull

Removes matching elements from the array field. The criteria can be a literal value
(full value equality), an operator condition applied to each element, or a document
sub-filter matched against object elements:

```json
{"$pull":{"tags":"a"}}
{"$pull":{"scores":{"$gt":80}}}
{"$pull":{"items":{"qty":{"$lt":1}}}}
```

A criteria object whose keys all start with `$` is treated as a per-element operator
condition; any other object is a sub-filter that only object elements can match —
the same shape convention as query [`$elemMatch`](/docs/query-operators/).

> **Note:** `$pull` and `$pullAll` silently no-op on an absent field, instead of
> auto-creating it like `$push` / `$addToSet` / `$pop`; all of them error on a
> non-array field.

### $pullAll

Removes every element equal to *any* value in the supplied array (equality only —
use `$pull` for conditions):

```json
{"$pullAll":{"tags":["a","b"]}}
```

The spec must be an array of values; anything else fails with
`$pullAll requires an array of values`.

### $pop

Removes a single element from the end or start of the array.

```json
{"$pop":{"tags":1}}
```

The direction must be `1` (remove the last element) or `-1` (remove the first); any
other value fails with `$pop value must be 1 (last) or -1 (first)`. `$pop` on
an empty array is a safe no-op. It auto-creates an absent field as an empty array and
errors on a non-array field.

```text
$pop  1  on ["a","b"]  →  ["a"]   (removes last)
$pop -1  on ["a","b"]  →  ["b"]   (removes first)
```

## Dotted paths and positional operators

Every operator's field name is a dotted update path. Segments descend into embedded
documents by key and into arrays by numeric index, and three positional segments
address array elements without hard-coding an index:

| Segment | Meaning | Example |
| --- | --- | --- |
| `a.b.c` | Nested object fields (created on `$set` if absent). | `{"$set":{"addr.city":"athens"}}` |
| `items.0` | A numeric array index. | `{"$set":{"items.0.qty":5}}` |
| `items.$` | Query-bound positional: the first element the *filter* matched. | `{"$set":{"items.$.qty":5}}` |
| `items.$[]` | All-elements positional: applies to every element. | `{"$inc":{"scores.$[]":1}}` |
| `items.$[i]` | Filtered positional: elements matching the `arrayFilters` entry for `i`. | `{"$set":{"items.$[i].qty":0}}` |

The query-bound `$` requires the update's filter to identify exactly one condition
on that array path — a multi-field element match must use query `$elemMatch` so the
binding is unambiguous.

`$[id]` is driven by MongoDB-style `arrayFilters`: each filter document references
exactly one identifier, every `$[id]` used in the update must have a matching
filter, and every supplied filter must be used. `plan-update` accepts them with
`--array-filters`:

```sh
omgdb plan-update app.omgdb orders '{"status":"open"}' \
  '{"$set":{"items.$[cheap].flagged":true}}' \
  --array-filters '[{"cheap.price":{"$lt":10}}]'
```

> **Note:** Updates can never touch `_id`. Any update path beginning with `_id`
> (including a `$rename` source or target) is rejected up front with
> ``updates cannot modify immutable `_id` path``, before any document is matched.

## Applying updates through the agent-safe flow

Update operators are consumed by the mutation protocol rather than written directly.
A typical round trip:

1. **Plan** — compute matched documents and a before/after sample without writing.
2. **Apply** — execute the plan by token inside a single transaction.
3. **Rollback** — restore the recorded before-state if needed.

```rust
let plan = plan_update(
    &s,
    "u",
    &obj(r#"{"name":"ada"}"#),
    &obj(r#"{"$set":{"role":"admin"},"$inc":{"age":1}}"#),
)?;
// plan reports matched == 1; the store is still unchanged (age 30, no role)

let result = apply_change(&mut s, &token)?;
// after apply: age == 31, role == "admin"

rollback(&mut s, &change_id)?; // restores age 30, role removed
```

See [agent mutations](/docs/agent-mutations/) for the full plan / apply / rollback
reference, including the NDJSON plan and audit files.

## Caveats

- **Operator ordering.** Operators are applied in the spec's iteration order, so the
  result of multiple operators touching the same field depends on that order.
- **`$bit` masks are integers only**, and the target field must already exist as an
  integer.
- **`$currentDate` writes date values only** — the `timestamp` type is rejected by
  name.
- An unrecognized operator key returns an unknown-operator error with a did-you-mean
  suggestion when a known operator is within edit distance 2 (`$sett` suggests
  `$set`); a non-object update or a non-object operator spec returns a malformed
  error.

For selecting the documents an update applies to, see
[query operators](/docs/query-operators/).
