CouchSet

Provisioning and transactions

Create resources explicitly, plan index drift, and use transaction or CAS safety.

CouchSet never creates scopes, collections, or indexes just because you imported a definition or bound a model. Run DDL with admin credentials when you mean to. Transactions and CAS cover multi-document and single-document safety separately from provisioning.

Explicit provisioning

await db.ensureCollections();
await db.ensureIndexes({ waitForIndexes: true });

These need Manage Scopes and query-index privileges.

HelperBehavior
ensureCollectionsCreates missing named scopes, then collections. Concurrent already-exists is OK. Never updates, renames, or drops. Does not create _default. Named collections in _default are supported. Conflicting settings for the same target fail.
ensureIndexesCREATE INDEX IF NOT EXISTS only — never compares, changes, drops, or rebuilds an existing index

Singleton / legacy models also support indexStatements(), ensureIndexes(), and top-level static ensureIndexes where applicable.

Dynamic registration

const reports = await db.registerModel(reportDefinition, {
  provision: { collections: true, indexes: true, waitForIndexes: true },
});

Binding db.model(definition) alone still performs no I/O. Only an explicit provision option (or the ensure/plan helpers above) touches the cluster.

Index drift

const plan = await db.planIndexes(); // create | matching | replace
await db.applyIndexPlan(plan);
await db.applyIndexPlan(plan, { dropReplaced: true });

When a live definition drifts, the plan creates a versioned physical replacement and waits for it to become online before an old index can be dropped. That avoids an unindexed window. Dropping the old index is affirmative (dropReplaced: true).

Search indexes use the parallel planSearchIndexes / applySearchIndexPlan path (see Search, vector, and geospatial).

CLI counterparts (collections plan|apply, indexes plan|apply, search-indexes plan|apply) live under Operations and CLI — still review-then-apply, no silent drops.

Transactions

await db.transaction(async (tx) => {
  const sessions = tx.model(sessionDefinition);
  const session = await sessions.getById(id);
  if (session) await sessions.remove(session);
});

Transaction-bound models expose attempt-scoped get / insert / replace / remove. Inserts need an explicit ID.

Couchbase may retry the callback. Do not send email, fire webhooks, or perform other irreversible external effects inside it — do that after the transaction resolves, or use an outbox / idempotency key. Hooks and validation can also repeat. Commit-ambiguous errors surface to the caller; CouchSet does not claim success or re-run external work.

CAS

const current = await model.getWithCas(id);
await model.replaceIfCas(id, nextValue, current.cas);

const result = await model.consumeOnce(id, current.cas);
// { status: 'consumed' } | { status: 'missing' } | { status: 'conflict' }

consumeOnce issues exactly one conditional remove. It does not re-read and retry with a fresh CAS.

Hydrated save({ cas }) and related replace options are covered in Relationships and hydration.

Test fixtures

const { model, cleanup } = await createCouchsetTestFixture(db, definition);
try {
  // isolated named scope/collection via registerModel + provision
} finally {
  await cleanup(); // drops fixture collection (and generated scope when applicable)
}

Fixture cleanup is the only helper that drops a resource. Always run cleanup() in teardown.

Pitfalls

  • Expecting db.model(...) or import to provision collections/indexes.
  • Using ensureIndexes to “fix” drifted definitions — use planIndexes / applyIndexPlan.
  • Side effects inside db.transaction callbacks (retries).
  • Assuming consumeOnce retries on conflict.
  • Leaving test fixtures without cleanup().

See also

On this page