couch

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

couch

A small, dependency-light wrapper around the kivik CouchDB driver that makes connections, models, and design documents easier to work with.

go get github.com/invopop/couch

What it gives you

  • couch.Config / couch.Client — configure a connection from parts and namespace every database behind a prefix.
  • couch.Model — an embeddable base document that manages _id, _rev, attachments, and created_at / updated_at timestamps. couch.Document is the same without timestamps.
  • couch.Store / couch.Fetch / couch.Delete — persistence helpers that stamp timestamps, track the revision, and map errors to couch.ErrNotFound / couch.ErrAlreadyExists.
  • couch.Design / couch.View — declare design documents and sync them idempotently (only rewritten when their views/filters change).
  • couch/changes — consume CouchDB _changes feeds with a resumable, persisted cursor and a worker pool.
  • invopop/at — millisecond-precision timestamps used by couch.Model. It lived here as couch/at until it was moved out, so anything can use the type without depending on this library.

Usage

package main

import (
	"context"
	"errors"
	"log"

	"github.com/invopop/couch"
)

// Embed couch.Model to get _id/_rev + created_at/updated_at for free.
type Widget struct {
	couch.Model
	Name string `json:"name"`
}

func main() {
	ctx := context.Background()

	conf := couch.NewConfig("myapp") // databases are namespaced as myapp_<name>
	conf.Host = "localhost"
	conf.Username, conf.Password = "admin", "secret"

	client, err := couch.New(conf)
	if err != nil {
		log.Fatal(err)
	}
	if err := client.Ping(ctx); err != nil {
		log.Fatal(err)
	}

	db := client.DB("widgets") // resolves to "myapp_widgets"
	if err := client.Create(ctx, db); err != nil {
		log.Fatal(err)
	}

	w := &Widget{Name: "gadget"}
	w.SetID("widget-1")
	if err := couch.Store(ctx, db, w); err != nil { // sets timestamps + _rev
		log.Fatal(err)
	}

	got := &Widget{}
	got.SetID("widget-1")
	switch err := couch.Fetch(ctx, db, got); {
	case errors.Is(err, couch.ErrNotFound):
		log.Println("not found")
	case err != nil:
		log.Fatal(err)
	}
}
Design documents
d := couch.NewDesign("widgets")
d.SetView("by_name", &couch.View{
	Map: `function(doc) { if (doc.name) { emit(doc.name, null); } }`,
})
if err := client.SyncDesigns(ctx, db, []*couch.Design{d}); err != nil {
	log.Fatal(err)
}
Change feeds

See changes for consuming a database's _changes feed with a resumable cursor. Sharding helpers (ShardByYear, …) live in the root package.

Next describes each change rather than just naming it, so deletions are reported instead of dropped:

for {
    c, err := feed.Next(ctx)
    if err != nil { /* retry */ }
    if c.ID == "" { break } // feed stopped

    if c.Deleted {
        // A tombstone: nothing left to fetch, and anything mirroring this
        // document downstream should drop its copy.
        continue
    }
    // load and process c.ID
}

That covers documents removed by hand in the database as much as those the application deleted. Model.Deleted carries the same _deleted marker, so a tombstone read from a feed, a view or a fetch arrives as a model rather than a bare ID — and Store refuses to persist one, since writing _deleted back is how a document gets deleted.

License

Apache 2.0 — see LICENSE.

Documentation

Overview

Package couch provides a small wrapper around the kivik CouchDB driver to make it easier to configure connections and persist models.

Index

Constants

View Source
const DefaultSeparator = "_"

DefaultSeparator determines the character(s) to use to separate a prefix from the database name. Underscore is the default to be consistent with SQL table naming and JSON attributes.

Variables

View Source
var (
	// ErrNotFound is returned when a document does not exist.
	ErrNotFound = errors.New("not found")
	// ErrAlreadyExists is returned on a document revision conflict.
	ErrAlreadyExists = errors.New("already exists")
)

Errors returned by the persistence helpers, wrapping the underlying kivik/CouchDB failure. Match them with errors.Is.

Functions

func Delete

func Delete(ctx context.Context, db *kivik.DB, m Persistable) error

Delete removes the provided persistable object from the database.

func Fetch

func Fetch(ctx context.Context, db *kivik.DB, d Persistable) error

Fetch wraps around the kivik persistence methods to update the model with the data from the database or raise an error if it does not exist.

func FetchModel

func FetchModel(ctx context.Context, db *kivik.DB, m Persistable) error

FetchModel is the now deprecated way of Fetching a model from the database.

func RevAfter

func RevAfter(a, b string) bool

RevAfter returns true if CouchDB revision a is newer than revision b. Revisions have the format "<seq>-<hash>", e.g. "13-a9e7c9c1...". This must be used instead of direct string comparison (a > b) because lexicographic ordering breaks when sequence numbers cross digit boundaries (e.g. "9-xxx" > "13-xxx" is true lexicographically but incorrect).

func Store

func Store(ctx context.Context, db *kivik.DB, m Persistable) error

Store attempts to persist the provided persistable object to the database.

A model carrying the `_deleted` marker (see Model.Deleted) is refused: putting it back is how CouchDB deletes a document, and a save that silently deletes instead would be a nasty way to find that out. Deletions read from a change feed are meant to be reacted to, not written; use Delete to remove a document.

func StoreModel

func StoreModel(ctx context.Context, db *kivik.DB, m Persistable) error

StoreModel is the deprecated way of persisting updates to the database and simply wraps around the Store method.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client wraps around a kivik package Client and helps make it easier to configure the connection and prepare the database.

func New

func New(conf *Config, opts ...kivik.Option) (*Client, error)

New provides a new instance of the default CouchDB client. This call will block until the server responds or the context causes a timeout.

func (*Client) Create

func (c *Client) Create(ctx context.Context, db *kivik.DB, opts ...kivik.Option) error

Create checks that the database already exists, or creates it if required.

func (*Client) DB

func (c *Client) DB(name string) *kivik.DB

DB is used to provide a database instance at the provided name.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

Ping attempts to establish a connection and will block and retry for any timeouts or network errors. This is recommended to be used after the client has been initialized to ensure the connection is ready to use.

func (*Client) SyncDesigns

func (c *Client) SyncDesigns(ctx context.Context, db *kivik.DB, designs []*Design) error

SyncDesigns ensures the database is up to date with the latest design documents.

type Config

type Config struct {
	Scheme    string `json:"scheme"`
	Host      string `json:"host"`
	Port      string `json:"port"`
	Username  string `json:"username"`
	Password  string `json:"password"`
	Prefix    string `json:"prefix"`
	Separator string `json:"separator"`
}

Config is used to define the connection details to a database.

func NewConfig

func NewConfig(prefix string) *Config

NewConfig generates a new configuration instance and requires a prefix so that we have a nice namespace before all database names.

type Design

type Design struct {
	Model
	Language string `json:"language"`

	// Options stores additional options for the design document.
	Options map[string]any `json:"options,omitempty"`

	Filters map[string]string `json:"filters,omitempty"`
	Views   map[string]*View  `json:"views,omitempty"`
}

Design represents the special design documents used to query documents using pre-defined indexes. Only designs that have changed will be synchronised with the database using a simple SHA256 comparison algorithm that checks for changes in the views.

func NewDesign

func NewDesign(name string) *Design

NewDesign builds a new design document instance using the provided name.

func (*Design) Checksum

func (d *Design) Checksum() string

Checksum generates a SHA256 sum by joining all the filters and views together to form a single string and running the result through the digest algorithm. The result is a Hexadecimal string.

func (*Design) SetFilter

func (d *Design) SetFilter(name string, filter string)

SetFilter adds the provided filter to the design document. Existing filters with the same name will be replaced.

func (*Design) SetView

func (d *Design) SetView(name string, view *View)

SetView adds the provided view with the given name.

func (*Design) Sync

func (d *Design) Sync(ctx context.Context, db *kivik.DB) error

Sync compares the checksums of the current design and the previous

type Document

type Document struct {
	ID  string `json:"_id,omitempty"`
	Rev string `json:"_rev,omitempty"`
}

Document is a simplified object that conforms to the Persistable implementation. Unlike the Model implementation, it doesn't include any timestamping.

func (*Document) GetID

func (d *Document) GetID() string

GetID provides the current document ID.

func (*Document) GetRev

func (d *Document) GetRev() string

GetRev provides the document's revision.

func (*Document) Persisted

func (d *Document) Persisted() bool

Persisted returns true if the revision has been set, a value that should always be provided by the database server.

func (*Document) SetID

func (d *Document) SetID(id string)

SetID sets the model's ID

func (*Document) SetRev

func (d *Document) SetRev(rev string)

SetRev update's the documents's revision ID. The should mainly be used by persistence layers.

func (*Document) UpdateTimestamps

func (d *Document) UpdateTimestamps()

UpdateTimestamps in the context of a simple CouchDB document does nothing.

type Model

type Model struct {
	ID  string `json:"_id,omitempty"`
	Rev string `json:"_rev,omitempty"`

	// Attachments keeps together the special list of attachments that belong to
	// model. Without this placeholder, they'll get deleted after an update.
	Attachments kivik.Attachments `json:"_attachments,omitempty"`

	// Deleted reflects CouchDB's `_deleted` marker: true means this is a
	// tombstone rather than a document. It is set when reading a deletion —
	// from a change feed started WithDeletions, or anywhere else a tombstone
	// surfaces — so consumers can react to a document being removed, including
	// one deleted by hand in the database.
	//
	// It is read-only in practice: Store refuses to persist a model carrying
	// it, since writing `_deleted` back is how a document gets deleted and
	// having that happen as a side effect of a save would be surprising. Use
	// Delete instead.
	Deleted bool `json:"_deleted,omitempty"`

	CreatedAt at.Timestamp `json:"created_at"`
	UpdatedAt at.Timestamp `json:"updated_at"`
}

Model is a standard representation of a model to be stored in CouchDB that takes care of the ID, Revision, and adds timestamps.

func (*Model) GetCreatedAt

func (m *Model) GetCreatedAt() at.Timestamp

GetCreatedAt provides the model's CreatedAt timestamp in situations where the model is being treated as an interface. May be zero if the model has not been prepared for persistence.

func (*Model) GetDeleted added in v0.2.0

func (m *Model) GetDeleted() bool

GetDeleted reports whether the model represents a deleted document, so persistence layers can check the marker through an interface rather than depending on the concrete type.

func (*Model) GetID

func (m *Model) GetID() string

GetID provides the document's ID

func (*Model) GetRev

func (m *Model) GetRev() string

GetRev provides the document's Revision

func (*Model) GetUpdatedAt

func (m *Model) GetUpdatedAt() at.Timestamp

GetUpdatedAt provides the model's UpdatedAt timestamp in situations where the model is being treated as an interface. May be zero if the model has not been prepared for persistence.

func (*Model) Persisted

func (m *Model) Persisted() bool

Persisted returns true if the revision has been set, a value that should always be provided by the database server.

func (*Model) Reset

func (m *Model) Reset()

Reset sets the rev, created and update at timestamps to zero usually so that the same model can be persisted to multiple database without having the revision and timestamps copied between instances.

func (*Model) SetID

func (m *Model) SetID(id string)

SetID sets the model's ID

func (*Model) SetRev

func (m *Model) SetRev(rev string)

SetRev update's the model's revision ID. The should mainly be used by persistence layers.

func (*Model) UpdateTimestamps

func (m *Model) UpdateTimestamps()

UpdateTimestamps ensures the model's created and update at stamps are set.

type Persistable

type Persistable interface {
	UpdateTimestamps()
	GetID() string
	GetRev() string
	SetID(string)
	SetRev(string)
}

Persistable defines what is expected from a model for it to be persisted to the database.

type ShardByYear

type ShardByYear struct {
	// contains filtered or unexported fields
}

ShardByYear implements database sharding by year.

func NewShardByYear

func NewShardByYear(name string, start int) *ShardByYear

NewShardByYear provides a common implementation of sharding databases according to the year encoded in a time-based UUID (versions 1, 6 and 7). For this to work, the model must implement a ShardValue method that returns its UUID as a string:

```

func (m *Model) ShardValue() interface{} {
  return m.ID
}

```

Shards are always ordered by reverse chronological order, so the newest shards are listed first.

This slightly naive implementation assumes that the service will be restarted and migrated at least once per year so that the following year's shard is prepared.

If the ShardValue is a string that is not a UUID, it is assumed to be the name of the shard and used directly.

func NewShardByYearWithStatic

func NewShardByYearWithStatic(name string, start int, static string) *ShardByYear

NewShardByYearWithStatic creates a new shard rule that supports time-based UUIDs **and** random/name-based ones (versions 3, 4 and 5).

The "static" parameter enables support for non-time-based IDs that will be persisted to a fixed shard instead of by year. This is useful to being able to distinguish between data that is always relevant (static) and data that becomes less useful over time.

func (*ShardByYear) Key

func (s *ShardByYear) Key(v any) (string, error)

Key converts the shardable key's value into a usable shard. The value must be a string: either a UUID (whose timestamp determines the year) or an already-prepared shard name.

func (*ShardByYear) List

func (s *ShardByYear) List() []string

List provides an array of acceptable shards

func (*ShardByYear) Template

func (s *ShardByYear) Template() string

Template provides the base name into which the shard will be inserted.

type ShardRules

type ShardRules interface {
	// Template provides the base name into which the shard will be inserted.
	// It must be an fmt.Sprintf format string with a single %s verb where
	// the shard name is substituted (see NewShards).
	Template() string

	// List provides an array of acceptable shards
	List() []string

	// Key provides a usable string from a shardable value.
	Key(v interface{}) (string, error)
}

ShardRules provides the basic details we require to properly handle sharding of a type of object.

type Shardable

type Shardable interface {
	ShardValue() interface{}
}

Shardable defines what we expect from a document, entity, or model that we intend to persist to the database.

type Shards

type Shards struct {
	// contains filtered or unexported fields
}

Shards is a special implementation of sharding at the software level. The aim is to make it easier to manage a set of separate CouchDB databases each of which is used according to sharding details provided.

func NewShards

func NewShards(client *Client, rules ShardRules) *Shards

NewShards instantiates a new sharding wrapper. Databases cannot be assigned dynamically, a complete list of databases must be prepared.

func (*Shards) For

func (s *Shards) For(m Shardable) (*kivik.DB, error)

For determines which shard to use for the provided "Shardable" model.

func (*Shards) Get

func (s *Shards) Get(name string) *kivik.DB

Get provides the requested database instance, or nil if the name is invalid.

func (*Shards) List

func (s *Shards) List() []*kivik.DB

List provides an array of database objects, in the original shard order.

func (*Shards) Map

func (s *Shards) Map() map[string]*kivik.DB

Map provides the map of names to databases to be used for sharding. This is especially useful for performing migrations but caution should be taken in any other scenario as order is not guaranteed!

func (*Shards) Names

func (s *Shards) Names() []string

Names provides the complete list of shard names in use.

type View

type View struct {
	Map    string `json:"map"`
	Reduce string `json:"reduce,omitempty"`
}

View is a basic definition of a CouchDB view.

Directories

Path Synopsis
Package changes makes it easier to listen to CouchDB change feeds.
Package changes makes it easier to listen to CouchDB change feeds.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL