Building xdb
xdb is a Go SDK that provides a consistent abstraction layer over multiple databases/storage engines. xdb is designed, for application developers, to simplify data modeling, ingestion, and querying.
Not all databases are created equal. Each database comes with its own tradeoffs. xdb allows developers to build, scale, and maintain applications without worrying about the underlying database infrastructure and operations. By providing a consistent data and APIs, xdb can be used to "layer" different databases to combine their capabilities and work around their limitations.
Inspiration
The idea of xdb data model originated while implementing an ingestion worker for Dgraph. Dgraph provides a N-Quad mutation API to insert or update data. The simplicity of the N-Quad format allowed implementation of a generic worker that could ingest any N-Quad. Adding new domain models was as simple as adding a domain-to-nquad mapping. There were no SQL migrations, no query changes, etc.
How can we borrow this simplicity and apply it all data ingestion and querying usecases?
Why worry about the underlying database management, build & maintain SQL queries, etc.?
What if we build an abstraction that allows developers to:
- focus on just modeling their domain, ingesting data, and querying it?
- swap out the underlying database without any changes to queries?
- switch between row-oriented and column-oriented storage engines?
- combine capabilities of different databases to work around their individual limitations?
- use multiple types of databases - relational, graph, search, time-series, etc. in a single application?
N-Quads
The N-Quad is a very simple, yet powerful, way to represent attributes and relationships of most domain models. Dgraph extended the N-Quad format to support additional metadata for relationships.
Here's a reduced example of a social network domain model:
<Post:9bsv0s5ocl6002kdg0fg> <title> "Hello World" .
<Post:9bsv0s5ocl6002kdg0fg> <body> "...content..." .
<Post:9bsv0s5ocl6002kdg0fg> <author> <User:1> .
<Post:9bsv0s5ocl6002kdg0fg> <tags> <Tag:golang> .
<Post:9bsv0s5ocl6002kdg0fg> <tags> <Tag:xdb> .
<User:1> <follows> <User:2> (since="2009-11-10T23:00:00Z") .
<User:2> <follows> <User:5> (since="2010-12-10T23:00:00Z") .
<User:3> <follows> <Tag:golang> .
Data Model
XDB has a simple data model inspired by N-Quads.
Key
Key
is the primary key of a record. A Key
is a combination of Kind
and ID
.
Attribute
Attribute
is a tuple of key
, name
and a value
. It's structurally similar to N-Quads.
Edge
Edge
is an attribute whose value is a Key
of target record. Edges can have additional metadata about the relation.
Value
XDB supports all Go primitive types as values. Custom types can be used by implementing the encoding.BinaryMarshaler
or json.Marshaler
interfaces.
Schema
XDB has a strict, yet easy to use, schema. A good mental model for the schema is to think of it as a intended shape of application domain, similar to defining a Terraform resource.
The schema acts as a intermediary between the application domain model and the underlying database(s).
An example of a schema for Twitter:
schema:
- kind: Tweet
attributes:
- name: text
type: string
- name: created_at
type: time
edges:
- name: author
type: User
- name: mentions
type: User
array: true
- name: contains
type: Hashtag
array: true
- kind: User
attributes:
- name: name
type: string
edges:
- name: follows
type: User
array: true
metadata:
- name: since
type: time
Developer Experience
Using XDB is as simple as defining a schema, initializing a store, and writing data.
For this example, we will use BadgerDB as the underlying storage engine.
import (
"context"
"time"
"github.com/xdb-dev/xdb"
"github.com/xdb-dev/xdb/kv/badger"
)
func main() {
schema, _ := schema.Load("schema.yaml")
store, _ := badger.NewStore(
badger.Dir("/tmp/xdb"),
badger.Schema(schema),
)
// Create a new post
post := xdb.NewKey("Post", "9bsv0s45jdj002vjlkt0")
author := xdb.NewKey("User", "9bsv0s3p32i002qvnf50")
tags := []*xdb.Key{
xdb.NewKey("Tag", "xdb"),
xdb.NewKey("Tag", "database"),
}
// Create post's attributes and edges
attrs := []*xdb.Tuple{
xdb.NewAttr(post, "title", xdb.String("Introduction to xdb")),
xdb.NewAttr(post, "body", xdb.String("...")),
xdb.NewAttr(post, "published_at", xdb.Time(time.Now())),
xdb.NewEdge(post, "author", author),
xdb.NewEdge(post, "tags", tags[0]),
xdb.NewEdge(post, "tags", tags[1]),
}
// Save attributes
if err := store.PutTuples(ctx, attrs...); err != nil {
panic(err)
}
}