github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/datastore/datastore.go (about) 1 package datastore 2 3 import ( 4 "context" 5 "fmt" 6 "slices" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/rs/zerolog" 12 13 "github.com/authzed/spicedb/pkg/tuple" 14 15 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 16 17 "github.com/authzed/spicedb/pkg/datastore/options" 18 core "github.com/authzed/spicedb/pkg/proto/core/v1" 19 ) 20 21 var Engines []string 22 23 // SortedEngineIDs returns the full set of engine IDs, sorted. 24 func SortedEngineIDs() []string { 25 engines := append([]string{}, Engines...) 26 sort.Strings(engines) 27 return engines 28 } 29 30 // EngineOptions returns the full set of engine IDs, sorted and quoted into a string. 31 func EngineOptions() string { 32 ids := SortedEngineIDs() 33 quoted := make([]string, 0, len(ids)) 34 for _, id := range ids { 35 quoted = append(quoted, `"`+id+`"`) 36 } 37 return strings.Join(quoted, ", ") 38 } 39 40 // Ellipsis is a special relation that is assumed to be valid on the right 41 // hand side of a tuple. 42 const Ellipsis = "..." 43 44 // FilterMaximumIDCount is the maximum number of resource IDs or subject IDs that can be sent into 45 // a filter. 46 const FilterMaximumIDCount uint16 = 100 47 48 // RevisionChanges represents the changes in a single transaction. 49 type RevisionChanges struct { 50 Revision Revision 51 52 // RelationshipChanges are any relationships that were changed at this revision. 53 RelationshipChanges []*core.RelationTupleUpdate 54 55 // ChangedDefinitions are any definitions that were added or changed at this revision. 56 ChangedDefinitions []SchemaDefinition 57 58 // DeletedNamespaces are any namespaces that were deleted. 59 DeletedNamespaces []string 60 61 // DeletedCaveats are any caveats that were deleted. 62 DeletedCaveats []string 63 64 // IsCheckpoint, if true, indicates that the datastore has reported all changes 65 // up until and including the Revision and that no additional schema updates can 66 // have occurred before this point. 67 IsCheckpoint bool 68 } 69 70 func (rc *RevisionChanges) MarshalZerologObject(e *zerolog.Event) { 71 e.Str("revision", rc.Revision.String()) 72 e.Bool("is-checkpoint", rc.IsCheckpoint) 73 e.Array("deleted-namespaces", strArray(rc.DeletedNamespaces)) 74 e.Array("deleted-caveats", strArray(rc.DeletedCaveats)) 75 76 changedNames := make([]string, 0, len(rc.ChangedDefinitions)) 77 for _, cd := range rc.ChangedDefinitions { 78 changedNames = append(changedNames, fmt.Sprintf("%T:%s", cd, cd.GetName())) 79 } 80 81 e.Array("changed-definitions", strArray(changedNames)) 82 e.Int("num-changed-relationships", len(rc.RelationshipChanges)) 83 } 84 85 // RelationshipsFilter is a filter for relationships. 86 type RelationshipsFilter struct { 87 // OptionalResourceType is the namespace/type for the resources to be found. 88 OptionalResourceType string 89 90 // OptionalResourceIds are the IDs of the resources to find. If nil empty, any resource ID will be allowed. 91 // Cannot be used with OptionalResourceIDPrefix. 92 OptionalResourceIds []string 93 94 // OptionalResourceIDPrefix is the prefix to use for resource IDs. If empty, any prefix is allowed. 95 // Cannot be used with OptionalResourceIds. 96 OptionalResourceIDPrefix string 97 98 // OptionalResourceRelation is the relation of the resource to find. If empty, any relation is allowed. 99 OptionalResourceRelation string 100 101 // OptionalSubjectsSelectors is the selectors to use for subjects of the relationship. If nil, all subjects are allowed. 102 // If specified, relationships matching *any* selector will be returned. 103 OptionalSubjectsSelectors []SubjectsSelector 104 105 // OptionalCaveatName is the filter to use for caveated relationships, filtering by a specific caveat name. 106 // If nil, all caveated and non-caveated relationships are allowed 107 OptionalCaveatName string 108 } 109 110 // Test returns true iff the given relationship is matched by this filter. 111 func (rf RelationshipsFilter) Test(relationship *core.RelationTuple) bool { 112 if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.ResourceAndRelation.Namespace { 113 return false 114 } 115 116 if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.ResourceAndRelation.ObjectId) { 117 return false 118 } 119 120 if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.ResourceAndRelation.ObjectId, rf.OptionalResourceIDPrefix) { 121 return false 122 } 123 124 if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.ResourceAndRelation.Relation { 125 return false 126 } 127 128 if len(rf.OptionalSubjectsSelectors) > 0 { 129 for _, selector := range rf.OptionalSubjectsSelectors { 130 if selector.Test(relationship.Subject) { 131 return true 132 } 133 } 134 return false 135 } 136 137 if rf.OptionalCaveatName != "" { 138 if relationship.Caveat == nil || relationship.Caveat.CaveatName != rf.OptionalCaveatName { 139 return false 140 } 141 } 142 143 return true 144 } 145 146 // RelationshipsFilterFromPublicFilter constructs a datastore RelationshipsFilter from an API-defined RelationshipFilter. 147 func RelationshipsFilterFromPublicFilter(filter *v1.RelationshipFilter) (RelationshipsFilter, error) { 148 var resourceIds []string 149 if filter.OptionalResourceId != "" { 150 resourceIds = []string{filter.OptionalResourceId} 151 } 152 153 var subjectsSelectors []SubjectsSelector 154 if filter.OptionalSubjectFilter != nil { 155 var subjectIds []string 156 if filter.OptionalSubjectFilter.OptionalSubjectId != "" { 157 subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId} 158 } 159 160 relationFilter := SubjectRelationFilter{} 161 162 if filter.OptionalSubjectFilter.OptionalRelation != nil { 163 relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation() 164 if relation != "" { 165 relationFilter = relationFilter.WithNonEllipsisRelation(relation) 166 } else { 167 relationFilter = relationFilter.WithEllipsisRelation() 168 } 169 } 170 171 subjectsSelectors = append(subjectsSelectors, SubjectsSelector{ 172 OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType, 173 OptionalSubjectIds: subjectIds, 174 RelationFilter: relationFilter, 175 }) 176 } 177 178 if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" { 179 return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix") 180 } 181 182 if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 { 183 return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set") 184 } 185 186 return RelationshipsFilter{ 187 OptionalResourceType: filter.ResourceType, 188 OptionalResourceIds: resourceIds, 189 OptionalResourceIDPrefix: filter.OptionalResourceIdPrefix, 190 OptionalResourceRelation: filter.OptionalRelation, 191 OptionalSubjectsSelectors: subjectsSelectors, 192 }, nil 193 } 194 195 // SubjectsSelector is a selector for subjects. 196 type SubjectsSelector struct { 197 // OptionalSubjectType is the namespace/type for the subjects to be found, if any. 198 OptionalSubjectType string 199 200 // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed. 201 OptionalSubjectIds []string 202 203 // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field 204 // is set, any relation is allowed. 205 RelationFilter SubjectRelationFilter 206 } 207 208 // Test returns true iff the given subject is matched by this filter. 209 func (ss SubjectsSelector) Test(subject *core.ObjectAndRelation) bool { 210 if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.Namespace { 211 return false 212 } 213 214 if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectId) { 215 return false 216 } 217 218 if !ss.RelationFilter.IsEmpty() { 219 if ss.RelationFilter.IncludeEllipsisRelation && subject.Relation == tuple.Ellipsis { 220 return true 221 } 222 223 if ss.RelationFilter.NonEllipsisRelation != "" && ss.RelationFilter.NonEllipsisRelation != subject.Relation { 224 return false 225 } 226 227 if ss.RelationFilter.OnlyNonEllipsisRelations && subject.Relation == tuple.Ellipsis { 228 return false 229 } 230 } 231 232 return true 233 } 234 235 // SubjectRelationFilter is the filter to use for relation(s) of subjects being queried. 236 type SubjectRelationFilter struct { 237 // NonEllipsisRelation is the relation of the subject type to find. If empty, 238 // IncludeEllipsisRelation must be true. 239 NonEllipsisRelation string 240 241 // IncludeEllipsisRelation, if true, indicates that the ellipsis relation 242 // should be included as an option. 243 IncludeEllipsisRelation bool 244 245 // OnlyNonEllipsisRelations, if true, indicates that only non-ellipsis relations 246 // should be included. 247 OnlyNonEllipsisRelations bool 248 } 249 250 // WithOnlyNonEllipsisRelations indicates that only non-ellipsis relations should be included. 251 func (sf SubjectRelationFilter) WithOnlyNonEllipsisRelations() SubjectRelationFilter { 252 sf.OnlyNonEllipsisRelations = true 253 sf.NonEllipsisRelation = "" 254 sf.IncludeEllipsisRelation = false 255 return sf 256 } 257 258 // WithEllipsisRelation indicates that the subject filter should include the ellipsis relation 259 // as an option for the subjects' relation. 260 func (sf SubjectRelationFilter) WithEllipsisRelation() SubjectRelationFilter { 261 sf.IncludeEllipsisRelation = true 262 sf.OnlyNonEllipsisRelations = false 263 return sf 264 } 265 266 // WithNonEllipsisRelation indicates that the specified non-ellipsis relation should be included as an 267 // option for the subjects' relation. 268 func (sf SubjectRelationFilter) WithNonEllipsisRelation(relation string) SubjectRelationFilter { 269 sf.NonEllipsisRelation = relation 270 sf.OnlyNonEllipsisRelations = false 271 return sf 272 } 273 274 // WithRelation indicates that the specified relation should be included as an 275 // option for the subjects' relation. 276 func (sf SubjectRelationFilter) WithRelation(relation string) SubjectRelationFilter { 277 if relation == tuple.Ellipsis { 278 return sf.WithEllipsisRelation() 279 } 280 return sf.WithNonEllipsisRelation(relation) 281 } 282 283 // IsEmpty returns true if the subject relation filter is empty. 284 func (sf SubjectRelationFilter) IsEmpty() bool { 285 return !sf.IncludeEllipsisRelation && sf.NonEllipsisRelation == "" && !sf.OnlyNonEllipsisRelations 286 } 287 288 // SubjectsFilter is a filter for subjects. 289 type SubjectsFilter struct { 290 // SubjectType is the namespace/type for the subjects to be found. 291 SubjectType string 292 293 // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed. 294 OptionalSubjectIds []string 295 296 // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field 297 // is set, any relation is allowed. 298 RelationFilter SubjectRelationFilter 299 } 300 301 func (sf SubjectsFilter) AsSelector() SubjectsSelector { 302 return SubjectsSelector{ 303 OptionalSubjectType: sf.SubjectType, 304 OptionalSubjectIds: sf.OptionalSubjectIds, 305 RelationFilter: sf.RelationFilter, 306 } 307 } 308 309 // SchemaDefinition represents a namespace or caveat definition under a schema. 310 type SchemaDefinition interface { 311 GetName() string 312 } 313 314 // RevisionedDefinition holds a schema definition and its last updated revision. 315 type RevisionedDefinition[T SchemaDefinition] struct { 316 // Definition is the namespace or caveat definition. 317 Definition T 318 319 // LastWrittenRevision is the revision at which the namespace or caveat was last updated. 320 LastWrittenRevision Revision 321 } 322 323 func (rd RevisionedDefinition[T]) GetLastWrittenRevision() Revision { 324 return rd.LastWrittenRevision 325 } 326 327 // RevisionedNamespace is a revisioned version of a namespace definition. 328 type RevisionedNamespace = RevisionedDefinition[*core.NamespaceDefinition] 329 330 // Reader is an interface for reading relationships from the datastore. 331 type Reader interface { 332 CaveatReader 333 334 // QueryRelationships reads relationships, starting from the resource side. 335 QueryRelationships( 336 ctx context.Context, 337 filter RelationshipsFilter, 338 options ...options.QueryOptionsOption, 339 ) (RelationshipIterator, error) 340 341 // ReverseQueryRelationships reads relationships, starting from the subject. 342 ReverseQueryRelationships( 343 ctx context.Context, 344 subjectsFilter SubjectsFilter, 345 options ...options.ReverseQueryOptionsOption, 346 ) (RelationshipIterator, error) 347 348 // ReadNamespaceByName reads a namespace definition and the revision at which it was created or 349 // last written. It returns an instance of ErrNamespaceNotFound if not found. 350 ReadNamespaceByName(ctx context.Context, nsName string) (ns *core.NamespaceDefinition, lastWritten Revision, err error) 351 352 // ListAllNamespaces lists all namespaces defined. 353 ListAllNamespaces(ctx context.Context) ([]RevisionedNamespace, error) 354 355 // LookupNamespacesWithNames finds all namespaces with the matching names. 356 LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]RevisionedNamespace, error) 357 } 358 359 type ReadWriteTransaction interface { 360 Reader 361 CaveatStorer 362 363 // WriteRelationships takes a list of tuple mutations and applies them to the datastore. 364 WriteRelationships(ctx context.Context, mutations []*core.RelationTupleUpdate) error 365 366 // DeleteRelationships deletes relationships that match the provided filter, with 367 // the optional limit. If a limit is provided and reached, the method will return 368 // true as the first return value. Otherwise, the boolean can be ignored. 369 DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, 370 options ...options.DeleteOptionsOption, 371 ) (bool, error) 372 373 // WriteNamespaces takes proto namespace definitions and persists them. 374 WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error 375 376 // DeleteNamespaces deletes namespaces including associated relationships. 377 DeleteNamespaces(ctx context.Context, nsNames ...string) error 378 379 // BulkLoad takes a relationship source iterator, and writes all of the 380 // relationships to the backing datastore in an optimized fashion. This 381 // method can and will omit checks and otherwise cut corners in the 382 // interest of performance, and should not be relied upon for OLTP-style 383 // workloads. 384 BulkLoad(ctx context.Context, iter BulkWriteRelationshipSource) (uint64, error) 385 } 386 387 // TxUserFunc is a type for the function that users supply when they invoke a read-write transaction. 388 type TxUserFunc func(context.Context, ReadWriteTransaction) error 389 390 // ReadyState represents the ready state of the datastore. 391 type ReadyState struct { 392 // Message is a human-readable status message for the current state. 393 Message string 394 395 // IsReady indicates whether the datastore is ready. 396 IsReady bool 397 } 398 399 // BulkWriteRelationshipSource is an interface for transferring relationships 400 // to a backing datastore with a zero-copy methodology. 401 type BulkWriteRelationshipSource interface { 402 // Next Returns a pointer to a relation tuple if one is available, or nil if 403 // there are no more or there was an error. 404 // 405 // Note: sources may re-use the same memory address for every tuple, data 406 // may change on every call to next even if the pointer has not changed. 407 Next(ctx context.Context) (*core.RelationTuple, error) 408 } 409 410 type WatchContent int 411 412 const ( 413 WatchRelationships WatchContent = 1 << 0 414 WatchSchema WatchContent = 1 << 1 415 WatchCheckpoints WatchContent = 1 << 2 416 ) 417 418 // WatchOptions are options for a Watch call. 419 type WatchOptions struct { 420 // Content is the content to watch. 421 Content WatchContent 422 423 // CheckpointInterval is the interval to use for checkpointing in the watch. 424 // If given the zero value, the datastore's default will be used. If smaller 425 // than the datastore's minimum, the minimum will be used. 426 CheckpointInterval time.Duration 427 428 // WatchBufferLength is the length of the buffer for the watch channel. If 429 // given the zero value, the datastore's default will be used. 430 WatchBufferLength uint16 431 432 // WatchBufferWriteTimeout is the timeout for writing to the watch channel. 433 // If given the zero value, the datastore's default will be used. 434 WatchBufferWriteTimeout time.Duration 435 } 436 437 // WatchJustRelationships returns watch options for just relationships. 438 func WatchJustRelationships() WatchOptions { 439 return WatchOptions{ 440 Content: WatchRelationships, 441 } 442 } 443 444 // WatchJustSchema returns watch options for just schema. 445 func WatchJustSchema() WatchOptions { 446 return WatchOptions{ 447 Content: WatchSchema, 448 } 449 } 450 451 // WithCheckpointInterval sets the checkpoint interval on a watch options, returning 452 // an updated options struct. 453 func (wo WatchOptions) WithCheckpointInterval(interval time.Duration) WatchOptions { 454 return WatchOptions{ 455 Content: wo.Content, 456 CheckpointInterval: interval, 457 } 458 } 459 460 // Datastore represents tuple access for a single namespace. 461 type Datastore interface { 462 // SnapshotReader creates a read-only handle that reads the datastore at the specified revision. 463 // Any errors establishing the reader will be returned by subsequent calls. 464 SnapshotReader(Revision) Reader 465 466 // ReadWriteTx starts a read/write transaction, which will be committed if no error is 467 // returned and rolled back if an error is returned. 468 ReadWriteTx(context.Context, TxUserFunc, ...options.RWTOptionsOption) (Revision, error) 469 470 // OptimizedRevision gets a revision that will likely already be replicated 471 // and will likely be shared amongst many queries. 472 OptimizedRevision(ctx context.Context) (Revision, error) 473 474 // HeadRevision gets a revision that is guaranteed to be at least as fresh as 475 // right now. 476 HeadRevision(ctx context.Context) (Revision, error) 477 478 // CheckRevision checks the specified revision to make sure it's valid and 479 // hasn't been garbage collected. 480 CheckRevision(ctx context.Context, revision Revision) error 481 482 // RevisionFromString will parse the revision text and return the specific type of Revision 483 // used by the specific datastore implementation. 484 RevisionFromString(serialized string) (Revision, error) 485 486 // Watch notifies the caller about changes to the datastore, based on the specified options. 487 // 488 // All events following afterRevision will be sent to the caller. 489 Watch(ctx context.Context, afterRevision Revision, options WatchOptions) (<-chan *RevisionChanges, <-chan error) 490 491 // ReadyState returns a state indicating whether the datastore is ready to accept data. 492 // Datastores that require database schema creation will return not-ready until the migrations 493 // have been run to create the necessary tables. 494 ReadyState(ctx context.Context) (ReadyState, error) 495 496 // Features returns an object representing what features this 497 // datastore can support. 498 Features(ctx context.Context) (*Features, error) 499 500 // Statistics returns relevant values about the data contained in this cluster. 501 Statistics(ctx context.Context) (Stats, error) 502 503 // Close closes the data store. 504 Close() error 505 } 506 507 type strArray []string 508 509 // MarshalZerologArray implements zerolog array marshalling. 510 func (strs strArray) MarshalZerologArray(a *zerolog.Array) { 511 for _, val := range strs { 512 a.Str(val) 513 } 514 } 515 516 // StartableDatastore is an optional extension to the datastore interface that, when implemented, 517 // provides the ability for callers to start background operations on the datastore. 518 type StartableDatastore interface { 519 Datastore 520 521 // Start starts any background operations on the datastore. The context provided, if canceled, will 522 // also cancel the background operation(s) on the datastore. 523 Start(ctx context.Context) error 524 } 525 526 // RepairOperation represents a single kind of repair operation that can be run in a repairable 527 // datastore. 528 type RepairOperation struct { 529 // Name is the command-line name for the repair operation. 530 Name string 531 532 // Description is the human-readable description for the repair operation. 533 Description string 534 } 535 536 // RepairableDatastore is an optional extension to the datastore interface that, when implemented, 537 // provides the ability for callers to repair the datastore's data in some fashion. 538 type RepairableDatastore interface { 539 Datastore 540 541 // Repair runs the repair operation on the datastore. 542 Repair(ctx context.Context, operationName string, outputProgress bool) error 543 544 // RepairOperations returns the available repair operations for the datastore. 545 RepairOperations() []RepairOperation 546 } 547 548 // UnwrappableDatastore represents a datastore that can be unwrapped into the underlying 549 // datastore. 550 type UnwrappableDatastore interface { 551 // Unwrap returns the wrapped datastore. 552 Unwrap() Datastore 553 } 554 555 // UnwrapAs recursively attempts to unwrap the datastore into the specified type 556 // In none of the layers of the datastore implement the specified type, nil is returned. 557 func UnwrapAs[T any](datastore Datastore) T { 558 var ds T 559 uwds := datastore 560 561 for { 562 var ok bool 563 ds, ok = uwds.(T) 564 if ok { 565 break 566 } 567 568 wds, ok := uwds.(UnwrappableDatastore) 569 if !ok { 570 break 571 } 572 573 uwds = wds.Unwrap() 574 } 575 576 return ds 577 } 578 579 // Feature represents a capability that a datastore can support, plus an 580 // optional message explaining the feature is available (or not). 581 type Feature struct { 582 Enabled bool 583 Reason string 584 } 585 586 // Features holds values that represent what features a database can support. 587 type Features struct { 588 // Watch is enabled if the underlying datastore can support the Watch api. 589 Watch Feature 590 } 591 592 // ObjectTypeStat represents statistics for a single object type (namespace). 593 type ObjectTypeStat struct { 594 // NumRelations is the number of relations defined in a single object type. 595 NumRelations uint32 596 597 // NumPermissions is the number of permissions defined in a single object type. 598 NumPermissions uint32 599 } 600 601 // Stats represents statistics for the entire datastore. 602 type Stats struct { 603 // UniqueID is a unique string for a single datastore. 604 UniqueID string 605 606 // EstimatedRelationshipCount is a best-guess estimate of the number of relationships 607 // in the datastore. Computing it should use a lightweight method such as reading 608 // table statistics. 609 EstimatedRelationshipCount uint64 610 611 // ObjectTypeStatistics returns a slice element for each object type (namespace) 612 // stored in the datastore. 613 ObjectTypeStatistics []ObjectTypeStat 614 } 615 616 // RelationshipIterator is an iterator over matched tuples. 617 type RelationshipIterator interface { 618 // Next returns the next tuple in the result set. 619 Next() *core.RelationTuple 620 621 // Cursor returns a cursor that can be used to resume reading of relationships 622 // from the last relationship returned. Only applies if a sort ordering was 623 // requested. 624 Cursor() (options.Cursor, error) 625 626 // Err after receiving a nil response, the caller must check for an error. 627 Err() error 628 629 // Close cancels the query and closes any open connections. 630 Close() 631 } 632 633 // Revision is an interface for a comparable revision type that can be different for 634 // each datastore implementation. 635 type Revision interface { 636 fmt.Stringer 637 638 // Equal returns whether the revisions should be considered equal. 639 Equal(Revision) bool 640 641 // GreaterThan returns whether the receiver is probably greater than the right hand side. 642 GreaterThan(Revision) bool 643 644 // LessThan returns whether the receiver is probably less than the right hand side. 645 LessThan(Revision) bool 646 } 647 648 type nilRevision struct{} 649 650 func (nilRevision) Equal(rhs Revision) bool { 651 return rhs == NoRevision 652 } 653 654 func (nilRevision) GreaterThan(_ Revision) bool { 655 return false 656 } 657 658 func (nilRevision) LessThan(_ Revision) bool { 659 return true 660 } 661 662 func (nilRevision) String() string { 663 return "nil" 664 } 665 666 // NoRevision is a zero type for the revision that will make changing the 667 // revision type in the future a bit easier if necessary. Implementations 668 // should use any time they want to signal an empty/error revision. 669 var NoRevision Revision = nilRevision{}