satellite/metabase: add tests ListObjects

Change-Id: Ia84b560251245fe0050426537751958f2c85e9e1
This commit is contained in:
dlamarmorgan 2023-10-24 14:00:40 -07:00 committed by Storj Robot
parent 513c3cc632
commit 77cac8b465
2 changed files with 778 additions and 2 deletions

View File

@ -886,6 +886,198 @@ func BenchmarkNonRecursiveObjectsListing(b *testing.B) {
func TestListObjectsVersioned(t *testing.T) {
metabasetest.Run(t, func(ctx *testcontext.Context, t *testing.T, db *metabase.DB) {
t.Run("2 objects, one with versions one without", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
a0 := metabasetest.RandObjectStream()
b0 := metabasetest.RandObjectStream()
b0.ProjectID = a0.ProjectID
b0.BucketName = a0.BucketName
b0.Version = 1000
if a0.ObjectKey > b0.ObjectKey {
b0.ObjectKey, a0.ObjectKey = a0.ObjectKey, b0.ObjectKey
}
b1 := b0
b1.Version = 500
objA0 := metabasetest.CreateObject(ctx, t, db, a0, 0)
objB0 := metabasetest.CreateObjectVersioned(ctx, t, db, b0, 0)
objB1 := metabasetest.CreateObjectVersionedOutOfOrder(ctx, t, db, b1, 0, 1001)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: a0.ProjectID,
BucketName: a0.BucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objectEntryFromRaw(metabase.RawObject(objA0)),
objectEntryFromRaw(metabase.RawObject(objB1)),
},
}}.Check(ctx, t, db)
metabasetest.Verify{
Objects: []metabase.RawObject{
metabase.RawObject(objA0),
metabase.RawObject(objB0),
metabase.RawObject(objB1),
},
}.Check(ctx, t, db)
})
t.Run("3 objects, one with versions one without and one pending", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
a0 := metabasetest.RandObjectStream()
b0 := metabasetest.RandObjectStream()
c0 := metabasetest.RandObjectStream()
b0.ProjectID = a0.ProjectID
b0.BucketName = a0.BucketName
b0.Version = 1000
c0.ProjectID = a0.ProjectID
c0.BucketName = a0.BucketName
c0.Version = 1000
if a0.ObjectKey > b0.ObjectKey {
b0.ObjectKey, a0.ObjectKey = a0.ObjectKey, b0.ObjectKey
}
b1 := b0
b1.Version = 500
objA0 := metabasetest.CreateObject(ctx, t, db, a0, 0)
objB0 := metabasetest.CreateObjectVersioned(ctx, t, db, b0, 0)
objB1 := metabasetest.CreateObjectVersionedOutOfOrder(ctx, t, db, b1, 0, 1001)
metabasetest.CreatePendingObject(ctx, t, db, c0, 0)
now := time.Now()
zombieDeadline := now.Add(24 * time.Hour)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: a0.ProjectID,
BucketName: a0.BucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objectEntryFromRaw(metabase.RawObject(objA0)),
objectEntryFromRaw(metabase.RawObject(objB1)),
},
}}.Check(ctx, t, db)
metabasetest.Verify{
Objects: []metabase.RawObject{
metabase.RawObject(objA0),
metabase.RawObject(objB0),
metabase.RawObject(objB1),
{
ObjectStream: metabase.ObjectStream{
ProjectID: c0.ProjectID,
BucketName: c0.BucketName,
ObjectKey: c0.ObjectKey,
Version: 1000,
StreamID: c0.StreamID,
},
CreatedAt: now,
Status: metabase.Pending,
Encryption: metabasetest.DefaultEncryption,
ZombieDeletionDeadline: &zombieDeadline,
},
},
}.Check(ctx, t, db)
})
t.Run("2 objects one with versions and one pending, list pending", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
a0 := metabasetest.RandObjectStream()
a0.Version = 1000
b0 := metabasetest.RandObjectStream()
b0.ProjectID = a0.ProjectID
b0.BucketName = a0.BucketName
b0.Version = 1000
if a0.ObjectKey > b0.ObjectKey {
b0.ObjectKey, a0.ObjectKey = a0.ObjectKey, b0.ObjectKey
}
a1 := a0
a1.Version = 1001
metabasetest.BeginObjectExactVersion{
Opts: metabase.BeginObjectExactVersion{
ObjectStream: metabase.ObjectStream{
ProjectID: b0.ProjectID,
BucketName: b0.BucketName,
ObjectKey: b0.ObjectKey,
Version: b0.Version,
StreamID: b0.StreamID,
},
Encryption: metabasetest.DefaultEncryption,
},
}.Check(ctx, t, db)
now := time.Now()
zombieDeadline := now.Add(24 * time.Hour)
objA0 := metabasetest.CreateObjectVersioned(ctx, t, db, a0, 0)
objA1 := metabasetest.CreateObjectVersioned(ctx, t, db, a1, 0)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: a0.ProjectID,
BucketName: a0.BucketName,
Recursive: true,
Pending: true,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
{
ObjectKey: b0.ObjectKey,
Version: 1000,
StreamID: b0.StreamID,
CreatedAt: now,
Status: metabase.Pending,
Encryption: metabasetest.DefaultEncryption,
},
},
}}.Check(ctx, t, db)
metabasetest.Verify{
Objects: []metabase.RawObject{
metabase.RawObject(objA0),
metabase.RawObject(objA1),
{
ObjectStream: metabase.ObjectStream{
ProjectID: b0.ProjectID,
BucketName: b0.BucketName,
ObjectKey: b0.ObjectKey,
Version: 1000,
StreamID: b0.StreamID,
},
CreatedAt: now,
Status: metabase.Pending,
Encryption: metabasetest.DefaultEncryption,
ZombieDeletionDeadline: &zombieDeadline,
},
},
}.Check(ctx, t, db)
})
t.Run("2 objects, each with 2 versions", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
@ -1007,7 +1199,563 @@ func TestListObjectsVersioned(t *testing.T) {
}.Check(ctx, t, db)
})
// TODO(ver): more exhaustive tests (committed/deletemarker, unversioned/versioned)
// TODO(ver): test with non-recursive listing
t.Run("2 objects, each with two versions and multiple delete_markers", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
a0 := metabasetest.RandObjectStream()
a0.Version = 1000
b0 := metabasetest.RandObjectStream()
b0.ProjectID = a0.ProjectID
b0.BucketName = a0.BucketName
b0.Version = 1000
if a0.ObjectKey > b0.ObjectKey {
b0.ObjectKey, a0.ObjectKey = a0.ObjectKey, b0.ObjectKey
}
objA0 := metabasetest.CreateObjectVersioned(ctx, t, db, a0, 0)
objB0 := metabasetest.CreateObjectVersioned(ctx, t, db, b0, 0)
deletionResultA0 := metabasetest.DeleteObjectLastCommitted{
Opts: metabase.DeleteObjectLastCommitted{
ObjectLocation: objA0.Location(),
Versioned: true,
},
Result: metabase.DeleteObjectResult{
Markers: []metabase.Object{
{
ObjectStream: metabase.ObjectStream{
ProjectID: objA0.ProjectID,
BucketName: objA0.BucketName,
ObjectKey: objA0.ObjectKey,
Version: 1001,
},
Status: metabase.DeleteMarkerVersioned,
CreatedAt: time.Now(),
},
},
},
}.Check(ctx, t, db)
deletionResultB0 := metabasetest.DeleteObjectLastCommitted{
Opts: metabase.DeleteObjectLastCommitted{
ObjectLocation: objB0.Location(),
Versioned: true,
},
Result: metabase.DeleteObjectResult{
Markers: []metabase.Object{
{
ObjectStream: metabase.ObjectStream{
ProjectID: objB0.ProjectID,
BucketName: objB0.BucketName,
ObjectKey: objB0.ObjectKey,
Version: 1001,
},
Status: metabase.DeleteMarkerVersioned,
CreatedAt: time.Now(),
},
},
},
}.Check(ctx, t, db)
a1 := a0
a1.Version = 1002
b1 := b0
b1.Version = 1002
objA1 := metabasetest.CreateObjectVersioned(ctx, t, db, a1, 0)
objB1 := metabasetest.CreateObjectVersioned(ctx, t, db, b1, 0)
deletionResultA1 := metabasetest.DeleteObjectLastCommitted{
Opts: metabase.DeleteObjectLastCommitted{
ObjectLocation: objA1.Location(),
Versioned: true,
},
Result: metabase.DeleteObjectResult{
Markers: []metabase.Object{
{
ObjectStream: metabase.ObjectStream{
ProjectID: objA1.ProjectID,
BucketName: objA1.BucketName,
ObjectKey: objA1.ObjectKey,
Version: 1003,
},
Status: metabase.DeleteMarkerVersioned,
CreatedAt: time.Now(),
},
},
},
}.Check(ctx, t, db)
deletionResultB1 := metabasetest.DeleteObjectLastCommitted{
Opts: metabase.DeleteObjectLastCommitted{
ObjectLocation: objB1.Location(),
Versioned: true,
},
Result: metabase.DeleteObjectResult{
Markers: []metabase.Object{
{
ObjectStream: metabase.ObjectStream{
ProjectID: objB1.ProjectID,
BucketName: objB1.BucketName,
ObjectKey: objB1.ObjectKey,
Version: 1003,
},
Status: metabase.DeleteMarkerVersioned,
CreatedAt: time.Now(),
},
},
},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: a0.ProjectID,
BucketName: a0.BucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{},
}}.Check(ctx, t, db)
metabasetest.Verify{
Objects: []metabase.RawObject{
metabase.RawObject(deletionResultA1.Markers[0]),
metabase.RawObject(deletionResultB1.Markers[0]),
metabase.RawObject(objA1),
metabase.RawObject(objB1),
metabase.RawObject(deletionResultA0.Markers[0]),
metabase.RawObject(deletionResultB0.Markers[0]),
metabase.RawObject(objA0),
metabase.RawObject(objB0),
},
}.Check(ctx, t, db)
})
t.Run("3 objects, 1 unversioned, 2 with multiple versions, 1 with and 1 without delete_marker", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
a0 := metabasetest.RandObjectStream()
b0 := metabasetest.RandObjectStream()
b0.ProjectID = a0.ProjectID
b0.BucketName = a0.BucketName
b0.Version = 1000
c0 := metabasetest.RandObjectStream()
c0.ProjectID = a0.ProjectID
c0.BucketName = a0.BucketName
c0.Version = 1000
if a0.ObjectKey > b0.ObjectKey {
a0.ObjectKey, b0.ObjectKey = b0.ObjectKey, a0.ObjectKey
}
if a0.ObjectKey > c0.ObjectKey {
a0.ObjectKey, c0.ObjectKey = c0.ObjectKey, a0.ObjectKey
}
if b0.ObjectKey > c0.ObjectKey {
b0.ObjectKey, c0.ObjectKey = c0.ObjectKey, b0.ObjectKey
}
objA0 := metabasetest.CreateObject(ctx, t, db, a0, 0)
objB0 := metabasetest.CreateObjectVersioned(ctx, t, db, b0, 0)
objC0 := metabasetest.CreateObjectVersioned(ctx, t, db, c0, 0)
deletionResultC0 := metabasetest.DeleteObjectLastCommitted{
Opts: metabase.DeleteObjectLastCommitted{
ObjectLocation: objC0.Location(),
Versioned: true,
},
Result: metabase.DeleteObjectResult{
Markers: []metabase.Object{
{
ObjectStream: metabase.ObjectStream{
ProjectID: objC0.ProjectID,
BucketName: objC0.BucketName,
ObjectKey: objC0.ObjectKey,
Version: 1001,
},
Status: metabase.DeleteMarkerVersioned,
CreatedAt: time.Now(),
},
},
},
}.Check(ctx, t, db)
b1 := b0
b1.Version = 1001
c1 := c0
c1.Version = 1002
objB1 := metabasetest.CreateObjectVersioned(ctx, t, db, b1, 0)
objC1 := metabasetest.CreateObjectVersioned(ctx, t, db, c1, 0)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: a0.ProjectID,
BucketName: a0.BucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objectEntryFromRaw(metabase.RawObject(objA0)),
objectEntryFromRaw(metabase.RawObject(objB1)),
objectEntryFromRaw(metabase.RawObject(objC1)),
},
}}.Check(ctx, t, db)
metabasetest.Verify{
Objects: []metabase.RawObject{
metabase.RawObject(objC1),
metabase.RawObject(objB1),
metabase.RawObject(deletionResultC0.Markers[0]),
metabase.RawObject(objC0),
metabase.RawObject(objB0),
metabase.RawObject(objA0),
},
}.Check(ctx, t, db)
})
t.Run("list recursive objects with versions", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
projectID, bucketName := uuid.UUID{1}, "bucky"
objects := metabasetest.CreateVersionedObjectsWithKeys(ctx, t, db, projectID, bucketName, map[metabase.ObjectKey][]metabase.Version{
"a": {1000, 1001},
"b/1": {1000, 1001},
"b/2": {1000, 1001},
"b/3": {1000, 1001},
"c": {1000, 1001},
"c/": {1000, 1001},
"c//": {1000, 1001},
"c/1": {1000, 1001},
"g": {1000, 1001},
})
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objects["a"],
objects["b/1"],
objects["b/2"],
objects["b/3"],
objects["c"],
objects["c/"],
objects["c//"],
objects["c/1"],
objects["g"],
},
}}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Cursor: metabase.ListObjectsCursor{Key: "a", Version: objects["a"].Version + 1},
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objects["b/1"],
objects["b/2"],
objects["b/3"],
objects["c"],
objects["c/"],
objects["c//"],
objects["c/1"],
objects["g"],
}},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Cursor: metabase.ListObjectsCursor{Key: "b", Version: 0},
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objects["b/1"],
objects["b/2"],
objects["b/3"],
objects["c"],
objects["c/"],
objects["c//"],
objects["c/1"],
objects["g"],
},
}}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/1"],
objects["b/2"],
objects["b/3"],
),
}}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "a"},
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/1"],
objects["b/2"],
objects["b/3"],
),
}}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "b/2", Version: -3},
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/2"],
objects["b/3"],
),
}}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Recursive: true,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "c/"},
},
Result: metabase.ListObjectsResult{},
}.Check(ctx, t, db)
})
t.Run("list non-recursive objects with versions", func(t *testing.T) {
defer metabasetest.DeleteAll{}.Check(ctx, t, db)
projectID, bucketName := uuid.UUID{1}, "bucky"
objects := metabasetest.CreateVersionedObjectsWithKeys(ctx, t, db, projectID, bucketName, map[metabase.ObjectKey][]metabase.Version{
"a": {1000, 1001},
"b/1": {1000, 1001},
"b/2": {1000, 1001},
"b/3": {1000, 1001},
"c": {1000, 1001},
"c/": {1000, 1001},
"c//": {1000, 1001},
"c/1": {1000, 1001},
"g": {1000, 1001},
})
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
objects["a"],
prefixEntry("b/"),
objects["c"],
prefixEntry("c/"),
objects["g"],
}},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Cursor: metabase.ListObjectsCursor{Key: "a", Version: objects["a"].Version + 1},
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
prefixEntry("b/"),
objects["c"],
prefixEntry("c/"),
objects["g"],
}},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Cursor: metabase.ListObjectsCursor{Key: "b", Version: 0},
},
Result: metabase.ListObjectsResult{
Objects: []metabase.ObjectEntry{
prefixEntry("b/"),
objects["c"],
prefixEntry("c/"),
objects["g"],
}},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/1"],
objects["b/2"],
objects["b/3"],
)},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "a"},
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/1"],
objects["b/2"],
objects["b/3"],
)},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "b/2", Version: -3},
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("b/",
objects["b/2"],
objects["b/3"],
)},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "b/",
Cursor: metabase.ListObjectsCursor{Key: "c/"},
},
Result: metabase.ListObjectsResult{},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "c/",
Cursor: metabase.ListObjectsCursor{Key: "c/"},
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("c/",
objects["c/"],
prefixEntry("c//"),
objects["c/1"],
)},
}.Check(ctx, t, db)
metabasetest.ListObjects{
Opts: metabase.ListObjects{
ProjectID: projectID,
BucketName: bucketName,
Pending: false,
IncludeCustomMetadata: true,
IncludeSystemMetadata: true,
Prefix: "c//",
},
Result: metabase.ListObjectsResult{
Objects: withoutPrefix("c//",
objects["c//"],
)},
}.Check(ctx, t, db)
})
})
}

View File

@ -216,6 +216,34 @@ func CreateSegments(ctx *testcontext.Context, t testing.TB, db *metabase.DB, obj
}
}
// CreateVersionedObjectsWithKeys creates multiple versioned objects with the specified keys and versions.
func CreateVersionedObjectsWithKeys(ctx *testcontext.Context, t *testing.T, db *metabase.DB, projectID uuid.UUID, bucketName string, keys map[metabase.ObjectKey][]metabase.Version) map[metabase.ObjectKey]metabase.ObjectEntry {
objects := make(map[metabase.ObjectKey]metabase.ObjectEntry, len(keys))
for key, versions := range keys {
for _, version := range versions {
obj := RandObjectStream()
obj.ProjectID = projectID
obj.BucketName = bucketName
obj.ObjectKey = key
obj.Version = version
now := time.Now()
CreateObjectVersioned(ctx, t, db, obj, 0)
objects[key] = metabase.ObjectEntry{
ObjectKey: obj.ObjectKey,
Version: obj.Version,
StreamID: obj.StreamID,
CreatedAt: now,
Status: metabase.CommittedVersioned,
Encryption: DefaultEncryption,
}
}
}
return objects
}
// CreateTestObject is for testing metabase.CreateTestObject.
type CreateTestObject struct {
BeginObjectExactVersion *metabase.BeginObjectExactVersion