Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add unique secondary index #2131

Merged
merged 15 commits into from
Dec 19, 2023
6 changes: 5 additions & 1 deletion cli/index_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ func MakeIndexCreateCommand() *cobra.Command {
var collectionArg string
var nameArg string
var fieldsArg []string
var uniqueArg bool
var cmd = &cobra.Command{
Use: "create -c --collection <collection> --fields <fields> [-n --name <name>]",
Use: "create -c --collection <collection> --fields <fields> [-n --name <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.

Example: create an index for 'Users' collection on 'name' field:
defradb client index create --collection Users --fields name
Expand All @@ -44,6 +46,7 @@ Example: create a named index for 'Users' collection on 'name' field:
desc := client.IndexDescription{
Name: nameArg,
Fields: fields,
Unique: uniqueArg,
}
col, err := store.GetCollectionByName(cmd.Context(), collectionArg)
if err != nil {
Expand All @@ -62,6 +65,7 @@ Example: create a named index for 'Users' collection on 'name' field:
cmd.Flags().StringVarP(&collectionArg, "collection", "c", "", "Collection name")
cmd.Flags().StringVarP(&nameArg, "name", "n", "", "Index name")
cmd.Flags().StringSliceVar(&fieldsArg, "fields", []string{}, "Fields to index")
cmd.Flags().BoolVarP(&uniqueArg, "unique", "u", false, "Make the index unique")

return cmd
}
2 changes: 2 additions & 0 deletions client/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type IndexDescription struct {
ID uint32
// Fields contains the fields that are being indexed.
Fields []IndexedFieldDescription
// Unique indicates whether the index is unique.
Unique bool
}

// CollectIndexedFields returns all fields that are indexed by all collection indexes.
Expand Down
10 changes: 10 additions & 0 deletions db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const (
errExpectedJSONArray string = "expected JSON array"
errOneOneAlreadyLinked string = "target document is already linked to another document"
errIndexDoesNotMatchName string = "the index used does not match the given name"
errCanNotIndexNonUniqueField string = "can not create doc that violates unique index"
)

var (
Expand Down Expand Up @@ -631,3 +632,12 @@ func NewErrIndexDoesNotMatchName(index, name string) error {
errors.NewKV("Name", name),
)
}

func NewErrCanNotIndexNonUniqueField(dockey, fieldName string, value any) error {
return errors.New(
errCanNotIndexNonUniqueField,
errors.NewKV("Dockey", dockey),
errors.NewKV("Field name", fieldName),
errors.NewKV("Field value", value),
)
}
18 changes: 12 additions & 6 deletions db/fetcher/indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type IndexFetcher struct {
mapping *core.DocumentMapping
indexedField client.FieldDescription
docFields []client.FieldDescription
indexDesc client.IndexDescription
indexIter indexIterator
indexDataStoreKey core.IndexDataStoreKey
execInfo ExecInfo
Expand Down Expand Up @@ -70,6 +71,7 @@ func (f *IndexFetcher) Init(

for _, index := range col.Description().Indexes {
if index.Fields[0].Name == f.indexedField.Name {
f.indexDesc = index
f.indexDataStoreKey.IndexID = index.ID
break
}
Expand All @@ -84,7 +86,7 @@ func (f *IndexFetcher) Init(
}
}

iter, err := createIndexIterator(f.indexDataStoreKey, f.indexFilter, &f.execInfo)
iter, err := createIndexIterator(f.indexDataStoreKey, f.indexFilter, &f.execInfo, f.indexDesc.Unique)
if err != nil {
return err
}
Expand Down Expand Up @@ -112,28 +114,32 @@ func (f *IndexFetcher) FetchNext(ctx context.Context) (EncodedDocument, ExecInfo
for {
f.doc.Reset()

indexKey, hasValue, err := f.indexIter.Next()
res, err := f.indexIter.Next()
if err != nil {
return nil, ExecInfo{}, err
}

if !hasValue {
if !res.foundKey {
return nil, f.execInfo, nil
}

property := &encProperty{
Desc: f.indexedField,
Raw: indexKey.FieldValues[0],
Raw: res.key.FieldValues[0],
}

f.doc.key = indexKey.FieldValues[1]
if f.indexDesc.Unique {
f.doc.key = res.value
} else {
f.doc.key = res.key.FieldValues[1]
}
f.doc.properties[f.indexedField] = property
f.execInfo.FieldsFetched++

if f.docFetcher != nil && len(f.docFields) > 0 {
targetKey := base.MakeDocKey(f.col.Description(), string(f.doc.key))
spans := core.NewSpans(core.NewSpan(targetKey, targetKey.PrefixEnd()))
err = f.docFetcher.Start(ctx, spans)
err := f.docFetcher.Start(ctx, spans)
if err != nil {
return nil, ExecInfo{}, err
}
Expand Down
Loading