package
0.4.14
Repository: https://github.com/criticalstack/e2d.git
Documentation: pkg.go.dev

# README

e2db

e2db is an experimental abstraction layer built on top of etcd providing an ORM-like interface. It is heavily influenced by the design of storm.

Table of Contents

Getting Started

Open a database

import (
    "log"

    "github.com/criticalstack/e2d/pkg/e2db"
)

func main() {
    db, err := e2db.New(&e2db.Config{
        ClientAddr: ":2379",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}

Since e2db relies on the etcd clientv3, the connection must call the Close() method when finished.

Configuration

NameDescription
ClientAddrThe address for the etcd client server. This should not specify the URL parts like scheme as that will be built automatically.
NamespaceA namespace can be provided to transparently prefix all keys and isolate them from other non-e2db keys that may be in the database.
CertFileClient cert
KeyFileClient key
CAFileTrusted CA cert

To connect to an etcd server that has mTLS client authentication, all of the following values must be provided: CertFile, KeyFile, and CAFile. This will also ensure that the appropriate scheme of https is used when generating the ClientURL from the provided ClientAddr.

Error handling

e2db uses the package github.com/pkg/errors for handling errors. For example, a query that does not return rows will returned the wrapped error type ErrNoRows, so the function errors.Cause must be called to get the underlying type for comparison:

if errors.Cause(err) == e2db.ErrNoRows {
    // handle ErrNoRows error
}

Usage

Define a table

Table schema is defined by defining structs:

type User struct {
    ID       int `e2db:"increment"`
    Name     string `e2db:"index"`
    Email    string `e2db:"unique"`
    Role     string `e2db:"index,required"`
    Enabled  bool `e2db:"index"`
    Created  time.Time
}

Struct tags provide flexible ways of defining indexes or constraints:

TagDescription
idDefines a field as the primary key
incrementDefines a field as the primary key and automatically increments the value starting from 1
indexCreates an index for the field value
uniqueCreates an index for the field value along with a unique constraint
requiredField must have a value provided

Table metadata is stored the first time data is added for a table to ensure that other operations will not violate the table schema that has been established. Other important table-specific metadata includes table-level locks and auto-incrementing field information.

Index metadata is stored along with the table also and is modified in the same operation as the data (i.e. the cost of building the index is amortized with the operation).

So internally the table starts look like this:

KeyValue Description
/<namespace>/User/_tablegob-encoded table metadata
/<namespace>/User/_table/ID/lastlast increment value
/<namespace>/User/_table/lockN/A
/<namespace>/User/_index/Name/<value>/<pk>full key for the indexed item
/<namespace>/User/_index/Email/<value>full key for the indexed item
/<namespace>/User/_index/Role/<value>/<pk>full key for the indexed item
/<namespace>/User/_index/Created/<value>/<pk>full key for the indexed item

where an index key/value exists for every item that is indexed. In other words, for a table with schema like User, 5 rows will result in 20 key/value pairs being stored given the above configuration for User to satisfy building all the defined indexes.

Create a table object

Creating a table object can be achieved by passing in a concrete type for the defined table:

users := db.Table(new(User))

This can now be used as a reference to refer to that table. Under the hood, e2db is using this to lazily store and check any subsequent operations to match an existing schema (stored in the table metadata) with the one passed in. Checking this schema ensures that a table schema other than one already defined for a table will result in an error.

Insert a new object

user := User{
    Name: "Smoot Wellington",
    Email: "[email protected]",
    Role: "user",
    Enabled: true,
    Created: time.Now()
}
err := users.Insert(&user)

In this case there is an auto-incrementing field for ID so after the call to Insert the value for user.ID will be set (before it will be the zero value).

Fetch one object

Using the tag id or increment designates a field to be the tables primary key:

var u User
err := users.Find("ID", 1, &u)

Getting a single object back by index is accomplished the same way:

err := users.Find("Name", "Smoot Wellington", &u)

Fetch multiple objects

var u []User
err := users.Find("Role", "user", &u)

Or simply fetch all objects in a table:

err := users.All(&u)

Fetch multiple objects sorted by index

To sort by index in ascending order:

var u []User
err := users.OrderBy("Name").Find("Role", "user", &u)

For descending, simply call Reverse():

err := users.OrderBy("Name").Reverse().Find("Role", "user", &u)

Update an object

user.Role = "admin"
err := users.Update(&user)

Delete one object

err := users.Delete("ID", 1)

Delete multiple objects

err := users.Delete("Role", "user")

Drop a table

Table metadata is stored in the database to ensure that the types match before an operation is performed. If a table has changed or no longer needed it might need to be dropped so a new table can replace it:

err := users.Drop()

This can be used to help migrate from one schema version to another.

Advanced Usage

Transactions

Transactions can be used to reduce the amount of table locking that is occurring. This is helpful when doing bulk insert/update/delete operations:

err := users.Tx(func(tx *Tx) error {
    for _, row := range rows {
        if err := tx.Insert(row); err != nil {
	    return err
	}
    }
    return nil
})

In this case, only one lock will be acquired for the duration of the transaction.

Query filtering

err := users.Filter(q.Eq("Enabled", false)).Find("Role", "user", &u)

err := users.Filter(
    q.And(
        q.Eq("Enabled", false),
	q.Not("Name", "superadmin")
    )
).Find("Role", "user", &u)

err := users.Limit(5).Find("Role", "user", &u)

Distributed locks

Distributed locking is a powerful feature made possible by etcd. Arbitrary locks can be established based upon the key string passed to db.Lock(), which allows for any node using e2db to synchronize.

func syncSomething() error {
    unlock, err := db.Lock("sync/something", 30 * time.Second)
    if err != nil {
        return err
    }
    defer unlock()

    // do stuff

    return nil
}

An easier way to coordinate with distributed locks is simply racing for new object creation. This ends up being very useful in situations where, for example, you have multiple machines that need to share the same TLS cert/key pair for a web application. Something like this could be done to ensure that only the first machine that won the race for the lock will generate the TLS cert/key and then store that in e2db for the other instances to use:

type SharedFile struct {
    Path string `e2db:"id"`
    Mode os.FileMode
    Data []byte
}

err := db.Table(new(SharedFile)).Tx(func(tx *e2db.Tx) error {
    var files []*Files
    if err := tx.All(&files); err != nil {
        if errors.Cause(err) != e2db.ErrNoRows {
            return err
        }

        // If this is the first machine the TLS cert/key files won't exist, so
        // we must create them. This will only ever happen once.
        cert, key, err := generateTLS()
        if err != nil {
            return err
        }
        files = append(files, &SharedFile{"/tls.crt", 0600, cert})
        files = append(files, &SharedFile{"/tls.key", 0600, key})
    }

    // write the files to disk and insert into the SharedFile table
    for _, f := range files {
        if err := tx.Insert(f); err != nil {
            return err
        }
        if err := ioutil.WriteFile(f.Path, f.Data, f.Mode); err != nil {
            return err
        }
    }
    return nil
})

Table encryption

Table objects can optionally be encrypted with AES-256 GCM.

err := db.Table(new(User), e2db.WithEncryption("mySecretKey"))

This will encrypt any objects that are stored in this table, however, there are few caveats for usage:

  • No table metadata is stored to distinguish between encrypted/unecrypted objects, so one must be careful when setting up table encryption on a client.
  • Table metadata and indexes are not encrypted. The object is encrypted/signed with strong encryption, but the table metadata is plaintext and indexes are non-cryptographically hashed. Indexes in e2db use sha512-256, so while not plaintext, they are not cryptographically secure. This just means that using tags like index or unique should not be used on data that should be kept secret.
  • This feature is only helpful in very very specific use cases. Standard encryption-at-rest procedures should be considered before using e2db table encryption.

# Packages

No description provided by the author
No description provided by the author
No description provided by the author

# Functions

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Constants

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Variables

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
TODO(chris): could probably do something cool like automatic schema migrations.
No description provided by the author
No description provided by the author

# Interfaces

No description provided by the author
No description provided by the author

# Type aliases

No description provided by the author
No description provided by the author