mirror of
https://github.com/chaisql/chai.git
synced 2025-10-05 07:36:56 +08:00
1102 lines
32 KiB
Go
1102 lines
32 KiB
Go
package database_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/genjidb/genji/document"
|
|
"github.com/genjidb/genji/document/encoding/msgpack"
|
|
"github.com/genjidb/genji/engine/memoryengine"
|
|
errs "github.com/genjidb/genji/errors"
|
|
"github.com/genjidb/genji/internal/binarysort"
|
|
"github.com/genjidb/genji/internal/catalog"
|
|
"github.com/genjidb/genji/internal/database"
|
|
"github.com/genjidb/genji/internal/expr"
|
|
"github.com/genjidb/genji/internal/query/statement"
|
|
"github.com/genjidb/genji/internal/testutil"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var errDontCommit = errors.New("don't commit please")
|
|
|
|
func update(t testing.TB, db *database.Database, fn func(tx *database.Transaction) error) {
|
|
t.Helper()
|
|
|
|
tx, err := db.Begin(true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
|
|
err = fn(tx)
|
|
if err == errDontCommit {
|
|
tx.Rollback()
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
err = tx.Commit()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func newTestTable(t testing.TB) (*database.Table, func()) {
|
|
db, tx, fn := testutil.NewTestTx(t)
|
|
|
|
return createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test"}), fn
|
|
}
|
|
|
|
func createTable(t testing.TB, tx *database.Transaction, catalog database.Catalog, info database.TableInfo) *database.Table {
|
|
stmt := statement.CreateTableStmt{Info: info}
|
|
|
|
res, err := stmt.Run(&statement.Context{
|
|
Catalog: catalog,
|
|
Tx: tx,
|
|
})
|
|
require.NoError(t, err)
|
|
res.Close()
|
|
|
|
tb, err := catalog.GetTable(tx, stmt.Info.TableName)
|
|
require.NoError(t, err)
|
|
|
|
return tb
|
|
}
|
|
|
|
func createTableIfNotExists(t testing.TB, tx *database.Transaction, catalog database.Catalog, info database.TableInfo) *database.Table {
|
|
stmt := statement.CreateTableStmt{Info: info, IfNotExists: true}
|
|
|
|
res, err := stmt.Run(&statement.Context{
|
|
Catalog: catalog,
|
|
Tx: tx,
|
|
})
|
|
require.NoError(t, err)
|
|
res.Close()
|
|
|
|
tb, err := catalog.GetTable(tx, stmt.Info.TableName)
|
|
require.NoError(t, err)
|
|
|
|
return tb
|
|
}
|
|
|
|
func newDocument() *document.FieldBuffer {
|
|
return document.NewFieldBuffer().
|
|
Add("fielda", document.NewTextValue("a")).
|
|
Add("fieldb", document.NewTextValue("b"))
|
|
}
|
|
|
|
// TestTableIterate verifies Iterate behaviour.
|
|
func TestTableIterate(t *testing.T) {
|
|
t.Run("Should not fail with no documents", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
i := 0
|
|
err := tb.Iterate(func(d document.Document) error {
|
|
i++
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Zero(t, i)
|
|
})
|
|
|
|
t.Run("Should iterate over all documents", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, err := tb.Insert(newDocument())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
m := make(map[string]int)
|
|
err := tb.Iterate(func(d document.Document) error {
|
|
m[string(d.(document.Keyer).RawKey())]++
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, m, 10)
|
|
for _, c := range m {
|
|
require.Equal(t, 1, c)
|
|
}
|
|
})
|
|
|
|
t.Run("Should stop if fn returns error", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, err := tb.Insert(newDocument())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
i := 0
|
|
err := tb.Iterate(func(_ document.Document) error {
|
|
i++
|
|
if i >= 5 {
|
|
return errors.New("some error")
|
|
}
|
|
return nil
|
|
})
|
|
require.EqualError(t, err, "some error")
|
|
require.Equal(t, 5, i)
|
|
})
|
|
}
|
|
|
|
// TestTableGetDocument verifies GetDocument behaviour.
|
|
func TestTableGetDocument(t *testing.T) {
|
|
t.Run("Should fail if not found", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
r, err := tb.GetDocument([]byte("id"))
|
|
require.Equal(t, errs.ErrDocumentNotFound, err)
|
|
require.Nil(t, r)
|
|
})
|
|
|
|
t.Run("Should return the right document", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
// create two documents, one with an additional field
|
|
doc1 := newDocument()
|
|
vc := document.NewDoubleValue(40)
|
|
doc1.Add("fieldc", vc)
|
|
doc2 := newDocument()
|
|
|
|
d, err := tb.Insert(doc1)
|
|
require.NoError(t, err)
|
|
_, err = tb.Insert(doc2)
|
|
require.NoError(t, err)
|
|
|
|
// fetch doc1 and make sure it returns the right one
|
|
res, err := tb.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
fc, err := res.GetByField("fieldc")
|
|
require.NoError(t, err)
|
|
require.Equal(t, vc, fc)
|
|
})
|
|
}
|
|
|
|
// TestTableInsert verifies Insert behaviour.
|
|
func TestTableInsert(t *testing.T) {
|
|
t.Run("Should generate a key by default", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
doc := newDocument()
|
|
d1, err := tb.Insert(doc.Clone())
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, d1.(document.Keyer).RawKey())
|
|
|
|
d2, err := tb.Insert(doc.Clone())
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, d2.(document.Keyer).RawKey())
|
|
|
|
require.NotEqual(t, d1.(document.Keyer).RawKey(), d2.(document.Keyer).RawKey())
|
|
})
|
|
|
|
t.Run("Should generate the right docid on existing databases", func(t *testing.T) {
|
|
ng := memoryengine.NewEngine()
|
|
|
|
db, err := database.New(context.Background(), ng, database.Options{
|
|
Codec: msgpack.NewCodec(),
|
|
Catalog: catalog.New(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
insertDoc := func(db *database.Database) (rawKey []byte) {
|
|
update(t, db, func(tx *database.Transaction) error {
|
|
// create table if not exists
|
|
tb := createTableIfNotExists(t, tx, db.Catalog, database.TableInfo{TableName: "test"})
|
|
|
|
doc := newDocument()
|
|
d, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, d.(document.Keyer).RawKey())
|
|
rawKey = d.(document.Keyer).RawKey()
|
|
return nil
|
|
})
|
|
return
|
|
}
|
|
|
|
key1 := insertDoc(db)
|
|
|
|
err = db.Close()
|
|
require.NoError(t, err)
|
|
|
|
ng.Closed = false
|
|
|
|
// create new database object
|
|
db, err = database.New(context.Background(), ng, database.Options{
|
|
Codec: msgpack.NewCodec(),
|
|
Catalog: db.Catalog,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
key2 := insertDoc(db)
|
|
|
|
a, _ := binary.Uvarint(key1)
|
|
require.NoError(t, err)
|
|
|
|
b, _ := binary.Uvarint(key2)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, int64(a+1), int64(b))
|
|
})
|
|
|
|
t.Run("Should use the right field if primary key is specified", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{Path: testutil.ParseDocumentPath(t, "foo.a[1]"), Type: document.IntegerValue, IsPrimaryKey: true},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
var doc document.FieldBuffer
|
|
err = doc.UnmarshalJSON([]byte(`{"foo": {"a": [0, 10]}}`))
|
|
require.NoError(t, err)
|
|
|
|
// insert
|
|
d, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, binarysort.AppendInt64(nil, 10), d.(document.Keyer).RawKey())
|
|
|
|
// make sure the document is fetchable using the returned key
|
|
_, err = tb.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
|
|
// insert again
|
|
_, err = tb.Insert(doc)
|
|
require.Equal(t, errs.ErrDuplicateDocument, err)
|
|
})
|
|
|
|
t.Run("Should convert values into the right types if there are constraints", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{Path: testutil.ParseDocumentPath(t, "foo"), Type: document.ArrayValue},
|
|
{Path: testutil.ParseDocumentPath(t, "foo[0]"), Type: document.IntegerValue},
|
|
},
|
|
})
|
|
|
|
var doc document.FieldBuffer
|
|
err := doc.UnmarshalJSON([]byte(`{"foo": [100]}`))
|
|
require.NoError(t, err)
|
|
|
|
// insert
|
|
d, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
|
|
d, err = tb.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
|
|
v, err := testutil.ParseDocumentPath(t, "foo[0]").GetValueFromDocument(d)
|
|
require.NoError(t, err)
|
|
require.Equal(t, document.NewIntegerValue(100), v)
|
|
})
|
|
|
|
t.Run("Should fail if Pk not found in document or empty", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{Path: testutil.ParseDocumentPath(t, "foo"), Type: document.IntegerValue, IsPrimaryKey: true},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
tests := [][]byte{
|
|
nil,
|
|
{},
|
|
[]byte(nil),
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewBlobValue(test))
|
|
|
|
_, err := tb.Insert(doc)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Should update indexes if there are indexed fields", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test"})
|
|
|
|
err := db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
IndexName: "idxFoo", TableName: "test", Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
|
|
})
|
|
require.NoError(t, err)
|
|
idx, err := db.Catalog.GetIndex(tx, "idxFoo")
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
// create one document with the foo field
|
|
doc1 := newDocument()
|
|
foo := document.NewDoubleValue(10)
|
|
doc1.Add("foo", foo)
|
|
|
|
// create one document without the foo field
|
|
doc2 := newDocument()
|
|
|
|
d1, err := tb.Insert(doc1)
|
|
require.NoError(t, err)
|
|
d2, err := tb.Insert(doc2)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
err = idx.AscendGreaterOrEqual([]document.Value{{}}, func(val, k []byte) error {
|
|
switch count {
|
|
case 0:
|
|
// key2, which doesn't countain the field must appear first in the next,
|
|
// as null values are the smallest possible values
|
|
require.Equal(t, d2.(document.Keyer).RawKey(), k)
|
|
case 1:
|
|
require.Equal(t, d1.(document.Keyer).RawKey(), k)
|
|
}
|
|
count++
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, count)
|
|
})
|
|
|
|
t.Run("Should convert the fields if FieldsConstraints are specified", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), document.DocumentValue, false, false, false, nil, nil, true, []document.Path{testutil.ParseDocumentPath(t, "foo.bar")}},
|
|
{testutil.ParseDocumentPath(t, "foo.bar"), document.IntegerValue, false, false, false, nil, nil, true, []document.Path{testutil.ParseDocumentPath(t, "foo")}},
|
|
},
|
|
})
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewDocumentValue(
|
|
document.NewFieldBuffer().Add("bar", document.NewDoubleValue(10)),
|
|
)).
|
|
Add("bar", document.NewDoubleValue(10)).
|
|
Add("baz", document.NewTextValue("baz")).
|
|
Add("bat", document.NewIntegerValue(20))
|
|
|
|
// insert
|
|
d, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
|
|
// make sure the fields have been converted to the right types
|
|
d, err = tb.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
v, err := d.GetByField("foo")
|
|
require.NoError(t, err)
|
|
v, err = v.V.(document.Document).GetByField("bar")
|
|
require.NoError(t, err)
|
|
require.Equal(t, document.NewIntegerValue(10), v)
|
|
v, err = d.GetByField("bar")
|
|
require.NoError(t, err)
|
|
require.Equal(t, document.NewDoubleValue(10), v)
|
|
v, err = d.GetByField("baz")
|
|
require.NoError(t, err)
|
|
require.Equal(t, document.NewTextValue("baz"), v)
|
|
v, err = d.GetByField("bat")
|
|
require.NoError(t, err)
|
|
require.Equal(t, document.NewDoubleValue(20), v)
|
|
})
|
|
|
|
t.Run("Should fail if the fields cannot be converted to specified field constraints", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), document.DoubleValue, false, false, false, nil, nil, false, nil},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewArrayValue(document.NewValueBuffer(document.NewIntegerValue(1))))
|
|
|
|
// insert
|
|
_, err = tb.Insert(doc)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("Should fail if there is a not null field constraint on a document field and the field is null or missing", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
// no enforced type, not null
|
|
tb1 := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test1",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, nil, nil, false, nil},
|
|
},
|
|
})
|
|
|
|
// enforced type, not null
|
|
tb2 := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test2",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), document.IntegerValue, false, true, false, nil, nil, false, nil},
|
|
},
|
|
})
|
|
|
|
// insert with empty foo field should fail
|
|
_, err := tb1.Insert(document.NewFieldBuffer().
|
|
Add("bar", document.NewDoubleValue(1)))
|
|
require.Error(t, err)
|
|
|
|
// insert with null foo field should fail
|
|
_, err = tb1.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewNullValue()))
|
|
require.Error(t, err)
|
|
|
|
// otherwise it should work
|
|
_, err = tb1.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewDoubleValue(1)))
|
|
require.NoError(t, err)
|
|
|
|
// insert with empty foo field should fail
|
|
_, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("bar", document.NewDoubleValue(1)))
|
|
require.Error(t, err)
|
|
|
|
// insert with null foo field should fail
|
|
_, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewNullValue()))
|
|
require.Error(t, err)
|
|
|
|
// otherwise it should work
|
|
_, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewDoubleValue(1)))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Shouldn't fail if there is a not null field and default constraint on a document field and the field is null or missing", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
// no enforced type, not null
|
|
tb1 := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test1",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, expr.Constraint(testutil.IntegerValue(42)), nil, false, nil},
|
|
},
|
|
})
|
|
|
|
// enforced type, not null
|
|
tb2 := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test2",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), document.IntegerValue, false, true, false, expr.Constraint(testutil.IntegerValue(42)), nil, false, nil},
|
|
},
|
|
})
|
|
|
|
// insert with empty foo field shouldn't fail
|
|
d, err := tb1.Insert(document.NewFieldBuffer().
|
|
Add("bar", document.NewDoubleValue(1)))
|
|
require.NoError(t, err)
|
|
|
|
d, err = tb1.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
v, err := d.GetByField("foo")
|
|
require.NoError(t, err)
|
|
require.Equal(t, v.V.(float64), float64(42))
|
|
|
|
// insert with explicit null foo field should fail
|
|
_, err = tb1.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewNullValue()))
|
|
require.Error(t, err)
|
|
|
|
// otherwise it should work
|
|
_, err = tb1.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(1)))
|
|
require.NoError(t, err)
|
|
|
|
// insert with empty foo field shouldn't fail
|
|
d, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("bar", document.NewIntegerValue(1)))
|
|
require.NoError(t, err)
|
|
|
|
d, err = tb2.GetDocument(d.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
v, err = d.GetByField("foo")
|
|
require.NoError(t, err)
|
|
require.Equal(t, v.V.(int64), int64(42))
|
|
|
|
// insert with explicit null foo field should fail
|
|
_, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewNullValue()))
|
|
require.Error(t, err)
|
|
|
|
// otherwise it should work
|
|
_, err = tb2.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewDoubleValue(1)))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should fail if there is a not null field constraint on an array value and the value is null", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test1",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo[1]"), 0, false, true, false, nil, nil, false, nil},
|
|
},
|
|
})
|
|
|
|
// insert table with only one value
|
|
_, err := tb.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewArrayValue(document.NewValueBuffer().Append(document.NewIntegerValue(1)))))
|
|
require.Error(t, err)
|
|
_, err = tb.Insert(document.NewFieldBuffer().
|
|
Add("foo", document.NewArrayValue(document.NewValueBuffer().
|
|
Append(document.NewIntegerValue(1)).Append(document.NewIntegerValue(2)))))
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should fail if the pk is duplicated", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, true, true, false, nil, nil, false, nil},
|
|
}})
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(10))
|
|
|
|
// insert first
|
|
_, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
|
|
// insert again, should fail
|
|
_, err = tb.Insert(doc)
|
|
require.Equal(t, errs.ErrDuplicateDocument, err)
|
|
})
|
|
|
|
t.Run("Should run the onConflict function if the pk is duplicated", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, true, true, false, nil, nil, false, nil},
|
|
}})
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(10))
|
|
|
|
var called int
|
|
onConflict := func(t *database.Table, key []byte, d document.Document, err error) (document.Document, error) {
|
|
called++
|
|
return d, nil
|
|
}
|
|
|
|
// insert first
|
|
_, err := tb.InsertWithConflictResolution(doc, onConflict)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, called)
|
|
|
|
// insert again, should call onConflict
|
|
_, err = tb.InsertWithConflictResolution(doc, onConflict)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, called)
|
|
})
|
|
|
|
t.Run("Should fail if there is a unique constraint violation", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test"})
|
|
err := db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
TableName: "test",
|
|
IndexName: "idx_test_foo",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
|
|
Unique: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(10))
|
|
|
|
// insert first
|
|
_, err = tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
|
|
// insert again, should fail
|
|
_, err = tb.Insert(doc)
|
|
require.Equal(t, errs.ErrDuplicateDocument, err)
|
|
})
|
|
|
|
t.Run("Should run the onConflict function if there is a unique constraint violation", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test"})
|
|
err := db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
TableName: "test",
|
|
IndexName: "idx_test_foo",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "foo")},
|
|
Unique: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(10))
|
|
|
|
var called int
|
|
onConflict := func(t *database.Table, key []byte, d document.Document, err error) (document.Document, error) {
|
|
called++
|
|
return d, nil
|
|
}
|
|
|
|
// insert first
|
|
_, err = tb.InsertWithConflictResolution(doc, onConflict)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, called)
|
|
|
|
// insert again, should call onConflict
|
|
_, err = tb.InsertWithConflictResolution(doc, onConflict)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, called)
|
|
})
|
|
|
|
t.Run("Should run the onConflict function if there is a NOT NULL constraint violation", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
tb := createTable(t, tx, db.Catalog, database.TableInfo{
|
|
TableName: "test",
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, nil, nil, false, nil},
|
|
},
|
|
})
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("bar", document.NewIntegerValue(10))
|
|
|
|
var called int
|
|
onConflict := func(t *database.Table, key []byte, d document.Document, err error) (document.Document, error) {
|
|
called++
|
|
return d, nil
|
|
}
|
|
|
|
// insert
|
|
_, err := tb.InsertWithConflictResolution(doc, onConflict)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, called)
|
|
})
|
|
|
|
t.Run("Should replace document if the pk is duplicated, using OnInsertConflictDoReplace", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, true, true, false, nil, nil, false, nil},
|
|
}})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("foo", document.NewIntegerValue(10))
|
|
|
|
// insert first
|
|
d1, err := tb.Insert(doc)
|
|
require.NoError(t, err)
|
|
|
|
// insert again, should call OnInsertConflictDoReplace
|
|
d2, err := tb.InsertWithConflictResolution(doc, database.OnInsertConflictDoReplace)
|
|
require.NoError(t, err)
|
|
require.Equal(t, d1, d2)
|
|
})
|
|
|
|
t.Run("Should not replace document if there is a NOT NULL constraint violation, using OnInsertConflictDoReplace", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test", &database.TableInfo{
|
|
FieldConstraints: []*database.FieldConstraint{
|
|
{testutil.ParseDocumentPath(t, "foo"), 0, false, true, false, nil, nil, false, nil},
|
|
}})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test")
|
|
require.NoError(t, err)
|
|
|
|
doc := document.NewFieldBuffer().
|
|
Add("bar", document.NewIntegerValue(10))
|
|
|
|
// insert
|
|
_, err = tb.InsertWithConflictResolution(doc, database.OnInsertConflictDoReplace)
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestTableDelete verifies Delete behaviour.
|
|
func TestTableDelete(t *testing.T) {
|
|
t.Run("Should fail if not found", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
err := tb.Delete([]byte("id"))
|
|
require.Equal(t, errs.ErrDocumentNotFound, err)
|
|
})
|
|
|
|
t.Run("Should delete the right document", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
// create two documents, one with an additional field
|
|
doc1 := newDocument()
|
|
doc1.Add("fieldc", document.NewIntegerValue(40))
|
|
doc2 := newDocument()
|
|
|
|
d1, err := tb.Insert(doc1.Clone())
|
|
require.NoError(t, err)
|
|
d2, err := tb.Insert(doc2.Clone())
|
|
require.NoError(t, err)
|
|
|
|
// delete the document
|
|
err = tb.Delete([]byte(d1.(document.Keyer).RawKey()))
|
|
require.NoError(t, err)
|
|
|
|
// try again, should fail
|
|
err = tb.Delete([]byte(d1.(document.Keyer).RawKey()))
|
|
require.Equal(t, errs.ErrDocumentNotFound, err)
|
|
|
|
// make sure it didn't also delete the other one
|
|
res, err := tb.GetDocument(d2.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
_, err = res.GetByField("fieldc")
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestTableReplace verifies Replace behaviour.
|
|
func TestTableReplace(t *testing.T) {
|
|
t.Run("Should fail if not found", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
_, err := tb.Replace([]byte("id"), newDocument())
|
|
require.Equal(t, errs.ErrDocumentNotFound, err)
|
|
})
|
|
|
|
t.Run("Should replace the right document", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
// create two different documents
|
|
doc1 := newDocument()
|
|
doc2 := document.NewFieldBuffer().
|
|
Add("fielda", document.NewTextValue("c")).
|
|
Add("fieldb", document.NewTextValue("d"))
|
|
|
|
d1, err := tb.Insert(doc1)
|
|
require.NoError(t, err)
|
|
d2, err := tb.Insert(doc2)
|
|
require.NoError(t, err)
|
|
|
|
// create a third document
|
|
doc3 := document.NewFieldBuffer().
|
|
Add("fielda", document.NewTextValue("e")).
|
|
Add("fieldb", document.NewTextValue("f"))
|
|
|
|
// replace doc1 with doc3
|
|
d3, err := tb.Replace(d1.(document.Keyer).RawKey(), doc3)
|
|
require.NoError(t, err)
|
|
|
|
// make sure it replaced it correctly
|
|
res, err := tb.GetDocument(d1.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
f, err := res.GetByField("fielda")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "e", f.V.(string))
|
|
|
|
testutil.RequireDocEqual(t, d3, res)
|
|
|
|
// make sure it didn't also replace the other one
|
|
res, err = tb.GetDocument(d2.(document.Keyer).RawKey())
|
|
require.NoError(t, err)
|
|
f, err = res.GetByField("fielda")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "c", f.V.(string))
|
|
})
|
|
|
|
t.Run("Should update indexes", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test1"})
|
|
createTable(t, tx, db.Catalog, database.TableInfo{TableName: "test2"})
|
|
|
|
// simple indexes
|
|
err := db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Paths: []document.Path{document.NewPath("a")},
|
|
Unique: true,
|
|
TableName: "test1",
|
|
IndexName: "idx_foo_a",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// composite indexes
|
|
err = db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Paths: []document.Path{document.NewPath("x"), document.NewPath("y")},
|
|
Unique: true,
|
|
TableName: "test2",
|
|
IndexName: "idx_foo_x_y",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test1")
|
|
require.NoError(t, err)
|
|
|
|
// insert two different documents
|
|
d1, err := tb.Insert(testutil.MakeDocument(t, `{"a": 1, "b": 1}`))
|
|
require.NoError(t, err)
|
|
d2, err := tb.Insert(testutil.MakeDocument(t, `{"a": 2, "b": 2}`))
|
|
require.NoError(t, err)
|
|
|
|
beforeIdxA := testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_a")
|
|
|
|
// --- a
|
|
// replace d1 without modifying indexed key
|
|
_, err = tb.Replace(d1.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"a": 1, "b": 3}`))
|
|
require.NoError(t, err)
|
|
|
|
// indexes should be the same as before
|
|
require.Equal(t, beforeIdxA, testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_a"))
|
|
|
|
// replace d2 and modify indexed key
|
|
_, err = tb.Replace(d2.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"a": 3, "b": 3}`))
|
|
require.NoError(t, err)
|
|
|
|
// indexes should be different for d2
|
|
got := testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_a")
|
|
require.Equal(t, beforeIdxA[0], got[0])
|
|
require.NotEqual(t, beforeIdxA[1], got[1])
|
|
|
|
// replace d1 with duplicate indexed key
|
|
_, err = tb.Replace(d1.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"a": 3, "b": 3}`))
|
|
|
|
// index should be the same as before
|
|
require.Equal(t, errs.ErrDuplicateDocument, err)
|
|
|
|
// --- x, y
|
|
tb, err = db.Catalog.GetTable(tx, "test2")
|
|
require.NoError(t, err)
|
|
// insert two different documents
|
|
dc1, err := tb.Insert(testutil.MakeDocument(t, `{"x": 1, "y": 1, "z": 1}`))
|
|
require.NoError(t, err)
|
|
dc2, err := tb.Insert(testutil.MakeDocument(t, `{"x": 2, "y": 2, "z": 2}`))
|
|
require.NoError(t, err)
|
|
|
|
beforeIdxXY := testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_x_y")
|
|
// replace dc1 without modifying indexed key
|
|
_, err = tb.Replace(dc1.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"x": 1, "y": 1, "z": 2}`))
|
|
require.NoError(t, err)
|
|
|
|
// index should be the same as before
|
|
require.Equal(t, beforeIdxXY, testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_x_y"))
|
|
|
|
// replace dc2 and modify indexed key
|
|
_, err = tb.Replace(dc2.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"x": 3, "y": 3, "z": 3}`))
|
|
require.NoError(t, err)
|
|
|
|
// indexes should be different for d2
|
|
got = testutil.GetIndexContent(t, tx, db.Catalog, "idx_foo_x_y")
|
|
require.Equal(t, beforeIdxXY[0], got[0])
|
|
require.NotEqual(t, beforeIdxXY[1], got[1])
|
|
|
|
// replace dc2 with duplicate indexed key
|
|
_, err = tb.Replace(dc1.(document.Keyer).RawKey(), testutil.MakeDocument(t, `{"x": 3, "y": 3, "z": 3}`))
|
|
|
|
// index should be the same as before
|
|
require.Equal(t, errs.ErrDuplicateDocument, err)
|
|
})
|
|
}
|
|
|
|
// TestTableTruncate verifies Truncate behaviour.
|
|
func TestTableTruncate(t *testing.T) {
|
|
t.Run("Should succeed if table empty", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
err := tb.Truncate()
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should truncate the table", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
// create two documents
|
|
doc1 := newDocument()
|
|
doc2 := newDocument()
|
|
|
|
_, err := tb.Insert(doc1)
|
|
require.NoError(t, err)
|
|
_, err = tb.Insert(doc2)
|
|
require.NoError(t, err)
|
|
|
|
err = tb.Truncate()
|
|
require.NoError(t, err)
|
|
|
|
err = tb.Iterate(func(_ document.Document) error {
|
|
return errors.New("should not iterate")
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestTableIndexes(t *testing.T) {
|
|
t.Run("Should succeed if table has no indexes", func(t *testing.T) {
|
|
tb, cleanup := newTestTable(t)
|
|
defer cleanup()
|
|
|
|
list, err := tb.GetIndexes()
|
|
require.NoError(t, err)
|
|
require.Empty(t, list)
|
|
})
|
|
|
|
t.Run("Should return a list of all the indexes", func(t *testing.T) {
|
|
db, tx, cleanup := newTestTx(t)
|
|
defer cleanup()
|
|
|
|
err := db.Catalog.CreateTable(tx, "test1", nil)
|
|
require.NoError(t, err)
|
|
|
|
err = db.Catalog.CreateTable(tx, "test2", nil)
|
|
require.NoError(t, err)
|
|
|
|
err = db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Unique: true,
|
|
IndexName: "idx1a",
|
|
TableName: "test1",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "a")},
|
|
})
|
|
require.NoError(t, err)
|
|
err = db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Unique: false,
|
|
IndexName: "idx1b",
|
|
TableName: "test1",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "b")},
|
|
})
|
|
require.NoError(t, err)
|
|
err = db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Unique: false,
|
|
IndexName: "idx1ab",
|
|
TableName: "test1",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "a"), testutil.ParseDocumentPath(t, "b")},
|
|
})
|
|
require.NoError(t, err)
|
|
err = db.Catalog.CreateIndex(tx, &database.IndexInfo{
|
|
Unique: false,
|
|
IndexName: "idx2a",
|
|
TableName: "test2",
|
|
Paths: []document.Path{testutil.ParseDocumentPath(t, "a")},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tb, err := db.Catalog.GetTable(tx, "test1")
|
|
require.NoError(t, err)
|
|
|
|
m, err := tb.GetIndexes()
|
|
require.NoError(t, err)
|
|
require.Len(t, m, 3)
|
|
})
|
|
}
|
|
|
|
// BenchmarkTableInsert benchmarks the Insert method with 1, 10, 1000 and 10000 successive insertions.
|
|
func BenchmarkTableInsert(b *testing.B) {
|
|
for size := 1; size <= 10000; size *= 10 {
|
|
b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
|
|
var fb document.FieldBuffer
|
|
|
|
for i := int64(0); i < 10; i++ {
|
|
fb.Add(fmt.Sprintf("name-%d", i), document.NewIntegerValue(i))
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.StopTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
tb, cleanup := newTestTable(b)
|
|
|
|
b.StartTimer()
|
|
for j := 0; j < size; j++ {
|
|
tb.Insert(&fb)
|
|
}
|
|
b.StopTimer()
|
|
cleanup()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkTableScan benchmarks the Scan method with 1, 10, 1000 and 10000 successive insertions.
|
|
func BenchmarkTableScan(b *testing.B) {
|
|
for size := 1; size <= 10000; size *= 10 {
|
|
b.Run(fmt.Sprintf("%.05d", size), func(b *testing.B) {
|
|
tb, cleanup := newTestTable(b)
|
|
defer cleanup()
|
|
|
|
var fb document.FieldBuffer
|
|
|
|
for i := int64(0); i < 10; i++ {
|
|
fb.Add(fmt.Sprintf("name-%d", i), document.NewIntegerValue(i))
|
|
}
|
|
|
|
for i := 0; i < size; i++ {
|
|
_, err := tb.Insert(&fb)
|
|
require.NoError(b, err)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
tb.Iterate(func(document.Document) error {
|
|
return nil
|
|
})
|
|
}
|
|
b.StopTimer()
|
|
})
|
|
}
|
|
}
|