Skip to content

Commit 9335bfe

Browse files
committed
chore: name the document holding a contested unique index value
1 parent 22b8bdc commit 9335bfe

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

internal/db/index.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ func saveUniqueKey(
458458
if tolerateSameDoc && string(existing) == string(val) {
459459
return nil
460460
}
461-
return newUniqueIndexError(doc, fieldsDescs)
461+
return newUniqueIndexError(ctx, doc, fieldsDescs, existing)
462462
}
463463
}
464464

@@ -468,8 +468,33 @@ func saveUniqueKey(
468468
return nil
469469
}
470470

471-
func newUniqueIndexError(doc *client.Document, fieldsDescs []client.CollectionFieldDescription) error {
472-
kvs := make([]errors.KV, 0, len(fieldsDescs))
471+
// incumbentDocID resolves the document already holding a unique index entry, from the
472+
// short ID the entry stores. Returns an empty string if it cannot be resolved, since this
473+
// only decorates an error that is being returned either way.
474+
func incumbentDocID(ctx context.Context, encodedShortID []byte) string {
475+
shortID, err := keys.DecodeDocShortID(encodedShortID)
476+
if err != nil {
477+
return ""
478+
}
479+
docID, found, err := id.GetDocID(ctx, shortID)
480+
if err != nil || !found {
481+
return ""
482+
}
483+
return docID
484+
}
485+
486+
func newUniqueIndexError(
487+
ctx context.Context,
488+
doc *client.Document,
489+
fieldsDescs []client.CollectionFieldDescription,
490+
existing []byte,
491+
) error {
492+
kvs := make([]errors.KV, 0, len(fieldsDescs)+1)
493+
// Naming the document that already holds the value is what makes the two comparable;
494+
// without it the error only says which one lost.
495+
if incumbent := incumbentDocID(ctx, existing); incumbent != "" {
496+
kvs = append(kvs, errors.NewKV("HeldBy", incumbent))
497+
}
473498
for iter := range fieldsDescs {
474499
fieldVal, err := doc.TryGetValue(fieldsDescs[iter].Name)
475500
var val any

internal/db/index_backfill_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,38 @@ func TestBackfillBatchTxn_ConflictsWhenReadDocIsModified(t *testing.T) {
370370
require.True(t, errors.Is(commitErr, corekv.ErrTxnConflict),
371371
"expected ErrTxnConflict but got: %v", commitErr)
372372
}
373+
374+
// A unique violation names the document that lost. Naming the one that already holds the
375+
// value is what lets the two be compared, which is the only way to tell a genuine duplicate
376+
// from two documents that disagree on a field they do not share an index on.
377+
func TestSaveUniqueKey_ErrorNamesTheDocumentHoldingTheValue(t *testing.T) {
378+
ctx := context.Background()
379+
380+
db, err := newBadgerDB(ctx)
381+
require.NoError(t, err)
382+
t.Cleanup(func() { db.Close() })
383+
384+
_, err = db.AddCollection(ctx, userSchema)
385+
require.NoError(t, err)
386+
col, err := db.GetCollectionByName(ctx, "User")
387+
require.NoError(t, err)
388+
389+
_, err = col.NewIndex(ctx, client.NewIndexRequest{
390+
Fields: []client.IndexedFieldDescription{{Name: "name"}},
391+
Unique: true,
392+
})
393+
require.NoError(t, err)
394+
395+
incumbent, err := client.NewDocFromJSON(ctx, []byte(`{"name":"alice","age":1}`), col.Version())
396+
require.NoError(t, err)
397+
require.NoError(t, col.AddDocument(ctx, incumbent))
398+
399+
// Same indexed value, different age, so a different document that cannot have the slot.
400+
duplicate, err := client.NewDocFromJSON(ctx, []byte(`{"name":"alice","age":2}`), col.Version())
401+
require.NoError(t, err)
402+
require.NotEqual(t, incumbent.ID().String(), duplicate.ID().String())
403+
404+
err = col.AddDocument(ctx, duplicate)
405+
require.ErrorContains(t, err, "violates unique index")
406+
require.ErrorContains(t, err, incumbent.ID().String())
407+
}

0 commit comments

Comments
 (0)