---
title: "String operations"
description: "Reference for the Rust client's operations::string builders and expressions::string builders for server-side string operations and expressions."
---

# String operations

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

[String](https://aerospike.com/docs/develop/data-types/string) operations let the server search, transform, extract, and normalize text in a String bin. That avoids fetching a bin, editing it in your application, and writing it back.

This reference covers the Aerospike Rust client surface, for developers already using [`Client::operate()`](https://aerospike.com/docs/develop/client/rust/usage/atomic/multi) and comfortable building [filter and modify expressions](https://docs.rs/aerospike/latest/aerospike/expressions/index.html): the `operations::string` builders for `operate()` calls, and the `expressions::string` builders for filter and modify expressions. After reading this page, you can choose the right builder for a task, configure a `StringPolicy`, and read `operate()` results, including the positional `Record::results` list.

It requires Aerospike Database 8.2.0 or later and Aerospike Rust client 3.0.0 or later. See [Version requirements](#version-requirements) before upgrading a production cluster. For operation semantics, argument details, and the full 37-operation catalog, see the [String operations reference](https://aerospike.com/docs/develop/data-types/string/operations) and [String expressions reference](https://aerospike.com/docs/develop/expressions/string).

## Setup

The examples on this page use the following connection and key:

```rust
use aerospike::{as_bin, as_key, Client, ClientPolicy, WritePolicy};

use aerospike::operations::string as str_op;

use aerospike::operations::string::{StringPolicy, StringWriteFlags};

let client = Client::new(&ClientPolicy::default(), "127.0.0.1:3000").await?;

let key = as_key!("sandbox", "users", "jdoe123");
```

> 📖 **API reference**: [`Client::new`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.new) | [`operations::string`](https://docs.rs/aerospike/latest/aerospike/operations/string/index.html)

## Round-trip elimination

Without String operations, normalizing a bin takes a read, an application-side edit, and a write:

```rust
use aerospike::{as_bin, Bins, ReadPolicy, WritePolicy};

// Before: fetch, modify, write

let record = client.get(&ReadPolicy::default(), &key, Bins::All).await?;

let email = String::try_from(record.bins.get("email").unwrap().clone())?

    .trim()

    .to_lowercase();

client

    .put(&WritePolicy::default(), &key, &[as_bin!("email", email)])

    .await?;
```

`operations::string` runs the same edit inside a single `operate()` call, on the server:

```rust
let policy = StringPolicy::default();

// After: one round trip

client

    .operate(

        &WritePolicy::default(),

        &key,

        &[str_op::trim(&policy, "email"), str_op::lower(&policy, "email")],

    )

    .await?;

// Verify: "  Jane.Doe@Example.com  " becomes "jane.doe@example.com"

let record = client.get(&ReadPolicy::default(), &key, Bins::All).await?;

println!("{:?}", record.bins.get("email"));
```

> 📖 **API reference**: [`Client::operate`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.operate) | [`operations::string::trim`](https://docs.rs/aerospike/latest/aerospike/operations/string/fn.trim.html) | [`operations::string::lower`](https://docs.rs/aerospike/latest/aerospike/operations/string/fn.lower.html)

## Where String Operations can be used

Every String operation is available in two forms, both exported at module level:

| Surface | Module | Used with | Argument order |
| --- | --- | --- | --- |
| Operation | `operations::string` | `Client::operate()` | Bin name first: `str_op::substr_range(bin, start, end)` |
| Expression | `expressions::string` | [`BasePolicy::filter_expression`](https://docs.rs/aerospike/latest/aerospike/policy/struct.BasePolicy.html), [`operations::exp::read_exp`/`write_exp`](https://aerospike.com/docs/develop/client/rust/usage/atomic/multi) | Source expression first: `str_exp::substr_range(src, start, end)` |

::: rust’s expression argument order is not bin-last
Most Aerospike clients put the source or bin argument last in an expression builder, matching the collection data type (CDT) list, map, and bitwise expression builders. The Rust client’s `expressions::string` module puts `src` first instead, matching the bin-first order of its own `operations::string` builders. `str_exp::substr(src, start)` and `str_exp::substr_range(src, start, end)` take `src` before the index arguments. A modify builder like `str_exp::upper(policy, src)` takes `src` after the policy, but still ahead of every other operand. Don’t assume the argument order from another Aerospike client’s string-expression API carries over to Rust.
:::

`operations::string` builders read or modify a bin directly, returning an `Operation` for `Client::operate()`. `expressions::string` builders return an `Expression` that composes inside a larger expression, with no separate build or compile step.

A modify-style `expressions::string` builder (`upper`, `replace`, `trim`, and similar) returns the transformed string as a value. It does not write the result back to the bin on its own. To persist a modify expression’s result, write it back with [`operations::exp::write_exp`](https://docs.rs/aerospike/latest/aerospike/operations/exp/fn.write_exp.html), or use the `operations::string` equivalent instead. See [Nested strings](#nested-strings) for a worked filter and projection example.

Modify operations also take a policy as their first argument, ahead of the bin name or source expression:

```rust
use aerospike::expressions::string_bin;

// Operation: policy, then bin name

str_op::upper(&StringPolicy::default(), "text");

// Expression: policy, then source expression, still not last

str_exp::upper(&StringPolicy::default(), string_bin("text".into()));
```

## String write policy

Modify operations take a `&StringPolicy`, which wraps a `StringWriteFlags` value:

```rust
let default_policy = StringPolicy::default();                              // DEFAULT (0)

let no_fail = StringPolicy::new(StringWriteFlags::NO_FAIL);                 // NO_FAIL (4)

let create_only = StringPolicy::new(StringWriteFlags::CREATE_ONLY);         // CREATE_ONLY (1)

let update_only = StringPolicy::new(StringWriteFlags::UPDATE_ONLY);         // UPDATE_ONLY (2)
```

| Flag | Value | Effect |
| --- | --- | --- |
| `StringWriteFlags::DEFAULT` | 0 | Allow create or update. |
| `StringWriteFlags::CREATE_ONLY` | 1 | Apply only if the bin doesn’t already exist. A live bin fails with `BinExistsError`. Only 9 additive operations accept this flag; see the note below the table. |
| `StringWriteFlags::UPDATE_ONLY` | 2 | Apply only to an existing bin. An absent bin is a no-op rather than a create. Valid on every string modify operation. Cannot combine with `CREATE_ONLY`. |
| `StringWriteFlags::NO_FAIL` | 4 | Suppress the failure if the operation can’t be applied, leaving the bin (and the value a modify expression evaluates to) at the unmodified source string. See the caution below the table for what this does and doesn’t cover. |

`StringWriteFlags::CREATE_ONLY` is only valid on the additive operations that can create a bin from an empty string: [`insert`](https://aerospike.com/docs/develop/data-types/string/operations#insert), [`overwrite`](https://aerospike.com/docs/develop/data-types/string/operations#overwrite), [`concat`](https://aerospike.com/docs/develop/data-types/string/operations#concat), `concat_list`, [`append`](https://aerospike.com/docs/develop/data-types/string/operations#append), [`prepend`](https://aerospike.com/docs/develop/data-types/string/operations#prepend), [`pad_start`](https://aerospike.com/docs/develop/data-types/string/operations#pad_start), [`pad_end`](https://aerospike.com/docs/develop/data-types/string/operations#pad_end), and [`repeat`](https://aerospike.com/docs/develop/data-types/string/operations#repeat). Every other modify operation rejects `CREATE_ONLY` with `ParameterError`. `CREATE_ONLY` is also invalid combined with `UPDATE_ONLY`, and invalid on an operation carrying a `CdtContext`. Both combinations return `ParameterError`.

Against a missing bin, `StringWriteFlags::DEFAULT` never fails, but it doesn’t make every operation create one. Only the nine additive operations listed for `CREATE_ONLY` above can create a bin from nothing, seeded from an empty string.

The other 11 modify operations (`trim`, `upper`, `replace`, and similar) can’t create a bin at all. Against a missing bin, they leave the record unchanged and still return success, so the absence of an error doesn’t mean the operation did anything. Read the bin back if you need to confirm a write happened.

`StringPolicy` is a per-operation argument, not client configuration: there’s no string-policy field on `ClientPolicy`. Pass a policy value to each call that needs non-default flags.

::: when no_fail can fail
`StringWriteFlags::NO_FAIL` covers argument and size validation, the operation itself, and a `CREATE_ONLY` conflict on a live bin. It does not cover a malformed argument list, a non-string bin, or invalid UTF-8 in either the source or the result.

`NO_FAIL` also suppresses a result that would exceed the [per-operation size cap](https://aerospike.com/docs/develop/data-types/string/operations#result-size-limits). The operation returns success, but the bin stays unchanged. This is indistinguishable from a normal success on the client, so it can mask a production bug where writes silently stop applying. On any production write path that sets `NO_FAIL`, verify the outcome with a read-after-write (or equivalent check) rather than trusting the absence of an error.
:::

On `expressions::string` builders, `NO_FAIL` is the only flag that carries meaning for most modify builders. `regex_replace` is a partial exception. Its write-flags argument accepts only `UPDATE_ONLY` and `NO_FAIL`. `CREATE_ONLY` is refused with `ParameterError` because the operation can’t create a bin. The write-flags argument is also separate from, and numbered after, the regex-flags argument. See the caution in [Modify operations](#modify-operations).

## Regex and numeric-type flags

`StringRegexFlags` (combine with bitwise `|`) controls `regex_compare` and `regex_replace`:

| Flag | Value | Applies to |
| --- | --- | --- |
| `StringRegexFlags::CASE_INSENSITIVE` | 1 | Both |
| `StringRegexFlags::MULTILINE` | 2 | Both |
| `StringRegexFlags::DOT_ALL` | 4 | Both |
| `StringRegexFlags::UNIX_LINES` | 8 | Both |
| `StringRegexFlags::GLOBAL` | 16 | `regex_replace` only. Replaces every match instead of only the first. `regex_compare` rejects it with `ParameterError`. |

`StringNumericType` narrows `is_numeric_typed`: `Any` (0, default), `Int` (1), or `Float` (2). `Float` requires the string to contain a `.` followed by at least one digit, so `is_numeric_typed(bin, StringNumericType::Float)` against `"5"` returns `false`, even though `"5"` parses fine under `Int` or the untyped `is_numeric`.

## Read operations

All read operations take the bin name as an `operations::string` argument, or the source expression as the first `expressions::string` argument. Both also take an optional trailing `.context(vec![...])` call with `CdtContext` values to reach a value nested in a List or Map, covered in [Nested strings](#nested-strings). Every operation requires the target to already be a String. Calling one against another bin type fails with `BinTypeError`.

A read operation (`strlen`, `contains`, `find`, `regex_compare`, and similar) against a bin that doesn’t exist on the record succeeds and yields `Value::Nil` in that operation’s `Record::results` slot. A bin of the wrong type fails with `BinTypeError`, per the note above.

Index and length values count Unicode codepoints, not bytes. Most characters are one codepoint, but some emoji use multiple codepoints for one visible character, called a grapheme cluster. Characters outside the Basic Multilingual Plane (Unicode codepoints above U+FFFF) can also differ. Negative indexes count from the end of the string. Out-of-bounds indexes are clamped to the valid range rather than returning an error. `regex_compare` uses [International Components for Unicode (ICU) regex](https://aerospike.com/docs/develop/data-types/string#unicode-semantics) syntax.

| Operation | Rust builders | Returns | Description |
| --- | --- | --- | --- |
| [`strlen`](https://aerospike.com/docs/develop/data-types/string/operations#strlen) | `strlen` | `Value::Int` | Codepoint count. |
| [`byte_length`](https://aerospike.com/docs/develop/data-types/string/operations#byte_length) | `byte_length` | `Value::Int` | UTF-8 byte count. Differs from `strlen` for non-ASCII text. |
| [`substr`](https://aerospike.com/docs/develop/data-types/string/operations#substr) | `substr_from`, `substr` | `Value::String` | Substring from `start` to the end, or the half-open range `[start, end)`. If `start >= end` after negative-index normalization, the result is the empty string. |
| [`char_at`](https://aerospike.com/docs/develop/data-types/string/operations#char_at) | `char_at` | `Value::String` | The one-codepoint string at `index`. |
| [`find`](https://aerospike.com/docs/develop/data-types/string/operations#find) | `find`, `find_nth` | `Value::Int` | Codepoint index of `needle`, or of a specific `occurrence` (1 = first, -1 = last). `-1` if not found. |
| [`contains`](https://aerospike.com/docs/develop/data-types/string/operations#contains) | `contains` | `Value::Bool` | Whether the bin contains `needle`. |
| [`starts_with`](https://aerospike.com/docs/develop/data-types/string/operations#starts_with) | `starts_with` | `Value::Bool` | Whether the bin begins with `prefix`. Unicode-canonical matching, not byte-exact. |
| [`ends_with`](https://aerospike.com/docs/develop/data-types/string/operations#ends_with) | `ends_with` | `Value::Bool` | Whether the bin ends with `suffix`. Unicode-canonical matching. |
| [`to_integer`](https://aerospike.com/docs/develop/data-types/string/operations#to_integer) | `to_integer` | `Value::Int` | Parses the string as an `i64`. Fails with `ParameterError` if the bin can’t be parsed as an integer. |
| [`to_double`](https://aerospike.com/docs/develop/data-types/string/operations#to_double) | `to_double` | `Value::Float` | Parses the string as a 64-bit float. Fails with `ParameterError` if the bin can’t be parsed as a double. |
| [`is_numeric`](https://aerospike.com/docs/develop/data-types/string/operations#is_numeric) | `is_numeric`, `is_numeric_typed` | `Value::Bool` | Whether the bin’s spelling matches an optional `StringNumericType` (`Any`, `Int`, or `Float`). |
| [`is_upper`](https://aerospike.com/docs/develop/data-types/string/operations#is_upper) / [`is_lower`](https://aerospike.com/docs/develop/data-types/string/operations#is_lower) | `is_upper`, `is_lower` | `Value::Bool` | Whether every cased codepoint is upper/lowercase. An empty string returns `true`. |
| [`to_blob`](https://aerospike.com/docs/develop/data-types/string/operations#to_blob) | `to_blob` | `Value::Blob` | The UTF-8 bytes of the string, as a [Blob](https://aerospike.com/docs/develop/data-types/blob). |
| [`split`](https://aerospike.com/docs/develop/data-types/string/operations#split) | `split`, `split_by_separator` | `Value::List` | Splits by Unicode codepoint, or by `separator` (a singleton list if `separator` isn’t found). |
| [`b64_decode`](https://aerospike.com/docs/develop/data-types/string/operations#b64_decode) | `b64_decode` | `Value::Blob` | Decodes the bin as base64 text into a [Blob](https://aerospike.com/docs/develop/data-types/blob). |
| [`regex_compare`](https://aerospike.com/docs/develop/data-types/string/operations#regex_compare) | `regex_compare`, `regex_compare_with_flags` | `Value::Bool` | Matches an ICU regex `pattern` against the bin, optionally with `StringRegexFlags`. |

::: a legacy, posix-flavored regex_compare also exists
The Rust client’s top-level [`expressions::regex_compare(regex, flags, bin)`](https://docs.rs/aerospike/latest/aerospike/expressions/fn.regex_compare.html) predates `expressions::string::regex_compare` and takes POSIX-style [`RegexFlag`](https://docs.rs/aerospike/latest/aerospike/enum.RegexFlag.html) values (`ICASE`, `EXTENDED`, `NOSUB`, `NEWLINE`), not `StringRegexFlags`. It has no `expressions::string` module prefix, no server-side dependency on Database 8.2.0, and no ICU regex syntax. It’s not deprecated, but for new code that needs ICU syntax, capture groups, or replace semantics, use `expressions::string::regex_compare` (or `regex_replace`) instead. Don’t mix `RegexFlag` and `StringRegexFlags` values; they use different bit assignments for the same names.
:::

## Modify operations

Modify operations write a transformed value back to the bin (`operations::string`) or return it as an expression value (`expressions::string`, which does not mutate the underlying bin). Every modify operation accepts `StringWriteFlags::DEFAULT`, `NO_FAIL`, and (except `regex_replace`) `CREATE_ONLY`/`UPDATE_ONLY`. See [String write policy](#string-write-policy).

::: destructive, irreversible writes
`operations::string` modify calls overwrite the stored bin value on the server, with no built-in undo. `snip`, `replace`, `replace_all`, `regex_replace`, and `overwrite` can truncate or permanently discard part of the value if the index, pattern, or range is wrong. Test destructive operations against sample data before running them against production records, and read the bin back after a production write to confirm the result matches what you expected.
:::

| Operation | Rust builders | Description |
| --- | --- | --- |
| [`insert`](https://aerospike.com/docs/develop/data-types/string/operations#insert) | `insert` | Splices `value` in at codepoint `index`. |
| [`overwrite`](https://aerospike.com/docs/develop/data-types/string/operations#overwrite) | `overwrite` | Overwrites codepoints starting at `index` with `value`. May grow the bin when `value` extends past the end. |
| [`concat`](https://aerospike.com/docs/develop/data-types/string/operations#concat) | `concat`, `concat_list` | Appends one string, or each element of a `&[&str]` slice, in order. |
| [`append`](https://aerospike.com/docs/develop/data-types/string/operations#append) | `append` | Appends `value`. Unicode/DBCS-aware, unlike the legacy [`operations::append`](https://docs.rs/aerospike/latest/aerospike/operations/fn.append.html). |
| [`prepend`](https://aerospike.com/docs/develop/data-types/string/operations#prepend) | `prepend` | Prepends `value`. Unicode/DBCS-aware, unlike the legacy [`operations::prepend`](https://docs.rs/aerospike/latest/aerospike/operations/fn.prepend.html). |
| [`pad_start`](https://aerospike.com/docs/develop/data-types/string/operations#pad_start) | `pad_start` | Left-pads with `pad_string` up to `target_length` codepoints. No-op if already at or above the target. |
| [`pad_end`](https://aerospike.com/docs/develop/data-types/string/operations#pad_end) | `pad_end` | Right-pads with `pad_string` up to `target_length` codepoints. |
| [`repeat`](https://aerospike.com/docs/develop/data-types/string/operations#repeat) | `repeat` | Repeats the bin `count` times. `count` must be non-negative. |
| [`snip`](https://aerospike.com/docs/develop/data-types/string/operations#snip) | `snip_from`, `snip` | Removes codepoints from `start` to the end, or the half-open range `[start, end)`. See the caution below about `snip_from` and write flags. |
| [`replace`](https://aerospike.com/docs/develop/data-types/string/operations#replace) | `replace` | Replaces the first occurrence of `needle` with `replacement`. |
| [`replace_all`](https://aerospike.com/docs/develop/data-types/string/operations#replace_all) | `replace_all` | Replaces every occurrence of `needle` with `replacement`. |
| [`upper`](https://aerospike.com/docs/develop/data-types/string/operations#upper) / [`lower`](https://aerospike.com/docs/develop/data-types/string/operations#lower) | `upper`, `lower` | Uppercases or lowercases the bin. |
| [`case_fold`](https://aerospike.com/docs/develop/data-types/string/operations#case_fold) | `case_fold` | Applies locale-independent case folding, for comparison keys. |
| [`normalize_nfc`](https://aerospike.com/docs/develop/data-types/string/operations#normalize_nfc) | `normalize_nfc` | Normalizes the bin to Unicode Normalization Form C (NFC). Already-normalized strings are unchanged. |
| [`trim`](https://aerospike.com/docs/develop/data-types/string/operations#trim) / [`trim_start`](https://aerospike.com/docs/develop/data-types/string/operations#trim_start) / [`trim_end`](https://aerospike.com/docs/develop/data-types/string/operations#trim_end) | `trim`, `trim_start`, `trim_end` | Removes whitespace from both ends, the start, or the end. |
| [`regex_replace`](https://aerospike.com/docs/develop/data-types/string/operations#regex_replace) | `regex_replace` | Replaces the first regex match, or every match when `StringRegexFlags::GLOBAL` is set. Only accepts `UPDATE_ONLY`/`NO_FAIL` write flags; see the caution below. |

::: snip_from carries no write flags
`snip_from` accepts a `&StringPolicy` argument for signature symmetry with the other modify builders, but ignores it. The server reads the `snip` argument list by position: `start`, `end`, then flags. A two-element payload of `[start, flags]` would land the flags value in the `end` slot and silently snip the empty range `[start, 0)` instead of truncating. `snip_from`’s one-argument wire form omits the flags element entirely rather than risk that. Use `snip(policy, bin, start, end)` when the write flags have to be honored.
:::
::: keep regex_replace regex flags and string policy/write flags arguments separated and in order
`regex_replace` takes a `regex_flags: StringRegexFlags` argument and a `policy: &StringPolicy` argument (wrapping `StringWriteFlags`), packed as two separate positional elements: regex flags first, then write flags. `StringWriteFlags::NO_FAIL` (4) and `StringRegexFlags::DOT_ALL` (4) share the same bit value. Passing a `StringWriteFlags` value where a `StringRegexFlags` value is expected, or vice versa, silently selects the wrong behavior instead of failing, because both types wrap a plain `i64`. Always pass `regex_flags` and `policy` as their own typed arguments, in that order, and don’t try to combine them into one bitmask.
:::

## Type conversion

`to_string` converts an Integer, Float, Boolean, String, or [Blob](https://aerospike.com/docs/develop/data-types/blob) bin to its string representation. It fails with `BinTypeError` for any other bin type.

```rust
let rec = client

    .operate(&WritePolicy::default(), &key, &[str_op::to_string("n")])

    .await?;

println!("{:?}", rec.bins.get("n"));
```

> 📖 **API reference**: [`operations::string::to_string`](https://docs.rs/aerospike/latest/aerospike/operations/string/fn.to_string.html)

`to_string` is the only string operation that does not accept a `CdtContext`. It’s a dedicated top-level wire operation (`TO_STRING`) that carries no msgpack payload at all. The bin is referenced solely by the operation header, so there’s no `.context()` builder to attach a path to.

To convert a value nested inside a List or [Map](https://aerospike.com/docs/develop/data-types/collections/map), extract the leaf first with [`operations::lists::get_by_index`](https://docs.rs/aerospike/latest/aerospike/operations/lists/fn.get_by_index.html) or [`operations::maps::get_by_key`](https://docs.rs/aerospike/latest/aerospike/operations/maps/fn.get_by_key.html), using the same `CdtContext`, then convert client-side. Alternatively, compose `expressions::string::to_string` with [`expressions::lists::get_by_index`](https://docs.rs/aerospike/latest/aerospike/expressions/lists/fn.get_by_index.html) or [`expressions::maps::get_by_key`](https://docs.rs/aerospike/latest/aerospike/expressions/maps/fn.get_by_key.html) inside an expression.

## Reading results and errors

### Booleans decode as booleans

`contains`, `starts_with`, `ends_with`, `is_numeric`, `is_upper`, `is_lower`, and `regex_compare` decode as `Value::Bool`, not an integer `0`/`1`:

```rust
let rec = client

    .operate(&WritePolicy::default(), &key, &[str_op::contains("email", "@")])

    .await?;

let has_at = rec.bins.get("email") == Some(&aerospike::Value::Bool(true));
```

### Multiple operations on one bin

The Rust client automatically requests a result slot for every operation in an `operate()` call that includes a String operation. You don’t need to set a policy flag first, unlike the equivalent setting in some other Aerospike clients. Results are available two ways:

-   `Record::results`: an `Option<Vec<Value>>` in submission order, one entry per operation. `Client::operate()` always populates this. For a plain read (`Client::get()`), it’s `None` unless `populate_positional_results` is set on the policy’s `base_policy` (default `false`). A modify operation that returns no value of its own contributes `Value::Nil` at its index, so positions line up exactly with the operation list you submitted.
-   `Record::bins`: an `IndexMap<String, Value>` keyed by bin name. `Value::Nil` results are dropped from `bins` rather than stored, so a modify op contributes nothing here. If more than one non-nil result lands on the same bin, the client wraps them in `Value::MultiResult(Vec<Value>)`, in submission order, but with any nil (modify) results removed. The indexes do not match the submitted operation list once a modify op is mixed in.

```rust
let policy = StringPolicy::default();

let rec = client

    .operate(

        &WritePolicy::default(),

        &key,

        &[

            str_op::trim(&policy, "email"), // modify: index 0 -> Value::Nil

            str_op::strlen("email"),        // read: index 1

            str_op::substr("email", 0, 5),  // read: index 2

        ],

    )

    .await?;

// Positional: preserves the Nil placeholder for the modify op.

let results = rec.results.as_ref().expect("operate() always populates results");

let length = &results[1]; // Value::Int

let head = &results[2];   // Value::String

// Using bins: the Nil from `trim` is dropped, so MultiResult here holds

// only [strlen, substr], two elements, not three.

match rec.bins.get("email") {

    Some(aerospike::Value::MultiResult(list)) => assert_eq!(list.len(), 2),

    _ => unreachable!(),

}
```

Because mixing a modify op with reads on the same bin shifts the `bins`\-based `MultiResult` index count, prefer `Record::results` whenever a call combines a modify operation with reads on the same bin. A single string operation on a bin, with nothing else targeting that bin, stores its value directly under the bin name in `bins`, with no `MultiResult` wrapper.

### Error detail and subcodes

`aerospike::Error` is a struct, not an enum, so string operation failures can’t be matched with an `Error::ServerError(...)` pattern. Check the result code with [`Error::server_result_code()`](https://docs.rs/aerospike/latest/aerospike/struct.Error.html#method.server_result_code), which returns `Option<ResultCode>`, following the client’s normal [error handling](https://aerospike.com/docs/develop/client/rust/error-handling) pattern. String operations commonly return `ResultCode::BinTypeError`, `ResultCode::ParameterError`, or `ResultCode::BinExistsError` for a `CREATE_ONLY` conflict.

Set `error_detail_verbosity` on the read or write policy’s `base_policy` to request a numeric subcode alongside the result code, which narrows down why a call failed:

```rust
let mut wpolicy = WritePolicy::default();

wpolicy.base_policy.error_detail_verbosity = 3;

match client

    .operate(&wpolicy, &key, &[str_op::insert(&StringPolicy::default(), "email", 0, "bad-utf8-arg")])

    .await

{

    Err(e) if e.server_result_code() == Some(ResultCode::ParameterError) => {

        // For example, PARAM_STRING_UTF8_INVALID (11).

        println!("subcode: {}", e.sub_code());

    }

    Err(e) => return Err(e.into()),

    Ok(_) => {}

}
```

> 📖 **API reference**: [`Error::server_result_code`](https://docs.rs/aerospike/latest/aerospike/struct.Error.html#method.server_result_code) | [`Error::sub_code`](https://docs.rs/aerospike/latest/aerospike/struct.Error.html#method.sub_code) | [`Error::server_error_detail`](https://docs.rs/aerospike/latest/aerospike/struct.Error.html#method.server_error_detail) | [`server_error::sub_code`](https://docs.rs/aerospike/latest/aerospike/server_error/index.html)

Subcode values are scoped to their parent result code and aren’t globally unique, so always check the result code first. Subcodes require Aerospike Database 8.2.0 or later; older servers ignore the verbosity request and no subcode is returned.

## Nested strings

`operations::string` builders take an optional trailing `.context(vec![...])` call with one or more `CdtContext` values to reach a string nested inside a List or [Map](https://aerospike.com/docs/develop/data-types/collections/map). The path must already resolve to a string: a non-string nested value fails with `BinTypeError`, and an invalid path (an out-of-bounds list index or a missing map key) also fails. See [nested context](https://aerospike.com/docs/develop/data-types/collections/context) for general `CdtContext` error behavior.

```rust
use aerospike::operations::cdt_context::{ctx_list_index, ctx_map_key};

use aerospike::Value;

// Uppercase a string nested in a list bin "items" at index 0.

let op = str_op::upper(&StringPolicy::default(), "items").context(vec![ctx_list_index(0)]);

client.operate(&WritePolicy::default(), &key, &[op]).await?;

// Read strlen of a string nested under a map key.

let op = str_op::strlen("profile").context(vec![ctx_map_key(Value::from("bio"))]);

let rec = client.operate(&WritePolicy::default(), &key, &[op]).await?;
```

> 📖 **API reference**: [`operations::cdt_context::ctx_list_index`](https://docs.rs/aerospike/latest/aerospike/operations/cdt_context/fn.ctx_list_index.html) | [`operations::cdt_context::ctx_map_key`](https://docs.rs/aerospike/latest/aerospike/operations/cdt_context/fn.ctx_map_key.html)

`expressions::string` builders don’t take a `CdtContext` at all. To apply a string expression to a nested value, project the value first with [`expressions::lists::get_by_index`](https://docs.rs/aerospike/latest/aerospike/expressions/lists/fn.get_by_index.html) or [`expressions::maps::get_by_key`](https://docs.rs/aerospike/latest/aerospike/expressions/maps/fn.get_by_key.html), which take a `ctx: &[CdtContext]` argument, then pass the result as the `src` argument. The following example builds a `strlen` condition on a nested map value, then uses it two ways: as a read filter, and as a projected read value.

```rust
use aerospike::expressions::maps::get_by_key;

use aerospike::expressions::string as str_exp;

use aerospike::expressions::{gt, int_val, map_bin, string_val, ExpType};

use aerospike::operations::exp::{read_exp, ExpReadFlags};

use aerospike::{MapReturnType, ReadPolicy};

let bio = get_by_key(

    MapReturnType::Value,

    ExpType::STRING,

    string_val("bio".into()),

    map_bin("profile".into()),

    &[], // no CdtContext needed; "bio" is a top-level key of the "profile" map

);

let is_long = gt(str_exp::strlen(bio.clone()), int_val(280));

// As a filter: fetch the record only if its bio is over 280 codepoints.

let mut read_policy = ReadPolicy::default();

read_policy.base_policy.filter_expression = Some(is_long.clone());

let record = client.get(&read_policy, &key, aerospike::Bins::All).await;

// As a projection: always fetch the record, with the condition's result in a computed bin.

let rec = client

    .operate(&WritePolicy::default(), &key, &[read_exp("is_long", is_long, ExpReadFlags::Default)])

    .await?;

let bio_is_long = rec.bins.get("is_long");
```

> 📖 **API reference**: [`expressions::maps::get_by_key`](https://docs.rs/aerospike/latest/aerospike/expressions/maps/fn.get_by_key.html) | [`operations::exp::read_exp`](https://docs.rs/aerospike/latest/aerospike/operations/exp/fn.read_exp.html)

`to_string` never accepts a `CdtContext`, on either surface. See [Type conversion](#type-conversion).

## Version requirements

String operations require Aerospike Database 8.2.0 or later on every node, and Aerospike Rust client 3.0.0 or later. A server prior to 8.2.0 doesn’t recognize the string opcodes and returns a generic `ParameterError`, not a distinct “unsupported feature” code. Check the cluster version rather than branching on the error. Query `Node::version()` (or `asinfo -v build` against each node) to confirm the cluster reports 8.2.0 or later.

::: rolling upgrades can cause intermittent failures
During a rolling upgrade, application code that calls String operations can succeed or fail on the same request depending on which node serves it. Nodes already on 8.2.0 or later accept String operations, but nodes not yet upgraded reject them with the same generic `ParameterError`. Confirm every node reports 8.2.0 or later before enabling String operations in application code, rather than waiting a fixed amount of time.
:::

## Deprecations

-   The legacy [`operations::append(bin)`](https://docs.rs/aerospike/latest/aerospike/operations/fn.append.html) and [`operations::prepend(bin)`](https://docs.rs/aerospike/latest/aerospike/operations/fn.prepend.html) are deprecated for String bins only, in favor of `operations::string::append`/`prepend`, which are Unicode/DBCS-aware. The legacy pair does a raw byte concatenation and doesn’t support `StringPolicy` or `CdtContext`.
-   Both legacy operations also accept Blob bins, which the string module can’t target. For a Blob bin, keep using `operations::append`/`prepend`. There’s no string-module replacement, and the legacy behavior there is unchanged and fully supported.

### Migrating from the legacy byte-concatenation APIs

The legacy `operations::append`/`prepend` do a raw byte concatenation with no UTF-8 validation at all. Switching a bin’s write path to `operations::string::append`/`prepend` is the first point at which that bin’s content is validated as UTF-8, not a stricter version of an existing check.

::: migration can cause ongoing write failures
If existing data written through the legacy byte-concatenation path contains invalid UTF-8, every future `operations::string::append`/`prepend` call against that bin fails with `ResultCode::OpNotApplicable`, carrying an `OPNOT_STRING_UTF8_INVALID` subcode. This isn’t a one-time failure: it repeats on every call until the bin’s content is repaired, so a hot code path can break continuously as soon as the change deploys.
:::

Before switching write paths in production:

1.  Audit affected bins for valid UTF-8 and repair any that fail. See [Repair legacy String bins](https://aerospike.com/docs/database/advanced/special-upgrades/820-upgrade#repair-legacy-string-bins).
2.  Roll out the switch per namespace or set and monitor for failures.
3.  If failures appear in production, the immediate mitigation is to revert that bin’s write path to the legacy `operations::append`/`prepend` call while you complete the repair.

## Next steps

-   [String operations reference](https://aerospike.com/docs/develop/data-types/string/operations): full semantics, index-bounds behavior, and error codes for all 37 operations
-   [String expressions reference](https://aerospike.com/docs/develop/expressions/string)
-   [String examples](https://aerospike.com/docs/develop/data-types/string/examples)
-   [String operations and UTF-8 validation](https://aerospike.com/docs/database/advanced/special-upgrades/820-upgrade#string-operations-and-utf-8-validation)
-   [Bin operations](https://aerospike.com/docs/develop/client/rust/usage/atomic/multi): the Rust client’s `Client::operate()` guide for CDT and other bin operations
-   [Error handling](https://aerospike.com/docs/develop/client/rust/error-handling): the `aerospike::Error`/`ResultCode` pattern for the Rust client
-   [Error codes](https://aerospike.com/docs/database/reference/error-codes): full server status code list, including String-operation-specific entries
-   [API reference (Rust)](https://docs.rs/aerospike/latest/aerospike/)