diff --git a/Makefile b/Makefile index 2338f9cb38..5d5a7d1338 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ multi-build: start: build ./build/defradb start +.PHONY: dump +dump: build + ./build/defradb client dump + .PHONY: deps\:golangci-lint deps\:golangci-lint: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ${GOPATH}/bin v1.43.0 @@ -32,19 +36,26 @@ deps\:go-acc: deps: deps\:golangci-lint deps\:go-acc go mod download +.PHONY: tidy +tidy: + go mod tidy + .PHONY: clean clean: go clean cli/defradb/main.go rm -f build/defradb -.PHONY: tidy -tidy: - go mod tidy +.PHONY: clean\:test +clean\:test: + go clean -testcache .PHONY: test test: go test ./... -race +.PHONY: test\:clean +test\:clean: clean\:test test + .PHONY: test\:bench test\:bench: go test ./... -race -bench=. diff --git a/client/core.go b/client/core.go index f640c12af1..b12990cf4b 100644 --- a/client/core.go +++ b/client/core.go @@ -68,6 +68,11 @@ type Collection interface { UpdateWithKey(context.Context, key.DocKey, interface{}, ...UpdateOpt) (*UpdateResult, error) UpdateWithKeys(context.Context, []key.DocKey, interface{}, ...UpdateOpt) (*UpdateResult, error) + DeleteWith(context.Context, interface{}, ...DeleteOpt) error + DeleteWithFilter(context.Context, interface{}, ...DeleteOpt) (*DeleteResult, error) + DeleteWithKey(context.Context, key.DocKey, ...DeleteOpt) (*DeleteResult, error) + DeleteWithKeys(context.Context, []key.DocKey, ...DeleteOpt) (*DeleteResult, error) + Get(context.Context, key.DocKey) (*document.Document, error) WithTxn(Txn) Collection @@ -75,12 +80,18 @@ type Collection interface { type UpdateOpt struct{} type CreateOpt struct{} +type DeleteOpt struct{} type UpdateResult struct { Count int64 DocKeys []string } +type DeleteResult struct { + Count int64 + DocKeys []string +} + type QueryResult struct { Errors []interface{} `json:"errors,omitempty"` Data interface{} `json:"data"` diff --git a/db/collection_delete.go b/db/collection_delete.go new file mode 100644 index 0000000000..977d1c3770 --- /dev/null +++ b/db/collection_delete.go @@ -0,0 +1,435 @@ +// Copyright 2020 Source Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. +package db + +import ( + "context" + "errors" + "fmt" + + block "github.com/ipfs/go-block-format" + cid "github.com/ipfs/go-cid" + ds "github.com/ipfs/go-datastore" + query "github.com/ipfs/go-datastore/query" + blockstore "github.com/ipfs/go-ipfs-blockstore" + dag "github.com/ipfs/go-merkledag" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/document" + "github.com/sourcenetwork/defradb/document/key" + "github.com/sourcenetwork/defradb/merkle/clock" + "github.com/sourcenetwork/defradb/query/graphql/parser" +) + +var ( + ErrInvalidDeleteTarget = errors.New("The doc delete targeter is an unknown type") + ErrDeleteTargetEmpty = errors.New("The doc delete targeter cannot be empty") + ErrDeleteEmpty = errors.New("The doc delete cannot be empty") +) + +// DeleteWith deletes a target document. Target can be a Filter statement, +// a single docKey, a single document, an array of docKeys, or an array of documents. +// If you want more type safety, use the respective typed versions of Delete. +// Eg: DeleteWithFilter or DeleteWithKey +func (c *Collection) DeleteWith( + ctx context.Context, + target interface{}, + opts ...client.DeleteOpt) error { + + switch t := target.(type) { + + case string, map[string]interface{}, *parser.Filter: + _, err := c.DeleteWithFilter(ctx, t, opts...) + return err + + case key.DocKey: + _, err := c.DeleteWithKey(ctx, t, opts...) + return err + + case []key.DocKey: + _, err := c.DeleteWithKeys(ctx, t, opts...) + return err + + case *document.SimpleDocument: + return c.DeleteWithDoc(t, opts...) + + case []*document.SimpleDocument: + return c.DeleteWithDocs(t, opts...) + + default: + return ErrInvalidDeleteTarget + + } +} + +// DeleteWithKey deletes using a DocKey to target a single document for delete. +func (c *Collection) DeleteWithKey(ctx context.Context, key key.DocKey, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + + txn, err := c.getTxn(ctx, false) + if err != nil { + return nil, err + } + + defer c.discardImplicitTxn(ctx, txn) + + res, err := c.deleteWithKey(ctx, txn, key, opts...) + if err != nil { + return nil, err + } + + return res, c.commitImplicitTxn(ctx, txn) +} + +func (c *Collection) deleteWithKey(ctx context.Context, txn *Txn, key key.DocKey, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + // Check the docKey we have been given to delete with actually has a corresponding + // document (i.e. document actually exists in the collection). + found, err := c.exists(ctx, txn, key) + if err != nil { + return nil, err + } + if !found { + return nil, ErrDocumentNotFound + } + + // Apply the function that will perform the full deletion of the document. + err = c.applyFullDelete(ctx, txn, key) + if err != nil { + return nil, err + } + + // Upon successfull deletion, record a summary. + results := &client.DeleteResult{ + Count: 1, + DocKeys: []string{key.String()}, + } + + return results, nil +} + +type dagDeleter struct { + bstore core.DAGStore + // queue *list.List +} + +func newDagDeleter(bstore core.DAGStore) dagDeleter { + return dagDeleter{ + bstore: bstore, + } +} + +// Here is what our db stores look like: +// /db +// -> block /blocks => /db/blocks +// -> datastore /data => /db/data +// -> headstore /heads => /db/heads +// -> systemstore /system => /db/system +// For the delete operation we are concerned with: +// 1) Deleting the actual blocks (blockstore). +// 2) Deleting datastore state. +// 3) Deleting headstore state. +func (c *Collection) applyFullDelete( + ctx context.Context, + txn *Txn, dockey key.DocKey) error { + + // Check the docKey we have been given to delete with actually has a corresponding + // document (i.e. document actually exists in the collection). + found, err := c.exists(ctx, txn, dockey) + if err != nil { + return err + } + if !found { + return ErrDocumentNotFound + } + + // 1. =========================== Delete blockstore state =========================== + // blocks: /db/blocks/CIQSDFKLJGHFKLSJGHHJKKLGHGLHSKLHKJGS => KLJSFHGLKJFHJKDLGKHDGLHGLFDHGLFDGKGHL + + // Covert dockey to compositeKey as follows: + // * dockey: bae-kljhLKHJG-lkjhgkldjhlzkdf-kdhflkhjsklgh-kjdhlkghjs + // => compositeKey: bae-kljhLKHJG-lkjhgkldjhlzkdf-kdhflkhjsklgh-kjdhlkghjs/C + compositeKey := dockey.Key.ChildString(core.COMPOSITE_NAMESPACE) + headset := clock.NewHeadSet(txn.Headstore(), compositeKey) + + // Get all the heads (cids). + heads, _, err := headset.List(ctx) + if err != nil { + return fmt.Errorf("Failed to get document heads: %w", err) + } + + dagDel := newDagDeleter(txn.DAGstore()) + // Delete DAG of all heads (and the heads themselves) + for _, head := range heads { + if err = dagDel.run(ctx, head); err != nil { + return err + } + } // ================================================ Successfully deleted the blocks + + // 2. =========================== Delete datastore state ============================ + dataQuery := query.Query{ + Prefix: c.getPrimaryIndexDocKey(dockey.Key).String(), + KeysOnly: true, + } + dataResult, err := txn.datastore.Query(ctx, dataQuery) + for e := range dataResult.Next() { + if e.Error != nil { + return err + } + + // docs: https://pkg.go.dev/github.com/ipfs/go-datastore + err = txn.datastore.Delete(ctx, ds.NewKey(e.Key)) + if err != nil { + return err + } + } + // Delete the parent marker key for this document. + err = txn.datastore.Delete(ctx, c.getPrimaryIndexDocKey(dockey.Key).Instance("v")) + if err != nil { + return err + } + // ======================== Successfully deleted the datastore state of this document + + // 3. =========================== Delete headstore state =========================== + headQuery := query.Query{ + Prefix: dockey.Key.String(), + KeysOnly: true, + } + headResult, err := txn.headstore.Query(ctx, headQuery) + for e := range headResult.Next() { + if e.Error != nil { + return err + } + err = txn.headstore.Delete(ctx, ds.NewKey(e.Key)) + if err != nil { + return err + } + } // ====================== Successfully deleted the headstore state of this document + + return nil +} + +func (d dagDeleter) run(ctx context.Context, targetCid cid.Cid) error { + // Validate the cid. + if targetCid == cid.Undef { + return nil + } + + // Get the block using the cid. + block, err := d.bstore.Get(ctx, targetCid) + if err == blockstore.ErrNotFound { + // If we have multiple heads corresponding to a dockey, one of the heads + // could have already deleted the parantal dag chain. + // Example: in the diagram below, HEAD#1 with cid1 deleted (represented by `:x`) + // all the parental nodes. Currently HEAD#2 goes to delete + // itself (represented by `:d`) and it's parental nodes, but as we see + // the parents were already deleted by HEAD#1 so we just stop there. + // + // | --> (E:x) HEAD#1->cid1 + // (A:x) --> (B:x) --> (C:x) --> (D:x) | + // | --> (F:d) HEAD#2->cid2 + return nil + + } else if err != nil { + return err + } + + // Attempt deleting the current block and it's links (in a mutally recursive fashion.) + return d.delete(ctx, targetCid, block) +} + +// (ipld.Block(ipldProtobufNode{Data: (cbor(crdt deltaPayload)), Links: (_head => parentCid, fieldName => fieldCid))) +func (d dagDeleter) delete( + ctx context.Context, + targetCid cid.Cid, + targetBlock block.Block) error { + + targetNode, err := dag.DecodeProtobuf(targetBlock.RawData()) + if err != nil { + return err + } + + // delete current block + if err := d.bstore.DeleteBlock(ctx, targetCid); err != nil { + return err + } + + for _, link := range targetNode.Links() { + // Call run on all the links (eventually delete is called on them too.) + if err := d.run(ctx, link.Cid); err != nil { + return err + } + } + + return nil +} + +// =================================== UNIMPLEMENTED =================================== + +// DeleteWithFilter deletes using a filter to target documents for delete. +// An deleter value is provided, which could be a string Patch, string Merge Patch +// or a parsed Patch, or parsed Merge Patch. +func (c *Collection) DeleteWithFilter(ctx context.Context, filter interface{}, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + // txn, err := c.getTxn(ctx, false) + // if err != nil { + // return nil, err + // } + // defer c.discardImplicitTxn(ctx, txn) + // res, err := c.deleteWithFilter(ctx, txn, filter, deleter, opts...) + // if err != nil { + // return nil, err + // } + // return res, c.commitImplicitTxn(ctx, txn) + + return nil, nil +} + +// DeleteWithKeys is the same as DeleteWithKey but accepts multiple keys as a slice. +// An deleter value is provided, which could be a string Patch, string Merge Patch +// or a parsed Patch, or parsed Merge Patch. +func (c *Collection) DeleteWithKeys(ctx context.Context, keys []key.DocKey, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + // txn, err := c.getTxn(ctx, false) + // if err != nil { + // return nil, err + // } + // defer c.discardImplicitTxn(ctx, txn) + // res, err := c.deleteWithKeys(ctx, txn, keys, deleter, opts...) + // if err != nil { + // return nil, err + // } + // return res, c.commitImplicitTxn(ctx, txn) + + return nil, nil +} + +// DeleteWithDoc deletes targeting the supplied document. +// An deleter value is provided, which could be a string Patch, string Merge Patch +// or a parsed Patch, or parsed Merge Patch. +func (c *Collection) DeleteWithDoc(doc *document.SimpleDocument, opts ...client.DeleteOpt) error { + return nil +} + +// DeleteWithDocs deletes all the supplied documents in the slice. +// An deleter value is provided, which could be a string Patch, string Merge Patch +// or a parsed Patch, or parsed Merge Patch. +func (c *Collection) DeleteWithDocs(docs []*document.SimpleDocument, opts ...client.DeleteOpt) error { + return nil +} + +//nolint:unused +func (c *Collection) deleteWithKeys(ctx context.Context, txn *Txn, keys []key.DocKey, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + // fmt.Println("updating keys:", keys) + // patch, err := parseDeleter(deleter) + // if err != nil { + // return nil, err + // } + // + // isPatch := false + // switch patch.(type) { + // case []map[string]interface{}: + // isPatch = true + // case map[string]interface{}: + // isPatch = false + // default: + // return nil, ErrInvalidDeleter + // } + // + // results := &client.DeleteResult{ + // DocKeys: make([]string, len(keys)), + // } + // for i, key := range keys { + // doc, err := c.Get(ctx, key) + // if err != nil { + // fmt.Println("error getting key to delete:", key) + // return nil, err + // } + // v, err := doc.ToMap() + // if err != nil { + // return nil, err + // } + // + // if isPatch { + // // todo + // } else { + // err = c.applyMerge(ctx, txn, v, patch.(map[string]interface{})) + // } + // if err != nil { + // return nil, nil + // } + // + // results.DocKeys[i] = key.String() + // results.Count++ + // } + // return results, nil + + return nil, nil +} + +//nolint:unused +func (c *Collection) deleteWithFilter(ctx context.Context, txn *Txn, filter interface{}, opts ...client.DeleteOpt) (*client.DeleteResult, error) { + // patch, err := parseDeleter(deleter) + // if err != nil { + // return nil, err + // } + + // isPatch := false + // isMerge := false + // switch patch.(type) { + // case []map[string]interface{}: + // isPatch = true + // case map[string]interface{}: + // isMerge = true + // default: + // return nil, ErrInvalidDeleter + // } + + // // scan through docs with filter + // query, err := c.makeSelectionQuery(ctx, txn, filter, opts...) + // if err != nil { + // return nil, err + // } + // if err := query.Start(); err != nil { + // return nil, err + // } + + // results := &client.DeleteResult{ + // DocKeys: make([]string, 0), + // } + + // // loop while we still have results from the filter query + // for { + // next, err := query.Next() + // if err != nil { + // return nil, err + // } + // // if theres no more records from the query, jump out of the loop + // if !next { + // break + // } + + // // Get the document, and apply the patch + // doc := query.Values() + // if isPatch { + // err = c.applyPatch(txn, doc, patch.([]map[string]interface{})) + // } else if isMerge { // else is fine here + // err = c.applyMerge(ctx, txn, doc, patch.(map[string]interface{})) + // } + // if err != nil { + // return nil, err + // } + + // // add successful deleted doc to results + // results.DocKeys = append(results.DocKeys, doc["_key"].(string)) + // results.Count++ + // } + + // return results, nil + + return nil, nil +} diff --git a/db/collection_update.go b/db/collection_update.go index 03d5b61ca3..d500f713be 100644 --- a/db/collection_update.go +++ b/db/collection_update.go @@ -33,8 +33,8 @@ type UpdateOpt struct{} type CreateOpt struct{} var ( - ErrInvalidTarget = errors.New("The doc targeter is an unknown type") - ErrTargetEmpty = errors.New("The doc targeter cannot be empty") + ErrInvalidUpdateTarget = errors.New("The doc update targeter is an unknown type") + ErrUpdateTargetEmpty = errors.New("The doc update targeter cannot be empty") ErrInvalidUpdater = errors.New("The doc updater is an unknown type") ErrUpdateEmpty = errors.New("The doc update cannot be empty") ErrInvalidMergeValueType = errors.New("The type of value in the merge patch doesn't match the schema") @@ -75,7 +75,7 @@ func (c *Collection) UpdateWith(ctx context.Context, target interface{}, updater case []*document.SimpleDocument: return c.UpdateWithDocs(t, updater, opts...) default: - return ErrInvalidTarget + return ErrInvalidUpdateTarget } } diff --git a/db/tests/mutation/simple/delete/simple_test.go b/db/tests/mutation/simple/delete/simple_test.go new file mode 100644 index 0000000000..14aad6ea2d --- /dev/null +++ b/db/tests/mutation/simple/delete/simple_test.go @@ -0,0 +1,173 @@ +// Copyright 2020 Source Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. +package delete + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/db/tests" + simpleTests "github.com/sourcenetwork/defradb/db/tests/mutation/simple" +) + +func TestMutationDeleteDocumentUsingSingleKey(t *testing.T) { + tests := []testUtils.QueryTestCase{ + { + Description: "Simple delete mutation while no document exists.", + Query: `mutation { + delete_user(id: "bae-028383cc-d6ba-5df7-959f-2bdce3536a05") { + _key + } + }`, + Docs: map[int][]string{}, + Results: nil, + ExpectedError: "No document for the given key exists", + }, + + { + Description: "Deletion of a document that doesn't exist in non-empty collection.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca811") { + _key + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Results: nil, + ExpectedError: "No document for the given key exists", + }, + + { + Description: "Deletion of a document without sub selection, should give error.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca810") + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Results: nil, + ExpectedError: "[Field \"delete_user\" of type \"[user]\" must have a sub selection.]", + }, + + { + Description: "Deletion of a document without _key sub-selection.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca810") { + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Results: nil, + ExpectedError: "Syntax Error GraphQL request (2:67) Unexpected empty IN {}\n\n1: mutation {\n2: \\u0009\\u0009\\u0009\\u0009\\u0009\\u0009delete_user(id: \"bae-8ca944fd-260e-5a44-b88f-326d9faca810\") {\n ^\n3: \\u0009\\u0009\\u0009\\u0009\\u0009\\u0009}\n", + }, + + { + Description: "Simple delete mutation where one element exists.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca810") { + _key + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Results: []map[string]interface{}{ + { + "_key": "bae-8ca944fd-260e-5a44-b88f-326d9faca810", + }, + }, + ExpectedError: "", + }, + + { + Description: "Simple delete mutation with an aliased _key name.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca810") { + FancyKey: _key + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Results: []map[string]interface{}{ + { + "FancyKey": "bae-8ca944fd-260e-5a44-b88f-326d9faca810", + }, + }, + ExpectedError: "", + }, + { + Description: "Delete an updated document and return an aliased _key name.", + Query: `mutation { + delete_user(id: "bae-8ca944fd-260e-5a44-b88f-326d9faca810") { + MyTestKey: _key + } + }`, + Docs: map[int][]string{ + 0: { + (`{ + "name": "Shahzad", + "age": 26, + "points": 48.5, + "verified": true + }`)}, + }, + Updates: map[int][]string{ + 0: { + (`{ + "age": 27, + "points": 48.2, + "verified": false + }`)}, + }, + Results: []map[string]interface{}{ + { + "MyTestKey": "bae-8ca944fd-260e-5a44-b88f-326d9faca810", + }, + }, + ExpectedError: "", + }, + } + + for _, test := range tests { + simpleTests.ExecuteTestCase(t, test) + } +} diff --git a/merkle/clock/heads.go b/merkle/clock/heads.go index 52ea236503..3ab5e85682 100644 --- a/merkle/clock/heads.go +++ b/merkle/clock/heads.go @@ -33,6 +33,11 @@ type heads struct { namespace ds.Key } +// NewHeadSet +func NewHeadSet(store core.DSReaderWriter, namespace ds.Key) *heads { + return newHeadset(store, namespace) +} + func newHeadset(store core.DSReaderWriter, namespace ds.Key) *heads { return &heads{ store: store, diff --git a/query/graphql/planner/delete.go b/query/graphql/planner/delete.go new file mode 100644 index 0000000000..65320dc44c --- /dev/null +++ b/query/graphql/planner/delete.go @@ -0,0 +1,131 @@ +// Copyright 2020 Source Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. +package planner + +import ( + "fmt" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/core" + "github.com/sourcenetwork/defradb/document/key" + "github.com/sourcenetwork/defradb/query/graphql/parser" +) + +type deleteNode struct { + p *Planner + + collection client.Collection + + filter *parser.Filter + ids []string + + isDeleting bool + deleteIter *valuesNode +} + +// Next only returns once. +func (n *deleteNode) Next() (bool, error) { + if n.isDeleting { + // create our result values node + if n.deleteIter == nil { + vnode := n.p.newContainerValuesNode(nil) + n.deleteIter = vnode + } + + // Apply the deletes + var results *client.DeleteResult + var err error + numids := len(n.ids) + if numids == 1 { + key, err2 := key.NewFromString(n.ids[0]) + if err2 != nil { + return false, err2 + } + results, err = n.collection.DeleteWithKey(n.p.ctx, key) + } else if numids > 1 { + // todo + keys := make([]key.DocKey, len(n.ids)) + for i, v := range n.ids { + keys[i], err = key.NewFromString(v) + if err != nil { + return false, err + } + } + results, err = n.collection.DeleteWithKeys(n.p.ctx, keys) + } else { // @todo: handle filter vs ID based + results, err = n.collection.DeleteWithFilter(n.p.ctx, n.filter) + } + + fmt.Println("delete node error:", err) + if err != nil { + return false, err + } + + // Consume the deletes into our valuesNode + fmt.Println(results) + for _, resKey := range results.DocKeys { + err := n.deleteIter.docs.AddDoc(map[string]interface{}{"_key": resKey}) + if err != nil { + return false, err + } + } + + n.isDeleting = false + + // lets release the results dockeys slice memory + results.DocKeys = nil + } + + return n.deleteIter.Next() +} + +func (n *deleteNode) Values() map[string]interface{} { + return n.deleteIter.Values() +} + +func (n *deleteNode) Spans(spans core.Spans) { + /* no-op */ +} + +func (n *deleteNode) Init() error { + return nil +} + +func (n *deleteNode) Start() error { + return nil +} + +func (n *deleteNode) Close() error { + return nil +} + +func (n *deleteNode) Source() planNode { + return nil +} + +func (p *Planner) DeleteDocs(parsed *parser.Mutation) (planNode, error) { + delete := &deleteNode{ + p: p, + filter: parsed.Filter, + ids: parsed.IDs, + isDeleting: true, + } + + // get collection + col, err := p.db.GetCollection(p.ctx, parsed.Schema) + if err != nil { + return nil, err + } + + delete.collection = col.WithTxn(p.txn) + + slct := parsed.ToSelect() + return p.SelectFromSource(slct, delete, true, nil) +} diff --git a/query/graphql/planner/planner.go b/query/graphql/planner/planner.go index 3d235902c3..cc24629806 100644 --- a/query/graphql/planner/planner.go +++ b/query/graphql/planner/planner.go @@ -138,6 +138,8 @@ func (p *Planner) newObjectMutationPlan(stmt *parser.Mutation) (planNode, error) return p.CreateDoc(stmt) case parser.UpdateObjects: return p.UpdateDocs(stmt) + case parser.DeleteObjects: + return p.DeleteDocs(stmt) default: return nil, fmt.Errorf("unknown mutation action %T", stmt.Type) } diff --git a/query/graphql/planner/update.go b/query/graphql/planner/update.go index 842b8028d2..2a25e6859f 100644 --- a/query/graphql/planner/update.go +++ b/query/graphql/planner/update.go @@ -54,14 +54,12 @@ func (n *updateNode) Next() (bool, error) { var err error numids := len(n.ids) if numids == 1 { - fmt.Println("single key") key, err2 := key.NewFromString(n.ids[0]) if err2 != nil { return false, err2 } results, err = n.collection.UpdateWithKey(n.p.ctx, key, n.patch) } else if numids > 1 { - fmt.Println("multi key") // todo keys := make([]key.DocKey, len(n.ids)) for i, v := range n.ids { @@ -72,7 +70,6 @@ func (n *updateNode) Next() (bool, error) { } results, err = n.collection.UpdateWithKeys(n.p.ctx, keys, n.patch) } else { - fmt.Println("filter") results, err = n.collection.UpdateWithFilter(n.p.ctx, n.filter, n.patch) } @@ -86,7 +83,6 @@ func (n *updateNode) Next() (bool, error) { for _, resKey := range results.DocKeys { err := n.updateIter.docs.AddDoc(map[string]interface{}{"_key": resKey}) if err != nil { - fmt.Println("document adding error : ", err) return false, err } } diff --git a/store/blockstore.go b/store/blockstore.go index 617f0a01f2..b504c38f43 100644 --- a/store/blockstore.go +++ b/store/blockstore.go @@ -40,6 +40,8 @@ import ( // is different than expected. var ErrHashMismatch = errors.New("block in storage has different hash than requested") +// defradb/store.ErrNotFound => error +// ipfs-blockstore.ErrNotFound => error // ErrNotFound is an error returned when a block is not found. var ErrNotFound = errors.New("blockstore: block not found") @@ -64,11 +66,11 @@ func (bs *bstore) HashOnRead(_ context.Context, enabled bool) { func (bs *bstore) Get(ctx context.Context, k cid.Cid) (blocks.Block, error) { if !k.Defined() { log.Error("undefined cid in blockstore") - return nil, ErrNotFound + return nil, blockstore.ErrNotFound } bdata, err := bs.store.Get(ctx, dshelp.MultihashToDsKey(k.Hash())) if err == ds.ErrNotFound { - return nil, ErrNotFound + return nil, blockstore.ErrNotFound } if err != nil { return nil, err @@ -122,7 +124,7 @@ func (bs *bstore) Has(ctx context.Context, k cid.Cid) (bool, error) { func (bs *bstore) GetSize(ctx context.Context, k cid.Cid) (int, error) { size, err := bs.store.GetSize(ctx, dshelp.MultihashToDsKey(k.Hash())) if err == ds.ErrNotFound { - return -1, ErrNotFound + return -1, blockstore.ErrNotFound } return size, err }