Recipes

Application-managed indexes

Coordinate a logical uniqueness check and publish primary/index records in one atomic batch.

Understand the boundary

ZeeDB has no secondary-index or uniqueness-constraint API. You can store an application index in another collection, but your application owns validation, concurrency coordination, migration, and repair.

For an email lookup, a possible model is:

CollectionKeyValue
usersUser IDEncoded user document
users_by_emailApplication-canonical emailUser ID bytes

Choose a canonicalization policy appropriate for your application. ZeeDB will not normalize the key for you.

Coordinate the logical operation

A uniqueness check and a write are separate API calls. Protect their whole sequence with an application mutex and ensure every mutation path for that invariant uses it.

mu.Lock()
defer mu.Unlock()

_, err := db.Get(ctx, "users_by_email", []byte(email))
if err == nil {
    return errors.New("email already in use")
}
if !errors.Is(err, zeedb.ErrNotFound) {
    return err
}

This sketch assumes a new primary key has also been checked under the same lock, inputs have been validated, and encodedUser has already been produced. It is not an SDK transaction callback.

Publish both records together

err = db.WriteBatch(ctx, []zeedb.Mutation{
    {Collection: "users", Key: []byte(userID), Value: encodedUser},
    {Collection: "users_by_email", Key: []byte(email), Value: []byte(userID)},
})

The batch makes the two resulting mutations visible atomically. The application mutex coordinates the earlier uniqueness check. Neither property replaces the other.

Hide the raw DB behind your repository if other callers might bypass the mutex. The process-level file lock does not coordinate goroutine-level application invariants.

Updates and deletes

An email change must read/validate the previous user, verify ownership of index entries, reject a conflicting new email, remove the old mapping, and write the replacement mapping with primary data in one batch. Hold the same application lock throughout.

Deleting a user should delete its owned index mapping in the same batch. A generic deletion of a guessed mapping can remove another user’s entry if ownership is not checked.

Failures and reconciliation

A validation failure leaves the batch unapplied. A write/checkpoint I/O failure is indeterminate: close/reopen and inspect both records before retrying. Do not interpret an error as proof that uniqueness was never reserved.

For repair or a migration, quiesce writers and check primary/index relationships using bounded scans. Several pages do not form one stable snapshot while other goroutines write. Plan restartable progress and make a closed-directory backup first.

Something to improve?Edit this page on GitHub