commit 7875d65f2aa33e0d47d7073ec94b99ea12ab75f8 Author: Josh Baker Date: Tue Jul 19 15:15:00 2016 -0700 initial commit diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4f2ee4d --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..58f5819 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2016 Josh Baker + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6f115d --- /dev/null +++ b/README.md @@ -0,0 +1,314 @@ +

+BuntDB +
+Build Status +Code Coverage +GoDoc +Version +

+ +==== + +BuntDB is a low-level, in-memory, key/value store in pure Go. +It persists to disk, is ACID compliant, and uses locking for multiple +readers and a single writer. It supports custom indexes and geospatial +data. It's ideal for projects that need a dependable database and favor +speed over data size. + +The desire to create BuntDB stems from the need for a new embeddable +database for [Tile38](https://github.com/tidwall/tile38). One that can work +both as a performant [Raft Store](https://github.com/tidwall/raft-boltdb), +and a Geospatial database. + +Much of the API is inspired by the fantastic [BoltDB](https://github.com/boltdb/bolt), +an amazing key/value store that can handle terrabytes of data on disk. + +Features +======== + +- In-memory database for [fast reads and writes](https://github.com/tidwall/raft-boltdb#benchmarks) +- Embeddable with a [simple API](https://godoc.org/github.com/tidwall/buntdb) +- [Spatial indexing](#spatial-indexes) for up to 4 dimensions; Useful for Geospatial data +- - Create [custom indexes](#custom-indexes) for any data type +- [Built-in types](#built-in-types) that are easy to get up & running; String, Uint, Int, Float +- Flexible [iteration](#iterating) of data; ascending, descending, and ranges +- Durable append-only file format. Adopts the [Redis AOF](http://redis.io/topics/persistence) process +- Option to evict old items with an [expiration](#data-expiration) TTL +- Tight codebase, under 1K loc using the `cloc` command. +- ACID semantics with locking [transactions](#transactions) that support rollbacks + +Getting Started +=============== + +## Installing + +To start using BuntDB, install Go and run `go get`: + +```sh +$ go get github.com/tidwall/buntdb +``` + +This will retrieve the library. + + +## Opening a database + +The primary object in BuntDB is a `DB`. To open or create your +database, use the `buntdb.Open()` function: + +```go +package main + +import ( + "log" + + "github.com/tidwall/buntdb" +) + +func main() { + // Open the data.db file. It will be created if it doesn't exist. + db, err := buntdb.Open("data.db") + if err != nil { + log.Fatal(err) + } + defer db.Close() + + ... +} +``` + +It's important to note that BuntDB does not currently support file locking, so avoid accessing the database from multiple processes. + +## Transactions +All reads and writes must be performed from inside a transaction. BuntDB can have one write transaction opened at a time, but can have many concurrent read transactions. Each transaction maintains a stable view of the database. In other words, once a transaction has begun, the data for that transaction cannot be changed by other transactions. + +Transactions run in a function that exposes a `Tx` object, which represents the transaction state. While inside a transaction, all database operations should be performed using this object. You should never access the origin `DB` object while inside a transaction. Doing so may have side-effects, such as blocking your application. + +When a transaction fails, it will roll back, and revert all chanages that ocurred to the database during that transaction. There's a single return value that you can use to close the transaction. For read/write transactions, returning an error this way will force the transaction to roll back. When a read/write transaction succeeds all changes are persisted to disk. + +### Read-only Transactions +A read-only transaction should be used when you don't need to make changes to the data. The advantage of a read-only transaction is that there can be many running concurrently. + +```go +err := db.View(func(tx *buntdb.Tx) error { + ... + return nil +}) +``` + +### Read/write Transactions +A read/write transaction is used when you need to make changes to your data. There can only be one read/write transaction running at a time. So make sure you close it as soon as you are done with it. + +```go +err := db.Update(func(tx *buntdb.Tx) error { + ... + return nil +}) +``` + +## Setting and getting key/values + +To set a value you must open a read/write tranasction: + +```go +err := db.Update(func(tx *buntdb.Tx) error { + err := tx.Set("mykey", "myvalue", nil) + return err +}) +``` + + +To get the value: + +```go +err := db.View(func(tx *buntdb.Tx) error { + val, err := tx.Get("mykey") + if err != nil{ + return err + } + fmt.Printf("value is %s\n", val) + return nil +}) +``` + +Getting non-existent values will case an `ErrNotFound` error. + +### Iterating +All keys/value pairs are ordered in the database by the key. To iterate over the keys: + +```go +err := db.View(func(tx *buntdb.Tx) error { +err := tx.Ascend("", func(key, value string) bool{ + fmt.Printf("key: %s, value: %s\n", key, value) + }) + return err +}) +``` + +There is also `AscendGreaterOrEqual`, `AscendLessThan`, `AscendRange`, `Descend`, `DescendLessOrEqual`, `DescendGreaterThan`, and `DescendRange`. Please see the [documentation](https://godoc.org/github.com/tidwall/buntdb) for more information on these functions. + + +## Custom Indexes +Initially all data is stored in a single [B-tree](https://en.wikipedia.org/wiki/B-tree) with each item having one key and one value. All of these items are ordered by the key. This is great for quickly getting a value from a key or [iterating](#iterating) over the keys. + +You can also create custom indexes that allow for ordering and [iterating](#iterating) over values. A custom index also uses a B-tree, but it's more flexible because it allows for custom ordering. + +For example, let's say you want to create an index for ordering names: + +```go +db.CreateIndex("names", "*", buntdb.IndexString) +``` + +This will create an index named `names` which stores and sorts all values. The second parameter is a pattern that is used to filter on keys. A `*` wildcard argument means that we want to accept all keys. `IndexString` is a built-in function that performs case-insensitive ordering on the values + +Now you can add various names: + +```go +db.Update(func(tx *buntdb.Tx) error { + tx.Set("user:0:name", "tom", nil) + tx.Set("user:1:name", "Randi", nil) + tx.Set("user:2:name", "jane", nil) + tx.Set("user:4:name", "Janet", nil) + tx.Set("user:5:name", "Paula", nil) + tx.Set("user:6:name", "peter", nil) + tx.Set("user:7:name", "Terri", nil) + return nil +}) +``` + +Finally you can iterate over the index: + +```go +db.View(func(tx *buntdb.Tx) error { + tx.Ascend("names", func(key, val string) bool { + fmt.Printf(buf, "%s %s\n", key, val) + return true + }) + return nil +}) +``` +The output should be: +``` +user:2:name jane +user:4:name Janet +user:5:name Paula +user:6:name peter +user:1:name Randi +user:7:name Terri +user:0:name tom +``` + +The pattern parameter can be used to filter on keys like this: + +```go +db.CreateIndex("names", "user:*", buntdb.IndexString) +``` + +Now only items with keys that have the prefix `user:` will be added to the `names` index. + + +### Built-in types +Along with `IndexString`, there is also `IndexInt`, `IndexUint`, and `IndexFloat`. +These are built-in types for indexing. You can choose to use these or create your own. + +So to create an index that is numerically ordered on an age key, we could use: + +```go +db.CreateIndex("ages", "user:*:age", buntdb.IndexInt) +``` + +And then add values: + +```go +db.Update(func(tx *buntdb.Tx) error { + tx.Set("user:0:age", "35", nil) + tx.Set("user:1:age", "49", nil) + tx.Set("user:2:age", "13", nil) + tx.Set("user:4:age", "63", nil) + tx.Set("user:5:age", "8", nil) + tx.Set("user:6:age", "3", nil) + tx.Set("user:7:age", "16", nil) + return nil +}) +``` + +```go +db.View(func(tx *buntdb.Tx) error { + tx.Ascend("ages", func(key, val string) bool { + fmt.Printf(buf, "%s %s\n", key, val) + return true + }) + return nil +}) +``` + +The output should be: +``` +user:6:name 3 +user:5:name 8 +user:2:name 13 +user:7:name 16 +user:0:name 35 +user:1:name 49 +user:4:name 63 +``` + +### Spatial Indexes +BuntDB has support for spatial indexes by storing rectangles in an [R-tree](https://en.wikipedia.org/wiki/R-tree). An R-tree is organized in a similar manner as a [B-tree](https://en.wikipedia.org/wiki/B-tree), and both are balanaced trees. But, an R-tree is special because it can operate on data that is in multiple dimensions. This is super handy for Geospatial applications. + +To create a spatial index use the `CreateSpatialIndex` function: + +```go +db.CreateSpatialIndex("fleet", "fleet:*:pos", buntdb.IndexRect) +``` + +Then `IndexRect` is a built-in function that converts rect strings to a format that the R-tree can use. It's easy to use this function out of the box, but you might find it better to create a custom one that renders from a different format, such as [Well-known text](https://en.wikipedia.org/wiki/Well-known_text) or [GeoJSON](http://geojson.org/). + +To add some lon,lat points to the `fleet` index: + +```go +db.Update(func(tx *buntdb.Tx) error { + tx.Set("fleet:0:pos", "[-115.567 33.532]", nil) + tx.Set("fleet:1:pos", "[-116.671 35.735]", nil) + tx.Set("fleet:2:pos", "[-113.902 31.234]", nil) + return nil +}) +``` + +And then you can run the `Intersects` function on the index: + +```go +db.View(func(tx *buntdb.Tx) error { + tx.Intersects("fleet", "[-117 30],[-112 36]", func(key, val string) bool { + ... + return true + }) + return nil +}) +``` + +This will get all three positions. + +The bracket syntax `[-117 30],[-112 36]` is unique to BuntDB, and it's how the built-in rectangles are processed, but you are not limited to this syntax. Whatever Rect function you choose to use during `CreateSpatialIndex` will be used to process the paramater, in this case it's `IndexRect`. + + +### Data Expiration +Items can be automatically evicted by using the `SetOptions` object in the `Set` function to set a `TTL`. + +```go +db.Update(func(tx *buntdb.Tx) error { + tx.Set("mykey", "myval", &buntdb.SetOptions{Expires:true, TTL:time.Second}) + return nil +}) +``` + +Now `mykey` will automatically be deleted after one second. You can remove the TTL by setting the value again with the same key/value, but with the options parameter set to nil. + +## Contact +Josh Baker [@tidwall](http://twitter.com/tidwall) + +## License + +BuntDB source code is available under the MIT [License](/LICENSE). diff --git a/buntdb.go b/buntdb.go new file mode 100644 index 0000000..4d25b5c --- /dev/null +++ b/buntdb.go @@ -0,0 +1,1357 @@ +// Package buntdb implements a low-level in-memory key/value store in pure Go. +// It persists to disk, is ACID compliant, and uses locking for multiple +// readers and a single writer. Bunt is ideal for projects that need +// a dependable database, and favor speed over data size. +package buntdb + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/tidwall/btree" + "github.com/tidwall/rtree" +) + +var ( + // ErrTxNotWritable is returned when performing a write operation on a + // read-only transaction. + ErrTxNotWritable = errors.New("tx not writable") + // ErrTxClosed is returned when committing or rolling back a transaction + // that has already been committed or rolled back. + ErrTxClosed = errors.New("tx closed") + // ErrNotFound is returned when an item or index is not in the database. + ErrNotFound = errors.New("not found") + // ErrInvalid is returned when the database file is an invalid format. + ErrInvalid = errors.New("invalid database") + // ErrDatabaseClosed is returned when the database is closed. + ErrDatabaseClosed = errors.New("database closed") + // ErrIndexExists is returned when an index already exists in the database. + ErrIndexExists = errors.New("index exists") + // ErrInvalidOperation is returned when an operation cannot be completed. + ErrInvalidOperation = errors.New("invalid operation") +) + +// Iterator allows callers of Ascend* or Descend* to iterate in-order +// over portions of an index. When this function returns false, iteration +// will stop and the associated Ascend* or Descend* function will immediately +// return. +type Iterator func(key, val string) bool + +// DB represents a collection of key-value pairs that persist on disk. +// Transactions are used for all forms of data access to the DB. +type DB struct { + mu sync.RWMutex // the gatekeeper for all fields + file *os.File // the underlying file + bufw *bufio.Writer // only write to this + keys *btree.BTree // a tree of all item ordered by key + exps *btree.BTree // a tree of items ordered by expiration + idxs map[string]*index // the index trees. + exmgr bool // indicates that expires manager is running. + flushes int // a count of the number of disk flushes + closed bool // set when the database has been closed + aoflen int // the number of lines in the aof file +} + +// exctx is a simple b-tree context for ordering by expiration. +type exctx struct { + db *DB +} + +// Open opens a database at the provided path. +// If the file does not exist then it will be created automatically. +func Open(path string) (*DB, error) { + db := &DB{} + db.keys = btree.New(16, nil) + db.exps = btree.New(16, &exctx{db}) + db.idxs = make(map[string]*index) + var err error + // Hardcoding 0666 as the default mode. + db.file, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return nil, err + } + if err := db.load(); err != nil { + db.file.Close() + return nil, err + } + db.bufw = bufio.NewWriter(db.file) + // start the background manager. + go db.backgroundManager() + return db, nil +} + +// Close releases all database resources. +// All transactions must be closed before closing the database. +func (db *DB) Close() error { + db.mu.Lock() + defer db.mu.Unlock() + if db.closed { + return ErrDatabaseClosed + } + db.closed = true + return db.file.Close() +} + +// index represents a b-tree or r-tree index and also acts as the +// b-tree/r-tree context for itself. +type index struct { + btr *btree.BTree // contains the items + rtr *rtree.RTree // contains the items + name string // name of the index + pattern string // a required key pattern + less func(a, b string) bool // less comparison function + rect func(item string) (min, max []float64) // rect from string function + db *DB // the origin database +} + +// CreateIndex builds a new index and populates it with items. +// The items are ordered in an b-tree and can be retrieved using the +// Ascend* and Descend* methods. +// An error will occur if an index with the same name already exists. +// +// When a pattern is provided, the index will be populated with +// keys that match the specified pattern. +// The less function compares if string 'a' is less than string 'b'. +// It allows for indexes to create custom ordering. It's possible +// that the strings may be textual or binary. It's up to the provided +// less function to handle the content format and comparison. +// There are some default less function that can be used such as +// IndexString, IndexBinary, etc. +func (db *DB) CreateIndex(name, pattern string, + less func(a, b string) bool) error { + return db.createIndex(name, pattern, less, nil) +} + +// CreateSpatialIndex builds a new index and populates it with items. +// The items are organized in an r-tree and can be retrieved using the +// Intersects method. +// An error will occur if an index with the same name already exists. +// +// The rect function converts a string to a rectangle. The rectangle is +// represented by two arrays, min and max. Both arrays may have a length +// between 1 and 4, and both arrays must match in length. A length of 1 is a +// one dimensional rectangle, and a length of 4 is a four dimension rectangle. +// The values of min must be less than the values of max at the same dimension. +// Thus min[0] must be less-than-or-equal-to max[0]. +// The IndexRect is a default function that can be used for the rect +// parameter. +func (db *DB) CreateSpatialIndex(name, pattern string, + rect func(item string) (min, max []float64)) error { + return db.createIndex(name, pattern, nil, rect) +} + +// createIndex is called by CreateIndex() and CreateSpatialIndex() +func (db *DB) createIndex( + name string, + pattern string, + less func(a, b string) bool, + rect func(item string) (min, max []float64), +) error { + if name == "" { + return ErrIndexExists + } + db.mu.Lock() + defer db.mu.Unlock() + if db.closed { + return ErrDatabaseClosed + } + if _, ok := db.idxs[name]; ok { + return ErrIndexExists + } + idx := &index{ + name: name, + pattern: pattern, + less: less, + rect: rect, + db: db, + } + if less != nil { + idx.btr = btree.New(16, idx) + } + if rect != nil { + idx.rtr = rtree.New(idx) + } + db.keys.Ascend(func(item btree.Item) bool { + dbi := item.(*dbItem) + if !wildcardMatch(dbi.key, idx.pattern) { + return true + } + if less != nil { + idx.btr.ReplaceOrInsert(dbi) + } + if rect != nil { + idx.rtr.Insert(dbi) + } + return true + }) + db.idxs[name] = idx + return nil +} + +// wilcardMatch returns true if str matches pattern. This is a very +// simple wildcard match where '*' matches on any number characters +// and '?' matches on any one character. +func wildcardMatch(str, pattern string) bool { + if pattern == "*" { + return true + } + return deepMatch(str, pattern) +} +func deepMatch(str, pattern string) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 { + return false + } + case '*': + return wildcardMatch(str, pattern[1:]) || + (len(str) > 0 && wildcardMatch(str[1:], pattern)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} + +// DropIndex removes an index. +func (db *DB) DropIndex(name string) error { + if name == "" { + return ErrInvalidOperation + } + db.mu.Lock() + defer db.mu.Unlock() + if db.closed { + return ErrDatabaseClosed + } + if _, ok := db.idxs[name]; !ok { + return ErrNotFound + } + delete(db.idxs, name) + return nil +} + +// Indexes returns a list of index names. +func (db *DB) Indexes() ([]string, error) { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return nil, ErrDatabaseClosed + } + names := make([]string, 0, len(db.idxs)) + for name := range db.idxs { + names = append(names, name) + } + sort.Strings(names) + return names, nil +} + +// insertIntoDatabase performs inserts an item in to the database and updates +// all indexes. If a previous item with the same key already exists, that item +// will be replaced with the new one, and return the previous item. +func (db *DB) insertIntoDatabase(item *dbItem) *dbItem { + var pdbi *dbItem + prev := db.keys.ReplaceOrInsert(item) + if prev != nil { + // A previous item was removed from the keys tree. Let's + // fully delete this item from all indexes. + pdbi = prev.(*dbItem) + if pdbi.opts != nil && pdbi.opts.ex { + // Remove it from the exipres tree. + db.exps.Delete(pdbi) + } + for _, idx := range db.idxs { + if idx.btr != nil { + // Remove it from the btree index. + idx.btr.Delete(pdbi) + } + if idx.rtr != nil { + // Remove it from the rtree index. + idx.rtr.Remove(pdbi) + } + } + } + if item.opts != nil && item.opts.ex { + // The new item has eviction options. Add it to the + // expires tree + db.exps.ReplaceOrInsert(item) + } + for _, idx := range db.idxs { + if !wildcardMatch(item.key, idx.pattern) { + continue + } + if idx.btr != nil { + // Add new item to btree index. + idx.btr.ReplaceOrInsert(item) + } + if idx.rtr != nil { + // Add new item to rtree index. + idx.rtr.Insert(item) + } + } + // we must return the previous item to the caller. + return pdbi +} + +// deleteFromDatabase removes and item from the database and indexes. The input +// item must only have the key field specified thus "&dbItem{key: key}" is all +// that is needed to fully remove the item with the matching key. If an item +// with the matching key was found in the database, it will be removed and +// returned to the caller. A nil return value means that the item was not +// found in the database +func (db *DB) deleteFromDatabase(item *dbItem) *dbItem { + var pdbi *dbItem + prev := db.keys.Delete(item) + if prev != nil { + pdbi = prev.(*dbItem) + if pdbi.opts != nil && pdbi.opts.ex { + // Remove it from the exipres tree. + db.exps.Delete(pdbi) + } + for _, idx := range db.idxs { + if idx.btr != nil { + // Remove it from the btree index. + idx.btr.Delete(pdbi) + } + if idx.rtr != nil { + // Remove it from the rtree index. + idx.rtr.Remove(pdbi) + } + } + } + return pdbi +} + +// backgroundManager runs continuously in the background and performs various +// operations such as removing expired items and syncing to disk. +func (db *DB) backgroundManager() { + flushes := 0 + t := time.NewTicker(time.Second) + defer t.Stop() + for range t.C { + stop := false + multiple := 0 + // Open a standard view. This will take a full lock of the + // database thus allowing for access to anything we need. + db.Update(func(tx *Tx) error { + if db.closed { + // the manager has stopped. exit now. + stop = true + return nil + } + if db.keys.Len() > 0 { + multiple = db.aoflen / db.keys.Len() + } + // produce a list of expired items that need removing + var remove []*dbItem + db.exps.AscendLessThan(&dbItem{ + opts: &dbItemOpts{ex: true, exat: time.Now()}, + }, func(item btree.Item) bool { + remove = append(remove, item.(*dbItem)) + return true + }) + for _, item := range remove { + if _, err := tx.Delete(item.key); err != nil { + // it's ok to get a "not found" because the + // 'Delete' method reports "not found" for + // expired items. + if err != ErrNotFound { + return err + } + } + } + // execute a disk sync. + if flushes != db.flushes { + db.file.Sync() + flushes = db.flushes + } + return nil + }) + if multiple > 4 { + db.Shrink() + } + if stop { + break + } + } +} +func (db *DB) Shrink() error { + db.mu.Lock() + if db.closed { + db.mu.Unlock() + return ErrDatabaseClosed + } + fname := db.file.Name() + tmpname := fname + ".tmp" + // the endpos is used to return to the end of the file when we are + // finished writing all of the current items. + aoflen := db.aoflen + endpos, err := db.file.Seek(0, 2) + if err != nil { + return err + } + db.mu.Unlock() + f, err := os.Create(tmpname) + if err != nil { + return err + } + defer func() { + f.Close() + os.RemoveAll(tmpname) + }() + + // we are going to read items in as chunks as to not hold up the database + // for too long. + naoflen := 0 + wr := bufio.NewWriter(f) + pivot := "" + done := false + for !done { + err := func() error { + db.mu.RLock() + defer db.mu.RUnlock() + if db.closed { + return ErrDatabaseClosed + } + n := 0 + done = true + db.keys.AscendGreaterOrEqual(&dbItem{key: pivot}, + func(item btree.Item) bool { + dbi := item.(*dbItem) + if n > 100 { + pivot = dbi.key + done = false + return false + } + dbi.writeSetTo(wr) + naoflen++ + n++ + return true + }, + ) + if err := wr.Flush(); err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + } + // We reached this far so all of the items have been written to a new tmp + // There's some more work to do by appending the new line from the aof + // to the tmp file and finally swap the files out. + err = func() error { + // We're wrapping this in a function to get the benefit of a defered + // lock/unlock. + db.mu.Lock() + defer db.mu.Unlock() + // We are going to open a new version of the aof file so that we do + // not change the seek position of the previous. This may cause a + // problem in the future if we choose to use syscall file locking. + aof, err := os.Open(fname) + if err != nil { + return err + } + defer aof.Close() + if _, err := aof.Seek(endpos, 0); err != nil { + return err + } + // Just copy all of the new commands that have occured since we + // started the shrink process. + if _, err := io.Copy(f, aof); err != nil { + return err + } + // Close all files + if err := aof.Close(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + if err := db.file.Close(); err != nil { + return err + } + // Anything failures below here is really bad. So just panic. + if err := os.Rename(tmpname, fname); err != nil { + panic(err) + } + db.file, err = os.OpenFile(fname, os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + panic(err) + } + if _, err := db.file.Seek(0, 2); err != nil { + return err + } + // reset the bufio writer + db.bufw = bufio.NewWriter(db.file) + // finally update the aoflen + db.aoflen = naoflen + (db.aoflen - aoflen) + return nil + }() + return err +} +func loadReadLine(r *bufio.Reader) (string, error) { + line, err := r.ReadBytes('\n') + if err != nil { + return "", err + } + if len(line) < 2 || line[len(line)-2] != '\r' { + return "", ErrInvalid + } + return string(line[:len(line)-2]), nil +} +func loadReadLineNum(r *bufio.Reader) (int, error) { + line, err := loadReadLine(r) + if err != nil { + return 0, err + } + n, err := strconv.ParseUint(line, 10, 64) + if err != nil { + return 0, err + } + return int(n), nil +} + +var errValidEOF = errors.New("valid eof") + +func loadReadCommand(r *bufio.Reader) ([]string, error) { + c, err := r.ReadByte() + if err != nil { + if err == io.EOF { + return nil, errValidEOF + } + return nil, err + } + if c != '*' { + return nil, ErrInvalid + } + n, err := loadReadLineNum(r) + if err != nil { + return nil, err + } + parts := make([]string, n) + for i := 0; i < len(parts); i++ { + c, err := r.ReadByte() + if err != nil { + return nil, err + } + if c != '$' { + return nil, ErrInvalid + } + n, err := loadReadLineNum(r) + if err != nil { + return nil, err + } + data := make([]byte, n) + if _, err = io.ReadFull(r, data); err != nil { + return nil, err + } + eol := make([]byte, 2) + if _, err = io.ReadFull(r, eol); err != nil { + return nil, err + } + if eol[0] != '\r' || eol[1] != '\n' { + return nil, ErrInvalid + } + parts[i] = string(data) + } + return parts, nil +} + +// load reads entries from the append only database file and fills the database. +// The file format uses the Redis append only file format, which is and a series +// of RESP commands. For more information on RESP please read +// http://redis.io/topics/protocol. The only supported RESP commands are DEL and +// SET. +func (db *DB) load() error { + r := bufio.NewReader(db.file) + for { + var item = &dbItem{} + parts, err := loadReadCommand(r) + if err != nil { + if err == errValidEOF { + break + } + if err == io.EOF { + return io.ErrUnexpectedEOF + } + return err + } + if len(parts) == 0 { + continue + } + db.aoflen++ + switch strings.ToLower(parts[0]) { + default: + return ErrInvalid + case "set": + if len(parts) < 3 || len(parts) == 4 || len(parts) > 5 { + return ErrInvalid + } + item.key, item.val = parts[1], parts[2] + if len(parts) == 5 { + if strings.ToLower(parts[3]) != "ex" { + return ErrInvalid + } + ex, err := strconv.ParseInt(parts[4], 10, 64) + if err != nil { + return err + } + dur := time.Duration(ex) * time.Second + item.opts = &dbItemOpts{ + ex: true, + exat: time.Now().Add(dur), + } + } + db.insertIntoDatabase(item) + case "del": + if len(parts) != 2 { + return ErrInvalid + } + item.key = parts[1] + db.deleteFromDatabase(item) + } + } + return nil +} + +// managed calls a block of code that is fully contained in a transaction. +// This method is intended to be wrapped by Update and View +func (db *DB) managed(writable bool, fn func(tx *Tx) error) (err error) { + var tx *Tx + tx, err = db.begin(writable) + if err != nil { + return + } + defer func() { + if err != nil { + // The caller returned an error. We must rollback. + tx.rollback() + return + } + if writable { + // Everything went well. Lets Commit() + err = tx.commit() + } else { + // read-only transaction can only roll back. + err = tx.rollback() + } + }() + tx.funcd = true + defer func() { + tx.funcd = false + }() + err = fn(tx) + return +} + +// View executes a function within a managed read-only transaction. +// When a non-nil error is returned from the function that error will be return +// to the caller of View(). +// +// Executing a manual commit or rollback from inside the function will result +// in a panic. +func (db *DB) View(fn func(tx *Tx) error) error { + return db.managed(false, fn) +} + +// Update executes a function within a managed read/write transaction. +// The transaction has been committed when no error is returned. +// In the event that an error is returned, the transaction will be rolled back. +// When a non-nil error is returned from the function, the transaction will be +// rolled back and the that error will be return to the caller of Update(). +// +// Executing a manual commit or rollback from inside the function will result +// in a panic. +func (db *DB) Update(fn func(tx *Tx) error) error { + return db.managed(true, fn) +} + +// Tx represents a transaction on the database. This transaction can either be +// read-only or read/write. Read-only transactions can be used for retrieving +// values for keys and iterating through keys and values. Read/write +// transactions can set and delete keys. +// +// All transactions must be committed or rolled-back when done. +type Tx struct { + db *DB // the underlying database. + writable bool // when false mutable operations fail. + funcd bool // when true Commit and Rollback panic. + rollbacks map[string]*dbItem // cotnains details for rolling back tx. + commits map[string]*dbItem // contains details for committing tx. +} + +// begin opens a new transaction. +// Multiple read-only transactions can be opened at the same time but there can +// only be one read/write transaction at a time. Attempting to open a read/write +// transactions while another one is in progress will result in blocking until +// the current read/write transaction is completed. +// +// All transactions must be closed by calling Commit() or Rollback() when done. +func (db *DB) begin(writable bool) (*Tx, error) { + tx := &Tx{ + db: db, + writable: writable, + } + if writable { + tx.rollbacks = make(map[string]*dbItem) + tx.commits = make(map[string]*dbItem) + } + tx.lock() + if db.closed { + tx.unlock() + return nil, ErrDatabaseClosed + } + return tx, nil +} + +// lock locks the database based on the transaction type. +func (tx *Tx) lock() { + if tx.writable { + tx.db.mu.Lock() + } else { + tx.db.mu.RLock() + } +} + +// unlock unlocks the database based on the transaction type. +func (tx *Tx) unlock() { + if tx.writable { + tx.db.mu.Unlock() + } else { + tx.db.mu.RUnlock() + } +} + +// rollbackInner handles the underlying rollback logic. +// Intended to be called from Commit() and Rollback(). +func (tx *Tx) rollbackInner() { + for key, item := range tx.rollbacks { + tx.db.deleteFromDatabase(&dbItem{key: key}) + if item != nil { + // When an item is not nil, we will need to reinsert that item + // into the database overwriting the current one. + tx.db.insertIntoDatabase(item) + } + } +} + +// commit writes all changes to disk. +// An error is returned when a write error occurs, or when a Commit() is called +// from a read-only transaction. +func (tx *Tx) commit() error { + if tx.funcd { + panic("managed tx commit not allowed") + } + if tx.db == nil { + return ErrTxClosed + } else if !tx.writable { + return ErrTxNotWritable + } + var err error + if len(tx.commits) > 0 { + // Each committed record is written to disk + for key, item := range tx.commits { + if item == nil { + (&dbItem{key: key}).writeDeleteTo(tx.db.bufw) + } else { + item.writeSetTo(tx.db.bufw) + } + } + // Flushing the buffer only once per transaction. + // If this operation fails then the write did failed and we must + // rollback. + if err = tx.db.bufw.Flush(); err != nil { + tx.rollbackInner() + } + // Increment the number of flushes. The background syncing uses this. + tx.db.flushes++ + tx.db.aoflen += len(tx.commits) + } + // Unlock the database and allow for another writable transaction. + tx.unlock() + // Clear the db field to disable this transaction from future use. + tx.db = nil + return err +} + +// rollback closes the transaction and reverts all mutable operations that +// were performed on the transaction such as Set() and Delete(). +// +// Read-only transactions can only be rolled back, not committed. +func (tx *Tx) rollback() error { + if tx.funcd { + panic("managed tx rollback not allowed") + } + if tx.db == nil { + return ErrTxClosed + } + // The rollback func does the heavy lifting. + if tx.writable { + tx.rollbackInner() + } + // unlock the database for more transactions. + tx.unlock() + // Clear the db field to disable this transaction from future use. + tx.db = nil + return nil +} + +// dbItemOpts holds various meta information about an item. +type dbItemOpts struct { + ex bool // does this item expire? + exat time.Time // when does this item expire? +} +type dbItem struct { + key, val string // the binary key and value + opts *dbItemOpts // optional meta information +} + +// writeSetTo writes an item as a single SET record to the a bufio Writer. +func (dbi *dbItem) writeSetTo(wr *bufio.Writer) { + if dbi.opts != nil && dbi.opts.ex { + ex := strconv.FormatUint( + uint64(dbi.opts.exat.Sub(time.Now())/time.Second), + 10, + ) + fmt.Fprintf(wr, + "*5\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n"+ + "$%d\r\n%s\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n", + len("set"), "set", len(dbi.key), dbi.key, + len(dbi.val), dbi.val, len("ex"), "ex", len(ex), ex, + ) + } else { + fmt.Fprintf(wr, "*3\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n", + len("set"), "set", len(dbi.key), dbi.key, len(dbi.val), dbi.val, + ) + } +} + +// writeSetTo writes an item as a single DEL record to the a bufio Writer. +func (dbi *dbItem) writeDeleteTo(wr *bufio.Writer) { + fmt.Fprintf(wr, + "*2\r\n$%d\r\n%s\r\n$%d\r\n%s\r\n", + len("del"), "del", len(dbi.key), dbi.key) +} + +// expired evaluates id the item has expired. This will always return false when +// the item does not have `opts.ex` set to true. +func (dbi *dbItem) expired() bool { + return dbi.opts != nil && dbi.opts.ex && time.Now().After(dbi.opts.exat) +} + +// MaxTime from http://stackoverflow.com/questions/25065055#32620397 +// This is a long time in the future. It's an imaginary number that is +// used for b-tree ordering. +var maxTime = time.Unix(1<<63-62135596801, 999999999) + +// expiresAt will return the time when the item will expire. When an item does +// not expire `maxTime` is used. +func (dbi *dbItem) expiresAt() time.Time { + if dbi.opts == nil || !dbi.opts.ex { + return maxTime + } + return dbi.opts.exat +} + +// Less determines if a b-tree item is less than another. This is required +// for ordering, inserting, and deleting items from a b-tree. It's important +// to note that the ctx parameter is used to help with determine which +// formula to use on an item. Each b-tree should use a different ctx when +// sharing the same item. +func (dbi *dbItem) Less(item btree.Item, ctx interface{}) bool { + dbi2 := item.(*dbItem) + switch ctx := ctx.(type) { + case *exctx: + // The expires b-tree formula + if dbi2.expiresAt().After(dbi.expiresAt()) { + return true + } + if dbi.expiresAt().After(dbi2.expiresAt()) { + return false + } + case *index: + if ctx.less != nil { + // Using an index + if ctx.less(dbi.val, dbi2.val) { + return true + } + if ctx.less(dbi2.val, dbi.val) { + return false + } + } + } + // Always fall back to the key comparison. This creates absolute uniqueness. + return dbi.key < dbi2.key +} + +// Rect converts a string to a rectangle. +// An invalid rectangle will cause a panic. +func (dbi *dbItem) Rect(ctx interface{}) (min, max []float64) { + switch ctx := ctx.(type) { + case *index: + return ctx.rect(dbi.val) + } + return nil, nil +} + +// SetOptions represents options that may be included with the Set() command. +type SetOptions struct { + // Expires indicates that the Set() key-value will expire + Expires bool + // TTL is how much time the key-value will exist in the database + // before being evicted. The Expires field must also be set to true. + // TTL stands for Time-To-Live. + TTL time.Duration +} + +// Set inserts or replaces an item in the database based on the key. +// The opt params may be used for addtional functionality such as forcing +// the item to be evicted at a specified time. When the return value +// for err is nil the operation succeded. When the return value of +// replaced is true, then the operaton replaced an existing item whose +// value will be returned through the previousValue variable. +// The results of this operation will not be available to other +// transactions until the current transaction has successfully commited. +func (tx *Tx) Set(key, value string, opts *SetOptions) (previousValue string, + replaced bool, err error) { + if tx.db == nil { + return "", false, ErrTxClosed + } else if !tx.writable { + return "", false, ErrTxNotWritable + } + item := &dbItem{key: key, val: value} + if opts != nil { + if opts.Expires { + // The caller is requesting that this item expires. Convert the + // TTL to an absolute time and bind it to the item. + item.opts = &dbItemOpts{ex: true, exat: time.Now().Add(opts.TTL)} + } + } + // Insert the item into the keys tree. + prev := tx.db.insertIntoDatabase(item) + if prev == nil { + // An item with the same key did not previously exist. Let's create a + // rollback entry with a nil value. A nil value indicates that the + // entry should be deleted on rollback. When the value is *not* nil, + // that means the entry should be reverted. + tx.rollbacks[key] = nil + } else { + // A previous item already exists in the database. Let's create a + // rollback entry with the item as the value. We need to check the map + // to see if there isn't already an item that matches the same key. + if _, ok := tx.rollbacks[key]; !ok { + tx.rollbacks[key] = prev + } + if !item.expired() { + previousValue, replaced = item.val, true + } + } + // For commits we simply assign the item to the map. We use this map to + // write the entry to disk. + tx.commits[key] = item + return previousValue, replaced, nil +} + +// Get returns a value for a key. If the item does not exist or if the item +// has expired then ErrNotFound is returned. +func (tx *Tx) Get(key string) (val string, err error) { + if tx.db == nil { + return "", ErrTxClosed + } + prev := tx.db.keys.Get(&dbItem{key: key}) + if prev == nil { + return "", ErrNotFound + } + item := prev.(*dbItem) + if item.expired() { + // The item exists in the tree, but has expired. Let's assume that + // the caller is only interested in items that have not expired. + return "", ErrNotFound + } + return item.val, nil +} + +// Delete removes an item from the database based on the item's key. If the item +// does not exist or if the item has expired then ErrNotFound is returned. +// +// Only writable transaction can be used for Delete() calls. +func (tx *Tx) Delete(key string) (val string, err error) { + if tx.db == nil { + return "", ErrTxClosed + } else if !tx.writable { + return "", ErrTxNotWritable + } + item := tx.db.deleteFromDatabase(&dbItem{key: key}) + if item == nil { + return "", ErrNotFound + } + if _, ok := tx.rollbacks[key]; !ok { + tx.rollbacks[key] = item + } + tx.commits[key] = nil + // Even though the item has been deleted, we still want to check + // if it has expired. An expired item should not be returned. + if item.expired() { + // The item exists in the tree, but has expired. Let's assume that + // the caller is only interested in items that have not expired. + return "", ErrNotFound + } + return item.val, nil +} + +// scan iterates through a specified index and calls user-defined iterator +// function for each item encountered. +// The desc param indicates that the iterator should descend. +// The gt param indicates that there is a greaterThan limit. +// The lt param indicates that there is a lessThan limit. +// The index param tells the scanner to use the specified index tree. An +// empty string for the index means to scan the keys, not the values. +// The start and stop params are the greaterThan, lessThan limits. For +// descending order, these will be lessThan, greaterThan. +// An error will be returned if the tx is closed or the index is not found. +func (tx *Tx) scan( + desc, gt, lt bool, index, start, stop string, iterator Iterator, +) error { + if tx.db == nil { + return ErrTxClosed + } + // wrap a btree specific iterator around the user-defined iterator. + iter := func(item btree.Item) bool { + dbi := item.(*dbItem) + return iterator(dbi.key, dbi.val) + } + var tr *btree.BTree + if index == "" { + // empty index means we will use the keys tree. + tr = tx.db.keys + } else { + idx := tx.db.idxs[index] + if idx == nil { + // index was not found. return error + return ErrNotFound + } + tr = idx.btr + if tr == nil { + return nil + } + } + // create some limit items + var itemA, itemB *dbItem + if gt || lt { + itemA = &dbItem{key: start} + itemB = &dbItem{key: stop} + } + // execute the scan on the underlying tree. + if desc { + if gt { + if lt { + tr.DescendRange(itemA, itemB, iter) + } else { + tr.DescendGreaterThan(itemA, iter) + } + } else if lt { + tr.DescendLessOrEqual(itemA, iter) + } else { + tr.Descend(iter) + } + } else { + if gt { + if lt { + tr.AscendRange(itemA, itemB, iter) + } else { + tr.AscendGreaterOrEqual(itemA, iter) + } + } else if lt { + tr.AscendLessThan(itemA, iter) + } else { + tr.Ascend(iter) + } + } + return nil +} + +// Ascend calls the iterator for every item in the database within the range +// [first, last], until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) Ascend(index string, iterator Iterator) error { + return tx.scan(false, false, false, index, "", "", iterator) +} + +// AscendGreaterOrEqual calls the iterator for every item in the database within +// the range [pivot, last], until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) AscendGreaterOrEqual( + index, pivot string, iterator Iterator, +) error { + return tx.scan(false, true, false, index, pivot, "", iterator) +} + +// AscendLessThan calls the iterator for every item in the database within the +// range [first, pivot), until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) AscendLessThan(index, pivot string, iterator Iterator) error { + return tx.scan(false, false, true, index, pivot, "", iterator) +} + +// AscendRange calls the iterator for every item in the database within +// the range [greaterOrEqual, lessThan), until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) AscendRange(index, greaterOrEqual, lessThan string, + iterator Iterator) error { + return tx.scan( + false, true, true, index, greaterOrEqual, lessThan, iterator, + ) +} + +// Descend calls the iterator for every item in the database within the range +// [last, first], until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) Descend(index string, iterator Iterator) error { + return tx.scan(true, false, false, index, "", "", iterator) +} + +// DescendGreaterThan calls the iterator for every item in the database within +// the range [last, pivot), until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) DescendGreaterThan(index, pivot string, iterator Iterator) error { + return tx.scan(true, true, false, index, pivot, "", iterator) +} + +// DescendLessOrEqual calls the iterator for every item in the database within +// the range [pivot, first], until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) DescendLessOrEqual(index, pivot string, iterator Iterator) error { + return tx.scan(true, false, true, index, pivot, "", iterator) +} + +// DescendRange calls the iterator for every item in the database within +// the range [lessOrEqual, greaterThan), until iterator returns false. +// When an index is provided, the results will be ordered by the item values +// as specified by the less() function of the defined index. +// When an index is not provided, the results will be ordered by the item key. +// An invalid index will return an error. +func (tx *Tx) DescendRange(index, lessOrEqual, greaterThan string, + iterator Iterator) error { + return tx.scan( + true, true, true, index, lessOrEqual, greaterThan, iterator, + ) +} + +// rect is used by Intersects +type rect struct { + min, max []float64 +} + +func (r *rect) Rect(ctx interface{}) (min, max []float64) { + return r.min, r.max +} + +// Intersects searches for rectangle items that intersect a target rect. +// The specified index must have been created by AddIndex() and the target +// is represented by the rect string. This string will be processed by the +// same bounds function that was passed to the CreateSpatialIndex() function. +// An invalid index will return an error. +func (tx *Tx) Intersects(index, bounds string, iterator Iterator) error { + if tx.db == nil { + return ErrTxClosed + } + if index == "" { + // cannot search on keys tree. just return nil. + return nil + } + // wrap a rtree specific iterator around the user-defined iterator. + iter := func(item rtree.Item) bool { + dbi := item.(*dbItem) + return iterator(dbi.key, dbi.val) + } + idx := tx.db.idxs[index] + if idx == nil { + // index was not found. return error + return ErrNotFound + } + if idx.rtr == nil { + // not an r-tree index. just return nil + return nil + } + // execute the search + var min, max []float64 + if idx.rect != nil { + min, max = idx.rect(bounds) + } + idx.rtr.Search(&rect{min, max}, iter) + return nil +} + +// Len returns the number of items in the database +func (tx *Tx) Len() (int, error) { + if tx.db == nil { + return 0, ErrTxClosed + } + return tx.db.keys.Len(), nil +} + +// Rect is helper function that returns a string representation +// of a rect. IndexRect() is the reverse function and can be used +// to generate a rect from a string. +func Rect(min, max []float64) string { + if min == nil && max == nil { + return "" + } + diff := len(min) != len(max) + if !diff { + for i := 0; i < len(min); i++ { + if min[i] != max[i] { + diff = true + break + } + } + } + var b bytes.Buffer + b.WriteByte('[') + for i, v := range min { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(strconv.FormatFloat(v, 'f', -1, 64)) + } + if diff { + b.WriteString("],[") + for i, v := range max { + if i > 0 { + b.WriteByte(' ') + } + b.WriteString(strconv.FormatFloat(v, 'f', -1, 64)) + } + } + b.WriteByte(']') + return b.String() +} + +// Point is a helper function that converts a series of float64s +// to a rectangle for a spatial index. +func Point(coords ...float64) string { + return Rect(coords, coords) +} + +// IndexRect is a helper function that converts string to a rect. +// Rect() is the reverse function and can be used to generate a string +// from a rect. +func IndexRect(a string) (min, max []float64) { + parts := strings.Split(a, ",") + for i := 0; i < len(parts) && i < 2; i++ { + part := parts[i] + if len(part) >= 2 && part[0] == '[' && part[len(part)-1] == ']' { + pieces := strings.Split(part[1:len(part)-1], " ") + if i == 0 { + min = make([]float64, 0, len(pieces)) + } else { + max = make([]float64, 0, len(pieces)) + } + for j := 0; j < len(pieces); j++ { + piece := pieces[j] + if piece != "" { + n, _ := strconv.ParseFloat(piece, 64) + if i == 0 { + min = append(min, n) + } else { + max = append(max, n) + } + } + } + } + } + if len(parts) == 1 { + max = min + } + return +} + +// IndexString is a helper function that return true if 'a' is less than 'b'. +// This is a case-insensitive comparison. Use the IndexBinary() for comparing +// case-sensitive strings. +func IndexString(a, b string) bool { + // This is a faster approach to strings.ToLower because it does not + // create new strings. + for i := 0; i < len(a) && i < len(b); i++ { + ca, cb := a[i], b[i] + if ca >= 'A' && ca <= 'Z' { + ca += 32 + } + if cb >= 'A' && cb <= 'Z' { + cb += 32 + } + if ca < cb { + return true + } else if ca > cb { + return false + } + } + return len(a) < len(b) +} + +// IndexBinary is a helper function that returns true if 'a' is less than 'b'. +// This compares the raw binary of the string. +func IndexBinary(a, b string) bool { + return a < b +} + +// IndexInt is a helper function that returns true if 'a' is less than 'b'. +func IndexInt(a, b string) bool { + ia, _ := strconv.ParseInt(a, 10, 64) + ib, _ := strconv.ParseInt(b, 10, 64) + return ia < ib +} + +// IndexUint is a helper function that returns true if 'a' is less than 'b'. +// This compares uint64s that are added to the database using the +// Uint() conversion function. +func IndexUint(a, b string) bool { + ia, _ := strconv.ParseUint(a, 10, 64) + ib, _ := strconv.ParseUint(b, 10, 64) + return ia < ib +} + +// IndexFloat is a helper function that returns true if 'a' is less than 'b'. +// This compares float64s that are added to the database using the +// Float() conversion function. +func IndexFloat(a, b string) bool { + ia, _ := strconv.ParseFloat(a, 64) + ib, _ := strconv.ParseFloat(b, 64) + return ia < ib +} diff --git a/buntdb_test.go b/buntdb_test.go new file mode 100644 index 0000000..11ae53e --- /dev/null +++ b/buntdb_test.go @@ -0,0 +1,791 @@ +package buntdb + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "math/rand" + "os" + "strings" + "testing" + "time" +) + +func TestBackgroudOperations(t *testing.T) { + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + for i := 0; i < 1000; i++ { + if err := db.Update(func(tx *Tx) error { + for j := 0; j < 200; j++ { + tx.Set(fmt.Sprintf("hello%d", j), "planet", nil) + } + tx.Set("hi", "world", &SetOptions{Expires: true, TTL: time.Second / 2}) + return nil + }); err != nil { + t.Fatal(err) + } + } + n := 0 + db.View(func(tx *Tx) error { + n, _ = tx.Len() + return nil + }) + if n != 201 { + t.Fatalf("expecting '%v', got '%v'", 201, n) + } + time.Sleep(time.Millisecond * 1500) + db.Close() + db, err = Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + n = 0 + db.View(func(tx *Tx) error { + n, _ = tx.Len() + return nil + }) + if n != 200 { + t.Fatalf("expecting '%v', got '%v'", 200, n) + } +} +func TestVariousTx(t *testing.T) { + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + if err := db.Update(func(tx *Tx) error { + tx.Set("hello", "planet", nil) + return nil + }); err != nil { + t.Fatal(err) + } + errBroken := errors.New("broken") + if err := db.Update(func(tx *Tx) error { + tx.Set("hello", "world", nil) + return errBroken + }); err != errBroken { + t.Fatalf("did not correctly receive the user-defined transaction error.") + } + var val string + db.View(func(tx *Tx) error { + val, _ = tx.Get("hello") + return nil + }) + if val == "world" { + t.Fatal("a rollbacked transaction got through") + } + if val != "planet" { + t.Fatal("expecting '%v', got '%v'", "planet", val) + } + if err := db.Update(func(tx *Tx) error { + tx.db = nil + if _, _, err := tx.Set("hello", "planet", nil); err != ErrTxClosed { + t.Fatal("expecting a tx closed error") + } + if _, err := tx.Delete("hello"); err != ErrTxClosed { + t.Fatal("expecting a tx closed error") + } + if _, err := tx.Get("hello"); err != ErrTxClosed { + t.Fatal("expecting a tx closed error") + } + tx.db = db + tx.writable = false + if _, _, err := tx.Set("hello", "planet", nil); err != ErrTxNotWritable { + t.Fatal("expecting a tx not writable error") + } + if _, err := tx.Delete("hello"); err != ErrTxNotWritable { + t.Fatal("expecting a tx not writable error") + } + tx.writable = true + if _, err := tx.Get("something"); err != ErrNotFound { + t.Fatalf("expecting not found error") + } + if _, err := tx.Delete("something"); err != ErrNotFound { + t.Fatalf("expecting not found error") + } + tx.Set("var", "val", &SetOptions{Expires: true, TTL: 0}) + if _, err := tx.Get("var"); err != ErrNotFound { + t.Fatalf("expecting not found error") + } + if _, err := tx.Delete("var"); err != ErrNotFound { + tx.unlock() + t.Fatalf("expecting not found error") + } + return nil + }); err != nil { + t.Fatal(err) + } + + // test for invalid commits + if err := db.Update(func(tx *Tx) error { + // we are going to do some hackery + defer func() { + if v := recover(); v != nil { + if v.(string) != "managed tx commit not allowed" { + t.Fatal(v.(string)) + } + } + }() + tx.commit() + return nil + }); err != nil { + t.Fatal(err) + } + + // test for invalid commits + if err := db.Update(func(tx *Tx) error { + // we are going to do some hackery + defer func() { + if v := recover(); v != nil { + if v.(string) != "managed tx rollback not allowed" { + t.Fatal(v.(string)) + } + } + }() + tx.rollback() + return nil + }); err != nil { + t.Fatal(err) + } + + // test for closed transactions + if err := db.Update(func(tx *Tx) error { + tx.db = nil + return nil + }); err != ErrTxClosed { + t.Fatal("expecting tx closed error") + } + db.mu.Unlock() + + // test for invalid writes + if err := db.Update(func(tx *Tx) error { + tx.writable = false + return nil + }); err != ErrTxNotWritable { + t.Fatal("expecting tx not writable error") + } + db.mu.Unlock() + // test for closed transactions + if err := db.View(func(tx *Tx) error { + tx.db = nil + return nil + }); err != ErrTxClosed { + t.Fatal("expecting tx closed error") + } + db.mu.RUnlock() + // flush to unwritable file + if err := db.Update(func(tx *Tx) error { + tx.Set("var1", "val1", nil) + tx.db.file.Close() + return nil + }); err == nil { + t.Fatal("should not be able to commit when the file is closed") + } + db.file, err = os.OpenFile("data.db", os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + t.Fatal(err) + } + if _, err := db.file.Seek(0, 2); err != nil { + t.Fatal(err) + } + db.bufw = bufio.NewWriter(db.file) + db.CreateIndex("blank", "*", nil) + // test scanning + if err := db.Update(func(tx *Tx) error { + tx.Set("nothing", "here", nil) + return nil + }); err != nil { + t.Fatal(err) + } + if err := db.View(func(tx *Tx) error { + s := "" + tx.Ascend("", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "hello:planet\nnothing:here\n" { + t.Fatal("invalid scan") + } + tx.db = nil + err = tx.Ascend("", func(key, val string) bool { return true }) + if err != ErrTxClosed { + tx.unlock() + t.Fatal("expecting tx closed error") + } + tx.db = db + err = tx.Ascend("na", func(key, val string) bool { return true }) + if err != ErrNotFound { + t.Fatal("expecting not found error") + } + err = tx.Ascend("blank", func(key, val string) bool { return true }) + if err != nil { + t.Fatal(err) + } + s = "" + tx.AscendLessThan("", "liger", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "hello:planet\n" { + t.Fatal("invalid scan") + } + + s = "" + tx.Descend("", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "nothing:here\nhello:planet\n" { + t.Fatal("invalid scan") + } + + s = "" + tx.DescendLessOrEqual("", "liger", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "hello:planet\n" { + t.Fatal("invalid scan") + } + + s = "" + tx.DescendGreaterThan("", "liger", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "nothing:here\n" { + t.Fatal("invalid scan") + } + s = "" + tx.DescendRange("", "liger", "apple", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "hello:planet\n" { + t.Fatal("invalid scan") + } + return nil + }); err != nil { + t.Fatal(err) + } + + // test some spatial stuff + db.CreateSpatialIndex("spat", "rect:*", IndexRect) + db.CreateSpatialIndex("junk", "rect:*", nil) + db.Update(func(tx *Tx) error { + tx.Set("rect:1", "[10 10],[20 20]", nil) + tx.Set("rect:2", "[15 15],[25 25]", nil) + tx.Set("shape:1", "[12 12],[25 25]", nil) + s := "" + tx.Intersects("spat", "[5 5],[13 13]", func(key, val string) bool { + s += key + ":" + val + "\n" + return true + }) + if s != "rect:1:[10 10],[20 20]\n" { + t.Fatal("invalid scan") + } + tx.db = nil + err := tx.Intersects("spat", "[5 5],[13 13]", func(key, val string) bool { + return true + }) + if err != ErrTxClosed { + t.Fatal("expecting tx closed error") + } + tx.db = db + err = tx.Intersects("", "[5 5],[13 13]", func(key, val string) bool { + return true + }) + if err != nil { + t.Fatal(err) + } + err = tx.Intersects("na", "[5 5],[13 13]", func(key, val string) bool { + return true + }) + if err != ErrNotFound { + t.Fatal("expecting not found error") + } + err = tx.Intersects("junk", "[5 5],[13 13]", func(key, val string) bool { + return true + }) + if err != nil { + t.Fatal(err) + } + n, err := tx.Len() + if err != nil { + t.Fatal(err) + } + if n != 5 { + t.Fatal("expecting %v, got %v", 5, n) + } + tx.db = nil + _, err = tx.Len() + if err != ErrTxClosed { + t.Fatal("expecting tx closed error") + } + tx.db = db + return nil + }) + + // test after closing + db.Close() + if err := db.Update(func(tx *Tx) error { return nil }); err != ErrDatabaseClosed { + t.Fatalf("should not be able to perform transactionso on a closed database.") + } +} +func TestNoExpiringItem(t *testing.T) { + item := &dbItem{key: "key", val: "val"} + if !item.expiresAt().Equal(maxTime) { + t.Fatal("item.expiresAt() != maxTime") + } + if min, max := item.Rect(nil); min != nil || max != nil { + t.Fatal("item min,max should both be nil") + } +} + +// test database format loading +func TestDatabaseFormat(t *testing.T) { + // should succeed + func() { + resp := strings.Join([]string{ + "*3\r\n$3\r\nset\r\n$4\r\nvar1\r\n$4\r\n1234\r\n", + "*3\r\n$3\r\nset\r\n$4\r\nvar2\r\n$4\r\n1234\r\n", + "*2\r\n$3\r\ndel\r\n$4\r\nvar1\r\n", + "*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nex\r\n$2\r\n10\r\n", + }, "") + os.RemoveAll("data.db") + ioutil.WriteFile("data.db", []byte(resp), 0666) + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + }() + testBadFormat := func(resp string) { + os.RemoveAll("data.db") + ioutil.WriteFile("data.db", []byte(resp), 0666) + db, err := Open("data.db") + if err == nil { + db.Close() + os.RemoveAll("data.db") + t.Fatalf("invalid database should not be allowed") + } + } + testBadFormat("*3\r") + testBadFormat("*3\n") + testBadFormat("*a\r\n") + testBadFormat("*2\r\n") + testBadFormat("*2\r\n%3") + testBadFormat("*2\r\n$") + testBadFormat("*2\r\n$3\r\n") + testBadFormat("*2\r\n$3\r\ndel") + testBadFormat("*2\r\n$3\r\ndel\r\r") + testBadFormat("*0\r\n*2\r\n$3\r\ndel\r\r") + testBadFormat("*1\r\n$3\r\nnop\r\n") + testBadFormat("*1\r\n$3\r\ndel\r\n") + testBadFormat("*1\r\n$3\r\nset\r\n") + testBadFormat("*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nxx\r\n$2\r\n10\r\n") + testBadFormat("*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nex\r\n$2\r\naa\r\n") +} + +func TestInsertsAndDeleted(t *testing.T) { + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + db.CreateIndex("any", "*", IndexString) + db.CreateSpatialIndex("rect", "*", IndexRect) + if err := db.Update(func(tx *Tx) error { + tx.Set("item1", "value1", &SetOptions{Expires: true, TTL: time.Second}) + tx.Set("item2", "value2", nil) + tx.Set("item3", "value3", &SetOptions{Expires: true, TTL: time.Second}) + return nil + }); err != nil { + t.Fatal(err) + } + + // test replacing items in the database + if err := db.Update(func(tx *Tx) error { + if _, _, err := tx.Set("item1", "nvalue1", nil); err != nil { + return err + } + if _, _, err := tx.Set("item2", "nvalue2", nil); err != nil { + return err + } + if _, err := tx.Delete("item3"); err != nil { + return err + } + return nil + }); err != nil { + t.Fatal(err) + } +} + +// test index compare functions +func TestIndexCompare(t *testing.T) { + if !IndexFloat("1.5", "1.6") { + t.Fatalf("expected true, got false") + } + if !IndexInt("-1", "2") { + t.Fatalf("expected true, got false") + } + if !IndexUint("10", "25") { + t.Fatalf("expected true, got false") + } + if !IndexBinary("Hello", "hello") { + t.Fatalf("expected true, got false") + } + if IndexString("hello", "hello") { + t.Fatalf("expected false, got true") + } + if IndexString("Hello", "hello") { + t.Fatalf("expected false, got true") + } + if IndexString("hello", "Hello") { + t.Fatalf("expected false, got true") + } + if !IndexString("gello", "Hello") { + t.Fatalf("expected true, got false") + } + if IndexString("Hello", "gello") { + t.Fatalf("expected false, got true") + } + if Rect(IndexRect("[1 2 3 4],[5 6 7 8]")) != "[1 2 3 4],[5 6 7 8]" { + t.Fatalf("expected '%v', got '%v'", "[1 2 3 4],[5 6 7 8]", Rect(IndexRect("[1 2 3 4],[5 6 7 8]"))) + } + if Rect(IndexRect("[1 2 3 4]")) != "[1 2 3 4]" { + t.Fatalf("expected '%v', got '%v'", "[1 2 3 4]", Rect(IndexRect("[1 2 3 4]"))) + } + if Rect(nil, nil) != "" { + t.Fatalf("expected '%v', got '%v'", "", Rect(nil, nil)) + } + if Point(1, 2, 3) != "[1 2 3]" { + t.Fatalf("expected '%v', got '%v'", "[1 2 3]", Point(1, 2, 3)) + } +} + +// test opening a folder. +func TestOpeningAFolder(t *testing.T) { + os.RemoveAll("dir.tmp") + os.Mkdir("dir.tmp", 0700) + defer os.RemoveAll("dir.tmp") + db, err := Open("dir.tmp") + if err == nil { + db.Close() + t.Fatalf("opening a directory should not be allowed") + } +} + +// test opening an invalid resp file. +func TestOpeningInvalidDatabaseFile(t *testing.T) { + os.RemoveAll("data.db") + ioutil.WriteFile("data.db", []byte("invalid\r\nfile"), 0666) + defer os.RemoveAll("data.db") + db, err := Open("data.db") + if err == nil { + db.Close() + t.Fatalf("invalid database should not be allowed") + } +} + +// test closing a closed database. +func TestOpeningClosedDatabase(t *testing.T) { + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + if err := db.Close(); err != nil { + t.Fatal(err) + } + if err := db.Close(); err != ErrDatabaseClosed { + t.Fatal("should not be able to close a closed database") + } +} + +func TestVariousIndexOperations(t *testing.T) { + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + // test creating an index with no index name. + err = db.CreateIndex("", "", nil) + if err == nil { + t.Fatal("should not be able to create an index with no name") + } + // test creating an index with a name that has already been used. + err = db.CreateIndex("hello", "", nil) + if err != nil { + t.Fatal(err) + } + err = db.CreateIndex("hello", "", nil) + if err == nil { + t.Fatal("should not be able to create a duplicate index") + } + db.Update(func(tx *Tx) error { + tx.Set("user:1", "tom", nil) + tx.Set("user:2", "janet", nil) + tx.Set("alt:1", "from", nil) + tx.Set("alt:2", "there", nil) + tx.Set("rect:1", "[1 2],[3 4]", nil) + tx.Set("rect:2", "[5 6],[7 8]", nil) + return nil + }) + // test creating an index after adding items. use pattern matching. have some items in the match and some not. + if err := db.CreateIndex("string", "user:*", IndexString); err != nil { + t.Fatal(err) + } + // test creating a spatial index after adding items. use pattern matching. have some items in the match and some not. + if err := db.CreateSpatialIndex("rect", "rect:*", IndexRect); err != nil { + t.Fatal(err) + } + // test dropping an index + if err := db.DropIndex("hello"); err != nil { + t.Fatal(err) + } + // test dropping an index with no name + if err := db.DropIndex(""); err == nil { + t.Fatal("should not be allowed to drop an index with no name") + } + // test dropping an index with no name + if err := db.DropIndex("na"); err == nil { + t.Fatal("should not be allowed to drop an index that does not exist") + } + // test retrieving index names + names, err := db.Indexes() + if err != nil { + t.Fatal(err) + } + if strings.Join(names, ",") != "rect,string" { + t.Fatalf("expecting '%v', got '%v'", "rect,string", strings.Join(names, ",")) + } + // test creating an index after closing database + if err := db.Close(); err != nil { + t.Fatal(err) + } + if err := db.CreateIndex("new-index", "", nil); err != ErrDatabaseClosed { + t.Fatal("should not be able to create an index on a closed database") + } + // test getting index names after closing database + if _, err := db.Indexes(); err != ErrDatabaseClosed { + t.Fatal("should not be able to get index names on a closed database") + } + // test dropping an index after closing database + if err := db.DropIndex("rect"); err != ErrDatabaseClosed { + t.Fatal("should not be able to drop an index on a closed database") + } +} + +func test(t *testing.T, a, b bool) { + if a != b { + t.Fatal("failed, bummer...") + } +} + +func TestPatternMatching(t *testing.T) { + test(t, wildcardMatch("hello", "hello"), true) + test(t, wildcardMatch("hello", "h*"), true) + test(t, wildcardMatch("hello", "h*o"), true) + test(t, wildcardMatch("hello", "h*l*o"), true) + test(t, wildcardMatch("hello", "h*z*o"), false) + test(t, wildcardMatch("hello", "*l*o"), true) + test(t, wildcardMatch("hello", "*l*"), true) + test(t, wildcardMatch("hello", "*?*"), true) + test(t, wildcardMatch("hello", "*"), true) + test(t, wildcardMatch("hello", "h?llo"), true) + test(t, wildcardMatch("hello", "h?l?o"), true) + test(t, wildcardMatch("", "*"), true) + test(t, wildcardMatch("", ""), true) + test(t, wildcardMatch("h", ""), false) + test(t, wildcardMatch("", "?"), false) +} + +func TestBasic(t *testing.T) { + rand.Seed(time.Now().UnixNano()) + os.RemoveAll("data.db") + db, err := Open("data.db") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll("data.db") + defer db.Close() + + // create a simple index + db.CreateIndex("users", "fun:user:*", IndexString) + + // create a spatial index + db.CreateSpatialIndex("rects", "rect:*", IndexRect) + if true { + db.Update(func(tx *Tx) error { + tx.Set("fun:user:0", "tom", nil) + tx.Set("fun:user:1", "Randi", nil) + tx.Set("fun:user:2", "jane", nil) + tx.Set("fun:user:4", "Janet", nil) + tx.Set("fun:user:5", "Paula", nil) + tx.Set("fun:user:6", "peter", nil) + tx.Set("fun:user:7", "Terri", nil) + return nil + }) + // add some random items + start := time.Now() + if err := db.Update(func(tx *Tx) error { + for _, i := range rand.Perm(100) { + tx.Set(fmt.Sprintf("tag:%d", i+100), fmt.Sprintf("val:%d", rand.Int()%100+100), nil) + } + return nil + }); err != nil { + t.Fatal(err) + } + if false { + println(time.Now().Sub(start).String(), db.keys.Len()) + } + // add some random rects + if err := db.Update(func(tx *Tx) error { + tx.Set("rect:1", Rect([]float64{10, 10}, []float64{20, 20}), nil) + tx.Set("rect:2", Rect([]float64{15, 15}, []float64{24, 24}), nil) + tx.Set("rect:3", Rect([]float64{17, 17}, []float64{27, 27}), nil) + return nil + }); err != nil { + t.Fatal(err) + } + } + // verify the data has been created + buf := &bytes.Buffer{} + db.View(func(tx *Tx) error { + tx.Ascend("users", func(key, val string) bool { + fmt.Fprintf(buf, "%s %s\n", key, val) + return true + }) + err = tx.AscendRange("", "tag:170", "tag:172", func(key, val string) bool { + fmt.Fprintf(buf, "%s\n", key) + return true + }) + if err != nil { + t.Fatal(err) + } + err = tx.AscendGreaterOrEqual("", "tag:195", func(key, val string) bool { + fmt.Fprintf(buf, "%s\n", key) + return true + }) + if err != nil { + t.Fatal(err) + } + err = tx.AscendGreaterOrEqual("", "rect:", func(key, val string) bool { + if !strings.HasPrefix(key, "rect:") { + return false + } + min, max := IndexRect(val) + fmt.Fprintf(buf, "%s: %v,%v\n", key, min, max) + return true + }) + expect := make([]string, 2) + n := 0 + tx.Intersects("rects", "[0 0],[15 15]", func(key, val string) bool { + if n == 2 { + t.Fatalf("too many rects where received, expecting only two") + } + min, max := IndexRect(val) + s := fmt.Sprintf("%s: %v,%v\n", key, min, max) + if key == "rect:1" { + expect[0] = s + } else if key == "rect:2" { + expect[1] = s + } + n++ + return true + }) + for _, s := range expect { + buf.WriteString(s) + } + return nil + }) + res := ` +fun:user:2 jane +fun:user:4 Janet +fun:user:5 Paula +fun:user:6 peter +fun:user:1 Randi +fun:user:7 Terri +fun:user:0 tom +tag:170 +tag:171 +tag:195 +tag:196 +tag:197 +tag:198 +tag:199 +rect:1: [10 10],[20 20] +rect:2: [15 15],[24 24] +rect:3: [17 17],[27 27] +rect:1: [10 10],[20 20] +rect:2: [15 15],[24 24] +` + res = strings.Replace(res, "\r", "", -1) + if strings.TrimSpace(buf.String()) != strings.TrimSpace(res) { + t.Fatalf("expected [%v], got [%v]", strings.TrimSpace(res), strings.TrimSpace(buf.String())) + } +} +func testRectStringer(min, max []float64) error { + nmin, nmax := IndexRect(Rect(min, max)) + if len(nmin) != len(min) { + return fmt.Errorf("rect=%v,%v, expect=%v,%v", nmin, nmax, min, max) + } + for i := 0; i < len(min); i++ { + if min[i] != nmin[i] || max[i] != nmax[i] { + return fmt.Errorf("rect=%v,%v, expect=%v,%v", nmin, nmax, min, max) + } + } + return nil +} +func TestRectStrings(t *testing.T) { + test(t, Rect(IndexRect(Point(1))) == "[1]", true) + test(t, Rect(IndexRect(Point(1, 2, 3, 4))) == "[1 2 3 4]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[1 2]")))) == "[1 2]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[2 2]")))) == "[1 2],[2 2]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[2 2],[3]")))) == "[1 2],[2 2]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1 2]")))) == "[1 2]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1.5 2 4.5 5.6]")))) == "[1.5 2 4.5 5.6]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[1.5 2 4.5 5.6 -1],[]")))) == "[1.5 2 4.5 5.6 -1],[]", true) + test(t, Rect(IndexRect(Rect(IndexRect("[]")))) == "[]", true) + test(t, Rect(IndexRect(Rect(IndexRect("")))) == "", true) + if err := testRectStringer(nil, nil); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{}, []float64{}); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{1}, []float64{2}); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{1, 2}, []float64{3, 4}); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{1, 2, 3}, []float64{4, 5, 6}); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{1, 2, 3, 4}, []float64{5, 6, 7, 8}); err != nil { + t.Fatal(err) + } + if err := testRectStringer([]float64{1, 2, 3, 4, 5}, []float64{6, 7, 8, 9, 0}); err != nil { + t.Fatal(err) + } +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..01c6d75 Binary files /dev/null and b/logo.png differ