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 support for one-one mutation from sec. side #1247

Merged
merged 9 commits into from
Mar 29, 2023
2 changes: 1 addition & 1 deletion client/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func (f FieldDescription) IsObjectArray() bool {

// IsPrimaryRelation returns true if this field is a relation, and is the primary side.
func (f FieldDescription) IsPrimaryRelation() bool {
return f.RelationType > 0 && f.RelationType&Relation_Type_Primary == 0
return f.RelationType > 0 && f.RelationType&Relation_Type_Primary != 0
}

// IsSet returns true if the target relation type is set.
Expand Down
66 changes: 25 additions & 41 deletions db/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/fxamacker/cbor/v2"
"github.com/ipfs/go-cid"
Expand Down Expand Up @@ -738,6 +737,15 @@ func (c *collection) save(
txn datastore.Txn,
doc *client.Document,
) (cid.Cid, error) {
// NOTE: We delay the final Clean() call until we know
// the commit on the transaction is successful. If we didn't
// wait, and just did it here, then *if* the commit fails down
// the line, then we have no way to roll back the state
// side-effect on the document func called here.
txn.OnSuccess(func() {
doc.Clean()
})

Comment on lines +740 to +748
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I wonder why this was in the loop before? I feel like it would have been a redundant additions to the OnSuccess functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like it would have been a redundant additions to the OnSuccess functions.

yes :P

I wonder why this was in the loop before?

I assume it was just a historical accident and replaced some logic that predated the OnSuccess funcs without moving it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just me being dumb 🙃

// New batch transaction/store (optional/todo)
// Ensute/Set doc object marker
// Loop through doc values
Expand All @@ -758,10 +766,25 @@ func (c *collection) save(
return cid.Undef, client.NewErrFieldNotExist(k)
}

if c.isFieldNameRelationID(k) {
fieldDescription, valid := c.desc.GetField(k)
if !valid {
return cid.Undef, client.NewErrFieldNotExist(k)
}

relationFieldDescription, isSecondaryRelationID := c.isSecondaryIDField(fieldDescription)
if isSecondaryRelationID {
primaryId := val.Value().(string)

err = c.patchPrimaryDoc(ctx, txn, relationFieldDescription, primaryKey.DocKey, primaryId)
if err != nil {
return cid.Undef, err
}

// If this field was a secondary relation ID the related document will have been
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: appreciate this comment

// updated instead and we should discard this value
continue
}

node, _, err := c.saveDocValue(ctx, txn, fieldKey, val)
if err != nil {
return cid.Undef, err
Expand All @@ -772,15 +795,6 @@ func (c *collection) save(
docProperties[k] = val.Value()
}

// NOTE: We delay the final Clean() call until we know
// the commit on the transaction is successful. If we didn't
// wait, and just did it here, then *if* the commit fails down
// the line, then we have no way to roll back the state
// side-effect on the document func called here.
txn.OnSuccess(func() {
doc.Clean()
})

link := core.DAGLink{
Name: k,
Cid: node.Cid(),
Expand Down Expand Up @@ -1116,33 +1130,3 @@ func (c *collection) tryGetSchemaFieldID(fieldName string) (uint32, bool) {
}
return uint32(0), false
}

// isFieldNameRelationID returns true if the given field is the id field backing a relationship.
func (c *collection) isFieldNameRelationID(fieldName string) bool {
fieldDescription, valid := c.desc.GetField(fieldName)
if !valid {
return false
}

return c.isFieldDescriptionRelationID(&fieldDescription)
}

// isFieldDescriptionRelationID returns true if the given field is the id field backing a relationship.
func (c *collection) isFieldDescriptionRelationID(fieldDescription *client.FieldDescription) bool {
if fieldDescription.RelationType == client.Relation_Type_INTERNAL_ID {
relationDescription, valid := c.desc.GetField(strings.TrimSuffix(fieldDescription.Name, "_id"))
if !valid {
return false
}
if relationDescription.IsPrimaryRelation() {
return true
}
}
return false
}

// makeCollectionKey returns a formatted collection key for the system data store.
// it assumes the name of the collection is non-empty.
// func makeCollectionDataKey(collectionID uint32) core.Key {
// return collectionNs.ChildString(name)
// }
69 changes: 66 additions & 3 deletions db/collection_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package db

import (
"context"
"fmt"
"strings"

cbor "github.com/fxamacker/cbor/v2"
"github.com/sourcenetwork/immutable"
Expand Down Expand Up @@ -302,8 +304,21 @@ func (c *collection) applyMerge(
return client.NewErrFieldNotExist(mfield)
}

if c.isFieldDescriptionRelationID(&fd) {
return client.NewErrFieldNotExist(mfield)
relationFieldDescription, isSecondaryRelationID := c.isSecondaryIDField(fd)
if isSecondaryRelationID {
primaryId, err := getString(mval)
if err != nil {
return err
}

err = c.patchPrimaryDoc(ctx, txn, relationFieldDescription, keyStr, primaryId)
if err != nil {
return err
}

// If this field was a secondary relation ID the related document will have been
// updated instead and we should discard this merge item
continue
}

cborVal, err := validateFieldSchema(mval, fd)
Expand All @@ -322,7 +337,7 @@ func (c *collection) applyMerge(
if err != nil {
return err
}
// links[mfield] = c

links = append(links, core.DAGLink{
Name: mfield,
Cid: c.Cid(),
Expand Down Expand Up @@ -370,6 +385,54 @@ func (c *collection) applyMerge(
return nil
}

// isSecondaryIDField returns true if the given field description represents a secondary relation field ID.
func (c *collection) isSecondaryIDField(fieldDesc client.FieldDescription) (client.FieldDescription, bool) {
if fieldDesc.RelationType != client.Relation_Type_INTERNAL_ID {
return client.FieldDescription{}, false
}

relationFieldDescription, valid := c.Description().GetField(strings.TrimSuffix(fieldDesc.Name, "_id"))
return relationFieldDescription, valid && !relationFieldDescription.IsPrimaryRelation()
}

// patchPrimaryDoc patches the (primary) document linked to from the document of the given dockey via the
// given (secondary) relationship field description (hosted on the collection of the document matching the
// given dockey).
//
// The given field value should be the string representation of the dockey of the primary document to be
// patched.
func (c *collection) patchPrimaryDoc(
ctx context.Context,
txn datastore.Txn,
relationFieldDescription client.FieldDescription,
docKey string,
fieldValue string,
) error {
primaryDockey, err := client.NewDocKeyFromString(fieldValue)
if err != nil {
return err
}

primaryCol, err := c.db.getCollectionByName(ctx, txn, relationFieldDescription.Schema)
if err != nil {
return err
}
primaryCol = primaryCol.WithTxn(txn)

primaryField, _ := primaryCol.Description().GetRelation(relationFieldDescription.RelationName)

_, err = primaryCol.UpdateWithKey(
ctx,
primaryDockey,
fmt.Sprintf(`{"%s": "%s"}`, primaryField.Name+"_id", docKey),
)
if err != nil {
return err
}

return nil
}

// validateFieldSchema takes a given value as an interface,
// and ensures it matches the supplied field description.
// It will do any minor parsing, like dates, and return
Expand Down
40 changes: 38 additions & 2 deletions tests/integration/collection/create/one_to_one/save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
testUtils "github.com/sourcenetwork/defradb/tests/integration/collection"
)

func TestCollectionCreateSaveErrorsGivenRelationSetOnWrongSide(t *testing.T) {
func TestCollectionCreateSaveErrorsNonExistantKeyViaSecondarySide(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
`{
Expand All @@ -41,12 +41,15 @@ func TestCollectionCreateSaveErrorsGivenRelationSetOnWrongSide(t *testing.T) {
},
},
},
ExpectedError: "The given field does not exist",
ExpectedError: "no document for the given key exists",
}

executeTestCase(t, test)
}

// Note: This test should probably not pass, as it contains a
// reference to a document that doesnt exist. It is doubly odd
// given that saving from the secondary side errors as expected
Comment on lines +50 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is there an issue for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is conceptually part of the problem that we dont really do much/any data validation on write, I'm not sure their is an existing issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #935 for write validation

func TestCollectionCreateSaveCreatesDoc(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
Expand Down Expand Up @@ -89,3 +92,36 @@ func TestCollectionCreateSaveCreatesDoc(t *testing.T) {

executeTestCase(t, test)
}

func TestCollectionCreateSaveFromSecondarySide(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
`{
"name": "Painted House",
"author_id": "bae-2edb7fdd-cad7-5ad4-9c7d-6920245a96ed"
}`,
),
)
if err != nil {
assert.Fail(t, err.Error())
}

test := testUtils.TestCase{
Docs: map[string][]string{
"author": {
`{
"name": "John Grisham"
}`,
},
},
CollectionCalls: map[string][]func(client.Collection) error{
"book": []func(c client.Collection) error{
func(c client.Collection) error {
return c.Save(context.Background(), doc)
},
},
},
}

executeTestCase(t, test)
}
57 changes: 53 additions & 4 deletions tests/integration/collection/update/one_to_one/save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
testUtils "github.com/sourcenetwork/defradb/tests/integration/collection"
)

func TestUpdateOneToOneSaveErrorsGivenWrongSideOfRelation(t *testing.T) {
func TestUpdateOneToOneSaveErrorsGivenNonExistantKeyViaSecondarySide(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
`{
Expand All @@ -35,7 +35,7 @@ func TestUpdateOneToOneSaveErrorsGivenWrongSideOfRelation(t *testing.T) {
err = doc.SetWithJSON(
[]byte(
`{
"author_id": "ValueDoesntMatter"
"author_id": "bae-fd541c25-229e-5280-b44b-e5c2af3e374d"
}`,
),
)
Expand All @@ -58,14 +58,15 @@ func TestUpdateOneToOneSaveErrorsGivenWrongSideOfRelation(t *testing.T) {
},
},
},
ExpectedError: "The given field does not exist",
ExpectedError: "no document for the given key exists",
}

executeTestCase(t, test)
}

// Note: This test should probably not pass, as it contains a
// reference to a document that doesnt exist.
// reference to a document that doesnt exist. It is doubly odd
// given that saving from the secondary side errors as expected
func TestUpdateOneToOneSavesGivenNewRelationValue(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
Expand Down Expand Up @@ -108,3 +109,51 @@ func TestUpdateOneToOneSavesGivenNewRelationValue(t *testing.T) {

executeTestCase(t, test)
}

func TestUpdateOneToOneSaveFromSecondarySide(t *testing.T) {
doc, err := client.NewDocFromJSON(
[]byte(
`{
"name": "Painted House"
}`,
),
)
if err != nil {
assert.Fail(t, err.Error())
}

err = doc.SetWithJSON(
[]byte(
`{
"author_id": "bae-2edb7fdd-cad7-5ad4-9c7d-6920245a96ed"
}`,
),
)
if err != nil {
assert.Fail(t, err.Error())
}

test := testUtils.TestCase{
Docs: map[string][]string{
"author": {
`{
"name": "John Grisham"
}`,
},
"book": {
`{
"name": "Painted House"
}`,
},
},
CollectionCalls: map[string][]func(client.Collection) error{
"book": []func(c client.Collection) error{
func(c client.Collection) error {
return c.Save(context.Background(), doc)
},
},
},
}

executeTestCase(t, test)
}
Loading