diff --git a/api/apicollectionv1/0_traverse.go b/api/apicollectionv1/0_traverse.go index 2d47747..454ece7 100644 --- a/api/apicollectionv1/0_traverse.go +++ b/api/apicollectionv1/0_traverse.go @@ -2,6 +2,7 @@ package apicollectionv1 import ( "encoding/json" + "fmt" "sort" "github.com/SierraSoftworks/connor" @@ -59,7 +60,7 @@ func traverseFullscan(input []byte, col *collection.Collection, f func(row *coll func traverseUnique(input []byte, col *collection.Collection, f func(row *collection.Row)) error { params := &struct { - Field string + Index string Value string }{} err := json.Unmarshal(input, ¶ms) @@ -67,14 +68,22 @@ func traverseUnique(input []byte, col *collection.Collection, f func(row *collec return err } - row, err := col.FindByRow(params.Field, params.Value) + index, exist := col.Indexes[params.Index] + if !exist { + return fmt.Errorf("index '%s' does not exist", params.Index) + } + + traverseOptions, err := json.Marshal(collection.IndexMapTraverse{ + Value: params.Value, + }) if err != nil { - return err - // w.WriteHeader(http.StatusNotFound) - // return fmt.Errorf("item %s='%s' does not exist", params.Field, params.Value) + return fmt.Errorf("marshal traverse options: %s", err.Error()) } - f(row) + index.Traverse(traverseOptions, func(row *collection.Row) bool { + f(row) + return true + }) return nil } diff --git a/api/apicollectionv1/createIndex.go b/api/apicollectionv1/createIndex.go index 27f178d..ae5644a 100644 --- a/api/apicollectionv1/createIndex.go +++ b/api/apicollectionv1/createIndex.go @@ -10,19 +10,19 @@ import ( "github.com/fulldump/inceptiondb/service" ) -func createIndex(ctx context.Context, options *collection.IndexOptions) (*listIndexesItem, error) { +func createIndex(ctx context.Context, input *collection.CreateIndexOptions) (*listIndexesItem, error) { s := GetServicer(ctx) collectionName := box.GetUrlParameter(ctx, "collectionName") - collection, err := s.GetCollection(collectionName) + col, err := s.GetCollection(collectionName) if err == service.ErrorCollectionNotFound { - collection, err = s.CreateCollection(collectionName) + col, err = s.CreateCollection(collectionName) } if err != nil { return nil, err // todo: handle/wrap this properly } - err = collection.Index(options) + err = col.Index(input) if err != nil { return nil, err } @@ -30,8 +30,8 @@ func createIndex(ctx context.Context, options *collection.IndexOptions) (*listIn box.GetResponse(ctx).WriteHeader(http.StatusCreated) return &listIndexesItem{ - Name: options.Field, - Field: options.Field, - Sparse: options.Sparse, + Name: input.Name, + Kind: input.Kind, + // todo: return parameteres somehow }, nil } diff --git a/api/apicollectionv1/find.go b/api/apicollectionv1/find.go index 4e3b32d..daf2d47 100644 --- a/api/apicollectionv1/find.go +++ b/api/apicollectionv1/find.go @@ -3,10 +3,8 @@ package apicollectionv1 import ( "context" "encoding/json" - "fmt" "io" "net/http" - "strings" "github.com/fulldump/box" @@ -21,31 +19,41 @@ func find(ctx context.Context, w http.ResponseWriter, r *http.Request) error { } input := struct { - Mode string + Index string }{ - Mode: "fullscan", + Index: "", } err = json.Unmarshal(rquestBody, &input) if err != nil { return err } - f, exist := findModes[input.Mode] - if !exist { - box.GetResponse(ctx).WriteHeader(http.StatusBadRequest) - return fmt.Errorf("bad mode '%s', must be [%s]. See docs: TODO", input.Mode, strings.Join(GetKeys(findModes), "|")) - } - s := GetServicer(ctx) collectionName := box.GetUrlParameter(ctx, "collectionName") - collection, err := s.GetCollection(collectionName) + col, err := s.GetCollection(collectionName) if err != nil { return err // todo: handle/wrap this properly } - return f(rquestBody, collection, w) + index, exists := col.Indexes[input.Index] + if !exists { + traverseFullscan(rquestBody, col, func(row *collection.Row) { + w.Write(row.Payload) + w.Write([]byte("\n")) + }) + return nil + } + + index.Traverse(rquestBody, func(row *collection.Row) bool { + w.Write(row.Payload) + w.Write([]byte("\n")) + return true + }) + + return nil } +// TODO: remove this var findModes = map[string]func(input []byte, col *collection.Collection, w http.ResponseWriter) error{ "fullscan": func(input []byte, col *collection.Collection, w http.ResponseWriter) error { return traverseFullscan(input, col, writeRow(w)) diff --git a/api/apicollectionv1/getIndex.go b/api/apicollectionv1/getIndex.go index 65d6009..c9ecd53 100644 --- a/api/apicollectionv1/getIndex.go +++ b/api/apicollectionv1/getIndex.go @@ -22,11 +22,13 @@ func getIndex(ctx context.Context, input getIndexInput) (*listIndexesItem, error } for name, index := range collection.Indexes { + _ = index if name == input.Name { return &listIndexesItem{ - Name: name, - Field: name, - Sparse: index.Sparse, + Name: name, + // Field: name, + // Sparse: index.Sparse, + // todo: fild properly }, nil } } diff --git a/api/apicollectionv1/listIndexes.go b/api/apicollectionv1/listIndexes.go index e2ce9e6..fc88806 100644 --- a/api/apicollectionv1/listIndexes.go +++ b/api/apicollectionv1/listIndexes.go @@ -2,15 +2,16 @@ package apicollectionv1 import ( "context" + "encoding/json" "net/http" "github.com/fulldump/box" ) type listIndexesItem struct { - Name string `json:"name"` - Field string `json:"field"` - Sparse bool `json:"sparse"` + Name string `json:"name"` + Kind string `json:"kind"` + Parameters json.RawMessage `json:"parameters"` // todo: find a better name } func listIndexes(ctx context.Context, w http.ResponseWriter) ([]*listIndexesItem, error) { @@ -24,10 +25,10 @@ func listIndexes(ctx context.Context, w http.ResponseWriter) ([]*listIndexesItem result := []*listIndexesItem{} for name, index := range collection.Indexes { + _ = index result = append(result, &listIndexesItem{ - Name: name, - Field: name, - Sparse: index.Sparse, + Name: name, + // TODO: complete the rest of fields }) } diff --git a/api/apicollectionv1/patch.go b/api/apicollectionv1/patch.go index af99ab5..94644e0 100644 --- a/api/apicollectionv1/patch.go +++ b/api/apicollectionv1/patch.go @@ -3,10 +3,8 @@ package apicollectionv1 import ( "context" "encoding/json" - "fmt" "io" "net/http" - "strings" "github.com/fulldump/box" @@ -21,48 +19,46 @@ func patch(ctx context.Context, w http.ResponseWriter, r *http.Request) error { } input := struct { - Mode string + Index string }{ - Mode: "fullscan", + Index: "", } err = json.Unmarshal(rquestBody, &input) if err != nil { return err } - f, exist := patchModes[input.Mode] - if !exist { - box.GetResponse(ctx).WriteHeader(http.StatusBadRequest) - return fmt.Errorf("bad mode '%s', must be [%s]. See docs: TODO", input.Mode, strings.Join(GetKeys(patchModes), "|")) - } - s := GetServicer(ctx) collectionName := box.GetUrlParameter(ctx, "collectionName") - collection, err := s.GetCollection(collectionName) + col, err := s.GetCollection(collectionName) if err != nil { return err // todo: handle/wrap this properly } - return f(rquestBody, collection, w) -} + e := json.NewEncoder(w) -var patchModes = map[string]func(input []byte, col *collection.Collection, w http.ResponseWriter) error{ - "fullscan": func(input []byte, col *collection.Collection, w http.ResponseWriter) error { - return traverseFullscan(input, col, patchRow(input, col, json.NewEncoder(w))) - }, - "unique": func(input []byte, col *collection.Collection, w http.ResponseWriter) (err error) { - return traverseUnique(input, col, patchRow(input, col, json.NewEncoder(w))) - }, + index, exists := col.Indexes[input.Index] + if !exists { + traverseFullscan(rquestBody, col, func(row *collection.Row) { + patchRow(rquestBody, col, row, e) + }) + return nil + } + + index.Traverse(rquestBody, func(row *collection.Row) bool { + patchRow(rquestBody, col, row, e) + return true + }) + + return nil } -func patchRow(input []byte, col *collection.Collection, e *json.Encoder) func(row *collection.Row) { - return func(row *collection.Row) { - patch := struct { - Patch interface{} - }{} - json.Unmarshal(input, &patch) // TODO: handle err +func patchRow(input []byte, col *collection.Collection, row *collection.Row, e *json.Encoder) { + patch := struct { + Patch interface{} + }{} + json.Unmarshal(input, &patch) // TODO: handle err - _ = col.Patch(row, patch.Patch) // TODO: handle err - e.Encode(row.Payload) - } + _ = col.Patch(row, patch.Patch) // TODO: handle err + e.Encode(row.Payload) } diff --git a/api/apicollectionv1/remove.go b/api/apicollectionv1/remove.go index 650b459..4feeedc 100644 --- a/api/apicollectionv1/remove.go +++ b/api/apicollectionv1/remove.go @@ -3,10 +3,8 @@ package apicollectionv1 import ( "context" "encoding/json" - "fmt" "io" "net/http" - "strings" "github.com/fulldump/box" @@ -21,50 +19,44 @@ func remove(ctx context.Context, w http.ResponseWriter, r *http.Request) error { } input := struct { - Mode string + Index string }{ - Mode: "fullscan", + Index: "", } err = json.Unmarshal(rquestBody, &input) if err != nil { return err } - f, exist := removeModes[input.Mode] - if !exist { - box.GetResponse(ctx).WriteHeader(http.StatusBadRequest) - return fmt.Errorf("bad mode '%s', must be [%s]. See docs: TODO", input.Mode, strings.Join(GetKeys(removeModes), "|")) - } - s := GetServicer(ctx) collectionName := box.GetUrlParameter(ctx, "collectionName") - collection, err := s.GetCollection(collectionName) + col, err := s.GetCollection(collectionName) if err != nil { return err // todo: handle/wrap this properly } - return f(rquestBody, collection, w) -} - -var removeModes = map[string]func(input []byte, col *collection.Collection, w http.ResponseWriter) error{ - "fullscan": func(input []byte, col *collection.Collection, w http.ResponseWriter) error { - traverseFullscan(input, col, removeRow(col, w)) + index, exists := col.Indexes[input.Index] + if !exists { + traverseFullscan(rquestBody, col, func(row *collection.Row) { + removeRow(col, row, w) + }) return nil - }, - "unique": func(input []byte, col *collection.Collection, w http.ResponseWriter) (err error) { - return traverseUnique(input, col, removeRow(col, w)) - }, -} + } -func removeRow(col *collection.Collection, w http.ResponseWriter) func(row *collection.Row) { - return func(row *collection.Row) { + index.Traverse(rquestBody, func(row *collection.Row) bool { + removeRow(col, row, w) + return true + }) - err := col.Remove(row) - if err != nil { - return - } + return nil +} - w.Write(row.Payload) - w.Write([]byte("\n")) +func removeRow(col *collection.Collection, row *collection.Row, w http.ResponseWriter) { + err := col.Remove(row) + if err != nil { + return } + + w.Write(row.Payload) + w.Write([]byte("\n")) } diff --git a/collection/collection.go b/collection/collection.go index aa04b1b..9b4b3fb 100644 --- a/collection/collection.go +++ b/collection/collection.go @@ -60,11 +60,12 @@ func OpenCollection(filename string) (*Collection, error) { return nil, err } case "index": - options := &IndexOptions{} + // TODO: implement this + options := &CreateIndexOptions{} json.Unmarshal(command.Payload, options) // Todo: handle error properly - err := collection.indexRows(options) + err := collection.createIndex(options, false) if err != nil { - fmt.Printf("WARNING: create index '%s': %s\n", options.Field, err.Error()) + fmt.Printf("WARNING: create index '%s': %s\n", options.Name, err.Error()) } case "remove": params := struct { @@ -179,35 +180,53 @@ func (c *Collection) TraverseRange(from, to int, f func(row *Row)) { // todo: im } } -func (c *Collection) indexRows(options *IndexOptions) error { +type CreateIndexOptions struct { + Name string `json:"name"` + Kind string `json:"kind"` // todo: find a better name + Parameters json.RawMessage `json:"parameters"` +} - index := Index{ - Entries: map[string]*Row{}, - RWmutex: &sync.RWMutex{}, - Sparse: options.Sparse, // include the whole `options` struct? - } - for _, row := range c.Rows { - err := indexRow(index, options.Field, row) - if err != nil { - return fmt.Errorf("index row: %w, data: %s", err, string(row.Payload)) - } +// IndexMap create a unique index with a name +// Constraints: values can be only scalar strings or array of strings +func (c *Collection) Index(options *CreateIndexOptions) error { + return c.createIndex(options, true) +} + +func (c *Collection) createIndex(options *CreateIndexOptions, persist bool) error { + + if _, exists := c.Indexes[options.Name]; exists { + return fmt.Errorf("index '%s' already exists", options.Name) } - c.Indexes[options.Field] = index - return nil -} + var index Index -// Index create a unique index with a name -// Constraints: values can be only scalar strings or array of strings -func (c *Collection) Index(options *IndexOptions) error { + switch options.Kind { + case "map": + parameters := &IndexMapOptions{} + json.Unmarshal(options.Parameters, ¶meters) + index = NewIndexMap(parameters) + case "btree": + // todo: implement this + parameters := &IndexBTreeOptions{} + json.Unmarshal(options.Parameters, ¶meters) + index = NewIndexBTree(parameters) + default: + return fmt.Errorf("unexpected kind, it should be [map|btree]") + } + + c.Indexes[options.Name] = index - if _, exists := c.Indexes[options.Field]; exists { - return fmt.Errorf("index '%s' already exists", options.Field) + // Add all rows to the index + for _, row := range c.Rows { + err := index.AddRow(row) + if err != nil { + delete(c.Indexes, options.Name) + return fmt.Errorf("index row: %s, data: %s", err.Error(), string(row.Payload)) + } } - err := c.indexRows(options) - if err != nil { - return err + if !persist { + return nil } payload, err := json.Marshal(options) @@ -233,134 +252,28 @@ func (c *Collection) Index(options *IndexOptions) error { func indexInsert(indexes map[string]Index, row *Row) (err error) { for key, index := range indexes { - err = indexRow(index, key, row) + err = index.AddRow(row) if err != nil { // TODO: undo previous work? two phases (check+commit) ? - break + return fmt.Errorf("index add '%s': %s", key, err.Error()) } } return } -func indexRow(index Index, field string, row *Row) error { - - item := map[string]interface{}{} - - err := json.Unmarshal(row.Payload, &item) - if err != nil { - return fmt.Errorf("unmarshal: %w", err) - } - - itemValue, itemExists := item[field] - if !itemExists { - if index.Sparse { - // Do not index - return nil - } - return fmt.Errorf("field `%s` is indexed and mandatory", field) - } - - switch value := itemValue.(type) { - case string: - - index.RWmutex.RLock() - _, exists := index.Entries[value] - index.RWmutex.RUnlock() - if exists { - return fmt.Errorf("index conflict: field '%s' with value '%s'", field, value) - } - - index.RWmutex.Lock() - index.Entries[value] = row - index.RWmutex.Unlock() - case []interface{}: - for _, v := range value { - s := v.(string) // TODO: handle this casting error - if _, exists := index.Entries[s]; exists { - return fmt.Errorf("index conflict: field '%s' with value '%s'", field, value) - } - } - for _, v := range value { - s := v.(string) // TODO: handle this casting error - index.Entries[s] = row - } - default: - return fmt.Errorf("type not supported") - } - - return nil -} - func indexRemove(indexes map[string]Index, row *Row) (err error) { for key, index := range indexes { - err = unindexRow(index, key, row) + err = index.RemoveRow(row) if err != nil { // TODO: does this make any sense? - break + return fmt.Errorf("index remove '%s': %s", key, err.Error()) } } return } -func unindexRow(index Index, field string, row *Row) error { - - item := map[string]interface{}{} - - err := json.Unmarshal(row.Payload, &item) - if err != nil { - return fmt.Errorf("unmarshal: %w", err) - } - - itemValue, itemExists := item[field] - if !itemExists { - // Do not index - return nil - } - - switch value := itemValue.(type) { - case string: - delete(index.Entries, value) - case []interface{}: - for _, v := range value { - s := v.(string) // TODO: handle this casting error - delete(index.Entries, s) - } - default: - // Should this error? - return fmt.Errorf("type not supported") - } - - return nil -} - -// Deprecated -func (c *Collection) FindBy(field string, value string, data interface{}) error { - - row, err := c.FindByRow(field, value) - if err != nil { - return err - } - - return json.Unmarshal(row.Payload, &data) -} - -func (c *Collection) FindByRow(field string, value string) (*Row, error) { - - index, ok := c.Indexes[field] - if !ok { - return nil, fmt.Errorf("field '%s' is not indexed", field) - } - - row, ok := index.Entries[value] - if !ok { - return nil, fmt.Errorf("%s '%s' not found", field, value) - } - - return row, nil -} - func (c *Collection) Remove(r *Row) error { return c.removeByRow(r, true) } @@ -372,7 +285,7 @@ func lockBlock(m *sync.Mutex, f func() error) error { return f() } -func (c *Collection) removeByRow(row *Row, persist bool) error { +func (c *Collection) removeByRow(row *Row, persist bool) error { // todo: rename to 'removeRow' var i int err := lockBlock(c.rowsMutex, func() error { @@ -428,7 +341,7 @@ func (c *Collection) Patch(row *Row, patch interface{}) error { return c.patchByRow(row, patch, true) } -func (c *Collection) patchByRow(row *Row, patch interface{}, persist bool) error { +func (c *Collection) patchByRow(row *Row, patch interface{}, persist bool) error { // todo: rename to 'patchRow' patchBytes, err := json.Marshal(patch) if err != nil { diff --git a/collection/collection_test.go b/collection/collection_test.go index 44c3c84..26ed62e 100644 --- a/collection/collection_test.go +++ b/collection/collection_test.go @@ -101,16 +101,31 @@ func TestIndex(t *testing.T) { c.Insert(&User{"2", "Sara"}) // Run - c.Index(&IndexOptions{Field: "id"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"id"}`), + }) // Check user := &User{} - errFindBy := c.FindBy("id", "2", user) - AssertNil(errFindBy) + c.Indexes["my-index"].Traverse([]byte(`{"value":"2"}`), func(row *Row) bool { + json.Unmarshal(row.Payload, &user) + return false + }) AssertEqual(user.Name, "Sara") }) } +func findByIndex(index Index, options string, value interface{}) (n int) { + index.Traverse([]byte(options), func(row *Row) bool { + n++ + json.Unmarshal(row.Payload, &value) + return false + }) + return +} + func TestInsertAfterIndex(t *testing.T) { type User struct { Id string `json:"id"` @@ -122,13 +137,16 @@ func TestInsertAfterIndex(t *testing.T) { c, _ := OpenCollection(filename) // Run - c.Index(&IndexOptions{Field: "id"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"id"}`), + }) c.Insert(&User{"1", "Pablo"}) // Check user := &User{} - errFindBy := c.FindBy("id", "1", user) - AssertNil(errFindBy) + findByIndex(c.Indexes["my-index"], `{"value":"1"}`, user) AssertEqual(user.Name, "Pablo") }) } @@ -146,29 +164,40 @@ func TestIndexMultiValue(t *testing.T) { c.Insert(newUser) // Run - indexErr := c.Index(&IndexOptions{Field: "email"}) + indexErr := c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"email"}`), + }) // Check AssertNil(indexErr) u := &User{} - c.FindBy("email", "p18@yahoo.com", u) + findByIndex(c.Indexes["my-index"], `{"value":"p18@yahoo.com"}`, u) AssertEqual(u.Id, newUser.Id) }) } +// TODO: this should be a unit test for IndexMap func TestIndexSparse(t *testing.T) { Environment(func(filename string) { // Setup c, _ := OpenCollection(filename) - c.Insert(map[string]interface{}{"id": "1"}) + row, err := c.Insert(map[string]interface{}{"id": "1"}) // Run - errIndex := c.Index(&IndexOptions{Field: "email", Sparse: true}) + errIndex := c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"email", "sparse":true}`), + }) // Check AssertNil(errIndex) - AssertEqual(len(c.Indexes["email"].Entries), 0) + AssertNotNil(row) + AssertNil(err) + AssertEqual(len(c.Indexes["my-index"].(*IndexMap).Entries), 0) }) } @@ -180,7 +209,11 @@ func TestIndexNonSparse(t *testing.T) { c.Insert(map[string]interface{}{"id": "1"}) // Run - errIndex := c.Index(&IndexOptions{Field: "email", Sparse: false}) + errIndex := c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"email", "sparse":false}`), + }) // Check AssertNotNil(errIndex) @@ -201,10 +234,15 @@ func TestCollection_Index_Collision(t *testing.T) { c.Insert(&User{"1", "Sara"}) // Run - err := c.Index(&IndexOptions{Field: "id"}) + errIndex := c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"id"}`), + }) // Check - AssertNotNil(err) + AssertNotNil(errIndex) + AssertEqual(errIndex.Error(), `index row: index conflict: field 'id' with value '1', data: {"id":"1","name":"Sara"}`) }) } @@ -214,7 +252,11 @@ func TestPersistenceInsertAndIndex(t *testing.T) { // Setup c, _ := OpenCollection(filename) c.Insert(map[string]interface{}{"id": "1", "name": "Pablo", "email": []string{"pablo@email.com", "pablo2018@yahoo.com"}}) - c.Index(&IndexOptions{Field: "email"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"email"}`), + }) c.Insert(map[string]interface{}{"id": "2", "name": "Sara", "email": []string{"sara@email.com", "sara.jimenez8@yahoo.com"}}) c.Close() @@ -225,10 +267,9 @@ func TestPersistenceInsertAndIndex(t *testing.T) { Name string Email []string }{} - findByErr := c.FindBy("email", "sara@email.com", &user) + findByIndex(c.Indexes["my-index"], `{"value":"sara@email.com"}`, &user) // Check - AssertNil(findByErr) AssertEqual(user.Id, "2") }) @@ -239,7 +280,11 @@ func TestPersistenceDelete(t *testing.T) { // Setup c, _ := OpenCollection(filename) - c.Index(&IndexOptions{Field: "email"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"email"}`), + }) c.Insert(map[string]interface{}{"id": "1", "name": "Pablo", "email": []string{"pablo@email.com", "pablo2018@yahoo.com"}}) row, _ := c.Insert(map[string]interface{}{"id": "2", "name": "Sara", "email": []string{"sara@email.com", "sara.jimenez8@yahoo.com"}}) c.Insert(map[string]interface{}{"id": "3", "name": "Ana", "email": []string{"ana@email.com", "ana@yahoo.com"}}) @@ -254,14 +299,11 @@ func TestPersistenceDelete(t *testing.T) { Name string Email []string }{} - findByErr := c.FindBy("email", "sara@email.com", &user) + n := findByIndex(c.Indexes["my-index"], `{"value":"sara@email.com"}`, &user) // Check - AssertNotNil(findByErr) - AssertEqual(findByErr.Error(), "email 'sara@email.com' not found") - + AssertEqual(n, 0) AssertEqual(len(c.Rows), 2) - }) } @@ -271,7 +313,11 @@ func TestPersistenceDeleteTwice(t *testing.T) { // Setup c, _ := OpenCollection(filename) - c.Index(&IndexOptions{Field: "id"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"id"}`), + }) row, _ := c.Insert(map[string]interface{}{"id": "1"}) c.Remove(row) c.Close() @@ -291,7 +337,11 @@ func TestPersistenceUpdate(t *testing.T) { // Setup c, _ := OpenCollection(filename) - c.Index(&IndexOptions{Field: "id"}) + c.Index(&CreateIndexOptions{ + Name: "my-index", + Kind: "map", + Parameters: json.RawMessage(`{"field":"id"}`), + }) row, _ := c.Insert(map[string]interface{}{"id": "1", "name": "Pablo", "email": []string{"pablo@email.com", "pablo2018@yahoo.com"}}) c.Patch(row, map[string]interface{}{"name": "Jaime"}) c.Close() @@ -303,14 +353,13 @@ func TestPersistenceUpdate(t *testing.T) { Name string Email []string }{} - findByErr := c.FindBy("id", "1", &user) + n := findByIndex(c.Indexes["my-index"], `{"value":"1"}`, &user) // Check - AssertNil(findByErr) + AssertEqual(n, 1) AssertEqual(user.Name, "Jaime") AssertEqual(len(c.Rows), 1) - }) } @@ -323,11 +372,15 @@ func TestInsert1M_serial(t *testing.T) { c, _ := OpenCollection(filename) defer c.Close() - c.Index(&IndexOptions{ - Field: "uuid", + c.Index(&CreateIndexOptions{ + Name: "index1", + Kind: "map", + Parameters: json.RawMessage(`{"field":"uuid"}`), }) - c.Index(&IndexOptions{ - Field: "i", + c.Index(&CreateIndexOptions{ + Name: "index2", + Kind: "map", + Parameters: json.RawMessage(`{"field":"i"}`), }) // Run @@ -352,11 +405,15 @@ func TestInsert1M_concurrent(t *testing.T) { c, _ := OpenCollection(filename) defer c.Close() - c.Index(&IndexOptions{ - Field: "uuid", + c.Index(&CreateIndexOptions{ + Name: "index1", + Kind: "map", + Parameters: json.RawMessage(`{"field":"uuid"}`), }) - c.Index(&IndexOptions{ - Field: "i", + c.Index(&CreateIndexOptions{ + Name: "index2", + Kind: "map", + Parameters: json.RawMessage(`{"field":"i"}`), }) // Run diff --git a/collection/index.go b/collection/index.go index 67ec3cc..7517d49 100644 --- a/collection/index.go +++ b/collection/index.go @@ -1,19 +1,7 @@ package collection -import ( - "sync" -) - -// Index should be an interface to allow multiple kinds and implementations -type Index struct { - Entries map[string]*Row - RWmutex *sync.RWMutex - Sparse bool -} - -// IndexOptions should have attributes like unique, sparse, multikey, sorted, background, etc... -// Index should be an interface to have multiple indexes implementations, key value, B-Tree, bitmap, geo, cache... -type IndexOptions struct { - Field string `json:"field"` - Sparse bool `json:"sparse"` +type Index interface { + AddRow(row *Row) error + RemoveRow(row *Row) error + Traverse(options []byte, f func(row *Row) bool) // todo: return error? } diff --git a/collection/indexbtree.go b/collection/indexbtree.go index e7c47a0..8f78494 100644 --- a/collection/indexbtree.go +++ b/collection/indexbtree.go @@ -9,10 +9,31 @@ import ( ) type IndexBtree struct { - Btree *btree.BTreeG[*RowOrdered] - Fields []string - Sparse bool - Unique bool + Btree *btree.BTreeG[*RowOrdered] + Options *IndexBTreeOptions +} + +func (b *IndexBtree) RemoveRow(r *Row) error { + + // TODO: duplicated code: + values := []interface{}{} + data := map[string]interface{}{} + json.Unmarshal(r.Payload, &data) + + for _, field := range b.Options.Fields { + values = append(values, data[field]) + } + + b.Btree.Delete(&RowOrdered{ + Row: r, // probably r is not needed + Values: values, + }) + + return nil +} + +type IndexBtreeTraverse struct { + Reverse bool `json:"reverse"` } type RowOrdered struct { @@ -20,7 +41,19 @@ type RowOrdered struct { Values []interface{} } -func NewIndexBTree(fields []string, sparse, unique bool) *IndexBtree { // todo: group all arguments into a BTreeConfig struct +type IndexBTreeOptions struct { + Fields []string + Sparse bool + Unique bool +} + +// todo: not used? +type IndexBtreeOptions struct { + Name string `json:"name"` + *IndexBTreeOptions +} + +func NewIndexBTree(options *IndexBTreeOptions) *IndexBtree { // todo: group all arguments into a BTreeConfig struct index := btree.NewG(32, func(a, b *RowOrdered) bool { @@ -34,20 +67,20 @@ func NewIndexBTree(fields []string, sparse, unique bool) *IndexBtree { // todo: case string: valB, ok := valB.(string) if !ok { - panic("Type B should be string for field " + fields[i]) + panic("Type B should be string for field " + options.Fields[i]) } return valA < valB case float64: valB, ok := valB.(float64) if !ok { - panic("Type B should be float64 for field " + fields[i]) + panic("Type B should be float64 for field " + options.Fields[i]) } return valA < valB // todo: case bool default: - panic("Type A not supported, field " + fields[i]) + panic("Type A not supported, field " + options.Fields[i]) } } @@ -55,10 +88,8 @@ func NewIndexBTree(fields []string, sparse, unique bool) *IndexBtree { // todo: }) return &IndexBtree{ - Btree: index, - Fields: fields, - Sparse: sparse, - Unique: unique, + Btree: index, + Options: options, } } @@ -67,7 +98,7 @@ func (b *IndexBtree) AddRow(r *Row) error { data := map[string]interface{}{} json.Unmarshal(r.Payload, &data) - for _, field := range b.Fields { + for _, field := range b.Options.Fields { values = append(values, data[field]) } @@ -87,7 +118,10 @@ type TraverseOptions struct { Reverse bool } -func (b *IndexBtree) Traverse(options TraverseOptions, f func(*Row) bool) { +func (b *IndexBtree) Traverse(optionsData []byte, f func(*Row) bool) { + + options := &IndexBtreeTraverse{} + json.Unmarshal(optionsData, options) // todo: handle error if options.Reverse { b.Btree.Descend(func(r *RowOrdered) bool { diff --git a/collection/indexbtree_test.go b/collection/indexbtree_test.go index 3f7cfad..286107c 100644 --- a/collection/indexbtree_test.go +++ b/collection/indexbtree_test.go @@ -14,7 +14,11 @@ import ( func Test_IndexBTree_HappyPath(t *testing.T) { - index := NewIndexBTree([]string{"id"}, false, true) + index := NewIndexBTree(&IndexBTreeOptions{ + Fields: []string{"id"}, + Sparse: false, + Unique: true, + }) n := 4 for i := 0; i < n; i++ { @@ -31,7 +35,7 @@ func Test_IndexBTree_HappyPath(t *testing.T) { `{"id":0}`, `{"id":1}`, `{"id":2}`, `{"id":3}`, } payloads := []string{} - index.Traverse(TraverseOptions{}, func(row *Row) bool { + index.Traverse([]byte(`{}`), func(row *Row) bool { payloads = append(payloads, string(row.Payload)) return true }) @@ -43,7 +47,7 @@ func Test_IndexBTree_HappyPath(t *testing.T) { `{"id":3}`, `{"id":2}`, `{"id":1}`, `{"id":0}`, } reversedPayloads := []string{} - index.Traverse(TraverseOptions{Reverse: true}, func(row *Row) bool { + index.Traverse([]byte(`{"reverse":true}`), func(row *Row) bool { reversedPayloads = append(reversedPayloads, string(row.Payload)) return true }) diff --git a/collection/indexmap.go b/collection/indexmap.go new file mode 100644 index 0000000..4f86b5b --- /dev/null +++ b/collection/indexmap.go @@ -0,0 +1,134 @@ +package collection + +import ( + "encoding/json" + "fmt" + "sync" +) + +// IndexMap should be an interface to allow multiple kinds and implementations +type IndexMap struct { + Entries map[string]*Row + RWmutex *sync.RWMutex + Options *IndexMapOptions +} + +func (i *IndexMap) RemoveRow(row *Row) error { + + item := map[string]interface{}{} + + err := json.Unmarshal(row.Payload, &item) + if err != nil { + return fmt.Errorf("unmarshal: %w", err) + } + + field := i.Options.Field + entries := i.Entries + + itemValue, itemExists := item[field] + if !itemExists { + // Do not index + return nil + } + + switch value := itemValue.(type) { + case string: + delete(entries, value) + case []interface{}: + for _, v := range value { + s := v.(string) // TODO: handle this casting error + delete(entries, s) + } + default: + // Should this error? + return fmt.Errorf("type not supported") + } + + return nil +} + +func NewIndexMap(options *IndexMapOptions) *IndexMap { + return &IndexMap{ + Entries: map[string]*Row{}, + RWmutex: &sync.RWMutex{}, + Options: options, + } +} + +func (i *IndexMap) AddRow(row *Row) error { + + item := map[string]interface{}{} + err := json.Unmarshal(row.Payload, &item) + if err != nil { + return fmt.Errorf("unmarshal: %w", err) + } + + field := i.Options.Field + + itemValue, itemExists := item[field] + if !itemExists { + if i.Options.Sparse { + // Do not index + return nil + } + return fmt.Errorf("field `%s` is indexed and mandatory", field) + } + + mutex := i.RWmutex + entries := i.Entries + + switch value := itemValue.(type) { + case string: + + mutex.RLock() + _, exists := entries[value] + mutex.RUnlock() + if exists { + return fmt.Errorf("index conflict: field '%s' with value '%s'", field, value) + } + + mutex.Lock() + entries[value] = row + mutex.Unlock() + + case []interface{}: + for _, v := range value { + s := v.(string) // TODO: handle this casting error + if _, exists := entries[s]; exists { + return fmt.Errorf("index conflict: field '%s' with value '%s'", field, value) + } + } + for _, v := range value { + s := v.(string) // TODO: handle this casting error + entries[s] = row + } + default: + return fmt.Errorf("type not supported") + } + + return nil +} + +type IndexMapTraverse struct { + Value string `json:"value"` +} + +func (i *IndexMap) Traverse(optionsData []byte, f func(row *Row) bool) { + + options := &IndexMapTraverse{} + json.Unmarshal(optionsData, options) // todo: handle error + + row, ok := i.Entries[options.Value] + if !ok { + return + } + + f(row) +} + +// IndexMapOptions should have attributes like unique, sparse, multikey, sorted, background, etc... +// IndexMap should be an interface to have multiple indexes implementations, key value, B-Tree, bitmap, geo, cache... +type IndexMapOptions struct { + Field string `json:"field"` + Sparse bool `json:"sparse"` +} diff --git a/doc/examples/create_index.md b/doc/examples/create_index.md index bd4be42..fc1a033 100644 --- a/doc/examples/create_index.md +++ b/doc/examples/create_index.md @@ -5,8 +5,11 @@ Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:createIndex" \ -d '{ - "field": "id", - "sparse": true + "kind": "map", + "name": "my-index", + "parameters": { + "field": "id" + } }' ``` @@ -18,19 +21,22 @@ POST /v1/collections/my-collection:createIndex HTTP/1.1 Host: example.com { - "field": "id", - "sparse": true + "kind": "map", + "name": "my-index", + "parameters": { + "field": "id" + } } HTTP/1.1 201 Created -Content-Length: 41 +Content-Length: 51 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { - "field": "id", - "name": "id", - "sparse": true + "kind": "map", + "name": "my-index", + "parameters": null } ``` diff --git a/doc/examples/create_index_-_btree.md b/doc/examples/create_index_-_btree.md new file mode 100644 index 0000000..295ea07 --- /dev/null +++ b/doc/examples/create_index_-_btree.md @@ -0,0 +1,49 @@ +# Create index - btree + +Curl example: + +```sh +curl -X POST "https://example.com/v1/collections/my-collection:createIndex" \ +-d '{ + "kind": "btree", + "name": "my-index", + "parameters": { + "fields": [ + "category", + "product" + ] + } +}' +``` + + +HTTP request/response example: + +```http +POST /v1/collections/my-collection:createIndex HTTP/1.1 +Host: example.com + +{ + "kind": "btree", + "name": "my-index", + "parameters": { + "fields": [ + "category", + "product" + ] + } +} + +HTTP/1.1 201 Created +Content-Length: 53 +Content-Type: text/plain; charset=utf-8 +Date: Mon, 15 Aug 2022 02:08:13 GMT + +{ + "kind": "btree", + "name": "my-index", + "parameters": null +} +``` + + diff --git a/doc/examples/delete_-_by_index.md b/doc/examples/delete_-_by_index.md index e0be231..383901f 100644 --- a/doc/examples/delete_-_by_index.md +++ b/doc/examples/delete_-_by_index.md @@ -5,9 +5,8 @@ Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:remove" \ -d '{ - "field": "id", - "mode": "unique", - "value": "1" + "index": "my-index", + "value": "2" }' ``` @@ -19,9 +18,8 @@ POST /v1/collections/my-collection:remove HTTP/1.1 Host: example.com { - "field": "id", - "mode": "unique", - "value": "1" + "index": "my-index", + "value": "2" } HTTP/1.1 200 OK @@ -30,8 +28,8 @@ Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { - "id": "1", - "name": "Alfonso" + "id": "2", + "name": "Gerardo" } ``` diff --git a/doc/examples/find_-_bad_request.md b/doc/examples/find_-_by_btree.md similarity index 50% rename from doc/examples/find_-_bad_request.md rename to doc/examples/find_-_by_btree.md index 1cdc922..f46862f 100644 --- a/doc/examples/find_-_bad_request.md +++ b/doc/examples/find_-_by_btree.md @@ -1,11 +1,11 @@ -# Find - bad request +# Find - by BTree Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:find" \ -d '{ - "mode": "{invalid}" + "index": "my-index" }' ``` @@ -17,20 +17,19 @@ POST /v1/collections/my-collection:find HTTP/1.1 Host: example.com { - "mode": "{invalid}" + "index": "my-index" } -HTTP/1.1 400 Bad Request -Content-Length: 121 +HTTP/1.1 200 OK +Content-Length: 192 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT -{ - "error": { - "description": "Unexpected error", - "message": "bad mode '{invalid}', must be [fullscan|unique]. See docs: TODO" - } -} +{"category":"drink","id":"3","product":"milk"} +{"category":"drink","id":"2","product":"water"} +{"category":"fruit","id":"4","product":"apple"} +{"category":"fruit","id":"1","product":"orange"} + ``` diff --git a/doc/examples/find_-_by_btree_reverse_order.md b/doc/examples/find_-_by_btree_reverse_order.md new file mode 100644 index 0000000..e7574c4 --- /dev/null +++ b/doc/examples/find_-_by_btree_reverse_order.md @@ -0,0 +1,37 @@ +# Find - by BTree reverse order + +Curl example: + +```sh +curl -X POST "https://example.com/v1/collections/my-collection:find" \ +-d '{ + "index": "my-index", + "reverse": true +}' +``` + + +HTTP request/response example: + +```http +POST /v1/collections/my-collection:find HTTP/1.1 +Host: example.com + +{ + "index": "my-index", + "reverse": true +} + +HTTP/1.1 200 OK +Content-Length: 192 +Content-Type: text/plain; charset=utf-8 +Date: Mon, 15 Aug 2022 02:08:13 GMT + +{"category":"fruit","id":"1","product":"orange"} +{"category":"fruit","id":"4","product":"apple"} +{"category":"drink","id":"2","product":"water"} +{"category":"drink","id":"3","product":"milk"} + +``` + + diff --git a/doc/examples/find_-_by_unique_index.md b/doc/examples/find_-_by_unique_index.md index 6336e7f..09cd37b 100644 --- a/doc/examples/find_-_by_unique_index.md +++ b/doc/examples/find_-_by_unique_index.md @@ -5,8 +5,7 @@ Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:find" \ -d '{ - "field": "id", - "mode": "unique", + "index": "my-index", "value": "my-id" }' ``` @@ -19,8 +18,7 @@ POST /v1/collections/my-collection:find HTTP/1.1 Host: example.com { - "field": "id", - "mode": "unique", + "index": "my-index", "value": "my-id" } diff --git a/doc/examples/find_-_collection_not_found.md b/doc/examples/find_-_collection_not_found.md new file mode 100644 index 0000000..6141f4c --- /dev/null +++ b/doc/examples/find_-_collection_not_found.md @@ -0,0 +1,32 @@ +# Find - collection not found + +Curl example: + +```sh +curl -X POST "https://example.com/v1/collections/your-collection:find" \ +-d '{}' +``` + + +HTTP request/response example: + +```http +POST /v1/collections/your-collection:find HTTP/1.1 +Host: example.com + +{} + +HTTP/1.1 500 Internal Server Error +Content-Length: 78 +Content-Type: text/plain; charset=utf-8 +Date: Mon, 15 Aug 2022 02:08:13 GMT + +{ + "error": { + "description": "Unexpected error", + "message": "collection not found" + } +} +``` + + diff --git a/doc/examples/insert_-_unique_index_conflict.md b/doc/examples/insert_-_unique_index_conflict.md index 95f331a..b543a9a 100644 --- a/doc/examples/insert_-_unique_index_conflict.md +++ b/doc/examples/insert_-_unique_index_conflict.md @@ -25,14 +25,14 @@ Host: example.com } HTTP/1.1 409 Conflict -Content-Length: 103 +Content-Length: 125 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { "error": { "description": "Unexpected error", - "message": "index conflict: field 'id' with value 'my-id'" + "message": "index add 'my-index': index conflict: field 'id' with value 'my-id'" } } ``` diff --git a/doc/examples/list_indexes.md b/doc/examples/list_indexes.md index 7d06b29..bded7c6 100644 --- a/doc/examples/list_indexes.md +++ b/doc/examples/list_indexes.md @@ -16,15 +16,15 @@ Host: example.com HTTP/1.1 200 OK -Content-Length: 43 +Content-Length: 50 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT [ { - "field": "id", - "name": "id", - "sparse": true + "kind": "", + "name": "my-index", + "parameters": null } ] ``` diff --git a/doc/examples/patch_-_by_index.md b/doc/examples/patch_-_by_index.md index 9d1100f..be07595 100644 --- a/doc/examples/patch_-_by_index.md +++ b/doc/examples/patch_-_by_index.md @@ -5,8 +5,7 @@ Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:patch" \ -d '{ - "field": "id", - "mode": "unique", + "index": "my-index", "patch": { "name": "Pedro" }, @@ -22,8 +21,7 @@ POST /v1/collections/my-collection:patch HTTP/1.1 Host: example.com { - "field": "id", - "mode": "unique", + "index": "my-index", "patch": { "name": "Pedro" }, diff --git a/doc/examples/retrieve_index.md b/doc/examples/retrieve_index.md index aa76b02..d18838f 100644 --- a/doc/examples/retrieve_index.md +++ b/doc/examples/retrieve_index.md @@ -5,7 +5,7 @@ Curl example: ```sh curl -X POST "https://example.com/v1/collections/my-collection:getIndex" \ -d '{ - "name": "id" + "name": "my-index" }' ``` @@ -17,18 +17,18 @@ POST /v1/collections/my-collection:getIndex HTTP/1.1 Host: example.com { - "name": "id" + "name": "my-index" } HTTP/1.1 200 OK -Content-Length: 41 +Content-Length: 48 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { - "field": "id", - "name": "id", - "sparse": true + "kind": "", + "name": "my-index", + "parameters": null } ``` diff --git a/service/acceptance.go b/service/acceptance.go index bbee930..7119a9a 100644 --- a/service/acceptance.go +++ b/service/acceptance.go @@ -1,7 +1,10 @@ package service import ( + "bytes" "encoding/json" + "fmt" + "io" "net/http" "strings" @@ -122,26 +125,24 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request a.Alternative("Create index", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:createIndex"). - WithBodyJson(JSON{"field": "id", "sparse": true}).Do() + WithBodyJson(JSON{"name": "my-index", "kind": "map", "parameters": JSON{"field": "id"}}).Do() Save(resp, "Create index", ``) a.Alternative("Delete by index", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:remove"). WithBodyJson(JSON{ - "mode": "unique", - "field": "id", - "value": "1", + "index": "my-index", + "value": "2", }).Do() Save(resp, "Delete - by index", ``) - biff.AssertEqualJson(resp.BodyJson(), myDocuments[0]) + biff.AssertEqualJson(resp.BodyJson(), myDocuments[1]) biff.AssertEqual(resp.StatusCode, http.StatusOK) }) a.Alternative("Patch by index", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:patch"). WithBodyJson(JSON{ - "mode": "unique", - "field": "id", + "index": "my-index", "value": "3", "patch": JSON{ "name": "Pedro", @@ -264,22 +265,23 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request }) - a.Alternative("Create index", func(a *biff.A) { + a.Alternative("Create index - map", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:createIndex"). - WithBodyJson(JSON{"field": "id", "sparse": true}).Do() + WithBodyJson(JSON{"name": "my-index", "kind": "map", "parameters": JSON{"field": "id", "sparse": true}}).Do() - expectedBody := JSON{"field": "id", "name": "id", "sparse": true} + expectedBody := JSON{"name": "my-index", "kind": "map", "parameters": interface{}(nil)} biff.AssertEqual(resp.StatusCode, http.StatusCreated) biff.AssertEqualJson(resp.BodyJson(), expectedBody) a.Alternative("Get index", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:getIndex"). WithBodyJson(JSON{ - "name": "id", + "name": "my-index", }).Do() Save(resp, "Retrieve index", ``) biff.AssertEqual(resp.StatusCode, http.StatusOK) + expectedBody["kind"] = "" // Todo: fix this! biff.AssertEqualJson(resp.BodyJson(), expectedBody) }) @@ -287,7 +289,7 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request resp := apiRequest("POST", "/collections/my-collection:listIndexes").Do() Save(resp, "List indexes", ``) - expectedBody := []JSON{{"field": "id", "name": "id", "sparse": true}} + expectedBody := []JSON{{"kind": "", "name": "my-index", "parameters": interface{}(nil)}} biff.AssertEqual(resp.StatusCode, http.StatusOK) biff.AssertEqualJson(resp.BodyJson(), expectedBody) }) @@ -308,7 +310,7 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request expectedBody := JSON{ "error": JSON{ "description": "Unexpected error", - "message": "index conflict: field 'id' with value 'my-id'", + "message": "index add 'my-index': index conflict: field 'id' with value 'my-id'", }, } biff.AssertEqual(resp.StatusCode, http.StatusConflict) @@ -327,8 +329,7 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request resp := apiRequest("POST", "/collections/my-collection:find"). WithBodyJson(JSON{ - "mode": "unique", - "field": "id", + "index": "my-index", "value": "my-id", }).Do() Save(resp, "Find - by unique index", ``) @@ -339,18 +340,89 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request }) - a.Alternative("Find with {invalid} mode", func(a *biff.A) { + a.Alternative("Create index - btree", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:createIndex"). + WithBodyJson(JSON{"name": "my-index", "kind": "btree", "parameters": JSON{"fields": []string{"category", "product"}}}).Do() + Save(resp, "Create index - btree", ``) - resp := apiRequest("POST", "/collections/my-collection:find"). - WithBodyJson(JSON{ - "mode": "{invalid}", - }).Do() + expectedBody := JSON{"kind": "btree", "name": "my-index", "parameters": interface{}(nil)} + biff.AssertEqual(resp.StatusCode, http.StatusCreated) + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + + a.Alternative("Insert some documents", func(a *biff.A) { + + documents := []JSON{ + {"id": "1", "category": "fruit", "product": "orange"}, + {"id": "2", "category": "drink", "product": "water"}, + {"id": "3", "category": "drink", "product": "milk"}, + {"id": "4", "category": "fruit", "product": "apple"}, + } + + for _, document := range documents { + resp := apiRequest("POST", "/collections/my-collection:insert"). + WithBodyJson(document).Do() + fmt.Println(resp.StatusCode, resp.BodyString()) + } + + a.Alternative("Find with BTree", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:find"). + WithBodyJson(JSON{ + "index": "my-index", + }).Do() + Save(resp, "Find - by BTree", ``) + + expectedOrderIDs := []string{"3", "2", "4", "1"} + + d := json.NewDecoder(bytes.NewReader(resp.BodyBytes())) + i := 0 + for { + item := JSON{} + err := d.Decode(&item) + if err == io.EOF { + break + } + biff.AssertEqual(item["id"], expectedOrderIDs[i]) + i++ + } + }) + + a.Alternative("Find with BTree - reverse order", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:find"). + WithBodyJson(JSON{ + "index": "my-index", + "reverse": true, + }).Do() + Save(resp, "Find - by BTree reverse order", ``) + + expectedOrderIDs := []string{"1", "4", "2", "3"} + + d := json.NewDecoder(bytes.NewReader(resp.BodyBytes())) + i := 0 + for { + item := JSON{} + err := d.Decode(&item) + if err == io.EOF { + break + } + biff.AssertEqual(item["id"], expectedOrderIDs[i]) + i++ + } + }) + + }) + + }) + + a.Alternative("Find with collection not found", func(a *biff.A) { + + resp := apiRequest("POST", "/collections/your-collection:find"). + WithBodyJson(JSON{}).Do() - Save(resp, "Find - bad request", ``) + Save(resp, "Find - collection not found", ``) errorMessage := resp.BodyJson().(JSON)["error"].(JSON)["message"].(string) - biff.AssertEqual(errorMessage, "bad mode '{invalid}', must be [fullscan|unique]. See docs: TODO") - biff.AssertEqual(resp.StatusCode, http.StatusBadRequest) + biff.AssertEqual(errorMessage, "collection not found") + biff.AssertEqual(resp.StatusCode, http.StatusInternalServerError) // todo: it should return 404 }) }) @@ -382,6 +454,7 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request resp := apiRequest("POST", "/collections/my-collection:createIndex"). WithBodyJson(JSON{ + "kind": "map", "field": "id", }).Do()