String operations
For the complete documentation index see: llms.txt
All documentation pages available in markdown.
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() and comfortable building filter and modify expressions: 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 before upgrading a production cluster. For operation semantics, argument details, and the full 37-operation catalog, see the String operations reference and String expressions reference.
Setup
The examples on this page use the following connection and key:
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|operations::string
Round-trip elimination
Without String operations, normalizing a bin takes a read, an application-side edit, and a write:
use aerospike::{as_bin, Bins, ReadPolicy, WritePolicy};
// Before: fetch, modify, writelet 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:
let policy = StringPolicy::default();
// After: one round tripclient .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|operations::string::trim|operations::string::lower
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, operations::exp::read_exp/write_exp | Source expression first: str_exp::substr_range(src, start, end) |
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, or use the operations::string equivalent instead. See 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:
use aerospike::expressions::string_bin;
// Operation: policy, then bin namestr_op::upper(&StringPolicy::default(), "text");
// Expression: policy, then source expression, still not laststr_exp::upper(&StringPolicy::default(), string_bin("text".into()));String write policy
Modify operations take a &StringPolicy, which wraps a StringWriteFlags value:
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, overwrite, concat, concat_list, append, prepend, pad_start, pad_end, and 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.
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.
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. 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 syntax.
| Operation | Rust builders | Returns | Description |
|---|---|---|---|
strlen | strlen | Value::Int | Codepoint count. |
byte_length | byte_length | Value::Int | UTF-8 byte count. Differs from strlen for non-ASCII text. |
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 | char_at | Value::String | The one-codepoint string at index. |
find | find, find_nth | Value::Int | Codepoint index of needle, or of a specific occurrence (1 = first, -1 = last). -1 if not found. |
contains | contains | Value::Bool | Whether the bin contains needle. |
starts_with | starts_with | Value::Bool | Whether the bin begins with prefix. Unicode-canonical matching, not byte-exact. |
ends_with | ends_with | Value::Bool | Whether the bin ends with suffix. Unicode-canonical matching. |
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 | 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 | is_numeric, is_numeric_typed | Value::Bool | Whether the bin’s spelling matches an optional StringNumericType (Any, Int, or Float). |
is_upper / is_lower | is_upper, is_lower | Value::Bool | Whether every cased codepoint is upper/lowercase. An empty string returns true. |
to_blob | to_blob | Value::Blob | The UTF-8 bytes of the string, as a Blob. |
split | split, split_by_separator | Value::List | Splits by Unicode codepoint, or by separator (a singleton list if separator isn’t found). |
b64_decode | b64_decode | Value::Blob | Decodes the bin as base64 text into a Blob. |
regex_compare | regex_compare, regex_compare_with_flags | Value::Bool | Matches an ICU regex pattern against the bin, optionally with StringRegexFlags. |
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.
| Operation | Rust builders | Description |
|---|---|---|
insert | insert | Splices value in at codepoint index. |
overwrite | overwrite | Overwrites codepoints starting at index with value. May grow the bin when value extends past the end. |
concat | concat, concat_list | Appends one string, or each element of a &[&str] slice, in order. |
append | append | Appends value. Unicode/DBCS-aware, unlike the legacy operations::append. |
prepend | prepend | Prepends value. Unicode/DBCS-aware, unlike the legacy operations::prepend. |
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 | pad_end | Right-pads with pad_string up to target_length codepoints. |
repeat | repeat | Repeats the bin count times. count must be non-negative. |
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 | replace | Replaces the first occurrence of needle with replacement. |
replace_all | replace_all | Replaces every occurrence of needle with replacement. |
upper / lower | upper, lower | Uppercases or lowercases the bin. |
case_fold | case_fold | Applies locale-independent case folding, for comparison keys. |
normalize_nfc | normalize_nfc | Normalizes the bin to Unicode Normalization Form C (NFC). Already-normalized strings are unchanged. |
trim / trim_start / trim_end | trim, trim_start, trim_end | Removes whitespace from both ends, the start, or the end. |
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. |
Type conversion
to_string converts an Integer, Float, Boolean, String, or Blob bin to its string representation. It fails with BinTypeError for any other bin type.
let rec = client .operate(&WritePolicy::default(), &key, &[str_op::to_string("n")]) .await?;println!("{:?}", rec.bins.get("n"));📖 API reference:
operations::string::to_string
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, extract the leaf first with operations::lists::get_by_index or operations::maps::get_by_key, using the same CdtContext, then convert client-side. Alternatively, compose expressions::string::to_string with expressions::lists::get_by_index or expressions::maps::get_by_key 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:
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: anOption<Vec<Value>>in submission order, one entry per operation.Client::operate()always populates this. For a plain read (Client::get()), it’sNoneunlesspopulate_positional_resultsis set on the policy’sbase_policy(defaultfalse). A modify operation that returns no value of its own contributesValue::Nilat its index, so positions line up exactly with the operation list you submitted.Record::bins: anIndexMap<String, Value>keyed by bin name.Value::Nilresults are dropped frombinsrather 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 inValue::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.
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::Intlet 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(), which returns Option<ResultCode>, following the client’s normal 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:
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|Error::sub_code|Error::server_error_detail|server_error::sub_code
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. 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 for general CdtContext error behavior.
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|operations::cdt_context::ctx_map_key
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 or expressions::maps::get_by_key, 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.
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|operations::exp::read_exp
to_string never accepts a CdtContext, on either surface. See 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.
Deprecations
- The legacy
operations::append(bin)andoperations::prepend(bin)are deprecated for String bins only, in favor ofoperations::string::append/prepend, which are Unicode/DBCS-aware. The legacy pair does a raw byte concatenation and doesn’t supportStringPolicyorCdtContext. - 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.
Before switching write paths in production:
- Audit affected bins for valid UTF-8 and repair any that fail. See Repair legacy String bins.
- Roll out the switch per namespace or set and monitor for failures.
- If failures appear in production, the immediate mitigation is to revert that bin’s write path to the legacy
operations::append/prependcall while you complete the repair.
Next steps
- String operations reference: full semantics, index-bounds behavior, and error codes for all 37 operations
- String expressions reference
- String examples
- String operations and UTF-8 validation
- Bin operations: the Rust client’s
Client::operate()guide for CDT and other bin operations - Error handling: the
aerospike::Error/ResultCodepattern for the Rust client - Error codes: full server status code list, including String-operation-specific entries
- API reference (Rust)