Skip to content

Commit

Permalink
Rename module and types. Add "Key-Value" qualifier (#6)
Browse files Browse the repository at this point in the history
* rename entire project to bitempura
* update README
* make args for memory.NewDB optional. flesh out example in README
* rename Find/Put to Get/Set
* rename id to key
* rename Attributes to Value. make it the interface{} type
* allow nil Values. add missing Put->Set renames
* rename Document to VersionedValue
* update model named to VersionedKV and update README
* Update README
  • Loading branch information
elh authored Jan 25, 2022
1 parent 6196154 commit d10a77b
Show file tree
Hide file tree
Showing 11 changed files with 759 additions and 768 deletions.
106 changes: 73 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,98 @@
# bitemporal ⌛
# bitempura ⌛... ⏳!

[![Go Reference](https://pkg.go.dev/badge/github.com/elh/bitemporal.svg)](https://pkg.go.dev/github.com/elh/bitemporal)
[![Build Status](https://github.com/elh/bitemporal/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/elh/bitemporal/actions/workflows/go.yml?query=branch%3Amain)
[![Go Reference](https://pkg.go.dev/badge/github.com/elh/bitempura.svg)](https://pkg.go.dev/github.com/elh/bitempura)
[![Build Status](https://github.com/elh/bitempura/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/elh/bitempura/actions/workflows/go.yml?query=branch%3Amain)

Building intuition about [bitemporal databases](https://en.wikipedia.org/wiki/Bitemporal_Modeling) by building (a toy) one for myself.
**Bitempura.DB is a simple, [in-memory](https://github.com/elh/bitempura/blob/main/memory/db.go) [bitemporal](https://en.wikipedia.org/wiki/Bitemporal_Modeling) key-value database.**

<br />

## Bitemporality

Temporal databases model time as a core aspect of storing and querying data. A bitemporal database is one that supports these orthogonal axes.
* **Valid time**: When the fact was *true* in the real world. This is the *application domain's* notion of time.
* **Transaction time**: When the fact was *recorded* in the database. This is the *system's* notion of time.

Because every fact in a bitemporal database has these two dimensions, it enables use cases like...
Because every fact in a bitemporal database has these two dimensions, it enables use cases like this:
```go
// What was Bob's balance on Jan 1 as best we knew on Jan 8? (VT = Jan 1, TT = Jan 8)
doc, err := db.Find("Bob/balance", AsOfValidTime(jan1), AsOfTransactionTime(jan8))
// We initialize a DB and start using it like an ordinary key-value store.
db, err := memory.NewDB()
err := db.Set("Bob/balance", 100)
val, err := db.Get("Bob/balance")
err := db.Delete("Alice/balance")
// and so on...

// But what was it on Jan 1 as best we now know? (VT = Jan 1, TT = now)
doc2, err := db.Find("Bob/balance", AsOfValidTime(jan1))
// We later learn that Bob had a temporary pending charge we missed from Dec 30 to Jan 3. (VT start = Dec 30, VT end = Jan 3)
// Retroactively record it! This does not change his balance today nor does it destroy any history we had about that period.
err := db.Set("Bob/balance", 90, WithValidTime(dec30), WithEndValidTime(jan3))

// We just learned that Bob had a temporary charge from Dec 30 to Jan 3 (VT start = Dec 30, VT end = Jan 3).
// Retroactively add it.
err := db.Put("Bob/balance", Attributes{"dollars": 90}, WithValidTime(dec30), WithEndValidTime(jan3))
// We can at any point seamlessly ask questions about the real world past AND database record past!
// "What was Bob's balance on Jan 1 as best we knew on Jan 8?" (VT = Jan 1, TT = Jan 8)
val, err := db.Get("Bob/balance", AsOfValidTime(jan1), AsOfTransactionTime(jan8))

// And let's double check all of our transactions and known states
// More time passes and more corrections are made... When trying to make sense of what happened last month, we can ask again:
// "But what was it on Jan 1 as best we now know?" (VT = Jan 1, TT = now)
val, err := db.Get("Bob/balance", AsOfValidTime(jan1))

// And while we are at it, let's double check all of our transactions and known states for Bob's balance.
versions, err := db.History("Bob/balance")
```
*See [full exampes](https://github.com/elh/bitempura/blob/main/memory/db_examples_test.go)

Using a bitemporal database allows you to offload management of temporal application data (valid time) and data versions (transaction time) from your code and onto infrastructure. This provides a universal "time travel" capability across models in the database. Adopting bitemporality is proactive because by the time you realize you need to update (or have already updated) data, it may be too late. Context may already be lost or painful to reconstruct manually.

See [in memory reference implementation](https://github.com/elh/bitemporal/blob/main/memory/db.go)
Using a bitemporal database allows you to offload management of temporal application data (valid time) and data versions (transaction time) from your code and onto infrastructure. This provides a universal "time travel" capability across models in the database. Adopting these capabilities proactively is valuable because by the time you realize you need to update (or have already updated) data, it may be too late. Context may already be lost or painful to reconstruct manually.

### Design
<br />

* Initial DB API is inspired by XTDB (and Datomic).
* Record layout is inspired by Snodgrass' SQL implementations.
## Design

```go
// DB for bitemporal data.
//
// Temporal control options
// On writes: WithValidTime, WithEndValidTime
// On reads: AsOfValidTime, AsOfTransactionTime
// Temporal control options.
// ReadOpt's: AsOfValidTime, AsOfTransactionTime.
// WriteOpt's: WithValidTime, WithEndValidTime.
type DB interface {
// Find data by id (as of optional valid and transaction times).
Find(id string, opts ...ReadOpt) (*Document, error)
// Get data by key (as of optional valid and transaction times).
Get(key string, opts ...ReadOpt) (*VersionedKV, error)
// List all data (as of optional valid and transaction times).
List(opts ...ReadOpt) ([]*Document, error)
// Put stores attributes (with optional start and end valid time).
Put(id string, attributes Attributes, opts ...WriteOpt) error
// Delete removes attributes (with optional start and end valid time).
Delete(id string, opts ...WriteOpt) error

// History returns versions by descending end transaction time, descending end valid time
History(id string) ([]*Document, error)
List(opts ...ReadOpt) ([]*VersionedKV, error)
// Set stores value (with optional start and end valid time).
Set(key string, value Value, opts ...WriteOpt) error
// Delete removes value (with optional start and end valid time).
Delete(key string, opts ...WriteOpt) error

// History returns all versioned key-values for key by descending end transaction time, descending end valid time.
History(key string) ([]*VersionedKV, error)
}

// VersionedKV is a transaction time and valid time versioned key-value. Transaction and valid time starts are inclusive
// and ends are exclusive. No two VersionedKVs for the same key can overlap both transaction time and valid time.
type VersionedKV struct {
Key string
Value Value

TxTimeStart time.Time // inclusive
TxTimeEnd *time.Time // exclusive
ValidTimeStart time.Time // inclusive
ValidTimeEnd *time.Time // exclusive
}

// Value is the user-controlled data associated with a key (and valid and transaction time information) in the database.
type Value interface{}
```

See [TODO](https://github.com/elh/bitemporal/blob/main/TODO.md)
* DB interface is inspired by XTDB (and Datomic).
* Storage model is inspired by Snodgrass' SQL implementations.

<br />

## Author

I'm learning about [bitemporal databases](https://en.wikipedia.org/wiki/Bitemporal_Modeling) and thought the best way to build intuition about their internal design was by building a simple one for myself. My goals are:
* Sharing a viable, standalone key-value store lib
* Creating artifacts to teach others about temporal data
* Launching off this to new tools for gracefully extending existing SQL databases with bitemporality

Bitempura was the name of my time travelling shrimp. RIP 2049-2022. 🦐

See [TODO](https://github.com/elh/bitempura/blob/main/TODO.md) for more.
26 changes: 15 additions & 11 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
## TODO:
- [x] [API v1 done](https://github.com/elh/bitemporal/blob/main/db.go). [In memory implementation](https://github.com/elh/bitemporal/blob/main/memory/db.go)
- [x] Find
- [x] [API v1 done](https://github.com/elh/bitempura/blob/main/db.go). [In-memory implementation](https://github.com/elh/bitempura/blob/main/memory/db.go)
- [x] Get
- [x] List
- [x] Put
- [x] Set
- [x] Delete
- [x] [XTDB, Robinhood example tests pass](https://github.com/elh/bitemporal/blob/main/memory/db_examples_test.go)
- [x] [XTDB, Robinhood example tests pass](https://github.com/elh/bitempura/blob/main/memory/db_examples_test.go)
- [x] Split out in-memory implementation
- [x] History API?
- [ ] Separate "db" and "storage" models. first pass was blending XTDB APIs with Snodgrass style records and things are getting muddled.
- Storage layer will inform choices for querying ability at DB layer.
- [ ] Should data read and write APIs return tx time and valid time context at all?
- [ ] Consider common option handling, common repo test harness later. (Split out in-memory implementation follow on)
- [ ] SQL backed implementation
- [ ] Document new intuition about mutations + the 2D time graph
- [ ] ReadOpt's for History
- [ ] Thread safe writes
- [ ] Exported ReadOpt and WriteOpt handling
- [ ] Exported DB test harness
- [ ] Visualizations. Interactive?

Candidates
- [ ] Write about new intuition about mutations + the 2D time graph
- [ ] Valid time management as a custom "version rule"?
- [ ] "Domain time"?
- [ ] Explore geographical map idea. 2D of data + transaction time => 3 dimensions?
- [ ] Separate "db" and "storage" models? first pass was blending XTDB APIs with Snodgrass style records and things are getting muddled. Storage layer will inform choices for querying ability at DB layer.
- [ ] Should data read and write APIs return tx time and valid time context at all?
- [ ] SQL backed implementation?
- [ ] Consider Datomic accumulate and retract event style. Immutable storage layer?
- [ ] Visualizations. Interactive?
26 changes: 13 additions & 13 deletions db.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package bitemporal
package bitempura

import (
"time"
Expand All @@ -7,20 +7,20 @@ import (
// DB for bitemporal data.
//
// Temporal control options.
// On writes: WithValidTime, WithEndValidTime.
// On reads: AsOfValidTime, AsOfTransactionTime.
// ReadOpt's: AsOfValidTime, AsOfTransactionTime.
// WriteOpt's: WithValidTime, WithEndValidTime.
type DB interface {
// Find document by id as of specified times.
Find(id string, opts ...ReadOpt) (*Document, error)
// List all documents as of specified times.
List(opts ...ReadOpt) ([]*Document, error)
// Put stores attributes with optional configured valid times.
Put(id string, attributes Attributes, opts ...WriteOpt) error
// Delete removes attributes with optional configured valid times.
Delete(id string, opts ...WriteOpt) error
// Get data by key (as of optional valid and transaction times).
Get(key string, opts ...ReadOpt) (*VersionedKV, error)
// List all data (as of optional valid and transaction times).
List(opts ...ReadOpt) ([]*VersionedKV, error)
// Set stores value (with optional start and end valid time).
Set(key string, value Value, opts ...WriteOpt) error
// Delete removes value (with optional start and end valid time).
Delete(key string, opts ...WriteOpt) error

// History returns versions by descending end transaction time, descending end valid time
History(id string) ([]*Document, error)
// History returns all versioned key-values for key by descending end transaction time, descending end valid time.
History(key string) ([]*VersionedKV, error)
}

type WriteOptions struct {
Expand Down
5 changes: 3 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// Package bitemporal contains experiments with bitemporal data
package bitemporal
// Package bitempura contains experiments with bitemporal data.
// It defines an interface for bitemporal key-value databases.
package bitempura
37 changes: 17 additions & 20 deletions document.go
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
package bitemporal
package bitempura

import (
"errors"
"time"
)

// Document is the core data type. Transaction and valid time starts are inclusive and ends are exclusive
type Document struct {
// TODO(elh): Separate "db" model and "storage" model. The breakdown of "documents" and assignment of tx times is
// an internal detail that is implementation specific
ID string
TxTimeStart time.Time
TxTimeEnd *time.Time
ValidTimeStart time.Time
ValidTimeEnd *time.Time
Attributes Attributes
// VersionedKV is a transaction time and valid time versioned key-value. Transaction and valid time starts are inclusive
// and ends are exclusive. No two VersionedKVs for the same key can overlap both transaction time and valid time.
type VersionedKV struct {
Key string
Value Value

TxTimeStart time.Time // inclusive
TxTimeEnd *time.Time // exclusive
ValidTimeStart time.Time // inclusive
ValidTimeEnd *time.Time // exclusive
}

// Attributes is the user-controlled data tracked by the database.
type Attributes map[string]interface{}
// Value is the user-controlled data associated with a key (and valid and transaction time information) in the database.
type Value interface{}

// Validate a document
func (d *Document) Validate() error {
if d.ID == "" {
return errors.New("id is required")
// Validate a versioned key-value
func (d *VersionedKV) Validate() error {
if d.Key == "" {
return errors.New("key is required")
}
if d.TxTimeStart.IsZero() {
return errors.New("transaction time start cannot be zero value")
Expand All @@ -47,8 +47,5 @@ func (d *Document) Validate() error {
return errors.New("valid time start must be before end")
}
}
if d.Attributes == nil {
return errors.New("attributes cannot be null")
}
return nil
}
2 changes: 1 addition & 1 deletion errors.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package bitemporal
package bitempura

import "errors"

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/elh/bitemporal
module github.com/elh/bitempura

go 1.17

Expand Down
Loading

0 comments on commit d10a77b

Please sign in to comment.