Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC] Migrating data with foreign key constraints #288

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 21 additions & 3 deletions copydb/copydb.go
Expand Up @@ -58,15 +58,33 @@ func (this *CopydbFerry) Start() error {
}

func (this *CopydbFerry) CreateDatabasesAndTables() error {
logger := logrus.WithField("tag", "create_databases_and_tables")

// We need to create the same table/schemas on the target database
// as the ones we are copying.
logrus.Info("creating databases and tables on target")
for _, tableName := range this.Ferry.Tables.GetTableListWithPriority(this.config.TablesToBeCreatedFirst) {
logger.Info("creating databases and tables on target")
var prioritzedTableNames []string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var prioritzedTableNames []string
var prioritizedTableNames []string

if len(this.config.TablesToBeCreatedFirst) > 0 {
// if specified, use what the config tells us
logger.Debug("config contains table creation order")
prioritzedTableNames = this.Ferry.Tables.GetTableListWithPriority(this.config.TablesToBeCreatedFirst)
} else {
// otherwise infer the right order ourselves
logger.Debug("inferring table creation order from source database")
var err error
prioritzedTableNames, err = this.Ferry.Tables.GetTableCreationOrder(this.Ferry.SourceDB)
if err != nil {
return err
}
}

for _, tableName := range prioritzedTableNames {
logger.Debugf("creating database table %s", tableName)
t := strings.Split(tableName, ".")

err := this.createDatabaseIfExistsOnTarget(t[0])
if err != nil {
logrus.WithError(err).WithField("database", t[0]).Error("cannot create database, this may leave the target database in an insane state")
logger.WithField("database", t[0]).Error("cannot create database, this may leave the target database in an insane state")
return err
}

Expand Down
39 changes: 39 additions & 0 deletions docs/source/copydbforeignkey.rst
@@ -0,0 +1,39 @@
.. _copydbforeignkey:

===================================================================================
Running ``ghostferry-copydb`` in production for tables with Foreign Key Constraints
===================================================================================

Migrating tables with foreign keys constraints is an experimental feature in copydb and should be used at your own risk in production.


Prerequisites
-------------

Before migrating tables with foreign key constraints via copydb there are a couple of things to take care of

- Ghostferry needs to be ran with ``SkipForeignKeyConstraintsCheck = true``, which will disable ghostferry to check foreign key
constraints during initialization.

- Source DB should be ``read_only``.

- Need to disable foreign key constraint checks on target DB by passing the following config to target db

.. code-block:: json

"Params": {
"foreign_key_checks": "0"
}

- Even though foreign key constraint checks are disabled on target db, table and db creation must happen in a specific order (eg parent should be created
before child table). This creation order can be specified by passing ``TablesToBeCreatedFirst`` in the config, or else the table creation order will be
figured out by copydb.

Limitations
-------------

- Currently migrating tables with foreign key constraints is only possible if the source database is in read_only mode. Since tables with foreign key constraints can have referential actions for a foreign key such as ``ON DELETE CASCADE``, ``ON UPDATE CASCADE``. Cascading deletes and updates in child tablees caused by foreign key constraints don't show up in binlogs because these referential actions are dealt internally by InnoDB. This issue descibes briefly why the source database should be read_only during the migration - https://github.com/Shopify/ghostferry/issues/289.

- ``Interrupt-Resume`` functionality can be used as long as source database is read_only also during the interrupt period

- ``Inline Verifier`` can be used as long as it is ensured that the source database is read_only (even during the interrupt period)
3 changes: 2 additions & 1 deletion docs/source/copydbinprod.rst
Expand Up @@ -35,7 +35,8 @@ to consider about this are:

- There are no foreign key constraints in your tables.

- You should remove these constraints before running Ghostferry.
- You should remove these constraints before running Ghostferry or run Ghostferry with SkipForeignKeyConstraintsCheck = true and ensure
source database is read_only.

- ``ghostferry-copydb`` can only copy a whole table at a time.

Expand Down
14 changes: 13 additions & 1 deletion ferry.go
Expand Up @@ -424,7 +424,7 @@ func (f *Ferry) Initialize() (err error) {
f.logger.WithError(err).Error("source master is a read replica")
return err
}
} else {
} else if !f.Config.SkipForeignKeyConstraintsCheck {
isReplica, err := CheckDbIsAReplica(f.SourceDB)
if err != nil {
f.logger.WithError(err).Error("cannot check if source is a replica")
Expand Down Expand Up @@ -484,6 +484,18 @@ func (f *Ferry) Initialize() (err error) {
}
}

if f.Config.SkipForeignKeyConstraintsCheck {
isReadOnly, err := CheckDbIsAReplica(f.SourceDB)
if err != nil {
return err
}
if !isReadOnly {
err = errors.New("Source DB must be read_only")
f.logger.WithError(err).Error("Source DB should be read_only to migrate tables with foreign key constraints")
return err
}
}

if f.Config.DataIterationBatchSizePerTableOverride != nil {
err = f.Config.DataIterationBatchSizePerTableOverride.UpdateBatchSizes(f.SourceDB, f.Tables)
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Expand Up @@ -16,17 +16,21 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1 h1:KOwqsTYZdeuMacU7CxjMNYEKeBvLbxW+psodrbcEa3A=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
Expand All @@ -51,12 +55,15 @@ github.com/pingcap/errors v0.11.0 h1:DCJQB8jrHbQ1VVlMFIrbj2ApScNNotVmkSNplu2yUt4
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9 h1:AJD9pZYm72vMgPcQDww9rkZ1DnWfl0pXV3BOWlkYIjA=
github.com/pingcap/log v0.0.0-20191012051959-b742a5d432e9/go.mod h1:4rbK1p9ILyIfb6hU7OG2CiWSqMXnp3JMbiaVJ6mvoY8=
github.com/pingcap/parser v0.0.0-20190506092653-e336082eb825 h1:U9Kdnknj4n2v76Mg7wazevZ5N9U1OIaMwSNRVLEcLX0=
github.com/pingcap/parser v0.0.0-20190506092653-e336082eb825/go.mod h1:1FNvfp9+J0wvc4kl8eGNh7Rqrxveg15jJoWo/a0uHwA=
github.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330 h1:rRMLMjIMFulCX9sGKZ1hoov/iROMsKyC8Snc02nSukw=
github.com/pingcap/tipb v0.0.0-20190428032612-535e1abaa330/go.mod h1:RtkHW8WbcNxj8lsbzjaILci01CtYnYbIkQhjyZWrWVI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
Expand All @@ -73,6 +80,7 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand All @@ -95,12 +103,14 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e h1:JgcxKXxCjrA2tyDP/aNU9K0Ck5Czfk6C7e2tMw7+bSI=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -129,6 +139,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
Expand Down
69 changes: 69 additions & 0 deletions table_schema_cache.go
Expand Up @@ -342,6 +342,75 @@ func (c TableSchemaCache) GetTableListWithPriority(priorityList []string) (prior
return
}

func (c TableSchemaCache) GetTableCreationOrder(db *sql.DB) ([]string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change the name so it doesn't seem like a generic order that people should follow?

Suggested change
func (c TableSchemaCache) GetTableCreationOrder(db *sql.DB) ([]string, error) {
func (c TableSchemaCache) GetTableCreationOrderForFKConstraints(db *sql.DB) ([]string, error) {

visitedTables := map[string]bool{}
tableMap := map[string][]string{}
tableOrder := []string{}

for table := range c {
t := strings.Split(table, ".")
referencedTables, err := getForeignKeyTablesOfTable(db, t[0], t[1])
if err != nil {
logrus.WithField("table", table).Error("cannot fetch foreign keys")
}
tableMap[table] = referencedTables
}

for table := range c {
if _, found := visitedTables[table]; !found {
visitedTables, tableOrder = getTableCreationOrderUtil(visitedTables, tableOrder, tableMap, table)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you document how this algorithm is supposed to work (in either comments attached to a function here, or via a separate document)? It looks feasible to trace through myself, but it's better for a non-trivial algorithm to have its intent documented, so reviewers can focus on if the implementation is right or wrong, not reverse engineering what you're trying to do.

}
}

return tableOrder, nil
}

func getTableCreationOrderUtil(visitedTables map[string]bool, tableOrder []string, tableMap map[string][]string, currTable string) (map[string]bool, []string) {
visitedTables[currTable] = true
for _, referencedTable := range tableMap[currTable] {
if _, found := visitedTables[referencedTable]; !found {
visitedTables, tableOrder = getTableCreationOrderUtil(visitedTables, tableOrder, tableMap, referencedTable)
}
}
tableOrder = append(tableOrder, currTable)
return visitedTables, tableOrder
}

func getForeignKeyTablesOfTable(db *sql.DB, schema string, table string) ([]string, error) {
fkTables := []string{}
visitedTables := map[string]bool{}

query := fmt.Sprintf(`
SELECT CONCAT(REFERENCED_TABLE_SCHEMA, '.', REFERENCED_TABLE_NAME)
FROM information_schema.key_column_usage
WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND REFERENCED_TABLE_NAME IS NOT NULL
`,
schema,
table,
)

rows, err := db.Query(query)
if err != nil {
return fkTables, err
}

var tableName string
for rows.Next() {
err = rows.Scan(&tableName)
if err != nil {
return fkTables, err
}

if _, found := visitedTables[tableName]; !found && tableName != table {
fkTables = append(fkTables, tableName)
visitedTables[tableName] = true
}

}

return fkTables, nil
}

func showDatabases(c *sql.DB) ([]string, error) {
rows, err := c.Query("show databases")
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion test/go/ferry_test.go
Expand Up @@ -56,9 +56,12 @@ func (t *FerryTestSuite) TestSourceDatabaseWithForeignKeyConstraintFailsInitiali
ferry = testhelpers.NewTestFerry().Ferry
ferry.Config.SkipForeignKeyConstraintsCheck = true

err = ferry.Initialize()
_, err = t.Ferry.SourceDB.Exec("SET GLOBAL read_only = ON")

err = ferry.Initialize()
t.Require().Nil(err)

_, err = t.Ferry.SourceDB.Exec("SET GLOBAL read_only = OFF")
}

func TestFerryTestSuite(t *testing.T) {
Expand Down
51 changes: 51 additions & 0 deletions test/go/table_schema_cache_test.go
Expand Up @@ -3,6 +3,7 @@ package test
import (
"fmt"
"sort"
"strings"
"testing"

"github.com/Shopify/ghostferry"
Expand Down Expand Up @@ -444,6 +445,56 @@ func (this *TableSchemaCacheTestSuite) TestTargetToSourceRewritesErrorsOnDuplica
this.Require().Equal(err.Error(), "duplicate target to source rewrite detected")
}

func (this *TableSchemaCacheTestSuite) TestGetTableCreationOrderWithoutForeignKeyConstraints() {
tables, err := ghostferry.LoadTables(this.Ferry.SourceDB, this.Ferry.TableFilter, nil, nil, nil, nil)
this.Require().Nil(err)

creationOrder, err := tables.GetTableCreationOrder(this.Ferry.SourceDB)
this.Require().Nil(err)

this.Require().Equal(len(creationOrder), 3)
this.Require().ElementsMatch(creationOrder, tables.AllTableNames())
}

func (this *TableSchemaCacheTestSuite) TestGetTableCreationOrderWithForeignKeyConstraints() {
_, err := this.Ferry.SourceDB.Exec(fmt.Sprintf("CREATE TABLE `%s`.`table1` (`id1` BIGINT, PRIMARY KEY (`id1`))", testhelpers.TestSchemaName))
this.Require().Nil(err)
_, err = this.Ferry.SourceDB.Exec(fmt.Sprintf("CREATE TABLE `%s`.`table2` (`id2` BIGINT, PRIMARY KEY (`id2`), CONSTRAINT `fkc2` FOREIGN KEY (`id2`) REFERENCES `table1` (`id1`))", testhelpers.TestSchemaName))
this.Require().Nil(err)
_, err = this.Ferry.SourceDB.Exec(fmt.Sprintf("CREATE TABLE `%s`.`table3` (`id3` BIGINT, PRIMARY KEY (`id3`), CONSTRAINT `fkc3_1` FOREIGN KEY (`id3`) REFERENCES `table1` (`id1`), CONSTRAINT `fkc3_2` FOREIGN KEY (`id3`) REFERENCES `table2` (`id2`))", testhelpers.TestSchemaName))
this.Require().Nil(err)

tables, err := ghostferry.LoadTables(this.Ferry.SourceDB, this.Ferry.TableFilter, nil, nil, nil, nil)
this.Require().Nil(err)

creationOrder, err := tables.GetTableCreationOrder(this.Ferry.SourceDB)
this.Require().Nil(err)

// 3 tests from the base test setup plus 3 added above
this.Require().Equal(len(creationOrder), 6)
this.Require().ElementsMatch(creationOrder, tables.AllTableNames())

// verify the order: all we care for is that table1 is created before
// table2, which is created before table3
table1Index := -1
table2Index := -1
table3Index := -1
for i, tableName := range creationOrder {
if strings.HasSuffix(tableName, ".table1") {
table1Index = i
} else if strings.HasSuffix(tableName, ".table2") {
table2Index = i
} else if strings.HasSuffix(tableName, ".table3") {
table3Index = i
}
}
this.Require().NotEqual(table1Index, -1)
this.Require().NotEqual(table2Index, -1)
this.Require().NotEqual(table3Index, -1)
this.Require().True(table1Index < table2Index)
this.Require().True(table2Index < table3Index)
}

func TestTableSchemaCache(t *testing.T) {
testhelpers.SetupTest()
suite.Run(t, &TableSchemaCacheTestSuite{GhostferryUnitTestSuite: &testhelpers.GhostferryUnitTestSuite{}})
Expand Down