diff --git a/cli/errors.go b/cli/errors.go index f084ed21b0..ff283de5f9 100644 --- a/cli/errors.go +++ b/cli/errors.go @@ -17,9 +17,11 @@ import ( ) const ( - errInvalidLensConfig string = "invalid lens configuration" - errSchemaVersionNotOfSchema string = "the given schema version is from a different schema" - errRequiredFlag string = "the required flag [--%s|-%s] is %s" + errInvalidLensConfig string = "invalid lens configuration" + errSchemaVersionNotOfSchema string = "the given schema version is from a different schema" + errRequiredFlag string = "the required flag [--%s|-%s] is %s" + errInvalidAscensionOrder string = "invalid order: expected ASC or DESC" + errInvalidInxedFieldDescription string = "invalid or malformed field description" ) var ( @@ -55,3 +57,11 @@ func NewErrSchemaVersionNotOfSchema(schemaRoot string, schemaVersionID string) e errors.NewKV("SchemaVersionID", schemaVersionID), ) } + +func NewErrInvalidAscensionOrder(fieldName string) error { + return errors.New(errInvalidAscensionOrder, errors.NewKV("Field", fieldName)) +} + +func NewErrInvalidInxedFieldDescription(fieldName string) error { + return errors.New(errInvalidInxedFieldDescription, errors.NewKV("Field", fieldName)) +} diff --git a/cli/index_create.go b/cli/index_create.go index 0d724da15b..e9f4350fa0 100644 --- a/cli/index_create.go +++ b/cli/index_create.go @@ -11,6 +11,8 @@ package cli import ( + "strings" + "github.com/spf13/cobra" "github.com/sourcenetwork/defradb/client" @@ -22,26 +24,51 @@ func MakeIndexCreateCommand() *cobra.Command { var fieldsArg []string var uniqueArg bool var cmd = &cobra.Command{ - Use: "create -c --collection --fields [-n --name ] [--unique]", + Use: "create -c --collection --fields [-n --name ] [--unique]", Short: "Creates a secondary index on a collection's field(s)", Long: `Creates a secondary index on a collection's field(s). The --name flag is optional. If not provided, a name will be generated automatically. The --unique flag is optional. If provided, the index will be unique. +If no order is specified for the field, the default value will be "ASC" Example: create an index for 'Users' collection on 'name' field: defradb client index create --collection Users --fields name Example: create a named index for 'Users' collection on 'name' field: - defradb client index create --collection Users --fields name --name UsersByName`, + defradb client index create --collection Users --fields name --name UsersByName + +Example: create a unique index for 'Users' collection on 'name' in ascending order, and 'age' in descending order: + defradb client index create --collection Users --fields name:ASC,age:DESC --unique +`, ValidArgs: []string{"collection", "fields", "name"}, RunE: func(cmd *cobra.Command, args []string) error { store := mustGetContextStore(cmd) var fields []client.IndexedFieldDescription - for _, name := range fieldsArg { - fields = append(fields, client.IndexedFieldDescription{Name: name}) + + for _, field := range fieldsArg { + // For each field, parse it into a field name and ascension order, separated by a colon + // If there is no colon, assume the ascension order is ASC by default + const asc = "ASC" + const desc = "DESC" + parts := strings.Split(field, ":") + fieldName := parts[0] + order := asc + if len(parts) == 2 { + order = strings.ToUpper(parts[1]) + if order != asc && order != desc { + return NewErrInvalidAscensionOrder(field) + } + } else if len(parts) > 2 { + return NewErrInvalidInxedFieldDescription(field) + } + fields = append(fields, client.IndexedFieldDescription{ + Name: fieldName, + Descending: order == desc, + }) } + desc := client.IndexDescription{ Name: nameArg, Fields: fields, @@ -51,6 +78,7 @@ Example: create a named index for 'Users' collection on 'name' field: if err != nil { return err } + desc, err = col.CreateIndex(cmd.Context(), desc) if err != nil { return err diff --git a/docs/website/references/cli/defradb_client_index_create.md b/docs/website/references/cli/defradb_client_index_create.md index f37231771d..268cd9eb70 100644 --- a/docs/website/references/cli/defradb_client_index_create.md +++ b/docs/website/references/cli/defradb_client_index_create.md @@ -8,15 +8,20 @@ Creates a secondary index on a collection's field(s). The --name flag is optional. If not provided, a name will be generated automatically. The --unique flag is optional. If provided, the index will be unique. +If no order is specified for the field, the default value will be "ASC" Example: create an index for 'Users' collection on 'name' field: defradb client index create --collection Users --fields name Example: create a named index for 'Users' collection on 'name' field: defradb client index create --collection Users --fields name --name UsersByName + +Example: create a unique index for 'Users' collection on 'name' in ascending order, and 'age' in descending order: + defradb client index create --collection Users --fields name:ASC,age:DESC --unique + ``` -defradb client index create -c --collection --fields [-n --name ] [--unique] [flags] +defradb client index create -c --collection --fields [-n --name ] [--unique] [flags] ``` ### Options diff --git a/tests/clients/cli/wrapper_collection.go b/tests/clients/cli/wrapper_collection.go index cfa49b9e8e..eb9c5f5466 100644 --- a/tests/clients/cli/wrapper_collection.go +++ b/tests/clients/cli/wrapper_collection.go @@ -348,10 +348,24 @@ func (c *Collection) CreateIndex( } fields := make([]string, len(indexDesc.Fields)) + orders := make([]bool, len(indexDesc.Fields)) + for i := range indexDesc.Fields { fields[i] = indexDesc.Fields[i].Name + orders[i] = indexDesc.Fields[i].Descending + } + + orderedFields := make([]string, len(fields)) + + for i := range fields { + if orders[i] { + orderedFields[i] = fields[i] + ":DESC" + } else { + orderedFields[i] = fields[i] + ":ASC" + } } - args = append(args, "--fields", strings.Join(fields, ",")) + + args = append(args, "--fields", strings.Join(orderedFields, ",")) data, err := c.cmd.execute(ctx, args) if err != nil { diff --git a/tests/integration/index/create_unique_composite_test.go b/tests/integration/index/create_unique_composite_test.go index 7a7e9fc5e0..88778d2e64 100644 --- a/tests/integration/index/create_unique_composite_test.go +++ b/tests/integration/index/create_unique_composite_test.go @@ -178,3 +178,79 @@ func TestUniqueCompositeIndexCreate_IfFieldValuesAreUnique_Succeed(t *testing.T) testUtils.ExecuteTestCase(t, test) } + +func TestUniqueCompositeIndexCreate_IfFieldValuesAreOrdered_Succeed(t *testing.T) { + test := testUtils.TestCase{ + Description: "create unique composite index if all docs have unique fields combinations", + Actions: []any{ + testUtils.SchemaUpdate{ + Schema: ` + type User { + name: String + age: Int + email: String + } + `, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: ` + { + "name": "John", + "age": 21, + "email": "some@gmail.com" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: ` + { + "name": "John", + "age": 35, + "email": "another@gmail.com" + }`, + }, + testUtils.CreateDoc{ + CollectionID: 0, + Doc: ` + { + "name": "Andy", + "age": 35, + "email": "different@gmail.com" + }`, + }, + testUtils.CreateIndex{ + CollectionID: 0, + Fields: []testUtils.IndexedField{{Name: "name", Descending: true}, {Name: "age", Descending: false}, {Name: "email"}}, + IndexName: "name_age_unique_index", + Unique: true, + }, + testUtils.GetIndexes{ + CollectionID: 0, + ExpectedIndexes: []client.IndexDescription{ + { + Name: "name_age_unique_index", + ID: 1, + Unique: true, + Fields: []client.IndexedFieldDescription{ + { + Name: "name", + Descending: true, + }, + { + Name: "age", + Descending: false, + }, + { + Name: "email", + Descending: false, + }, + }, + }, + }, + }, + }, + } + + testUtils.ExecuteTestCase(t, test) +}