Skip to content

Latest commit

 

History

History
416 lines (331 loc) · 10.4 KB

File metadata and controls

416 lines (331 loc) · 10.4 KB

Couchbase Sub-Document Operations

Platform-agnostic reference shared by all server-connection-* and sdk-patterns-* skills.

Sub-document operations target specific paths within a JSON document. They avoid the cost of fetching or transmitting the full document — critical for large documents or high-frequency partial updates.

Table of Contents

  1. When to Use Sub-Document vs Full-Document
  2. Lookup Operations (Read)
  3. Mutation Operations (Write)
  4. Array Operations
  5. Counters
  6. Multi-Operation Batching
  7. Language Examples

When to Use Sub-Document vs Full-Document

Scenario Use
Read the whole document collection.get()
Read 1–3 fields from a large document lookup_in
Update 1–2 fields without changing others mutate_in
Increment a counter mutate_in + SD.increment
Append to a bounded array mutate_in + SD.array_append
Replace the entire document collection.replace() / upsert()
Atomic compare-and-swap collection.replace() with CAS

Sub-document operations are atomic per-path. Multiple paths in a single mutate_in call are applied atomically together.


Lookup Operations (Read)

from couchbase.collection import Collection
import couchbase.subdocument as SD

# Fetch specific fields
result = collection.lookup_in("user_alice", [
    SD.get("email"),
    SD.get("address.city"),
    SD.get("preferences.theme")
])
email  = result.content_as[str](0)
city   = result.content_as[str](1)
theme  = result.content_as[str](2)

# Check field existence without fetching value
result = collection.lookup_in("user_alice", [
    SD.exists("premiumSubscription")
])
has_premium = result.exists(0)

# Get document count (array length)
result = collection.lookup_in("order_1001", [
    SD.count("lineItems")
])
item_count = result.content_as[int](0)

Path syntax

Paths use dot notation for nested fields and [n] for array indices:

"address.city"           → doc.address.city
"tags[0]"                → doc.tags[0]
"events[-1]"             → doc.events (last element)
"metadata.tags[2].name"  → doc.metadata.tags[2].name

Mutation Operations (Write)

from couchbase.options import MutateInOptions
from datetime import timedelta

# Upsert a field (create or replace)
collection.mutate_in("user_alice", [
    SD.upsert("status", "active")
])

# Insert a field (fails if field already exists)
collection.mutate_in("user_alice", [
    SD.insert("createdAt", "2024-01-15T10:00:00Z")
])

# Replace a field (fails if field does not exist)
collection.mutate_in("user_alice", [
    SD.replace("email", "alice@newdomain.com")
])

# Remove a field
collection.mutate_in("user_alice", [
    SD.remove("temporaryToken")
])

# Multiple mutations in one atomic operation
collection.mutate_in("user_alice", [
    SD.upsert("status", "active"),
    SD.upsert("updatedAt", "2024-06-01T10:00:00Z"),
    SD.remove("resetToken"),
    SD.increment("loginCount", 1)
])

# With durability and expiry
collection.mutate_in("session_abc", [
    SD.upsert("lastSeen", "2024-06-01T10:00:00Z")
], MutateInOptions(
    expiry=timedelta(hours=1),
    durability=ServerDurability(DurabilityLevel.Majority)
))

upsert_full — create document if missing

# Create the document if it doesn't exist, then apply mutations
collection.mutate_in("user_new", [
    SD.upsert("name", "Bob"),
    SD.upsert("status", "pending")
], MutateInOptions(store_semantics=StoreSemantics.UPSERT))

StoreSemantics:

  • REPLACE (default) — document must exist
  • UPSERT — create if missing, update if exists
  • INSERT — fail if document already exists

Array Operations

# Append to end of array
collection.mutate_in("order_1001", [
    SD.array_append("events", {"type": "shipped", "ts": "2024-06-01"})
])

# Prepend to start of array
collection.mutate_in("feed_alice", [
    SD.array_prepend("items", {"postId": "post_999", "ts": "2024-06-01"})
])

# Insert at a specific index
collection.mutate_in("playlist_1", [
    SD.array_insert("tracks[2]", {"id": "track_42", "title": "New Song"})
])

# Add to array only if value is unique (set semantics)
collection.mutate_in("user_alice", [
    SD.array_addunique("tags", "premium")
])
# Raises PathExistsException if "premium" is already in tags

# Append multiple elements at once
collection.mutate_in("order_1001", [
    SD.array_append("events", [
        {"type": "packed", "ts": "2024-06-01T09:00:00Z"},
        {"type": "shipped", "ts": "2024-06-01T10:00:00Z"}
    ], multi=True)
])

Counters

increment and decrement are atomic — safe for concurrent updates without read-modify-write:

# Increment by 1
collection.mutate_in("stats_global", [
    SD.increment("pageViews", 1)
])

# Increment by arbitrary delta
collection.mutate_in("product_42", [
    SD.increment("viewCount", 5)
])

# Decrement
collection.mutate_in("inventory_42", [
    SD.decrement("stock", 1)
])

# Initialize counter to 0 if missing, then increment
collection.mutate_in("stats_global", [
    SD.upsert("newUsers", 0),
    SD.increment("newUsers", 1)
])

The result of increment/decrement is the new value after the operation:

result = collection.mutate_in("stats", [SD.increment("hits", 1)])
new_count = result.content_as[int](0)

Multi-Operation Batching

Combine lookups and mutations across multiple documents using the SDK's bulk API:

# Bulk lookup_in
from couchbase.result import MultiMutationResult

keys = ["user_alice", "user_bob", "user_carol"]
results = {}
for key in keys:
    results[key] = collection.lookup_in(key, [SD.get("email"), SD.get("status")])

For high-throughput scenarios, use async SDK methods (acollection.lookup_in) to pipeline operations.


Language Examples

Node.js

const { MutateInSpec, LookupInSpec } = require('couchbase');

// Lookup
const result = await collection.lookupIn("user_alice", [
    LookupInSpec.get("email"),
    LookupInSpec.exists("premiumSubscription")
]);
const email = result.content[0].value;
const hasPremium = result.content[1].value;

// Mutate
await collection.mutateIn("user_alice", [
    MutateInSpec.upsert("status", "active"),
    MutateInSpec.increment("loginCount", 1),
    MutateInSpec.arrayAppend("recentLogins", new Date().toISOString())
]);

Java

import com.couchbase.client.java.kv.*;

// Lookup
LookupInResult result = collection.lookupIn("user_alice", List.of(
    LookupInSpec.get("email"),
    LookupInSpec.exists("premiumSubscription")
));
String email = result.contentAs(0, String.class);
boolean hasPremium = result.exists(1);

// Mutate
collection.mutateIn("user_alice", List.of(
    MutateInSpec.upsert("status", "active"),
    MutateInSpec.increment("loginCount", 1L),
    MutateInSpec.arrayAppend("recentLogins", List.of(Instant.now().toString()))
));

Go

ops := []gocb.LookupInSpec{
    gocb.GetSpec("email", nil),
    gocb.ExistsSpec("premiumSubscription", nil),
}
result, _ := collection.LookupIn("user_alice", ops, nil)
var email string
result.ContentAt(0, &email)

mutOps := []gocb.MutateInSpec{
    gocb.UpsertSpec("status", "active", nil),
    gocb.IncrementSpec("loginCount", 1, nil),
}
collection.MutateIn("user_alice", mutOps, nil)

Python

from couchbase.subdocument import get, exists, upsert, increment, array_append

# Lookup
result = collection.lookup_in("user_alice", [
    get("email"),
    exists("premiumSubscription")
])
email = result.content_as[str](0)
has_premium = result.exists(1)

# Mutate
collection.mutate_in("user_alice", [
    upsert("status", "active"),
    increment("loginCount", 1),
    array_append("recentLogins", ["2024-01-01T00:00:00Z"])
])

.NET

using Couchbase.KeyValue;

// Lookup
var result = await collection.LookupInAsync("user_alice", specs =>
    specs.Get("email").Exists("premiumSubscription"));
var email = result.ContentAs<string>(0);
var hasPremium = result.Exists(1);

// Mutate
await collection.MutateInAsync("user_alice", specs =>
    specs.Upsert("status", "active")
         .Increment("loginCount", 1)
         .ArrayAppend("recentLogins", new[] { DateTime.UtcNow.ToString("o") }));

Rust

use couchbase::subdoc::{LookupInSpec, MutateInSpec};

// Lookup
let result = collection.lookup_in(
    "user_alice",
    vec![
        LookupInSpec::get("email", None),
        LookupInSpec::exists("premiumSubscription", None),
    ],
    None,
).await?;
let email: String = result.content_as(0)?;
let has_premium: bool = result.exists(1);

// Mutate
collection.mutate_in(
    "user_alice",
    vec![
        MutateInSpec::upsert("status", "active", None)?,
        MutateInSpec::increment("loginCount", 1, None)?,
    ],
    None,
).await?;

Scala

import com.couchbase.client.scala.kv._

// Lookup
val result = collection.lookupIn("user_alice", Seq(
  LookupInSpec.get("email"),
  LookupInSpec.exists("premiumSubscription")
)).get
val email = result.contentAs[String](0).get
val hasPremium = result.exists(1)

// Mutate
collection.mutateIn("user_alice", Seq(
  MutateInSpec.upsert("status", "active"),
  MutateInSpec.increment("loginCount", 1),
  MutateInSpec.arrayAppend("recentLogins", Seq("2024-01-01T00:00:00Z"))
)).get

PHP

use Couchbase\LookupGetSpec;
use Couchbase\LookupExistsSpec;
use Couchbase\MutateUpsertSpec;
use Couchbase\MutateIncrementSpec;
use Couchbase\MutateArrayAppendSpec;

// Lookup
$result = $collection->lookupIn("user_alice", [
    new LookupGetSpec("email"),
    new LookupExistsSpec("premiumSubscription")
]);
$email = $result->content(0);
$hasPremium = $result->exists(1);

// Mutate
$collection->mutateIn("user_alice", [
    new MutateUpsertSpec("status", "active"),
    new MutateIncrementSpec("loginCount", 1),
    new MutateArrayAppendSpec("recentLogins", ["2024-01-01T00:00:00Z"])
]);

Ruby

# Lookup
result = collection.lookup_in("user_alice", [
  Couchbase::LookupInSpec.get("email"),
  Couchbase::LookupInSpec.exists("premiumSubscription")
])
email = result.content(0)
has_premium = result.exists?(1)

# Mutate
collection.mutate_in("user_alice", [
  Couchbase::MutateInSpec.upsert("status", "active"),
  Couchbase::MutateInSpec.increment("loginCount", 1),
  Couchbase::MutateInSpec.array_append("recentLogins", ["2024-01-01T00:00:00Z"])
])