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: Allow users to add Views #2114

Merged
merged 15 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command {
schema_migrate,
)

view := MakeViewCommand()
view.AddCommand(
MakeViewAddCommand(),
)

index := MakeIndexCommand()
index.AddCommand(
MakeIndexCreateCommand(),
Expand Down Expand Up @@ -98,6 +103,7 @@ func NewDefraCommand(cfg *config.Config) *cobra.Command {
MakeDumpCommand(),
MakeRequestCommand(),
schema,
view,
index,
p2p,
backup,
Expand Down
1 change: 1 addition & 0 deletions cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
ErrNoLensConfig = errors.New("lens config cannot be empty")
ErrInvalidLensConfig = errors.New("invalid lens configuration")
ErrSchemaVersionNotOfSchema = errors.New(errSchemaVersionNotOfSchema)
ErrViewAddMissingArgs = errors.New("please provide a base query and output SDL for this view")
)

func NewErrInvalidLensConfig(inner error) error {
Expand Down
25 changes: 25 additions & 0 deletions cli/view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 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 cli

import (
"github.com/spf13/cobra"
)

func MakeViewCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "view",
Short: "Manage views within a running DefraDB instance",
Long: "Manage (add) views withing a running DefraDB instance",
}

return cmd
}
43 changes: 43 additions & 0 deletions cli/view_add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2023 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 cli

import "github.com/spf13/cobra"

func MakeViewAddCommand() *cobra.Command {
var cmd = &cobra.Command{
Use: "add [query] [sdl]",
Short: "Add new view",
Long: `Add new database view.

Example: add from an argument string:
defradb client view add 'Foo { name, ...}' 'type Foo { ... }'

Learn more about the DefraDB GraphQL Schema Language on https://docs.source.network.`,
RunE: func(cmd *cobra.Command, args []string) error {
store := mustGetStoreContext(cmd)

if len(args) != 2 {
return ErrViewAddMissingArgs
}

query := args[0]
sdl := args[1]

defs, err := store.AddView(cmd.Context(), query, sdl)
if err != nil {
return err
}
return writeJSON(cmd, defs)
},
}
return cmd
}
28 changes: 28 additions & 0 deletions client/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,34 @@ type Store interface {
// It will return an error if the provided schema version ID does not exist.
SetDefaultSchemaVersion(context.Context, string) error

// AddView creates a new Defra View.
//
// It takes a GQL query string, for example:
//
// Author {
// name
// books {
// name
// }
// }
//
//
// A GQL SDL that matches its output type must also be provided. There can only be one `type` declaration,
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick: 2 spaces

Also on the next line I think it should be "schema-only"
And on the next line again 2 spaces
Also in one the last line 2 spaces

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@AndrewSisley AndrewSisley Dec 21, 2023

Choose a reason for hiding this comment

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

  • schema-only

// any nested objects must be declared as embedded/schema-only types using the `interface` keyword.
// Relations must only be specified on the parent side of the relationship. For example:
//
// type AuthorView {
// name: String
// books: [BookView]
// }
// interface BookView {
// name: String
// }
//
// It will return the collection definitions of the types defined in the SDL if successful, otherwise an error
// will be returned. This function does not execute the given query.
AddView(ctx context.Context, gqlQuery string, sdl string) ([]CollectionDefinition, error)

// SetMigration sets the migration for the given source-destination schema version IDs. Is equivalent to
// calling `LensRegistry().SetMigration(ctx, cfg)`.
//
Expand Down
9 changes: 9 additions & 0 deletions client/descriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package client

import (
"fmt"

"github.com/sourcenetwork/defradb/client/request"
)

// CollectionDescription describes a Collection and all its associated metadata.
Expand All @@ -30,6 +32,13 @@ type CollectionDescription struct {
// The ID of the schema version that this collection is at.
SchemaVersionID string

// BaseQuery contains the base query of this view, if this collection is a view.
//
// The query will be saved, and then may be accessed by other actors on demand. Actor defined
// aggregates, filters and other logic (such as LensVM transforms) will execute on top of this
// base query before the result is returned to the actor.
BaseQuery *request.Select

// Indexes contains the secondary indexes that this Collection has.
Indexes []IndexDescription
}
Expand Down
87 changes: 87 additions & 0 deletions client/request/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
package request

import (
"encoding/json"

"github.com/sourcenetwork/immutable"
)

Expand Down Expand Up @@ -107,3 +109,88 @@ func (s *Select) validateGroupBy() []error {

return result
}

// selectJson is a private object used for handling json deserialization
// of `Select` objects.
type selectJson struct {
Field
DocKeys immutable.Option[[]string]
CID immutable.Option[string]
Root SelectionType
Limit immutable.Option[uint64]
Offset immutable.Option[uint64]
OrderBy immutable.Option[OrderBy]
GroupBy immutable.Option[GroupBy]
Filter immutable.Option[Filter]
ShowDeleted bool

// Properties above this line match the `Select` object and
// are deserialized using the normal/default logic.
// Properties below this line require custom logic in `UnmarshalJSON`
// in order to be deserialized correctly.
Comment on lines +127 to +130
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Although I like this comment, I find that overtime this might be hard to enforce / maintain. Perhaps in addition to this add a different prefix to variable names to the ones that require custom logic? i.e. CustomFields instead of Fields

Copy link
Contributor Author

@AndrewSisley AndrewSisley Dec 21, 2023

Choose a reason for hiding this comment

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

The current solution for sure has it's flaws, but I'm not sure I like that suggestion. I'll add a task to re-visit and see how I feel about it again in a little bit :)

  • Revisit this suggestion

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 still don't like the suggestion, and will leave as-is unless anyone continues this thread :)


Fields []map[string]json.RawMessage
}

func (s *Select) UnmarshalJSON(bytes []byte) error {
var selectMap selectJson
err := json.Unmarshal(bytes, &selectMap)
if err != nil {
return err
}

s.Field = selectMap.Field
s.DocKeys = selectMap.DocKeys
s.CID = selectMap.CID
s.Root = selectMap.Root
s.Limit = selectMap.Limit
s.Offset = selectMap.Offset
s.OrderBy = selectMap.OrderBy
s.GroupBy = selectMap.GroupBy
s.Filter = selectMap.Filter
s.ShowDeleted = selectMap.ShowDeleted
s.Fields = make([]Selection, len(selectMap.Fields))

for i, field := range selectMap.Fields {
fieldJson, err := json.Marshal(field)
Copy link
Contributor

Choose a reason for hiding this comment

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

question: why do you need to marshal the field first? Can't you unmarshal directly from map[string]json.RawMessage enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

selectJson acts as a strongly typed version of map[string]json.RawMessage with some compile time checks, and, most importantly, documentation. I prefer it over just going to map[string]json.RawMessage.

if err != nil {
return err
}

var fieldValue Selection
// We detect which concrete type each `Selection` object is by detecting
// non-nillable fields, if the key is present it must be of that type.
// They must be non-nillable as nil values may have their keys omitted from
// the json. This also relies on the fields being unique. We may wish to change
// this later to custom-serialize with a `_type` property.
if _, ok := field["Root"]; ok {
// This must be a Select, as only the `Select` type has a `Root` field
var fieldSelect Select
err := json.Unmarshal(fieldJson, &fieldSelect)
if err != nil {
return err
}
fieldValue = &fieldSelect
} else if _, ok := field["Targets"]; ok {
// This must be an Aggregate, as only the `Aggregate` type has a `Targets` field
var fieldAggregate Aggregate
err := json.Unmarshal(fieldJson, &fieldAggregate)
if err != nil {
return err
}
fieldValue = &fieldAggregate
} else {
// This must be a Field
var fieldField Field
err := json.Unmarshal(fieldJson, &fieldField)
if err != nil {
return err
}
fieldValue = &fieldField
}

s.Fields[i] = fieldValue
}

return nil
}
17 changes: 17 additions & 0 deletions db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const (
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"
errInvalidViewQuery string = "the query provided is not valid as a View"
)

var (
Expand Down Expand Up @@ -165,6 +166,7 @@ var (
ErrExpectedJSONArray = errors.New(errExpectedJSONArray)
ErrOneOneAlreadyLinked = errors.New(errOneOneAlreadyLinked)
ErrIndexDoesNotMatchName = errors.New(errIndexDoesNotMatchName)
ErrInvalidViewQuery = errors.New(errInvalidViewQuery)
)

// NewErrFieldOrAliasToFieldNotExist returns an error indicating that the given field or an alias field does not exist.
Expand Down Expand Up @@ -641,3 +643,18 @@ func NewErrCanNotIndexNonUniqueField(dockey, fieldName string, value any) error
errors.NewKV("Field value", value),
)
}

func NewErrInvalidViewQueryCastFailed(query string) error {
return errors.New(
errInvalidViewQuery,
errors.NewKV("Query", query),
errors.NewKV("Reason", "Internal errror, cast failed"),
)
}

func NewErrInvalidViewQueryMissingQuery() error {
return errors.New(
errInvalidViewQuery,
errors.NewKV("Reason", "No query provided"),
)
}
24 changes: 24 additions & 0 deletions db/txn_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,30 @@ func (db *explicitTxnDB) SetMigration(ctx context.Context, cfg client.LensConfig
return db.lensRegistry.SetMigration(ctx, cfg)
}

func (db *implicitTxnDB) AddView(ctx context.Context, query string, sdl string) ([]client.CollectionDefinition, error) {
txn, err := db.NewTxn(ctx, false)
if err != nil {
return nil, err
}
defer txn.Discard(ctx)

defs, err := db.addView(ctx, txn, query, sdl)
if err != nil {
return nil, err
}

err = txn.Commit(ctx)
if err != nil {
return nil, err
}

return defs, nil
}

func (db *explicitTxnDB) AddView(ctx context.Context, query string, sdl string) ([]client.CollectionDefinition, error) {
return db.addView(ctx, db.txn, query, sdl)
}

// BasicImport imports a json dataset.
// filepath must be accessible to the node.
func (db *implicitTxnDB) BasicImport(ctx context.Context, filepath string) error {
Expand Down
Loading