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 }