github.com/matrixorigin/matrixone@v0.7.0/pkg/vm/engine/tae/db/base_test.go (about) 1 // Copyright 2022 Matrix Origin 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package db 16 17 import ( 18 "errors" 19 "sync" 20 "testing" 21 "time" 22 23 checkpoint2 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/db/checkpoint" 24 25 "github.com/matrixorigin/matrixone/pkg/container/types" 26 "github.com/matrixorigin/matrixone/pkg/logutil" 27 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/catalog" 28 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/containers" 29 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/iface/handle" 30 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/iface/txnif" 31 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/model" 32 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/options" 33 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/tables/jobs" 34 "github.com/matrixorigin/matrixone/pkg/vm/engine/tae/testutils" 35 "github.com/panjf2000/ants/v2" 36 "github.com/stretchr/testify/assert" 37 ) 38 39 const ( 40 ModuleName = "TAEDB" 41 defaultTestDB = "db" 42 ) 43 44 func init() { 45 // workaround sca check 46 _ = tryAppendClosure 47 _ = dropDB 48 } 49 50 type testEngine struct { 51 *DB 52 t *testing.T 53 schema *catalog.Schema 54 tenantID uint32 // for almost tests, userID and roleID is not important 55 } 56 57 func newTestEngine(t *testing.T, opts *options.Options) *testEngine { 58 db := initDB(t, opts) 59 return &testEngine{ 60 DB: db, 61 t: t, 62 } 63 } 64 65 func (e *testEngine) bindSchema(schema *catalog.Schema) { e.schema = schema } 66 67 func (e *testEngine) bindTenantID(tenantID uint32) { e.tenantID = tenantID } 68 69 func (e *testEngine) restart() { 70 _ = e.DB.Close() 71 var err error 72 e.DB, err = Open(e.Dir, e.Opts) 73 // only ut executes this checker 74 e.DB.DiskCleaner.AddChecker( 75 func(item any) bool { 76 min := e.DB.TxnMgr.MinTSForTest() 77 checkpoint := item.(*checkpoint2.CheckpointEntry) 78 //logutil.Infof("min: %v, checkpoint: %v", min.ToString(), checkpoint.GetStart().ToString()) 79 return !checkpoint.GetEnd().GreaterEq(min) 80 }) 81 assert.NoError(e.t, err) 82 } 83 84 func (e *testEngine) Close() error { 85 return e.DB.Close() 86 } 87 88 func (e *testEngine) createRelAndAppend(bat *containers.Batch, createDB bool) (handle.Database, handle.Relation) { 89 return createRelationAndAppend(e.t, e.tenantID, e.DB, defaultTestDB, e.schema, bat, createDB) 90 } 91 92 // func (e *testEngine) getRows() int { 93 // txn, rel := e.getRelation() 94 // rows := rel.Rows() 95 // assert.NoError(e.t, txn.Commit()) 96 // return int(rows) 97 // } 98 99 func (e *testEngine) checkRowsByScan(exp int, applyDelete bool) { 100 txn, rel := e.getRelation() 101 checkAllColRowsByScan(e.t, rel, exp, applyDelete) 102 assert.NoError(e.t, txn.Commit()) 103 } 104 func (e *testEngine) dropRelation(t *testing.T) { 105 txn, err := e.StartTxn(nil) 106 assert.NoError(t, err) 107 db, err := txn.GetDatabase(defaultTestDB) 108 assert.NoError(t, err) 109 _, err = db.DropRelationByName(e.schema.Name) 110 assert.NoError(t, err) 111 assert.NoError(t, txn.Commit()) 112 } 113 func (e *testEngine) getRelation() (txn txnif.AsyncTxn, rel handle.Relation) { 114 return getRelation(e.t, e.tenantID, e.DB, defaultTestDB, e.schema.Name) 115 } 116 func (e *testEngine) getRelationWithTxn(txn txnif.AsyncTxn) (rel handle.Relation) { 117 return getRelationWithTxn(e.t, txn, defaultTestDB, e.schema.Name) 118 } 119 120 func (e *testEngine) compactBlocks(skipConflict bool) { 121 compactBlocks(e.t, e.tenantID, e.DB, defaultTestDB, e.schema, skipConflict) 122 } 123 124 func (e *testEngine) mergeBlocks(skipConflict bool) { 125 mergeBlocks(e.t, e.tenantID, e.DB, defaultTestDB, e.schema, skipConflict) 126 } 127 128 func (e *testEngine) getDB(name string) (txn txnif.AsyncTxn, db handle.Database) { 129 txn, err := e.DB.StartTxn(nil) 130 txn.BindAccessInfo(e.tenantID, 0, 0) 131 assert.NoError(e.t, err) 132 db, err = txn.GetDatabase(name) 133 assert.NoError(e.t, err) 134 return 135 } 136 137 func (e *testEngine) getTestDB() (txn txnif.AsyncTxn, db handle.Database) { 138 return e.getDB(defaultTestDB) 139 } 140 141 func (e *testEngine) DoAppend(bat *containers.Batch) { 142 txn, rel := e.getRelation() 143 err := rel.Append(bat) 144 assert.NoError(e.t, err) 145 assert.NoError(e.t, txn.Commit()) 146 } 147 148 func (e *testEngine) doAppendWithTxn(bat *containers.Batch, txn txnif.AsyncTxn, skipConflict bool) (err error) { 149 rel := e.getRelationWithTxn(txn) 150 err = rel.Append(bat) 151 if !skipConflict { 152 assert.NoError(e.t, err) 153 } 154 return 155 } 156 157 func (e *testEngine) tryAppend(bat *containers.Batch) { 158 txn, err := e.DB.StartTxn(nil) 159 txn.BindAccessInfo(e.tenantID, 0, 0) 160 assert.NoError(e.t, err) 161 db, err := txn.GetDatabase(defaultTestDB) 162 assert.NoError(e.t, err) 163 rel, err := db.GetRelationByName(e.schema.Name) 164 if err != nil { 165 _ = txn.Rollback() 166 return 167 } 168 169 err = rel.Append(bat) 170 if err != nil { 171 _ = txn.Rollback() 172 return 173 } 174 _ = txn.Commit() 175 } 176 func (e *testEngine) deleteAll(skipConflict bool) error { 177 txn, rel := e.getRelation() 178 it := rel.MakeBlockIt() 179 for it.Valid() { 180 blk := it.GetBlock() 181 view, err := blk.GetColumnDataByName(catalog.PhyAddrColumnName, nil) 182 assert.NoError(e.t, err) 183 defer view.Close() 184 view.ApplyDeletes() 185 err = rel.DeleteByPhyAddrKeys(view.GetData()) 186 assert.NoError(e.t, err) 187 it.Next() 188 } 189 // checkAllColRowsByScan(e.t, rel, 0, true) 190 err := txn.Commit() 191 if !skipConflict { 192 checkAllColRowsByScan(e.t, rel, 0, true) 193 assert.NoError(e.t, err) 194 } 195 return err 196 } 197 198 func (e *testEngine) truncate() { 199 txn, db := e.getTestDB() 200 _, err := db.TruncateByName(e.schema.Name) 201 assert.NoError(e.t, err) 202 assert.NoError(e.t, txn.Commit()) 203 } 204 func (e *testEngine) globalCheckpoint( 205 endTs types.TS, 206 versionInterval time.Duration, 207 enableAndCleanBGCheckpoint bool, 208 ) error { 209 if enableAndCleanBGCheckpoint { 210 e.DB.BGCheckpointRunner.DisableCheckpoint() 211 defer e.DB.BGCheckpointRunner.EnableCheckpoint() 212 e.DB.BGCheckpointRunner.CleanPenddingCheckpoint() 213 } 214 if e.DB.BGCheckpointRunner.GetPenddingIncrementalCount() == 0 { 215 testutils.WaitExpect(4000, func() bool { 216 flushed := e.DB.BGCheckpointRunner.IsAllChangesFlushed(types.TS{}, endTs, false) 217 return flushed 218 }) 219 flushed := e.DB.BGCheckpointRunner.IsAllChangesFlushed(types.TS{}, endTs, true) 220 assert.True(e.t, flushed) 221 } 222 err := e.DB.BGCheckpointRunner.ForceGlobalCheckpoint(endTs, versionInterval) 223 assert.NoError(e.t, err) 224 return nil 225 } 226 227 func (e *testEngine) incrementalCheckpoint( 228 end types.TS, 229 enableAndCleanBGCheckpoint bool, 230 waitFlush bool, 231 truncate bool, 232 ) error { 233 if enableAndCleanBGCheckpoint { 234 e.DB.BGCheckpointRunner.DisableCheckpoint() 235 defer e.DB.BGCheckpointRunner.EnableCheckpoint() 236 e.DB.BGCheckpointRunner.CleanPenddingCheckpoint() 237 } 238 if waitFlush { 239 testutils.WaitExpect(4000, func() bool { 240 flushed := e.DB.BGCheckpointRunner.IsAllChangesFlushed(types.TS{}, end, false) 241 return flushed 242 }) 243 flushed := e.DB.BGCheckpointRunner.IsAllChangesFlushed(types.TS{}, end, true) 244 assert.True(e.t, flushed) 245 } 246 err := e.DB.BGCheckpointRunner.ForceIncrementalCheckpoint(end) 247 assert.NoError(e.t, err) 248 if truncate { 249 lsn := e.DB.BGCheckpointRunner.MaxLSNInRange(end) 250 entry, err := e.DB.Wal.RangeCheckpoint(1, lsn) 251 assert.NoError(e.t, err) 252 assert.NoError(e.t, entry.WaitDone()) 253 testutils.WaitExpect(1000, func() bool { 254 return e.Scheduler.GetPenddingLSNCnt() == 0 255 }) 256 } 257 return nil 258 } 259 func initDB(t *testing.T, opts *options.Options) *DB { 260 dir := testutils.InitTestEnv(ModuleName, t) 261 db, _ := Open(dir, opts) 262 // only ut executes this checker 263 db.DiskCleaner.AddChecker( 264 func(item any) bool { 265 min := db.TxnMgr.MinTSForTest() 266 checkpoint := item.(*checkpoint2.CheckpointEntry) 267 //logutil.Infof("min: %v, checkpoint: %v", min.ToString(), checkpoint.GetStart().ToString()) 268 return !checkpoint.GetEnd().GreaterEq(min) 269 }) 270 return db 271 } 272 273 func withTestAllPKType(t *testing.T, tae *DB, test func(*testing.T, *DB, *catalog.Schema)) { 274 var wg sync.WaitGroup 275 pool, _ := ants.NewPool(100) 276 defer pool.Release() 277 for i := 0; i < 17; i++ { 278 schema := catalog.MockSchemaAll(18, i) 279 schema.BlockMaxRows = 10 280 schema.SegmentMaxBlocks = 2 281 wg.Add(1) 282 _ = pool.Submit(func() { 283 defer wg.Done() 284 test(t, tae, schema) 285 }) 286 } 287 wg.Wait() 288 } 289 290 func lenOfBats(bats []*containers.Batch) int { 291 rows := 0 292 for _, bat := range bats { 293 rows += bat.Length() 294 } 295 return rows 296 } 297 298 func printCheckpointStats(t *testing.T, tae *DB) { 299 t.Logf("GetCheckpointedLSN: %d", tae.Wal.GetCheckpointed()) 300 t.Logf("GetPenddingLSNCnt: %d", tae.Wal.GetPenddingCnt()) 301 t.Logf("GetCurrSeqNum: %d", tae.Wal.GetCurrSeqNum()) 302 } 303 304 func createDB(t *testing.T, e *DB, dbName string) { 305 txn, err := e.StartTxn(nil) 306 assert.NoError(t, err) 307 _, err = txn.CreateDatabase(dbName, "") 308 assert.NoError(t, err) 309 assert.NoError(t, txn.Commit()) 310 } 311 312 func dropDB(t *testing.T, e *DB, dbName string) { 313 txn, err := e.StartTxn(nil) 314 assert.NoError(t, err) 315 _, err = txn.DropDatabase(dbName) 316 assert.NoError(t, err) 317 assert.NoError(t, txn.Commit()) 318 } 319 320 func dropRelation(t *testing.T, e *DB, dbName, name string) { 321 txn, err := e.StartTxn(nil) 322 assert.NoError(t, err) 323 db, err := txn.GetDatabase(dbName) 324 assert.NoError(t, err) 325 _, err = db.DropRelationByName(name) 326 assert.NoError(t, err) 327 assert.NoError(t, txn.Commit()) 328 } 329 330 func createRelation(t *testing.T, e *DB, dbName string, schema *catalog.Schema, createDB bool) (db handle.Database, rel handle.Relation) { 331 txn, db, rel := createRelationNoCommit(t, e, dbName, schema, createDB) 332 assert.NoError(t, txn.Commit()) 333 return 334 } 335 336 func createRelationNoCommit(t *testing.T, e *DB, dbName string, schema *catalog.Schema, createDB bool) (txn txnif.AsyncTxn, db handle.Database, rel handle.Relation) { 337 txn, err := e.StartTxn(nil) 338 assert.NoError(t, err) 339 if createDB { 340 db, err = txn.CreateDatabase(dbName, "") 341 assert.NoError(t, err) 342 } else { 343 db, err = txn.GetDatabase(dbName) 344 assert.NoError(t, err) 345 } 346 rel, err = db.CreateRelation(schema) 347 assert.NoError(t, err) 348 return 349 } 350 351 func createRelationAndAppend( 352 t *testing.T, 353 tenantID uint32, 354 e *DB, 355 dbName string, 356 schema *catalog.Schema, 357 bat *containers.Batch, 358 createDB bool) (db handle.Database, rel handle.Relation) { 359 txn, err := e.StartTxn(nil) 360 txn.BindAccessInfo(tenantID, 0, 0) 361 assert.NoError(t, err) 362 if createDB { 363 db, err = txn.CreateDatabase(dbName, "") 364 assert.NoError(t, err) 365 } else { 366 db, err = txn.GetDatabase(dbName) 367 assert.NoError(t, err) 368 } 369 rel, err = db.CreateRelation(schema) 370 assert.NoError(t, err) 371 err = rel.Append(bat) 372 assert.NoError(t, err) 373 assert.Nil(t, txn.Commit()) 374 return 375 } 376 377 func getRelation(t *testing.T, tenantID uint32, e *DB, dbName, tblName string) (txn txnif.AsyncTxn, rel handle.Relation) { 378 txn, err := e.StartTxn(nil) 379 txn.BindAccessInfo(tenantID, 0, 0) 380 assert.NoError(t, err) 381 db, err := txn.GetDatabase(dbName) 382 assert.NoError(t, err) 383 rel, err = db.GetRelationByName(tblName) 384 assert.NoError(t, err) 385 return 386 } 387 388 func getRelationWithTxn(t *testing.T, txn txnif.AsyncTxn, dbName, tblName string) (rel handle.Relation) { 389 db, err := txn.GetDatabase(dbName) 390 assert.NoError(t, err) 391 rel, err = db.GetRelationByName(tblName) 392 assert.NoError(t, err) 393 return 394 } 395 396 func getDefaultRelation(t *testing.T, e *DB, name string) (txn txnif.AsyncTxn, rel handle.Relation) { 397 return getRelation(t, 0, e, defaultTestDB, name) 398 } 399 400 func getOneBlock(rel handle.Relation) handle.Block { 401 it := rel.MakeBlockIt() 402 return it.GetBlock() 403 } 404 405 func getOneBlockMeta(rel handle.Relation) *catalog.BlockEntry { 406 it := rel.MakeBlockIt() 407 return it.GetBlock().GetMeta().(*catalog.BlockEntry) 408 } 409 410 func checkAllColRowsByScan(t *testing.T, rel handle.Relation, expectRows int, applyDelete bool) { 411 schema := rel.GetMeta().(*catalog.TableEntry).GetSchema() 412 for _, def := range schema.ColDefs { 413 rows := getColumnRowsByScan(t, rel, def.Idx, applyDelete) 414 assert.Equal(t, expectRows, rows) 415 } 416 } 417 418 func getColumnRowsByScan(t *testing.T, rel handle.Relation, colIdx int, applyDelete bool) int { 419 rows := 0 420 forEachColumnView(rel, colIdx, func(view *model.ColumnView) (err error) { 421 if applyDelete { 422 view.ApplyDeletes() 423 } 424 rows += view.Length() 425 // t.Log(view.String()) 426 return 427 }) 428 return rows 429 } 430 431 func forEachColumnView(rel handle.Relation, colIdx int, fn func(view *model.ColumnView) error) { 432 forEachBlock(rel, func(blk handle.Block) (err error) { 433 view, err := blk.GetColumnDataById(colIdx, nil) 434 if view == nil { 435 logutil.Warnf("blk %v", blk.String()) 436 return 437 } 438 if err != nil { 439 return 440 } 441 defer view.Close() 442 err = fn(view) 443 return 444 }) 445 } 446 447 func forEachBlock(rel handle.Relation, fn func(blk handle.Block) error) { 448 it := rel.MakeBlockIt() 449 var err error 450 for it.Valid() { 451 if err = fn(it.GetBlock()); err != nil { 452 if errors.Is(err, handle.ErrIteratorEnd) { 453 return 454 } else { 455 panic(err) 456 } 457 } 458 it.Next() 459 } 460 } 461 462 func forEachSegment(rel handle.Relation, fn func(seg handle.Segment) error) { 463 it := rel.MakeSegmentIt() 464 var err error 465 for it.Valid() { 466 if err = fn(it.GetSegment()); err != nil { 467 if errors.Is(err, handle.ErrIteratorEnd) { 468 return 469 } else { 470 panic(err) 471 } 472 } 473 it.Next() 474 } 475 } 476 477 func appendFailClosure(t *testing.T, data *containers.Batch, name string, e *DB, wg *sync.WaitGroup) func() { 478 return func() { 479 if wg != nil { 480 defer wg.Done() 481 } 482 txn, _ := e.StartTxn(nil) 483 database, _ := txn.GetDatabase("db") 484 rel, _ := database.GetRelationByName(name) 485 err := rel.Append(data) 486 assert.NotNil(t, err) 487 assert.Nil(t, txn.Rollback()) 488 } 489 } 490 491 func appendClosure(t *testing.T, data *containers.Batch, name string, e *DB, wg *sync.WaitGroup) func() { 492 return func() { 493 if wg != nil { 494 defer wg.Done() 495 } 496 txn, _ := e.StartTxn(nil) 497 database, _ := txn.GetDatabase("db") 498 rel, _ := database.GetRelationByName(name) 499 err := rel.Append(data) 500 assert.Nil(t, err) 501 assert.Nil(t, txn.Commit()) 502 } 503 } 504 505 func tryAppendClosure(t *testing.T, data *containers.Batch, name string, e *DB, wg *sync.WaitGroup) func() { 506 return func() { 507 if wg != nil { 508 defer wg.Done() 509 } 510 txn, _ := e.StartTxn(nil) 511 database, _ := txn.GetDatabase("db") 512 rel, err := database.GetRelationByName(name) 513 if err != nil { 514 _ = txn.Rollback() 515 return 516 } 517 if err = rel.Append(data); err != nil { 518 _ = txn.Rollback() 519 return 520 } 521 _ = txn.Commit() 522 } 523 } 524 525 func compactBlocks(t *testing.T, tenantID uint32, e *DB, dbName string, schema *catalog.Schema, skipConflict bool) { 526 txn, rel := getRelation(t, tenantID, e, dbName, schema.Name) 527 528 var metas []*catalog.BlockEntry 529 it := rel.MakeBlockIt() 530 for it.Valid() { 531 blk := it.GetBlock() 532 meta := blk.GetMeta().(*catalog.BlockEntry) 533 if blk.Rows() < int(schema.BlockMaxRows) { 534 it.Next() 535 continue 536 } 537 metas = append(metas, meta) 538 it.Next() 539 } 540 _ = txn.Commit() 541 for _, meta := range metas { 542 txn, _ := getRelation(t, tenantID, e, dbName, schema.Name) 543 task, err := jobs.NewCompactBlockTask(nil, txn, meta, e.Scheduler) 544 if skipConflict && err != nil { 545 _ = txn.Rollback() 546 continue 547 } 548 assert.NoError(t, err) 549 err = task.OnExec() 550 if skipConflict { 551 if err != nil { 552 _ = txn.Rollback() 553 } else { 554 _ = txn.Commit() 555 } 556 } else { 557 assert.NoError(t, err) 558 assert.NoError(t, txn.Commit()) 559 } 560 } 561 } 562 563 func mergeBlocks(t *testing.T, tenantID uint32, e *DB, dbName string, schema *catalog.Schema, skipConflict bool) { 564 txn, _ := e.StartTxn(nil) 565 txn.BindAccessInfo(tenantID, 0, 0) 566 db, _ := txn.GetDatabase(dbName) 567 rel, _ := db.GetRelationByName(schema.Name) 568 569 var segs []*catalog.SegmentEntry 570 segIt := rel.MakeSegmentIt() 571 for segIt.Valid() { 572 seg := segIt.GetSegment().GetMeta().(*catalog.SegmentEntry) 573 if seg.GetAppendableBlockCnt() == int(seg.GetTable().GetSchema().SegmentMaxBlocks) { 574 segs = append(segs, seg) 575 } 576 segIt.Next() 577 } 578 _ = txn.Commit() 579 for _, seg := range segs { 580 txn, _ = e.StartTxn(nil) 581 txn.BindAccessInfo(tenantID, 0, 0) 582 db, _ = txn.GetDatabase(dbName) 583 rel, _ = db.GetRelationByName(schema.Name) 584 segHandle, err := rel.GetSegment(seg.ID) 585 if err != nil { 586 if skipConflict { 587 _ = txn.Rollback() 588 continue 589 } 590 assert.NoErrorf(t, err, "Txn Ts=%d", txn.GetStartTS()) 591 } 592 var metas []*catalog.BlockEntry 593 it := segHandle.MakeBlockIt() 594 for it.Valid() { 595 meta := it.GetBlock().GetMeta().(*catalog.BlockEntry) 596 metas = append(metas, meta) 597 it.Next() 598 } 599 segsToMerge := []*catalog.SegmentEntry{segHandle.GetMeta().(*catalog.SegmentEntry)} 600 task, err := jobs.NewMergeBlocksTask(nil, txn, metas, segsToMerge, nil, e.Scheduler) 601 if skipConflict && err != nil { 602 _ = txn.Rollback() 603 continue 604 } 605 assert.NoError(t, err) 606 err = task.OnExec() 607 if skipConflict { 608 if err != nil { 609 _ = txn.Rollback() 610 } else { 611 _ = txn.Commit() 612 } 613 } else { 614 assert.NoError(t, err) 615 assert.NoError(t, txn.Commit()) 616 } 617 } 618 } 619 620 /*func compactSegs(t *testing.T, e *DB, schema *catalog.Schema) { 621 txn, rel := getDefaultRelation(t, e, schema.Name) 622 segs := make([]*catalog.SegmentEntry, 0) 623 it := rel.MakeSegmentIt() 624 for it.Valid() { 625 seg := it.GetSegment().GetMeta().(*catalog.SegmentEntry) 626 segs = append(segs, seg) 627 it.Next() 628 } 629 for _, segMeta := range segs { 630 seg := segMeta.GetSegmentData() 631 factory, taskType, scopes, err := seg.BuildCompactionTaskFactory() 632 assert.NoError(t, err) 633 if factory == nil { 634 continue 635 } 636 task, err := e.Scheduler.ScheduleMultiScopedTxnTask(tasks.WaitableCtx, taskType, scopes, factory) 637 assert.NoError(t, err) 638 err = task.WaitDone() 639 assert.NoError(t, err) 640 } 641 assert.NoError(t, txn.Commit()) 642 }*/ 643 644 func getSingleSortKeyValue(bat *containers.Batch, schema *catalog.Schema, row int) (v any) { 645 v = bat.Vecs[schema.GetSingleSortKeyIdx()].Get(row) 646 return 647 }