parent
6e6a042a16
commit
eeb83daf4b
@ -0,0 +1,134 @@ |
|||||||
|
package database |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"reflect" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"text/template" |
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive" |
||||||
|
"go.mongodb.org/mongo-driver/mongo" |
||||||
|
"go.mongodb.org/mongo-driver/mongo/options" |
||||||
|
) |
||||||
|
|
||||||
|
// IndexEnsure function ensures index in mongo collection of document
|
||||||
|
// `index:""` -- https://docs.mongodb.com/manual/indexes/#create-an-index
|
||||||
|
// `index:"-"` -- (descending)
|
||||||
|
// `index:"-,+foo,+-bar"` -- https://docs.mongodb.com/manual/core/index-compound
|
||||||
|
// `index:"-,+foo,+-bar,unique"` -- https://docs.mongodb.com/manual/core/index-unique
|
||||||
|
// `index:"-,+foo,+-bar,unique,allowNull"` -- https://docs.mongodb.com/manual/core/index-partial
|
||||||
|
// `index:"-,unique,allowNull,expireAfter=86400"` -- https://docs.mongodb.com/manual/core/index-ttl
|
||||||
|
// `index:"-,unique,allowNull,expireAfter={{.Expire}}"` -- evaluate index as a golang template with `cfg` arguments
|
||||||
|
func (d *Database) IndexEnsure(cfg interface{}, document interface{}) error { |
||||||
|
|
||||||
|
el := reflect.ValueOf(document).Elem().Type() |
||||||
|
numField := el.NumField() |
||||||
|
documents := d.GetCollectionOf(document) |
||||||
|
|
||||||
|
for i := 0; i < numField; i++ { |
||||||
|
|
||||||
|
field := el.Field(i) |
||||||
|
tag := field.Tag |
||||||
|
|
||||||
|
indexTag, ok := tag.Lookup("index") |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
bsonTag, ok := tag.Lookup("bson") |
||||||
|
if !ok { |
||||||
|
return fmt.Errorf("bson tag is not defined for field:%v document:%v", field, document) |
||||||
|
} |
||||||
|
|
||||||
|
tmpBuffer := &bytes.Buffer{} |
||||||
|
tpl, err := template.New("").Parse(indexTag) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Errorf("invalid prop template, %v", indexTag)) |
||||||
|
} |
||||||
|
err = tpl.Execute(tmpBuffer, cfg) |
||||||
|
if err != nil { |
||||||
|
panic(fmt.Errorf("failed to evaluate prop template, %v", indexTag)) |
||||||
|
} |
||||||
|
|
||||||
|
indexString := tmpBuffer.String() |
||||||
|
indexValues := strings.Split(indexString, ",") |
||||||
|
bsonValues := strings.Split(bsonTag, ",") |
||||||
|
|
||||||
|
var f = false |
||||||
|
var t = true |
||||||
|
var key = bsonValues[0] |
||||||
|
var name = fmt.Sprintf("%s_%s_", indexString, key) |
||||||
|
|
||||||
|
if len(key) == 0 { |
||||||
|
panic(fmt.Errorf("cannot evaluate index key")) |
||||||
|
} |
||||||
|
|
||||||
|
index := primitive.M{key: 1} |
||||||
|
opts := &options.IndexOptions{ |
||||||
|
Background: &f, |
||||||
|
Unique: &f, |
||||||
|
Name: &name, |
||||||
|
} |
||||||
|
|
||||||
|
if indexValues[0] == "-" { |
||||||
|
index[key] = -1 |
||||||
|
} |
||||||
|
|
||||||
|
for _, prop := range indexValues[1:] { |
||||||
|
var left string |
||||||
|
var right string |
||||||
|
|
||||||
|
pair := strings.SplitN(prop, "=", 2) |
||||||
|
left = pair[0] |
||||||
|
if len(pair) > 1 { |
||||||
|
right = pair[1] |
||||||
|
} |
||||||
|
|
||||||
|
switch { |
||||||
|
case left == "unique": |
||||||
|
opts.Unique = &t |
||||||
|
|
||||||
|
case left == "allowNull": |
||||||
|
expression, isMap := opts.PartialFilterExpression.(primitive.M) |
||||||
|
if !isMap || expression == nil { |
||||||
|
expression = primitive.M{} |
||||||
|
} |
||||||
|
|
||||||
|
expression[key] = primitive.M{"$exists": true} |
||||||
|
opts.PartialFilterExpression = expression |
||||||
|
|
||||||
|
case left == "expireAfter": |
||||||
|
expireAfter, err := strconv.Atoi(right) |
||||||
|
if err != nil || expireAfter < 1 { |
||||||
|
panic(fmt.Errorf("invalid expireAfter value, err: %w", err)) |
||||||
|
} |
||||||
|
|
||||||
|
expireAfterInt32 := int32(expireAfter) |
||||||
|
opts.ExpireAfterSeconds = &expireAfterInt32 |
||||||
|
|
||||||
|
case len(left) > 0 && left[0] == '+': |
||||||
|
compoundValue := left[1:] |
||||||
|
if len(compoundValue) == 0 { |
||||||
|
panic(fmt.Errorf("invalid compound value")) |
||||||
|
} |
||||||
|
|
||||||
|
if compoundValue[0] == '-' { |
||||||
|
index[compoundValue[1:]] = -1 |
||||||
|
} else { |
||||||
|
index[compoundValue] = 1 |
||||||
|
} |
||||||
|
|
||||||
|
default: |
||||||
|
panic(fmt.Errorf("unsupported flag:%q in tag:%q of type:%s", prop, tag, el)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_, err = documents.Indexes().CreateOne(d.Context(), mongo.IndexModel{Keys: index, Options: opts}) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
package database_test |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
|
||||||
|
"github.com/mainnika/mongox-go-driver/v2/mongox-testing/database" |
||||||
|
"github.com/mainnika/mongox-go-driver/v2/mongox/base/oidbased" |
||||||
|
) |
||||||
|
|
||||||
|
func TestDatabase_Ensure(t *testing.T) { |
||||||
|
|
||||||
|
db, err := database.NewEphemeral("mongodb://localhost") |
||||||
|
if err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
defer db.Close() |
||||||
|
|
||||||
|
testvalues := []struct { |
||||||
|
doc interface{} |
||||||
|
settings map[string]interface{} |
||||||
|
index map[string]interface{} |
||||||
|
}{ |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"1"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:"-,unique,allowNull,expireAfter=86400"` |
||||||
|
Foo int `bson:"foo" json:"foo"` |
||||||
|
Bar int `bson:"bar" json:"bar"` |
||||||
|
}{}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"expireAfterSeconds": int32(86400), |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(-1), |
||||||
|
}, |
||||||
|
"name": "-,unique,allowNull,expireAfter=86400_foobar_", |
||||||
|
"partialFilterExpression": map[string]interface{}{ |
||||||
|
"foobar": map[string]interface{}{"$exists": true}, |
||||||
|
}, |
||||||
|
"unique": true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"2"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:",unique"` |
||||||
|
}{}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(1), |
||||||
|
}, |
||||||
|
"name": ",unique_foobar_", |
||||||
|
"unique": true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"3"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:"-,+foo,+-bar,unique,allowNull"` |
||||||
|
Foo int `bson:"foo" json:"foo"` |
||||||
|
Bar int `bson:"bar" json:"bar"` |
||||||
|
}{}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(-1), |
||||||
|
"foo": int32(1), |
||||||
|
"bar": int32(-1), |
||||||
|
}, |
||||||
|
"name": "-,+foo,+-bar,unique,allowNull_foobar_", |
||||||
|
"partialFilterExpression": map[string]interface{}{ |
||||||
|
"foobar": map[string]interface{}{"$exists": true}, |
||||||
|
}, |
||||||
|
"unique": true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"4"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:""` |
||||||
|
Foo int `bson:"foo" json:"foo"` |
||||||
|
Bar int `bson:"bar" json:"bar"` |
||||||
|
}{}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(1), |
||||||
|
}, |
||||||
|
"name": "_foobar_", |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"5"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:"-"` |
||||||
|
Foo int `bson:"foo" json:"foo"` |
||||||
|
Bar int `bson:"bar" json:"bar"` |
||||||
|
}{}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(-1), |
||||||
|
}, |
||||||
|
"name": "-_foobar_", |
||||||
|
}, |
||||||
|
}, |
||||||
|
{ |
||||||
|
doc: &struct { |
||||||
|
oidbased.Primary `bson:",inline" json:",inline" collection:"1"` |
||||||
|
|
||||||
|
Foobar int `bson:"foobar" json:"foobar" index:"-,unique,allowNull,expireAfter={{.Expire}}"` |
||||||
|
Foo int `bson:"foo" json:"foo"` |
||||||
|
Bar int `bson:"bar" json:"bar"` |
||||||
|
}{}, |
||||||
|
settings: map[string]interface{}{ |
||||||
|
"Expire": 86400, |
||||||
|
}, |
||||||
|
index: map[string]interface{}{ |
||||||
|
"background": false, |
||||||
|
"expireAfterSeconds": int32(86400), |
||||||
|
"key": map[string]interface{}{ |
||||||
|
"foobar": int32(-1), |
||||||
|
}, |
||||||
|
"name": "-,unique,allowNull,expireAfter=86400_foobar_", |
||||||
|
"partialFilterExpression": map[string]interface{}{ |
||||||
|
"foobar": map[string]interface{}{"$exists": true}, |
||||||
|
}, |
||||||
|
"unique": true, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tt := range testvalues { |
||||||
|
err = db.IndexEnsure(tt.settings, tt.doc) |
||||||
|
assert.NoError(t, err) |
||||||
|
|
||||||
|
indexes, _ := db.GetCollectionOf(tt.doc).Indexes().List(db.Context()) |
||||||
|
index := new(map[string]interface{}) |
||||||
|
|
||||||
|
indexes.Next(db.Context()) // skip _id_
|
||||||
|
indexes.Next(db.Context()) |
||||||
|
indexes.Decode(&index) |
||||||
|
|
||||||
|
for k, v := range tt.index { |
||||||
|
assert.Equal(t, v, (*index)[k]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue