vitess.io/vitess@v0.16.2/go/vt/mysqlctl/tmutils/schema_test.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tmutils
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/stretchr/testify/assert"
    25  	"github.com/stretchr/testify/require"
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
    29  )
    30  
    31  var basicTable1 = &tabletmanagerdatapb.TableDefinition{
    32  	Name:   "table1",
    33  	Schema: "table schema 1",
    34  	Type:   TableBaseTable,
    35  }
    36  var basicTable2 = &tabletmanagerdatapb.TableDefinition{
    37  	Name:   "table2",
    38  	Schema: "table schema 2",
    39  	Type:   TableBaseTable,
    40  }
    41  
    42  var table3 = &tabletmanagerdatapb.TableDefinition{
    43  	Name: "table2",
    44  	Schema: "CREATE TABLE `table3` (\n" +
    45  		"id bigint not null,\n" +
    46  		") Engine=InnoDB",
    47  	Type: TableBaseTable,
    48  }
    49  
    50  var view1 = &tabletmanagerdatapb.TableDefinition{
    51  	Name:   "view1",
    52  	Schema: "view schema 1",
    53  	Type:   TableView,
    54  }
    55  
    56  var view2 = &tabletmanagerdatapb.TableDefinition{
    57  	Name:   "view2",
    58  	Schema: "view schema 2",
    59  	Type:   TableView,
    60  }
    61  
    62  func TestToSQLStrings(t *testing.T) {
    63  	var testcases = []struct {
    64  		input *tabletmanagerdatapb.SchemaDefinition
    65  		want  []string
    66  	}{
    67  		{
    68  			// basic SchemaDefinition with create db statement, basic table and basic view
    69  			input: &tabletmanagerdatapb.SchemaDefinition{
    70  				DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}",
    71  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
    72  					basicTable1,
    73  					view1,
    74  				},
    75  			},
    76  			want: []string{"CREATE DATABASE `{{.DatabaseName}}`", basicTable1.Schema, view1.Schema},
    77  		},
    78  		{
    79  			// SchemaDefinition doesn't need any tables or views
    80  			input: &tabletmanagerdatapb.SchemaDefinition{
    81  				DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}",
    82  			},
    83  			want: []string{"CREATE DATABASE `{{.DatabaseName}}`"},
    84  		},
    85  		{
    86  			// and can even have an empty DatabaseSchema
    87  			input: &tabletmanagerdatapb.SchemaDefinition{},
    88  			want:  []string{""},
    89  		},
    90  		{
    91  			// with tables but no views
    92  			input: &tabletmanagerdatapb.SchemaDefinition{
    93  				DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}",
    94  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
    95  					basicTable1,
    96  					basicTable2,
    97  				},
    98  			},
    99  			want: []string{"CREATE DATABASE `{{.DatabaseName}}`", basicTable1.Schema, basicTable2.Schema},
   100  		},
   101  		{
   102  			// multiple tables and views should be ordered with all tables before views
   103  			input: &tabletmanagerdatapb.SchemaDefinition{
   104  				DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}",
   105  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   106  					view1,
   107  					view2,
   108  					basicTable1,
   109  					basicTable2,
   110  				},
   111  			},
   112  			want: []string{
   113  				"CREATE DATABASE `{{.DatabaseName}}`",
   114  				basicTable1.Schema, basicTable2.Schema,
   115  				view1.Schema, view2.Schema,
   116  			},
   117  		},
   118  		{
   119  			// valid table schema gets correctly rewritten to include DatabaseName
   120  			input: &tabletmanagerdatapb.SchemaDefinition{
   121  				DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}",
   122  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   123  					basicTable1,
   124  					table3,
   125  				},
   126  			},
   127  			want: []string{
   128  				"CREATE DATABASE `{{.DatabaseName}}`",
   129  				basicTable1.Schema,
   130  				"CREATE TABLE `{{.DatabaseName}}`.`table3` (\n" +
   131  					"id bigint not null,\n" +
   132  					") Engine=InnoDB",
   133  			},
   134  		},
   135  	}
   136  
   137  	for _, tc := range testcases {
   138  		got := SchemaDefinitionToSQLStrings(tc.input)
   139  		assert.Equal(t, tc.want, got)
   140  	}
   141  }
   142  
   143  func testDiff(t *testing.T, left, right *tabletmanagerdatapb.SchemaDefinition, leftName, rightName string, expected []string) {
   144  	t.Helper()
   145  
   146  	actual := DiffSchemaToArray(leftName, left, rightName, right)
   147  
   148  	equal := false
   149  	if len(actual) == len(expected) {
   150  		equal = true
   151  		for i, val := range actual {
   152  			if val != expected[i] {
   153  				equal = false
   154  				break
   155  			}
   156  		}
   157  	}
   158  	assert.Truef(t, equal, "expected: %v, actual: %v", expected, actual)
   159  }
   160  
   161  func TestSchemaDiff(t *testing.T) {
   162  	sd1 := &tabletmanagerdatapb.SchemaDefinition{
   163  		TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   164  			{
   165  				Name:   "table1",
   166  				Schema: "schema1",
   167  				Type:   TableBaseTable,
   168  			},
   169  			{
   170  				Name:   "table2",
   171  				Schema: "schema2",
   172  				Type:   TableBaseTable,
   173  			},
   174  		},
   175  	}
   176  
   177  	sd2 := &tabletmanagerdatapb.SchemaDefinition{TableDefinitions: make([]*tabletmanagerdatapb.TableDefinition, 0, 2)}
   178  
   179  	sd3 := &tabletmanagerdatapb.SchemaDefinition{
   180  		TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   181  			{
   182  				Name:   "table2",
   183  				Schema: "schema2",
   184  				Type:   TableBaseTable,
   185  			},
   186  		},
   187  	}
   188  
   189  	sd4 := &tabletmanagerdatapb.SchemaDefinition{
   190  		TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   191  			{
   192  				Name:   "table2",
   193  				Schema: "table2",
   194  				Type:   TableView,
   195  			},
   196  		},
   197  	}
   198  
   199  	sd5 := &tabletmanagerdatapb.SchemaDefinition{
   200  		TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   201  			{
   202  				Name:   "table2",
   203  				Schema: "table2",
   204  				Type:   TableBaseTable,
   205  			},
   206  		},
   207  	}
   208  
   209  	testDiff(t, sd1, sd1, "sd1", "sd2", []string{})
   210  
   211  	testDiff(t, sd2, sd2, "sd2", "sd2", []string{})
   212  
   213  	// two schemas are considered the same if both nil
   214  	testDiff(t, nil, nil, "sd1", "sd2", nil)
   215  
   216  	testDiff(t, sd1, nil, "sd1", "sd2", []string{
   217  		fmt.Sprintf("schemas are different:\nsd1: %v, sd2: <nil>", sd1),
   218  	})
   219  
   220  	testDiff(t, sd1, sd3, "sd1", "sd3", []string{
   221  		"sd1 has an extra table named table1",
   222  	})
   223  
   224  	testDiff(t, sd3, sd1, "sd3", "sd1", []string{
   225  		"sd1 has an extra table named table1",
   226  	})
   227  
   228  	testDiff(t, sd2, sd4, "sd2", "sd4", []string{
   229  		"sd4 has an extra view named table2",
   230  	})
   231  
   232  	testDiff(t, sd4, sd2, "sd4", "sd2", []string{
   233  		"sd4 has an extra view named table2",
   234  	})
   235  
   236  	testDiff(t, sd4, sd5, "sd4", "sd5", []string{
   237  		fmt.Sprintf("schemas differ on table type for table table2:\nsd4: VIEW\n differs from:\nsd5: BASE TABLE"), //nolint
   238  	})
   239  
   240  	sd1.DatabaseSchema = "CREATE DATABASE {{.DatabaseName}}"
   241  	sd2.DatabaseSchema = "DONT CREATE DATABASE {{.DatabaseName}}"
   242  	testDiff(t, sd1, sd2, "sd1", "sd2", []string{"schemas are different:\nsd1: CREATE DATABASE {{.DatabaseName}}\n differs from:\nsd2: DONT CREATE DATABASE {{.DatabaseName}}", "sd1 has an extra table named table1", "sd1 has an extra table named table2"})
   243  	sd2.DatabaseSchema = "CREATE DATABASE {{.DatabaseName}}"
   244  	testDiff(t, sd2, sd1, "sd2", "sd1", []string{"sd1 has an extra table named table1", "sd1 has an extra table named table2"})
   245  
   246  	sd2.TableDefinitions = append(sd2.TableDefinitions, &tabletmanagerdatapb.TableDefinition{Name: "table1", Schema: "schema1", Type: TableBaseTable})
   247  	testDiff(t, sd1, sd2, "sd1", "sd2", []string{"sd1 has an extra table named table2"})
   248  
   249  	sd2.TableDefinitions = append(sd2.TableDefinitions, &tabletmanagerdatapb.TableDefinition{Name: "table2", Schema: "schema3", Type: TableBaseTable})
   250  	testDiff(t, sd1, sd2, "sd1", "sd2", []string{"schemas differ on table table2:\nsd1: schema2\n differs from:\nsd2: schema3"})
   251  }
   252  
   253  func TestTableFilter(t *testing.T) {
   254  	includedTable := "t1"
   255  	includedTable2 := "t2"
   256  	excludedTable := "e1"
   257  	view := "v1"
   258  
   259  	includedTableRE := "/t.*/"
   260  	excludedTableRE := "/e.*/"
   261  
   262  	tcs := []struct {
   263  		desc          string
   264  		tables        []string
   265  		excludeTables []string
   266  		includeViews  bool
   267  
   268  		tableName string
   269  		tableType string
   270  
   271  		hasErr   bool
   272  		included bool
   273  	}{
   274  		{
   275  			desc:         "everything allowed includes table",
   276  			includeViews: true,
   277  
   278  			tableName: includedTable,
   279  			tableType: TableBaseTable,
   280  
   281  			included: true,
   282  		},
   283  		{
   284  			desc:         "everything allowed includes view",
   285  			includeViews: true,
   286  
   287  			tableName: view,
   288  			tableType: TableView,
   289  
   290  			included: true,
   291  		},
   292  		{
   293  			desc:         "table list includes matching 1st table",
   294  			tables:       []string{includedTable, includedTable2},
   295  			includeViews: true,
   296  
   297  			tableName: includedTable,
   298  			tableType: TableBaseTable,
   299  
   300  			included: true,
   301  		},
   302  		{
   303  			desc:         "table list includes matching 2nd table",
   304  			tables:       []string{includedTable, includedTable2},
   305  			includeViews: true,
   306  
   307  			tableName: includedTable2,
   308  			tableType: TableBaseTable,
   309  
   310  			included: true,
   311  		},
   312  		{
   313  			desc:         "table list excludes non-matching table",
   314  			tables:       []string{includedTable, includedTable2},
   315  			includeViews: true,
   316  
   317  			tableName: excludedTable,
   318  			tableType: TableBaseTable,
   319  
   320  			included: false,
   321  		},
   322  		{
   323  			desc:         "table list include view includes matching view",
   324  			tables:       []string{view},
   325  			includeViews: true,
   326  
   327  			tableName: view,
   328  			tableType: TableView,
   329  
   330  			included: true,
   331  		},
   332  		{
   333  			desc:         "table list exclude view excludes matching view",
   334  			tables:       []string{view},
   335  			includeViews: false,
   336  
   337  			tableName: view,
   338  			tableType: TableView,
   339  
   340  			included: false,
   341  		},
   342  		{
   343  			desc:         "table regexp list includes matching table",
   344  			tables:       []string{includedTableRE},
   345  			includeViews: false,
   346  
   347  			tableName: includedTable,
   348  			tableType: TableBaseTable,
   349  
   350  			included: true,
   351  		},
   352  		{
   353  			desc:          "exclude table list excludes matching table",
   354  			excludeTables: []string{excludedTable},
   355  
   356  			tableName: excludedTable,
   357  			tableType: TableBaseTable,
   358  
   359  			included: false,
   360  		},
   361  		{
   362  			desc:          "exclude table list includes non-matching table",
   363  			excludeTables: []string{excludedTable},
   364  
   365  			tableName: includedTable,
   366  			tableType: TableBaseTable,
   367  
   368  			included: true,
   369  		},
   370  		{
   371  			desc:          "exclude table list includes non-matching view",
   372  			excludeTables: []string{excludedTable},
   373  			includeViews:  true,
   374  
   375  			tableName: view,
   376  			tableType: TableView,
   377  
   378  			included: true,
   379  		},
   380  		{
   381  			desc:          "exclude table list excludes matching view",
   382  			excludeTables: []string{excludedTable},
   383  			includeViews:  true,
   384  
   385  			tableName: excludedTable,
   386  			tableType: TableView,
   387  
   388  			included: false,
   389  		},
   390  		{
   391  			desc:          "exclude table list excludes matching view",
   392  			excludeTables: []string{excludedTable},
   393  			includeViews:  true,
   394  
   395  			tableName: excludedTable,
   396  			tableType: TableView,
   397  
   398  			included: false,
   399  		},
   400  		{
   401  			desc:          "exclude table regexp list excludes matching table",
   402  			excludeTables: []string{excludedTableRE},
   403  			includeViews:  false,
   404  
   405  			tableName: excludedTable,
   406  			tableType: TableBaseTable,
   407  
   408  			included: false,
   409  		},
   410  		{
   411  			desc:          "table list with excludes includes matching table",
   412  			tables:        []string{includedTable},
   413  			excludeTables: []string{excludedTable},
   414  
   415  			tableName: includedTable,
   416  			tableType: TableBaseTable,
   417  
   418  			included: true,
   419  		},
   420  		{
   421  			desc:          "table list with excludes excludes matching excluded table",
   422  			tables:        []string{includedTable},
   423  			excludeTables: []string{excludedTable},
   424  
   425  			tableName: excludedTable,
   426  			tableType: TableBaseTable,
   427  
   428  			included: false,
   429  		},
   430  		{
   431  			desc:          "exclude table list does not list table",
   432  			excludeTables: []string{"nomatch1", "nomatch2", "/nomatch3/", "/nomatch4/", "/nomatch5/"},
   433  			includeViews:  true,
   434  
   435  			tableName: excludedTable,
   436  			tableType: TableBaseTable,
   437  
   438  			included: true,
   439  		},
   440  		{
   441  			desc:          "exclude table list with re match",
   442  			excludeTables: []string{"nomatch1", "nomatch2", "/nomatch3/", "/" + excludedTable + "/", "/nomatch5/"},
   443  			includeViews:  true,
   444  
   445  			tableName: excludedTable,
   446  			tableType: TableBaseTable,
   447  
   448  			included: false,
   449  		},
   450  		{
   451  			desc:   "bad table regexp",
   452  			tables: []string{"/*/"},
   453  
   454  			hasErr: true,
   455  		},
   456  		{
   457  			desc:          "bad exclude table regexp",
   458  			excludeTables: []string{"/*/"},
   459  
   460  			hasErr: true,
   461  		},
   462  	}
   463  
   464  	for _, tc := range tcs {
   465  		t.Run(tc.desc, func(t *testing.T) {
   466  			f, err := NewTableFilter(tc.tables, tc.excludeTables, tc.includeViews)
   467  			if tc.hasErr {
   468  				assert.Error(t, err)
   469  				return
   470  			}
   471  			assert.NoError(t, err)
   472  
   473  			assert.Equal(t, len(tc.tables), len(f.tableNames)+len(f.tableREs))
   474  			assert.Equal(t, len(tc.excludeTables), len(f.excludeTableNames)+len(f.excludeTableREs))
   475  			included := f.Includes(tc.tableName, tc.tableType)
   476  			assert.Equalf(t, tc.included, included, "filter: %v", f)
   477  		})
   478  	}
   479  }
   480  
   481  func TestFilterTables(t *testing.T) {
   482  	var testcases = []struct {
   483  		desc          string
   484  		input         *tabletmanagerdatapb.SchemaDefinition
   485  		tables        []string
   486  		excludeTables []string
   487  		includeViews  bool
   488  		want          *tabletmanagerdatapb.SchemaDefinition
   489  		wantError     error
   490  	}{
   491  		{
   492  			desc: "filter based on tables (whitelist)",
   493  			input: &tabletmanagerdatapb.SchemaDefinition{
   494  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   495  					basicTable1,
   496  					basicTable2,
   497  				},
   498  			},
   499  			tables: []string{basicTable1.Name},
   500  			want: &tabletmanagerdatapb.SchemaDefinition{
   501  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   502  					basicTable1,
   503  				},
   504  			},
   505  		},
   506  		{
   507  			desc: "filter based on excludeTables (denylist)",
   508  			input: &tabletmanagerdatapb.SchemaDefinition{
   509  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   510  					basicTable1,
   511  					basicTable2,
   512  				},
   513  			},
   514  			excludeTables: []string{basicTable1.Name},
   515  			want: &tabletmanagerdatapb.SchemaDefinition{
   516  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   517  					basicTable2,
   518  				},
   519  			},
   520  		},
   521  		{
   522  			desc: "excludeTables may filter out a whitelisted item from tables",
   523  			input: &tabletmanagerdatapb.SchemaDefinition{
   524  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   525  					basicTable1,
   526  					basicTable2,
   527  				},
   528  			},
   529  			tables:        []string{basicTable1.Name, basicTable2.Name},
   530  			excludeTables: []string{basicTable1.Name},
   531  			want: &tabletmanagerdatapb.SchemaDefinition{
   532  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   533  					basicTable2,
   534  				},
   535  			},
   536  		},
   537  		{
   538  			desc: "exclude views",
   539  			input: &tabletmanagerdatapb.SchemaDefinition{
   540  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   541  					basicTable1,
   542  					basicTable2,
   543  					view1,
   544  				},
   545  			},
   546  			includeViews: false,
   547  			want: &tabletmanagerdatapb.SchemaDefinition{
   548  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   549  					basicTable1,
   550  					basicTable2,
   551  				},
   552  			},
   553  		},
   554  		{
   555  			desc: "update schema version hash when list of tables has changed",
   556  			input: &tabletmanagerdatapb.SchemaDefinition{
   557  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   558  					basicTable1,
   559  					basicTable2,
   560  				},
   561  				Version: "dummy-version",
   562  			},
   563  			excludeTables: []string{basicTable1.Name},
   564  			want: &tabletmanagerdatapb.SchemaDefinition{
   565  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   566  					basicTable2,
   567  				},
   568  				Version: "6d1d294def9febdb21b35dd19a1dd4c6",
   569  			},
   570  		},
   571  		{
   572  			desc: "invalid regex for tables returns an error",
   573  			input: &tabletmanagerdatapb.SchemaDefinition{
   574  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   575  					basicTable1,
   576  				},
   577  			},
   578  			tables:    []string{"/(/"},
   579  			wantError: errors.New("cannot compile regexp ( for table: error parsing regexp: missing closing ): `(`"),
   580  		},
   581  		{
   582  			desc: "invalid regex for excludeTables returns an error",
   583  			input: &tabletmanagerdatapb.SchemaDefinition{
   584  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   585  					basicTable1,
   586  				},
   587  			},
   588  			excludeTables: []string{"/(/"},
   589  			wantError:     errors.New("cannot compile regexp ( for excludeTable: error parsing regexp: missing closing ): `(`"),
   590  		},
   591  		{
   592  			desc: "table substring doesn't match without regexp (include)",
   593  			input: &tabletmanagerdatapb.SchemaDefinition{
   594  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   595  					basicTable1,
   596  					basicTable2,
   597  				},
   598  			},
   599  			tables: []string{basicTable1.Name[1:]},
   600  			want: &tabletmanagerdatapb.SchemaDefinition{
   601  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{},
   602  			},
   603  		},
   604  		{
   605  			desc: "table substring matches with regexp (include)",
   606  			input: &tabletmanagerdatapb.SchemaDefinition{
   607  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   608  					basicTable1,
   609  					basicTable2,
   610  				},
   611  			},
   612  			tables: []string{"/" + basicTable1.Name[1:] + "/"},
   613  			want: &tabletmanagerdatapb.SchemaDefinition{
   614  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   615  					basicTable1,
   616  				},
   617  			},
   618  		},
   619  		{
   620  			desc: "table substring doesn't match without regexp (exclude)",
   621  			input: &tabletmanagerdatapb.SchemaDefinition{
   622  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   623  					basicTable1,
   624  					basicTable2,
   625  				},
   626  			},
   627  			excludeTables: []string{basicTable1.Name[1:]},
   628  			want: &tabletmanagerdatapb.SchemaDefinition{
   629  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   630  					basicTable1,
   631  					basicTable2,
   632  				},
   633  			},
   634  		},
   635  		{
   636  			desc: "table substring matches with regexp (exclude)",
   637  			input: &tabletmanagerdatapb.SchemaDefinition{
   638  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   639  					basicTable1,
   640  					basicTable2,
   641  				},
   642  			},
   643  			excludeTables: []string{"/" + basicTable1.Name[1:] + "/"},
   644  			want: &tabletmanagerdatapb.SchemaDefinition{
   645  				TableDefinitions: []*tabletmanagerdatapb.TableDefinition{
   646  					basicTable2,
   647  				},
   648  			},
   649  		},
   650  	}
   651  
   652  	for _, tc := range testcases {
   653  		t.Run(tc.desc, func(t *testing.T) {
   654  			got, err := FilterTables(tc.input, tc.tables, tc.excludeTables, tc.includeViews)
   655  			if tc.wantError != nil {
   656  				require.Error(t, err)
   657  				require.Equal(t, tc.wantError, err)
   658  			} else {
   659  				assert.NoError(t, err)
   660  				assert.Truef(t, proto.Equal(tc.want, got), "wanted: %v, got: %v", tc.want, got)
   661  			}
   662  		})
   663  	}
   664  }