github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/client.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package dosa
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"os"
    27  	"reflect"
    28  
    29  	"bytes"
    30  	"io"
    31  
    32  	"github.com/pkg/errors"
    33  )
    34  
    35  // DomainObject is a marker interface method for an Entity
    36  type DomainObject interface {
    37  	// dummy marker interface method
    38  	isDomainObject() bool
    39  }
    40  
    41  // Entity represents any object that can be persisted by DOSA
    42  type Entity struct{}
    43  
    44  // make entity a DomainObject
    45  func (*Entity) isDomainObject() bool {
    46  	return true
    47  }
    48  
    49  // DomainIndex is a marker interface method for an Index
    50  type DomainIndex interface {
    51  	// dummy marker interface method
    52  	isDomainIndex() bool
    53  }
    54  
    55  // Index represents any object that can be indexed by by DOSA
    56  type Index struct{}
    57  
    58  func (*Index) isDomainIndex() bool {
    59  	return true
    60  }
    61  
    62  // ErrNotInitialized is returned when a user didn't call Initialize
    63  type ErrNotInitialized struct{}
    64  
    65  // Error returns a constant string "client not initialized"
    66  func (*ErrNotInitialized) Error() string {
    67  	return "client not initialized"
    68  }
    69  
    70  // ErrorIsNotInitialized checks if the error is a "ErrNotInitialized"
    71  // (possibly wrapped)
    72  func ErrorIsNotInitialized(err error) bool {
    73  	_, ok := errors.Cause(err).(*ErrNotInitialized)
    74  	return ok
    75  }
    76  
    77  // ErrNotFound is an error when a row is not found (single or multiple)
    78  type ErrNotFound struct{}
    79  
    80  // Error returns a constant string "Not found" for this error
    81  func (*ErrNotFound) Error() string {
    82  	return "not found"
    83  }
    84  
    85  // ErrorIsNotFound checks if the error is a "ErrNotFound"
    86  // (possibly wrapped)
    87  func ErrorIsNotFound(err error) bool {
    88  	_, ok := errors.Cause(err).(*ErrNotFound)
    89  	return ok
    90  }
    91  
    92  // ErrAlreadyExists is an error returned when CreateIfNotExists but a row already exists
    93  type ErrAlreadyExists struct{}
    94  
    95  func (*ErrAlreadyExists) Error() string {
    96  	return "already exists"
    97  }
    98  
    99  // ErrorIsAlreadyExists checks if the error is caused by "ErrAlreadyExists"
   100  func ErrorIsAlreadyExists(err error) bool {
   101  	_, ok := errors.Cause(err).(*ErrAlreadyExists)
   102  	return ok
   103  }
   104  
   105  // Client defines the methods to operate with DOSA entities
   106  type Client interface {
   107  	// Initialize must be called before any data operation
   108  	Initialize(ctx context.Context) error
   109  
   110  	// Create creates an entity; it fails if the entity already exists.
   111  	// You must fill in all of the fields of the DomainObject before
   112  	// calling this method, or they will be inserted with the zero value
   113  	// This is a relatively expensive operation. Use Upsert whenever possible.
   114  	CreateIfNotExists(ctx context.Context, objectToCreate DomainObject) error
   115  
   116  	// Read fetches a row by primary key. A list of fields to read can be
   117  	// specified. Use All() or nil for all fields.
   118  	// Before calling this method, fill in the DomainObject with ALL
   119  	// of the primary key fields; the other field values will be populated
   120  	// as a result of the read
   121  	Read(ctx context.Context, fieldsToRead []string, objectToRead DomainObject) error
   122  
   123  	// TODO: Coming in v2.1
   124  	// MultiRead fetches several rows by primary key. A list of fields can be
   125  	// specified. Use All() or nil for all fields.
   126  	// MultiRead(context.Context, []string, ...DomainObject) (MultiResult, error)
   127  
   128  	// Upsert creates or update a row. A list of fields to update can be
   129  	// specified. Use All() or nil for all fields.
   130  	// Before calling this method, fill in the DomainObject with ALL
   131  	// of the primary key fields, along with whatever fields you specify
   132  	// to update in fieldsToUpdate (or all the fields if you use dosa.All())
   133  	Upsert(ctx context.Context, fieldsToUpdate []string, objectToUpdate DomainObject) error
   134  
   135  	// TODO: Coming in v2.1
   136  	// MultiUpsert creates or updates multiple rows. A list of fields to
   137  	// update can be specified. Use All() or nil for all fields.
   138  	// MultiUpsert(context.Context, []string, ...DomainObject) (MultiResult, error)
   139  
   140  	// Remove removes a row by primary key. The passed-in entity should contain
   141  	// the primary key field values, all other fields are ignored.
   142  	Remove(ctx context.Context, objectToRemove DomainObject) error
   143  
   144  	// RemoveRange removes all of the rows that fall within the range specified by the
   145  	// given RemoveRangeOp.
   146  	RemoveRange(ctx context.Context, removeRangeOp *RemoveRangeOp) error
   147  
   148  	// TODO: Coming in v2.1
   149  	// MultiRemove removes multiple rows by primary key. The passed-in entity should
   150  	// contain the primary key field values.
   151  	// MultiRemove(context.Context, ...DomainObject) (MultiResult, error)
   152  
   153  	// Range fetches entities within a range
   154  	// Before calling range, create a RangeOp and fill in the table
   155  	// along with the partition key information. You will get back
   156  	// an array of DomainObjects, which will be of the type you requested
   157  	// in the rangeOp.
   158  	//
   159  	// Range only fetches a portion of the range at a time (the size of that portion is defined
   160  	// by the Limit parameter of the RangeOp). A continuation token is returned so subsequent portions
   161  	// of the range can be fetched with additional calls to the range function.
   162  	Range(ctx context.Context, rangeOp *RangeOp) ([]DomainObject, string, error)
   163  
   164  	// WalkRange starts at the offset specified by the RangeOp and walks the entire
   165  	// range of values that fall within the RangeOp conditions. It will make multiple, sequential
   166  	// range requests, fetching values until there are no more left in the range.
   167  	//
   168  	// For each value fetched, the provided onNext function is called with the value as it's argument.
   169  	WalkRange(ctx context.Context, r *RangeOp, onNext func(value DomainObject) error) error
   170  
   171  	// ScanEverything fetches all entities of a type
   172  	// Before calling ScanEverything, create a scanOp to specify the
   173  	// table to scan. The return values are an array of objects, that
   174  	// you can type-assert to the appropriate dosa.Entity, a string
   175  	// that contains the continuation token, and any error.
   176  	// To scan the next set of rows, modify the scanOp to provide
   177  	// the string returned as an Offset()
   178  	ScanEverything(ctx context.Context, scanOp *ScanOp) ([]DomainObject, string, error)
   179  }
   180  
   181  // MultiResult contains the result for each entity operation in the case of
   182  // MultiRead, MultiUpsert and MultiRemove. If the operation succeeded for
   183  // an entity, the value for in the map will be nil; otherwise, the entity is
   184  // untouched and error is not nil.
   185  type MultiResult map[DomainObject]error
   186  
   187  // All is used for "fields []string" to read/update all fields.
   188  // It's a convenience function for code readability.
   189  func All() []string { return nil }
   190  
   191  // AdminClient has methods to manage schemas and scopes
   192  type AdminClient interface {
   193  	// Directories sets admin client search path
   194  	Directories(dirs []string) AdminClient
   195  	// Excludes sets patters to exclude when searching for entities
   196  	Excludes(excludes []string) AdminClient
   197  	// Scope sets the admin client scope
   198  	Scope(scope string) AdminClient
   199  	// CheckSchema checks the compatibility of schemas
   200  	CheckSchema(ctx context.Context, namePrefix string) (*SchemaStatus, error)
   201  	// CheckSchemaStatus checks the status of schema application
   202  	CheckSchemaStatus(ctx context.Context, namePrefix string, version int32) (*SchemaStatus, error)
   203  	// UpsertSchema upserts the schemas
   204  	UpsertSchema(ctx context.Context, namePrefix string) (*SchemaStatus, error)
   205  	// GetSchema finds entity definitions
   206  	GetSchema() ([]*EntityDefinition, error)
   207  	// CreateScope creates a new scope
   208  	CreateScope(ctx context.Context, s string) error
   209  	// TruncateScope keeps the scope and the schemas, but drops the data associated with the scope
   210  	TruncateScope(ctx context.Context, s string) error
   211  	// DropScope drops the scope and the data and schemas in the scope
   212  	DropScope(ctx context.Context, s string) error
   213  }
   214  
   215  type client struct {
   216  	initialized bool
   217  	registrar   Registrar
   218  	connector   Connector
   219  }
   220  
   221  // NewClient returns a new DOSA client for the registry and connector
   222  // provided. This is currently only a partial implementation to demonstrate
   223  // basic CRUD functionality.
   224  func NewClient(reg Registrar, conn Connector) Client {
   225  	return &client{
   226  		registrar: reg,
   227  		connector: conn,
   228  	}
   229  }
   230  
   231  // Initialize performs initial schema checks against all registered entities.
   232  func (c *client) Initialize(ctx context.Context) error {
   233  	if c.initialized {
   234  		return nil
   235  	}
   236  
   237  	// check schema for all registered entities
   238  	registered, err := c.registrar.FindAll()
   239  	if err != nil {
   240  		return err
   241  	}
   242  	eds := []*EntityDefinition{}
   243  	for _, re := range registered {
   244  		eds = append(eds, re.EntityDefinition())
   245  	}
   246  
   247  	// fetch latest version for all registered entities, assume order is preserved
   248  	version, err := c.connector.CheckSchema(ctx, c.registrar.Scope(), c.registrar.NamePrefix(), eds)
   249  	if err != nil {
   250  		return errors.Wrap(err, "CheckSchema failed")
   251  	}
   252  
   253  	// set version for all registered entities
   254  	for _, reg := range registered {
   255  		reg.SetVersion(version)
   256  	}
   257  	c.initialized = true
   258  	return nil
   259  }
   260  
   261  // CreateIfNotExists creates a row, but only if it does not exist. The entity
   262  // provided must contain values for all components of its primary key for the
   263  // operation to succeed.
   264  func (c *client) CreateIfNotExists(ctx context.Context, entity DomainObject) error {
   265  	return c.createOrUpsert(ctx, nil, entity, c.connector.CreateIfNotExists)
   266  }
   267  
   268  // Read fetches an entity by primary key, The entity provided must contain
   269  // values for all components of its primary key for the operation to succeed.
   270  // If `fieldsToRead` is provided, only a subset of fields will be
   271  // marshalled onto the given entity
   272  func (c *client) Read(ctx context.Context, fieldsToRead []string, entity DomainObject) error {
   273  	if !c.initialized {
   274  		return &ErrNotInitialized{}
   275  	}
   276  
   277  	// lookup registered entity, registry will return error if registration
   278  	// is not found
   279  	re, err := c.registrar.Find(entity)
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	// translate entity field values to a map of primary key name/values pairs
   285  	// required to perform a read
   286  	fieldValues := re.KeyFieldValues(entity)
   287  
   288  	// build a list of column names from a list of entities field names
   289  	columnsToRead, err := re.ColumnNames(fieldsToRead)
   290  	if err != nil {
   291  		return err
   292  	}
   293  
   294  	results, err := c.connector.Read(ctx, re.EntityInfo(), fieldValues, columnsToRead)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	// map results to entity fields
   300  	re.SetFieldValues(entity, results, columnsToRead)
   301  
   302  	return nil
   303  }
   304  
   305  // MultiRead fetches several entities by primary key, The entities provided
   306  // must contain values for all components of its primary key for the operation
   307  // to succeed. If `fieldsToRead` is provided, only a subset of fields will be
   308  // marshalled onto the given entities.
   309  func (c *client) MultiRead(context.Context, []string, ...DomainObject) (MultiResult, error) {
   310  	panic("not implemented")
   311  }
   312  
   313  type createOrUpsertType func(context.Context, *EntityInfo, map[string]FieldValue) error
   314  
   315  // Upsert updates some values of an entity, or creates it if it doesn't exist.
   316  // The entity provided must contain values for all components of its primary
   317  // key for the operation to succeed. If `fieldsToUpdate` is provided, only a
   318  // subset of fields will be updated.
   319  func (c *client) Upsert(ctx context.Context, fieldsToUpdate []string, entity DomainObject) error {
   320  	return c.createOrUpsert(ctx, fieldsToUpdate, entity, c.connector.Upsert)
   321  }
   322  
   323  func (c *client) createOrUpsert(ctx context.Context, fieldsToUpdate []string, entity DomainObject, fn createOrUpsertType) error {
   324  	if !c.initialized {
   325  		return &ErrNotInitialized{}
   326  	}
   327  
   328  	// lookup registered entity, registry will return error if registration
   329  	// is not found
   330  	re, err := c.registrar.Find(entity)
   331  	if err != nil {
   332  		return err
   333  	}
   334  
   335  	// translate entity field values to a map of primary key name/values pairs
   336  	keyFieldValues := re.KeyFieldValues(entity)
   337  
   338  	// translate remaining entity fields values to map of column name/value pairs
   339  	fieldValues, err := re.OnlyFieldValues(entity, fieldsToUpdate)
   340  	if err != nil {
   341  		return err
   342  	}
   343  
   344  	// merge key and remaining values
   345  	for k, v := range keyFieldValues {
   346  		fieldValues[k] = v
   347  	}
   348  
   349  	return fn(ctx, re.EntityInfo(), fieldValues)
   350  }
   351  
   352  // MultiUpsert updates several entities by primary key, The entities provided
   353  // must contain values for all components of its primary key for the operation
   354  // to succeed. If `fieldsToUpdate` is provided, only a subset of fields will be
   355  // updated.
   356  func (c *client) MultiUpsert(context.Context, []string, ...DomainObject) (MultiResult, error) {
   357  	panic("not implemented")
   358  }
   359  
   360  // Remove deletes an entity by primary key, The entity provided must contain
   361  // values for all components of its primary key for the operation to succeed.
   362  func (c *client) Remove(ctx context.Context, entity DomainObject) error {
   363  	if !c.initialized {
   364  		return &ErrNotInitialized{}
   365  	}
   366  
   367  	// lookup registered entity, registry will return error if registration
   368  	// is not found
   369  	re, err := c.registrar.Find(entity)
   370  	if err != nil {
   371  		return err
   372  	}
   373  
   374  	// translate entity field values to a map of primary key name/values pairs
   375  	keyFieldValues := re.KeyFieldValues(entity)
   376  
   377  	err = c.connector.Remove(ctx, re.EntityInfo(), keyFieldValues)
   378  	return err
   379  }
   380  
   381  // RemoveRange removes all of the rows that fall within the range specified by the
   382  // given RemoveRangeOp.
   383  func (c *client) RemoveRange(ctx context.Context, r *RemoveRangeOp) error {
   384  	if !c.initialized {
   385  		return &ErrNotInitialized{}
   386  	}
   387  
   388  	// look up the entity in the registry
   389  	re, err := c.registrar.Find(r.object)
   390  	if err != nil {
   391  		return errors.Wrap(err, "RemoveRange")
   392  	}
   393  
   394  	// now convert the client range columns to server side column conditions structure
   395  	columnConditions, err := convertConditions(r.conditions, re.table)
   396  	if err != nil {
   397  		return errors.Wrap(err, "RemoveRange")
   398  	}
   399  
   400  	return errors.Wrap(c.connector.RemoveRange(ctx, re.info, columnConditions), "RemoveRange")
   401  }
   402  
   403  // MultiRemove deletes several entities by primary key, The entities provided
   404  // must contain values for all components of its primary key for the operation
   405  // to succeed.
   406  func (c *client) MultiRemove(context.Context, ...DomainObject) (MultiResult, error) {
   407  	panic("not implemented")
   408  }
   409  
   410  // Range uses the connector to fetch DOSA entities for a given range.
   411  func (c *client) Range(ctx context.Context, r *RangeOp) ([]DomainObject, string, error) {
   412  	if !c.initialized {
   413  		return nil, "", &ErrNotInitialized{}
   414  	}
   415  	// look up the entity in the registry
   416  	re, err := c.registrar.Find(r.object)
   417  	if err != nil {
   418  		return nil, "", errors.Wrap(err, "Range")
   419  	}
   420  
   421  	// now convert the client range columns to server side column conditions structure
   422  	columnConditions, err := convertConditions(r.conditions, re.table)
   423  	if err != nil {
   424  		return nil, "", errors.Wrap(err, "Range")
   425  	}
   426  
   427  	// convert the fieldsToRead to the server side equivalent
   428  	fieldsToRead, err := re.ColumnNames(r.fieldsToRead)
   429  	if err != nil {
   430  		return nil, "", errors.Wrap(err, "Range")
   431  	}
   432  
   433  	// call the server side method
   434  	values, token, err := c.connector.Range(ctx, re.info, columnConditions, fieldsToRead, r.token, r.limit)
   435  	if err != nil {
   436  		return nil, "", errors.Wrap(err, "Range")
   437  	}
   438  
   439  	objectArray := objectsFromValueArray(r.object, values, re, nil)
   440  	return objectArray, token, nil
   441  }
   442  
   443  func (c *client) WalkRange(ctx context.Context, r *RangeOp, onNext func(value DomainObject) error) error {
   444  	for {
   445  		results, nextToken, err := c.Range(ctx, r)
   446  
   447  		if err != nil {
   448  			return err
   449  		}
   450  
   451  		for _, result := range results {
   452  			if cerr := onNext(result); cerr != nil {
   453  				return cerr
   454  			}
   455  		}
   456  
   457  		if len(nextToken) == 0 {
   458  			return nil
   459  		}
   460  		r = r.Offset(nextToken)
   461  	}
   462  }
   463  
   464  func objectsFromValueArray(object DomainObject, values []map[string]FieldValue, re *RegisteredEntity, columnsToRead []string) []DomainObject {
   465  	goType := reflect.TypeOf(object).Elem() // get the reflect.Type of the client entity
   466  	doType := reflect.TypeOf((*DomainObject)(nil)).Elem()
   467  	slice := reflect.MakeSlice(reflect.SliceOf(doType), 0, len(values)) // make a slice of these
   468  	elements := reflect.New(slice.Type())
   469  	elements.Elem().Set(slice)
   470  	for _, flist := range values { // for each row returned
   471  		newObject := reflect.New(goType).Interface()                             // make a new entity
   472  		re.SetFieldValues(newObject.(DomainObject), flist, columnsToRead)        // fill it in from server values
   473  		slice = reflect.Append(slice, reflect.ValueOf(newObject.(DomainObject))) // append to slice
   474  	}
   475  	return slice.Interface().([]DomainObject)
   476  }
   477  
   478  // ScanEverything uses the connector to fetch all DOSA entities of the given type.
   479  func (c *client) ScanEverything(ctx context.Context, sop *ScanOp) ([]DomainObject, string, error) {
   480  	if !c.initialized {
   481  		return nil, "", &ErrNotInitialized{}
   482  	}
   483  	// look up the entity in the registry
   484  	re, err := c.registrar.Find(sop.object)
   485  	if err != nil {
   486  		return nil, "", errors.Wrap(err, "failed to ScanEverything")
   487  	}
   488  	// convert the fieldsToRead to the server side equivalent
   489  	fieldsToRead, err := re.ColumnNames(sop.fieldsToRead)
   490  	if err != nil {
   491  		return nil, "", errors.Wrap(err, "failed to ScanEverything")
   492  	}
   493  
   494  	// call the server side method
   495  	values, token, err := c.connector.Scan(ctx, re.info, fieldsToRead, sop.token, sop.limit)
   496  	if err != nil {
   497  		return nil, "", err
   498  	}
   499  	objectArray := objectsFromValueArray(sop.object, values, re, nil)
   500  	return objectArray, token, nil
   501  
   502  }
   503  
   504  type adminClient struct {
   505  	scope     string
   506  	dirs      []string
   507  	excludes  []string
   508  	connector Connector
   509  }
   510  
   511  // NewAdminClient returns a new DOSA admin client for the connector provided.
   512  func NewAdminClient(conn Connector) AdminClient {
   513  	return &adminClient{
   514  		scope:     os.Getenv("USER"),
   515  		dirs:      []string{"."},
   516  		excludes:  []string{"_test.go"},
   517  		connector: conn,
   518  	}
   519  }
   520  
   521  // Directories sets the given paths to the client's list of file paths to scan
   522  // during schema operations. Defaults to ["."].
   523  func (c *adminClient) Directories(dirs []string) AdminClient {
   524  	c.dirs = dirs
   525  	return c
   526  }
   527  
   528  // Excludes sets the substrings used when considering filenames for inclusion
   529  // when searching for DOSA entities. Defaults to ["_test.go"]
   530  func (c *adminClient) Excludes(excludes []string) AdminClient {
   531  	c.excludes = excludes
   532  	return c
   533  }
   534  
   535  // Scope sets the scope used for schema operations. Defaults to $USER
   536  func (c *adminClient) Scope(scope string) AdminClient {
   537  	c.scope = scope
   538  	return c
   539  }
   540  
   541  // CheckSchema first searches for entity definitions within configured
   542  // directories before checking the compatibility of each entity for the givena
   543  // the namePrefix. The client's scope and search directories should be
   544  // configured on initialization and be non-empty when CheckSchema is called.
   545  // An error is returned if client is misconfigured (eg. invalid scope) or if
   546  // any of the entities found are incompatible, not found or not uniquely named.
   547  // The definition of "incompatible" and "not found" may vary but is ultimately
   548  // defined by the client connector implementation.
   549  func (c *adminClient) CheckSchema(ctx context.Context, namePrefix string) (*SchemaStatus, error) {
   550  	defs, err := c.GetSchema()
   551  	if err != nil {
   552  		return nil, errors.Wrapf(err, "GetSchema failed")
   553  	}
   554  	version, err := c.connector.CheckSchema(ctx, c.scope, namePrefix, defs)
   555  	if err != nil {
   556  		return nil, errors.Wrapf(err, "CheckSchema failed, directories: %s, excludes: %s, scope: %s", c.dirs, c.excludes, c.scope)
   557  	}
   558  	return &SchemaStatus{
   559  		Version: version,
   560  		Status:  "OK",
   561  	}, nil
   562  }
   563  
   564  func (c *adminClient) CheckSchemaStatus(ctx context.Context, namePrefix string, version int32) (*SchemaStatus, error) {
   565  	status, err := c.connector.CheckSchemaStatus(ctx, c.scope, namePrefix, version)
   566  	if err != nil {
   567  		return nil, errors.Wrapf(err, "CheckSchemaStatus status failed")
   568  	}
   569  	return status, nil
   570  }
   571  
   572  // UpsertSchema creates or updates the schema for entities in the given
   573  // namespace. See CheckSchema for more detail about scope and namePrefix.
   574  func (c *adminClient) UpsertSchema(ctx context.Context, namePrefix string) (*SchemaStatus, error) {
   575  	defs, err := c.GetSchema()
   576  	if err != nil {
   577  		return nil, errors.Wrapf(err, "GetSchema failed")
   578  	}
   579  	status, err := c.connector.UpsertSchema(ctx, c.scope, namePrefix, defs)
   580  	if err != nil {
   581  		return nil, errors.Wrapf(err, "UpsertSchema failed, directories: %s, excludes: %s, scope: %s", c.dirs, c.excludes, c.scope)
   582  	}
   583  	return status, nil
   584  }
   585  
   586  // GetSchema returns the derived entity definitions that are found within the
   587  // current search path of the client. GetSchema can be used to introspect the
   588  // state of schema before further operations are performed. For example,
   589  // GetSchema is called by both CheckSchema and UpsertSchema before their
   590  // respective operations are performed. An error is returned when:
   591  //   - invalid scope name (eg. length, invalid characters, see names.go)
   592  //   - invalid directory (eg. path does not exist, is not a directory)
   593  //   - unparseable entity (eg. invalid primary key)
   594  //   - no entities were found
   595  func (c *adminClient) GetSchema() ([]*EntityDefinition, error) {
   596  	// prevent bogus scope names from reaching connectors
   597  	if err := IsValidName(c.scope); err != nil {
   598  		return nil, errors.Wrapf(err, "invalid scope name %q", c.scope)
   599  	}
   600  	// "warnings" mean entity was found but contained invalid annotations
   601  	entities, warns, err := FindEntities(c.dirs, c.excludes)
   602  	if len(warns) > 0 {
   603  		return nil, NewEntityErrors(warns)
   604  	}
   605  	// I/O and AST parsing errors
   606  	if err != nil {
   607  		return nil, err
   608  	}
   609  	// prevent unnecessary connector calls when nothing was found
   610  	if len(entities) == 0 {
   611  		return nil, fmt.Errorf("no entities found; did you specify the right directories for your source?")
   612  	}
   613  
   614  	defs := make([]*EntityDefinition, len(entities))
   615  	for idx, e := range entities {
   616  		defs[idx] = &e.EntityDefinition
   617  	}
   618  	return defs, nil
   619  }
   620  
   621  // EntityErrors is a container for parse errors/warning.
   622  type EntityErrors struct {
   623  	warns []error
   624  }
   625  
   626  // NewEntityErrors returns a wrapper for errors encountered while parsing
   627  // entity struct tags.
   628  func NewEntityErrors(warns []error) *EntityErrors {
   629  	return &EntityErrors{warns: warns}
   630  }
   631  
   632  // Error makes parse errors discernable to end-user.
   633  func (ee *EntityErrors) Error() string {
   634  	var str bytes.Buffer
   635  	if _, err := io.WriteString(&str, "The following entities had warnings/errors:"); err != nil {
   636  		// for linting, WriteString will never return error
   637  		return "could not write errors to output buffer"
   638  	}
   639  	for _, err := range ee.warns {
   640  		str.WriteByte('\n')
   641  		if _, err := io.WriteString(&str, err.Error()); err != nil {
   642  			// for linting, WriteString will never return error
   643  			return "could not write errors to output buffer"
   644  		}
   645  	}
   646  	return str.String()
   647  }
   648  
   649  // CreateScope creates a new scope
   650  func (c *adminClient) CreateScope(ctx context.Context, s string) error {
   651  	return c.connector.CreateScope(ctx, s)
   652  }
   653  
   654  // TruncateScope keeps the scope and the schemas, but drops the data associated with the scope
   655  func (c *adminClient) TruncateScope(ctx context.Context, s string) error {
   656  	return c.connector.TruncateScope(ctx, s)
   657  }
   658  
   659  // DropScope drops the scope and the data and schemas in the scope
   660  func (c *adminClient) DropScope(ctx context.Context, s string) error {
   661  	return c.connector.DropScope(ctx, s)
   662  }