diff --git a/client/db.go b/client/db.go index 9a645288d4..9c26ace3c9 100644 --- a/client/db.go +++ b/client/db.go @@ -26,7 +26,6 @@ type DB interface { GetCollectionByName(context.Context, string) (Collection, error) GetCollectionBySchemaID(context.Context, string) (Collection, error) GetAllCollections(ctx context.Context) ([]Collection, error) - GetRelationshipIdField(fieldName, targetType, thisType string) (string, error) Root() ds.Batching Blockstore() blockstore.Blockstore diff --git a/client/descriptions.go b/client/descriptions.go index dafe1663c1..4119a2d53c 100644 --- a/client/descriptions.go +++ b/client/descriptions.go @@ -40,6 +40,18 @@ func (col CollectionDescription) GetField(name string) (FieldDescription, bool) return FieldDescription{}, false } +// GetRelation returns the field that supports the relation of the given name. +func (col CollectionDescription) GetRelation(name string) (FieldDescription, bool) { + if !col.Schema.IsEmpty() { + for _, field := range col.Schema.Fields { + if field.RelationName == name { + return field, true + } + } + } + return FieldDescription{}, false +} + func (col CollectionDescription) GetPrimaryIndex() IndexDescription { return col.Indexes[0] } diff --git a/core/parser.go b/core/parser.go new file mode 100644 index 0000000000..a704f3c09b --- /dev/null +++ b/core/parser.go @@ -0,0 +1,52 @@ +// Copyright 2022 Democratized Data Foundation +// +// 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 core + +import ( + "context" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" +) + +type SchemaDefinition struct { + // The name of this schema definition. + Name string + + // The serialized definition of this schema definition. + // todo: this needs to be properly typed and handled according to + // https://github.com/sourcenetwork/defradb/issues/863 + Body []byte +} + +type Schema struct { + // The individual declarations of types defined by this schema. + Definitions []SchemaDefinition + + // The collection descriptions created from/defined by this schema. + Descriptions []client.CollectionDescription +} + +// Parser represents the object responsible for handling stuff specific to +// a query language. This includes schema and query parsing, and introspection. +type Parser interface { + // Returns true if the given string is an introspection request. + IsIntrospection(request string) bool + + // Executes the given introspection request. + ExecuteIntrospection(request string) *client.QueryResult + + // Parses the given request, returning a strongly typed model of that request. + Parse(request string) (*request.Request, []error) + + // Adds the given schema to this parser's model. + AddSchema(ctx context.Context, schema string) (*Schema, error) +} diff --git a/db/collection_update.go b/db/collection_update.go index 099521e984..999495c7f5 100644 --- a/db/collection_update.go +++ b/db/collection_update.go @@ -23,7 +23,6 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/datastore" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" "github.com/sourcenetwork/defradb/planner" "github.com/sourcenetwork/defradb/query/graphql/parser" ) @@ -598,21 +597,19 @@ func (c *collection) makeSelectionQuery( txn datastore.Txn, filter any, ) (planner.Query, error) { - mapping := c.createMapping() - var f *mapper.Filter + var f client.Option[request.Filter] var err error switch fval := filter.(type) { case string: if fval == "" { return nil, errors.New("invalid filter") } - var p client.Option[request.Filter] - p, err = parser.NewFilterFromString(fval) + + f, err = parser.NewFilterFromString(fval) if err != nil { return nil, err } - f = mapper.ToFilter(p, mapping) - case *mapper.Filter: + case client.Option[request.Filter]: f = fval default: return nil, errors.New("invalid filter") @@ -620,59 +617,44 @@ func (c *collection) makeSelectionQuery( if filter == "" { return nil, errors.New("invalid filter") } - slct, err := c.makeSelectLocal(f, mapping) + slct, err := c.makeSelectLocal(f) if err != nil { return nil, err } - return c.db.queryExecutor.MakeSelectQuery(ctx, c.db, txn, slct) + planner := planner.New(ctx, c.db, txn) + return planner.MakePlan(&request.Request{ + Queries: []*request.OperationDefinition{ + { + Selections: []request.Selection{ + slct, + }, + }, + }, + }) } -func (c *collection) makeSelectLocal(filter *mapper.Filter, mapping *core.DocumentMapping) (*mapper.Select, error) { - slct := &mapper.Select{ - Targetable: mapper.Targetable{ - Field: mapper.Field{ - Name: c.Name(), - }, - Filter: filter, +func (c *collection) makeSelectLocal(filter client.Option[request.Filter]) (*request.Select, error) { + slct := &request.Select{ + Field: request.Field{ + Name: c.Name(), }, - Fields: make([]mapper.Requestable, len(c.desc.Schema.Fields)), - DocumentMapping: *mapping, + Filter: filter, + Fields: make([]request.Selection, 0), } for _, fd := range c.Schema().Fields { if fd.IsObject() { continue } - index := int(fd.ID) - slct.Fields = append(slct.Fields, &mapper.Field{ - Index: index, - Name: fd.Name, + slct.Fields = append(slct.Fields, &request.Field{ + Name: fd.Name, }) } return slct, nil } -func (c *collection) createMapping() *core.DocumentMapping { - mapping := core.NewDocumentMapping() - mapping.Add(core.DocKeyFieldIndex, request.DocKeyFieldName) - for _, fd := range c.Schema().Fields { - if fd.IsObject() { - continue - } - index := int(fd.ID) - mapping.Add(index, fd.Name) - mapping.RenderKeys = append(mapping.RenderKeys, - core.RenderKey{ - Index: index, - Key: fd.Name, - }, - ) - } - return mapping -} - // getTypeAndCollectionForPatch parses the Patch op path values // and compares it against the collection schema. // If it's within the schema, then patchIsSubType is false diff --git a/db/db.go b/db/db.go index 127617f10b..d14670faff 100644 --- a/db/db.go +++ b/db/db.go @@ -30,8 +30,7 @@ import ( "github.com/sourcenetwork/defradb/events" "github.com/sourcenetwork/defradb/logging" "github.com/sourcenetwork/defradb/merkle/crdt" - "github.com/sourcenetwork/defradb/planner" - "github.com/sourcenetwork/defradb/query/graphql/schema" + "github.com/sourcenetwork/defradb/query/graphql" ) var ( @@ -61,8 +60,7 @@ type db struct { events client.Events - schema *schema.SchemaManager - queryExecutor *planner.QueryExecutor + parser core.Parser // The options used to init the database options any @@ -92,14 +90,7 @@ func newDB(ctx context.Context, rootstore ds.Batching, options ...Option) (*db, multistore := datastore.MultiStoreFrom(root) crdtFactory := crdt.DefaultFactory.WithStores(multistore) - log.Debug(ctx, "Loading: schema manager") - sm, err := schema.NewSchemaManager() - if err != nil { - return nil, err - } - - log.Debug(ctx, "Loading: query executor") - exec, err := planner.NewQueryExecutor(sm) + parser, err := graphql.NewParser() if err != nil { return nil, err } @@ -110,9 +101,8 @@ func newDB(ctx context.Context, rootstore ds.Batching, options ...Option) (*db, crdtFactory: &crdtFactory, - schema: sm, - queryExecutor: exec, - options: options, + parser: parser, + options: options, } // apply options @@ -190,23 +180,6 @@ func (db *db) PrintDump(ctx context.Context) error { return printStore(ctx, db.multistore.Rootstore()) } -func (db *db) Executor() *planner.QueryExecutor { - return db.queryExecutor -} - -func (db *db) GetRelationshipIdField(fieldName, targetType, thisType string) (string, error) { - rm := db.schema.Relations - rel := rm.GetRelationByDescription(fieldName, targetType, thisType) - if rel == nil { - return "", errors.New("relation does not exists") - } - subtypefieldname, _, ok := rel.GetFieldFromSchemaType(targetType) - if !ok { - return "", errors.New("relation is missing referenced field") - } - return subtypefieldname, nil -} - // Close is called when we are shutting down the database. // This is the place for any last minute cleanup or releasing of resources (i.e.: Badger instance). func (db *db) Close(ctx context.Context) { diff --git a/db/query.go b/db/query.go index 72a4a3898e..34a94dfc64 100644 --- a/db/query.go +++ b/db/query.go @@ -14,10 +14,9 @@ import ( "context" "strings" - gql "github.com/graphql-go/graphql" - "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/datastore" + "github.com/sourcenetwork/defradb/planner" ) func (db *db) ExecQuery(ctx context.Context, query string) *client.QueryResult { @@ -34,7 +33,18 @@ func (db *db) ExecQuery(ctx context.Context, query string) *client.QueryResult { } defer txn.Discard(ctx) - results, err := db.queryExecutor.ExecQuery(ctx, db, txn, query) + request, errors := db.parser.Parse(query) + if len(errors) > 0 { + errorStrings := make([]any, len(errors)) + for i, err := range errors { + errorStrings[i] = err.Error() + } + res.Errors = errorStrings + return res + } + + planner := planner.New(ctx, db, txn) + results, err := planner.RunRequest(ctx, request) if err != nil { res.Errors = []any{err.Error()} return res @@ -54,13 +64,24 @@ func (db *db) ExecTransactionalQuery( query string, txn datastore.Txn, ) *client.QueryResult { - res := &client.QueryResult{} - // check if its Introspection query - if strings.Contains(query, "IntrospectionQuery") { + if db.parser.IsIntrospection(query) { return db.ExecIntrospection(query) } - results, err := db.queryExecutor.ExecQuery(ctx, db, txn, query) + res := &client.QueryResult{} + + request, errors := db.parser.Parse(query) + if len(errors) > 0 { + errorStrings := make([]any, len(errors)) + for i, err := range errors { + errorStrings[i] = err.Error() + } + res.Errors = errorStrings + return res + } + + planner := planner.New(ctx, db, txn) + results, err := planner.RunRequest(ctx, request) if err != nil { res.Errors = []any{err.Error()} return res @@ -71,20 +92,5 @@ func (db *db) ExecTransactionalQuery( } func (db *db) ExecIntrospection(query string) *client.QueryResult { - schema := db.schema.Schema() - // t := schema.Type("userFilterArg") - // spew.Dump(t.(*gql.InputObject).Fields()) - params := gql.Params{Schema: *schema, RequestString: query} - r := gql.Do(params) - - res := &client.QueryResult{ - Data: r.Data, - Errors: make([]any, len(r.Errors)), - } - - for i, err := range r.Errors { - res.Errors[i] = err - } - - return res + return db.parser.ExecuteIntrospection(query) } diff --git a/db/schema.go b/db/schema.go index e7ea55d300..84326bac8f 100644 --- a/db/schema.go +++ b/db/schema.go @@ -13,7 +13,6 @@ package db import ( "context" - "github.com/graphql-go/graphql/language/ast" dsq "github.com/ipfs/go-datastore/query" "github.com/sourcenetwork/defradb/core" @@ -21,23 +20,19 @@ import ( // LoadSchema takes the provided schema in SDL format, and applies it to the database, // and creates the necessary collections, query types, etc. -func (db *db) AddSchema(ctx context.Context, schema string) error { - // @todo: create collection after generating query types - types, astdoc, err := db.schema.Generator.FromSDL(ctx, schema) +func (db *db) AddSchema(ctx context.Context, schemaString string) error { + schema, err := db.parser.AddSchema(ctx, schemaString) if err != nil { return err } - colDesc, err := db.schema.Generator.CreateDescriptions(types) - if err != nil { - return err - } - for _, desc := range colDesc { + + for _, desc := range schema.Descriptions { if _, err := db.CreateCollection(ctx, desc); err != nil { return err } } - return db.saveSchema(ctx, astdoc) + return db.saveSchema(ctx, schema) } func (db *db) loadSchema(ctx context.Context) error { @@ -55,20 +50,16 @@ func (db *db) loadSchema(ctx context.Context) error { sdl += "\n" + string(buf) } - _, _, err = db.schema.Generator.FromSDL(ctx, sdl) + _, err = db.parser.AddSchema(ctx, sdl) return err } -func (db *db) saveSchema(ctx context.Context, astdoc *ast.Document) error { +func (db *db) saveSchema(ctx context.Context, schema *core.Schema) error { // save each type individually - for _, def := range astdoc.Definitions { - switch defType := def.(type) { - case *ast.ObjectDefinition: - body := defType.Loc.Source.Body[defType.Loc.Start:defType.Loc.End] - key := core.NewSchemaKey(defType.Name.Value) - if err := db.systemstore().Put(ctx, key.ToDS(), body); err != nil { - return err - } + for _, def := range schema.Definitions { + key := core.NewSchemaKey(def.Name) + if err := db.systemstore().Put(ctx, key.ToDS(), def.Body); err != nil { + return err } } return nil diff --git a/planner/arbitrary_join.go b/planner/arbitrary_join.go index c7cfc637ed..985e80484e 100644 --- a/planner/arbitrary_join.go +++ b/planner/arbitrary_join.go @@ -15,7 +15,7 @@ import ( "strings" "github.com/sourcenetwork/defradb/core" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // A data-source that may yield child items, parent items, or both depending on configuration diff --git a/planner/average.go b/planner/average.go index edd9fa0588..fa90bf0ffb 100644 --- a/planner/average.go +++ b/planner/average.go @@ -16,7 +16,7 @@ import ( "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type averageNode struct { diff --git a/planner/commit.go b/planner/commit.go index 7e971129ae..0f6583a5e3 100644 --- a/planner/commit.go +++ b/planner/commit.go @@ -21,7 +21,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/fetcher" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type dagScanNode struct { diff --git a/planner/count.go b/planner/count.go index 65c7954c14..8323b05368 100644 --- a/planner/count.go +++ b/planner/count.go @@ -20,7 +20,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/core/enumerable" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type countNode struct { diff --git a/planner/create.go b/planner/create.go index a4709cdac4..4c6eadd763 100644 --- a/planner/create.go +++ b/planner/create.go @@ -17,7 +17,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // createNode is used to construct and execute diff --git a/planner/datasource.go b/planner/datasource.go index 4b867422fa..3ab26c4b5b 100644 --- a/planner/datasource.go +++ b/planner/datasource.go @@ -16,7 +16,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // sourceInfo stores info about the data source diff --git a/planner/delete.go b/planner/delete.go index bcb43f436e..cb300ebdbf 100644 --- a/planner/delete.go +++ b/planner/delete.go @@ -13,7 +13,7 @@ package planner import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/core" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type deleteNode struct { diff --git a/planner/executor.go b/planner/executor.go deleted file mode 100644 index 7a3ae43345..0000000000 --- a/planner/executor.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022 Democratized Data Foundation -// -// 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 ( - "context" - "fmt" - - gql "github.com/graphql-go/graphql" - gqlp "github.com/graphql-go/graphql/language/parser" - "github.com/graphql-go/graphql/language/source" - - "github.com/sourcenetwork/defradb/client" - "github.com/sourcenetwork/defradb/client/request" - "github.com/sourcenetwork/defradb/datastore" - "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" - "github.com/sourcenetwork/defradb/query/graphql/parser" - "github.com/sourcenetwork/defradb/query/graphql/schema" -) - -// Query is an external hook into the planNode -// system. It allows outside packages to -// execute and manage a query plan graph directly. -// Instead of using one of the available functions -// like ExecQuery(...). -// Currently, this is used by the collection.Update -// system. -type Query planNode - -type QueryExecutor struct { - SchemaManager *schema.SchemaManager -} - -func NewQueryExecutor(manager *schema.SchemaManager) (*QueryExecutor, error) { - if manager == nil { - return nil, errors.New("schemaManager cannot be nil") - } - - return &QueryExecutor{ - SchemaManager: manager, - }, nil -} - -func (e *QueryExecutor) MakeSelectQuery( - ctx context.Context, - db client.DB, - txn datastore.Txn, - selectStmt *mapper.Select, -) (Query, error) { - if selectStmt == nil { - return nil, errors.New("cannot create query without a selection") - } - planner := makePlanner(ctx, db, txn) - return planner.makePlan(selectStmt) -} - -func (e *QueryExecutor) ExecQuery( - ctx context.Context, - db client.DB, - txn datastore.Txn, - query string, - args ...any, -) ([]map[string]any, error) { - q, err := e.ParseRequestString(query) - if err != nil { - return nil, err - } - - planner := makePlanner(ctx, db, txn) - return planner.runRequest(ctx, q) -} - -func (e *QueryExecutor) MakePlanFromParser( - ctx context.Context, - db client.DB, - txn datastore.Txn, - query *request.Request, -) (planNode, error) { - planner := makePlanner(ctx, db, txn) - return planner.makePlan(query) -} - -func (e *QueryExecutor) ParseRequestString(request string) (*request.Request, error) { - source := source.NewSource(&source.Source{ - Body: []byte(request), - Name: "GraphQL request", - }) - - ast, err := gqlp.Parse(gqlp.ParseParams{Source: source}) - if err != nil { - return nil, err - } - - schema := e.SchemaManager.Schema() - validationResult := gql.ValidateDocument(schema, ast, nil) - if !validationResult.IsValid { - return nil, errors.New(fmt.Sprintf("%v", validationResult.Errors)) - } - - query, parsingErrors := parser.ParseQuery(ast) - if len(parsingErrors) > 0 { - return nil, errors.New(fmt.Sprintf("%v", parsingErrors)) - } - - return query, nil -} diff --git a/planner/group.go b/planner/group.go index 5993b6584d..8329506f02 100644 --- a/planner/group.go +++ b/planner/group.go @@ -16,7 +16,7 @@ import ( "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // A node responsible for the grouping of documents by a given selection of fields. diff --git a/planner/limit.go b/planner/limit.go index f928e67247..746c403df3 100644 --- a/planner/limit.go +++ b/planner/limit.go @@ -12,7 +12,7 @@ package planner import ( "github.com/sourcenetwork/defradb/core" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // Limit the results, yielding only what the limit/offset permits diff --git a/mapper/aggregate.go b/planner/mapper/aggregate.go similarity index 100% rename from mapper/aggregate.go rename to planner/mapper/aggregate.go diff --git a/mapper/commitSelect.go b/planner/mapper/commitSelect.go similarity index 100% rename from mapper/commitSelect.go rename to planner/mapper/commitSelect.go diff --git a/mapper/descriptions.go b/planner/mapper/descriptions.go similarity index 100% rename from mapper/descriptions.go rename to planner/mapper/descriptions.go diff --git a/mapper/field.go b/planner/mapper/field.go similarity index 100% rename from mapper/field.go rename to planner/mapper/field.go diff --git a/mapper/mapper.go b/planner/mapper/mapper.go similarity index 100% rename from mapper/mapper.go rename to planner/mapper/mapper.go diff --git a/mapper/mutation.go b/planner/mapper/mutation.go similarity index 100% rename from mapper/mutation.go rename to planner/mapper/mutation.go diff --git a/mapper/requestable.go b/planner/mapper/requestable.go similarity index 100% rename from mapper/requestable.go rename to planner/mapper/requestable.go diff --git a/mapper/select.go b/planner/mapper/select.go similarity index 100% rename from mapper/select.go rename to planner/mapper/select.go diff --git a/mapper/targetable.go b/planner/mapper/targetable.go similarity index 100% rename from mapper/targetable.go rename to planner/mapper/targetable.go diff --git a/planner/order.go b/planner/order.go index 0556b5bff7..05d666cc54 100644 --- a/planner/order.go +++ b/planner/order.go @@ -15,7 +15,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // simplified planNode interface. diff --git a/planner/planner.go b/planner/planner.go index ccf802e640..be45b61f34 100644 --- a/planner/planner.go +++ b/planner/planner.go @@ -20,7 +20,7 @@ import ( "github.com/sourcenetwork/defradb/datastore" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/logging" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) var ( @@ -93,7 +93,7 @@ type Planner struct { ctx context.Context } -func makePlanner(ctx context.Context, db client.DB, txn datastore.Txn) *Planner { +func New(ctx context.Context, db client.DB, txn datastore.Txn) *Planner { return &Planner{ txn: txn, db: db, @@ -133,9 +133,6 @@ func (p *Planner) newPlan(stmt any) (planNode, error) { return p.Select(m) - case *mapper.Select: - return p.Select(n) - case *request.CommitSelect: m, err := mapper.ToCommitSelect(p.ctx, p.txn, n) if err != nil { @@ -504,8 +501,8 @@ func (p *Planner) executeRequest( return docs, err } -// runRequest plans how to run the request, then attempts to run the request and returns the results. -func (p *Planner) runRequest( +// RunRequest plans how to run the request, then attempts to run the request and returns the results. +func (p *Planner) RunRequest( ctx context.Context, query *request.Request, ) ([]map[string]any, error) { diff --git a/planner/query.go b/planner/query.go new file mode 100644 index 0000000000..778dd5e8ce --- /dev/null +++ b/planner/query.go @@ -0,0 +1,20 @@ +// Copyright 2022 Democratized Data Foundation +// +// 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 + +// Query is an external hook into the planNode +// system. It allows outside packages to +// execute and manage a query plan graph directly. +// Instead of using one of the available functions +// like ExecQuery(...). +// Currently, this is used by the collection.Update +// system. +type Query planNode diff --git a/planner/scan.go b/planner/scan.go index 65db8125ba..3736452a6e 100644 --- a/planner/scan.go +++ b/planner/scan.go @@ -15,7 +15,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/db/fetcher" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // scans an index for records diff --git a/planner/select.go b/planner/select.go index 37c81f5760..1895a2b2e9 100644 --- a/planner/select.go +++ b/planner/select.go @@ -19,7 +19,7 @@ import ( "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/db/fetcher" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) /* diff --git a/planner/sum.go b/planner/sum.go index aa81fec7d1..2458f0fd5f 100644 --- a/planner/sum.go +++ b/planner/sum.go @@ -18,7 +18,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/core/enumerable" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type sumNode struct { diff --git a/planner/top.go b/planner/top.go index 3ea6971129..369f0e0334 100644 --- a/planner/top.go +++ b/planner/top.go @@ -14,7 +14,7 @@ import ( "github.com/sourcenetwork/defradb/client/request" "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) const topLevelNodeKind string = "topLevelNode" diff --git a/planner/type_join.go b/planner/type_join.go index 237883fd79..f99aed8de7 100644 --- a/planner/type_join.go +++ b/planner/type_join.go @@ -18,7 +18,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" "github.com/sourcenetwork/defradb/query/graphql/schema" ) @@ -258,15 +258,6 @@ func (p *Planner) makeTypeJoinOne( return nil, err } - subTypeFieldName, err := p.db.GetRelationshipIdField( - subType.Name, - subType.CollectionName, - parent.parsed.CollectionName, - ) - if err != nil { - return nil, err - } - // get the correct sub field schema type (collection) subTypeFieldDesc, ok := parent.sourceInfo.collectionDescription.GetField(subType.Name) if !ok { @@ -277,12 +268,22 @@ func (p *Planner) makeTypeJoinOne( // check if the field we're querying is the primary side of the relation isPrimary := subTypeFieldDesc.RelationType&client.Relation_Type_Primary > 0 + subTypeCollectionDesc, err := p.getCollectionDesc(subType.CollectionName) + if err != nil { + return nil, err + } + + subTypeField, subTypeFieldNameFound := subTypeCollectionDesc.GetRelation(subTypeFieldDesc.RelationName) + if !subTypeFieldNameFound { + return nil, errors.New("couldn't find subtype field description for typeJoin node") + } + return &typeJoinOne{ p: p, root: source, subSelect: subType, subTypeName: subType.Name, - subTypeFieldName: subTypeFieldName, + subTypeFieldName: subTypeField.Name, subType: selectPlan, primary: isPrimary, docMapper: docMapper{parent.documentMapping}, @@ -445,21 +446,27 @@ func (p *Planner) makeTypeJoinMany( return nil, err } - rootName, err := p.db.GetRelationshipIdField( - subType.Name, - subType.CollectionName, - parent.parsed.CollectionName, - ) + subTypeFieldDesc, ok := parent.sourceInfo.collectionDescription.GetField(subType.Name) + if !ok { + return nil, errors.New("couldn't find subtype field description for typeJoin node") + } + + subTypeCollectionDesc, err := p.getCollectionDesc(subType.CollectionName) if err != nil { return nil, err } + rootField, rootNameFound := subTypeCollectionDesc.GetRelation(subTypeFieldDesc.RelationName) + if !rootNameFound { + return nil, errors.New("couldn't find subtype field description for typeJoin node") + } + return &typeJoinMany{ p: p, root: source, subSelect: subType, subTypeName: subType.Name, - rootName: rootName, + rootName: rootField.Name, subType: selectPlan, docMapper: docMapper{parent.documentMapping}, }, nil diff --git a/planner/update.go b/planner/update.go index 23cea4bd82..ac2eeb8e99 100644 --- a/planner/update.go +++ b/planner/update.go @@ -15,7 +15,7 @@ import ( "github.com/sourcenetwork/defradb/client" "github.com/sourcenetwork/defradb/core" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) type updateNode struct { diff --git a/planner/values.go b/planner/values.go index e6c1fa1b2c..c7cf836a61 100644 --- a/planner/values.go +++ b/planner/values.go @@ -16,7 +16,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/base" "github.com/sourcenetwork/defradb/db/container" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) // valuesNode contains a collection diff --git a/planner/versionedscan.go b/planner/versionedscan.go index d0e122f702..1638a03342 100644 --- a/planner/versionedscan.go +++ b/planner/versionedscan.go @@ -17,7 +17,7 @@ import ( "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/db/fetcher" "github.com/sourcenetwork/defradb/errors" - "github.com/sourcenetwork/defradb/mapper" + "github.com/sourcenetwork/defradb/planner/mapper" ) var ( diff --git a/query/graphql/parser.go b/query/graphql/parser.go new file mode 100644 index 0000000000..7f2f6893e2 --- /dev/null +++ b/query/graphql/parser.go @@ -0,0 +1,127 @@ +// Copyright 2022 Democratized Data Foundation +// +// 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 graphql + +import ( + "context" + "strings" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/client/request" + "github.com/sourcenetwork/defradb/core" + defrap "github.com/sourcenetwork/defradb/query/graphql/parser" + "github.com/sourcenetwork/defradb/query/graphql/schema" + + gql "github.com/graphql-go/graphql" + "github.com/graphql-go/graphql/language/ast" + gqlp "github.com/graphql-go/graphql/language/parser" + "github.com/graphql-go/graphql/language/source" +) + +var _ core.Parser = (*parser)(nil) + +type parser struct { + schemaManager schema.SchemaManager +} + +func NewParser() (*parser, error) { + schemaManager, err := schema.NewSchemaManager() + if err != nil { + return nil, err + } + + p := &parser{ + schemaManager: *schemaManager, + } + + return p, nil +} + +func (p *parser) IsIntrospection(request string) bool { + // todo: This needs to be done properly https://github.com/sourcenetwork/defradb/issues/911 + return strings.Contains(request, "IntrospectionQuery") +} + +func (p *parser) ExecuteIntrospection(request string) *client.QueryResult { + schema := p.schemaManager.Schema() + params := gql.Params{Schema: *schema, RequestString: request} + r := gql.Do(params) + + res := &client.QueryResult{ + Data: r.Data, + Errors: make([]any, len(r.Errors)), + } + + for i, err := range r.Errors { + res.Errors[i] = err + } + + return res +} + +func (p *parser) Parse(request string) (*request.Request, []error) { + source := source.NewSource(&source.Source{ + Body: []byte(request), + Name: "GraphQL request", + }) + + ast, err := gqlp.Parse(gqlp.ParseParams{Source: source}) + if err != nil { + return nil, []error{err} + } + + schema := p.schemaManager.Schema() + validationResult := gql.ValidateDocument(schema, ast, nil) + if !validationResult.IsValid { + errors := make([]error, len(validationResult.Errors)) + for i, err := range validationResult.Errors { + errors[i] = err + } + return nil, errors + } + + query, parsingErrors := defrap.ParseQuery(ast) + if len(parsingErrors) > 0 { + return nil, parsingErrors + } + + return query, nil +} + +func (p *parser) AddSchema(ctx context.Context, schema string) (*core.Schema, error) { + types, astdoc, err := p.schemaManager.Generator.FromSDL(ctx, schema) + if err != nil { + return nil, err + } + + colDesc, err := p.schemaManager.Generator.CreateDescriptions(types) + if err != nil { + return nil, err + } + + definitions := make([]core.SchemaDefinition, len(astdoc.Definitions)) + for i, astDefinition := range astdoc.Definitions { + objDef, isObjDef := astDefinition.(*ast.ObjectDefinition) + if !isObjDef { + continue + } + + definitions[i] = core.SchemaDefinition{ + Name: objDef.Name.Value, + Body: objDef.Loc.Source.Body[objDef.Loc.Start:objDef.Loc.End], + } + } + + return &core.Schema{ + Definitions: definitions, + Descriptions: colDesc, + }, nil +} diff --git a/query/graphql/parser/parser.go b/query/graphql/parser/parser.go new file mode 100644 index 0000000000..f6b79fcd6d --- /dev/null +++ b/query/graphql/parser/parser.go @@ -0,0 +1,17 @@ +// Copyright 2022 Democratized Data Foundation +// +// 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 parser + +import "github.com/sourcenetwork/defradb/query/graphql/schema" + +type Parser struct { + SchemaManager *schema.SchemaManager +} diff --git a/query/graphql/schema/descriptions.go b/query/graphql/schema/descriptions.go index b4d9485285..379ee1ded1 100644 --- a/query/graphql/schema/descriptions.go +++ b/query/graphql/schema/descriptions.go @@ -183,7 +183,7 @@ func (g *Generator) CreateDescriptions( fd.Schema = schemaName // check if its a one-to-one, one-to-many, many-to-many - rel := g.manager.Relations.GetRelationByDescription( + rel := g.manager.Relations.getRelationByDescription( fname, schemaName, t.Name()) if rel == nil { return nil, errors.New(fmt.Sprintf( diff --git a/query/graphql/schema/relations.go b/query/graphql/schema/relations.go index d542f99880..cb8a021583 100644 --- a/query/graphql/schema/relations.go +++ b/query/graphql/schema/relations.go @@ -52,7 +52,7 @@ func (rm *RelationManager) GetRelation(name string) (*Relation, error) { return rel, nil } -func (rm *RelationManager) GetRelationByDescription( +func (rm *RelationManager) getRelationByDescription( field, schemaType, objectType string, ) *Relation { for _, rel := range rm.relations { diff --git a/tests/bench/query/planner/utils.go b/tests/bench/query/planner/utils.go index e1fd28e595..5a3de03321 100644 --- a/tests/bench/query/planner/utils.go +++ b/tests/bench/query/planner/utils.go @@ -12,11 +12,13 @@ package planner import ( "context" + "fmt" "testing" + "github.com/sourcenetwork/defradb/core" "github.com/sourcenetwork/defradb/errors" "github.com/sourcenetwork/defradb/planner" - "github.com/sourcenetwork/defradb/query/graphql/schema" + "github.com/sourcenetwork/defradb/query/graphql" benchutils "github.com/sourcenetwork/defradb/tests/bench" "github.com/sourcenetwork/defradb/tests/bench/fixtures" ) @@ -27,16 +29,16 @@ func runQueryParserBench( fixture fixtures.Generator, query string, ) error { - exec, err := buildExecutor(ctx, fixture) + parser, err := buildParser(ctx, fixture) if err != nil { return err } b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := exec.ParseRequestString(query) - if err != nil { - return errors.Wrap("failed to parse query string", err) + _, errs := parser.Parse(query) + if errs != nil { + return errors.Wrap("failed to parse query string", errors.New(fmt.Sprintf("%v", errs))) } } b.StopTimer() @@ -56,14 +58,14 @@ func runMakePlanBench( } defer db.Close(ctx) - exec, err := buildExecutor(ctx, fixture) + parser, err := buildParser(ctx, fixture) if err != nil { return err } - q, err := exec.ParseRequestString(query) - if err != nil { - return errors.Wrap("failed to parse query string", err) + q, errs := parser.Parse(query) + if len(errs) > 0 { + return errors.Wrap("failed to parse query string", errors.New(fmt.Sprintf("%v", errs))) } txn, err := db.NewTxn(ctx, false) if err != nil { @@ -72,7 +74,8 @@ func runMakePlanBench( b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := exec.MakePlanFromParser(ctx, db, txn, q) + planner := planner.New(ctx, db, txn) + _, err := planner.MakePlan(q) if err != nil { return errors.Wrap("failed to make plan", err) } @@ -81,26 +84,23 @@ func runMakePlanBench( return nil } -func buildExecutor( +func buildParser( ctx context.Context, fixture fixtures.Generator, -) (*planner.QueryExecutor, error) { - sm, err := schema.NewSchemaManager() - if err != nil { - return nil, err - } +) (core.Parser, error) { schema, err := benchutils.ConstructSchema(fixture) if err != nil { return nil, err } - types, _, err := sm.Generator.FromSDL(ctx, schema) + + parser, err := graphql.NewParser() if err != nil { return nil, err } - _, err = sm.Generator.CreateDescriptions(types) + _, err = parser.AddSchema(ctx, schema) if err != nil { return nil, err } - return planner.NewQueryExecutor(sm) + return parser, nil } diff --git a/tests/integration/mutation/simple/delete/explain_simple_delete_test.go b/tests/integration/mutation/simple/delete/explain_simple_delete_test.go index 75cb71194f..c238bf6495 100644 --- a/tests/integration/mutation/simple/delete/explain_simple_delete_test.go +++ b/tests/integration/mutation/simple/delete/explain_simple_delete_test.go @@ -828,7 +828,7 @@ func TestExplainDeletionUsingMultiIdsAndSingleIdAndFilter_Failure(t *testing.T) Results: []dataMap{}, - ExpectedError: "[Field \"delete_user\" of type \"[user]\" must have a sub selection.]", + ExpectedError: "Field \"delete_user\" of type \"[user]\" must have a sub selection.", }, } diff --git a/tests/integration/mutation/simple/delete/multi_ids_test.go b/tests/integration/mutation/simple/delete/multi_ids_test.go index 199e35baa7..68b92f81de 100644 --- a/tests/integration/mutation/simple/delete/multi_ids_test.go +++ b/tests/integration/mutation/simple/delete/multi_ids_test.go @@ -306,7 +306,7 @@ func TestDeletionOfMultipleDocumentUsingMultipleKeys_Failure(t *testing.T) { }, }, Results: []map[string]any{}, - ExpectedError: "[Field \"delete_user\" of type \"[user]\" must have a sub selection.]", + ExpectedError: "Field \"delete_user\" of type \"[user]\" must have a sub selection.", }, { diff --git a/tests/integration/mutation/simple/delete/single_id_test.go b/tests/integration/mutation/simple/delete/single_id_test.go index f52bf8502c..64373c32f1 100644 --- a/tests/integration/mutation/simple/delete/single_id_test.go +++ b/tests/integration/mutation/simple/delete/single_id_test.go @@ -185,7 +185,7 @@ func TestDeletionOfADocumentUsingSingleKey_Failure(t *testing.T) { }, }, Results: []map[string]any{}, - ExpectedError: "[Field \"delete_user\" of type \"[user]\" must have a sub selection.]", + ExpectedError: "Field \"delete_user\" of type \"[user]\" must have a sub selection.", }, { diff --git a/tests/integration/mutation/simple/delete/with_filter_test.go b/tests/integration/mutation/simple/delete/with_filter_test.go index cd871b5bea..dee25d0ee9 100644 --- a/tests/integration/mutation/simple/delete/with_filter_test.go +++ b/tests/integration/mutation/simple/delete/with_filter_test.go @@ -314,7 +314,7 @@ func TestDeletionOfDocumentsWithFilter_Failure(t *testing.T) { Results: []map[string]any{}, - ExpectedError: "[Field \"delete_user\" of type \"[user]\" must have a sub selection.]", + ExpectedError: "Field \"delete_user\" of type \"[user]\" must have a sub selection.", }, { diff --git a/tests/integration/query/simple/simple_explain_test.go b/tests/integration/query/simple/simple_explain_test.go index fa41ca1fc8..46d14e4cc3 100644 --- a/tests/integration/query/simple/simple_explain_test.go +++ b/tests/integration/query/simple/simple_explain_test.go @@ -40,7 +40,7 @@ func TestExplainQuerySimpleOnFieldDirective_BadUsage(t *testing.T) { Results: []dataMap{}, - ExpectedError: "[Directive \"explain\" may not be used on FIELD.]", + ExpectedError: "Directive \"explain\" may not be used on FIELD.", } executeTestCase(t, test) }