diff --git a/go/cmd/vtctldclient/command/schema.go b/go/cmd/vtctldclient/command/schema.go index 3a3e0c6961d..ed640a4509f 100644 --- a/go/cmd/vtctldclient/command/schema.go +++ b/go/cmd/vtctldclient/command/schema.go @@ -58,6 +58,13 @@ For --sql, semi-colons and repeated values may be mixed, for example: Args: cobra.ExactArgs(1), RunE: commandApplySchema, } + CopySchemaShard = &cobra.Command{ + Use: "CopySchemaShard [--tables=,,...] [--exclude-tables=,,...] [--include-views] [--skip-verify] [--wait-replicas-timeout=10s] { || } ", + Short: "Copies the schema from a source shard's primary (or a specific tablet) to a destination shard. The schema is applied directly on the primary of the destination shard, and it is propagated to the replicas through binlogs.", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(2), + RunE: commandCopySchemaShard, + } // GetSchema makes a GetSchema gRPC call to a vtctld. GetSchema = &cobra.Command{ Use: "GetSchema [--tables TABLES ...] [--exclude-tables EXCLUDE_TABLES ...] [{--table-names-only | --table-sizes-only}] [--include-views] alias", @@ -173,6 +180,47 @@ func commandApplySchema(cmd *cobra.Command, args []string) error { return nil } +func commandCopySchemaShard(cmd *cobra.Command, args []string) error { + /* + tables := subFlags.String("tables", "", "Specifies a comma-separated list of tables to copy. Each is either an exact match, or a regular expression of the form /regexp/") + excludeTables := subFlags.String("exclude_tables", "", "Specifies a comma-separated list of tables to exclude. Each is either an exact match, or a regular expression of the form /regexp/") + includeViews := subFlags.Bool("include-views", true, "Includes views in the output") + skipVerify := subFlags.Bool("skip-verify", false, "Skip verification of source and target schema after copy") + // for backwards compatibility + waitReplicasTimeout := subFlags.Duration("wait_replicas_timeout", grpcvtctldserver.DefaultWaitReplicasTimeout, "The amount of time to wait for replicas to receive the schema change via replication.") + if err := subFlags.Parse(args); err != nil { + return err + } + + if subFlags.NArg() != 2 { + return fmt.Errorf("the and arguments are both required for the CopySchemaShard command. Instead of the argument, you can also specify which refers to a specific tablet of the shard in the source keyspace") + } + var tableArray []string + if *tables != "" { + tableArray = strings.Split(*tables, ",") + } + var excludeTableArray []string + if *excludeTables != "" { + excludeTableArray = strings.Split(*excludeTables, ",") + } + destKeyspace, destShard, err := topoproto.ParseKeyspaceShard(subFlags.Arg(1)) + if err != nil { + return err + } + + sourceKeyspace, sourceShard, err := topoproto.ParseKeyspaceShard(subFlags.Arg(0)) + if err == nil { + return wr.CopySchemaShardFromShard(ctx, tableArray, excludeTableArray, *includeViews, sourceKeyspace, sourceShard, destKeyspace, destShard, *waitReplicasTimeout, *skipVerify) + } + sourceTabletAlias, err := topoproto.ParseTabletAlias(subFlags.Arg(0)) + if err == nil { + return wr.CopySchemaShard(ctx, sourceTabletAlias, tableArray, excludeTableArray, *includeViews, destKeyspace, destShard, *waitReplicasTimeout, *skipVerify) + } + return err + */ + return nil +} + var getSchemaOptions = struct { Tables []string ExcludeTables []string @@ -375,6 +423,7 @@ func init() { ApplySchema.Flags().StringArrayVar(&applySchemaOptions.SQL, "sql", nil, "Semicolon-delimited, repeatable SQL commands to apply. Exactly one of --sql|--sql-file is required.") ApplySchema.Flags().StringVar(&applySchemaOptions.SQLFile, "sql-file", "", "Path to a file containing semicolon-delimited SQL commands to apply. Exactly one of --sql|--sql-file is required.") ApplySchema.Flags().Int64Var(&applySchemaOptions.BatchSize, "batch-size", 0, "How many queries to batch together. Only applicable when all queries are CREATE TABLE|VIEW") + Root.AddCommand(ApplySchema) GetSchema.Flags().StringSliceVar(&getSchemaOptions.Tables, "tables", nil, "List of tables to display the schema for. Each is either an exact match, or a regular expression of the form `/regexp/`.") diff --git a/go/test/endtoend/vtgate/schema/schema_test.go b/go/test/endtoend/vtgate/schema/schema_test.go index 1fc9dc2c285..4c28e29ca0d 100644 --- a/go/test/endtoend/vtgate/schema/schema_test.go +++ b/go/test/endtoend/vtgate/schema/schema_test.go @@ -17,10 +17,12 @@ limitations under the License. package schema import ( + "encoding/json" "flag" "fmt" "os" "path" + "reflect" "strings" "testing" "time" @@ -106,6 +108,9 @@ func TestSchemaChange(t *testing.T) { testApplySchemaBatch(t) testUnsafeAllowForeignKeys(t) testCreateInvalidView(t) + testCopySchemaShards(t, clusterInstance.Keyspaces[0].Shards[0].Vttablets[0].VttabletProcess.TabletPath, 2) + testCopySchemaShards(t, fmt.Sprintf("%s/0", keyspaceName), 3) + testCopySchemaShardWithDifferentDB(t, 4) testWithAutoSchemaFromChangeDir(t) } @@ -281,6 +286,58 @@ func checkTablesCount(t *testing.T, tablet *cluster.Vttablet, count int) { assert.Equal(t, len(queryResult.Rows), count) } +// testCopySchemaShards tests that schema from source is correctly applied to destination +func testCopySchemaShards(t *testing.T, source string, shard int) { + addNewShard(t, shard) + // InitShardPrimary creates the db, but there shouldn't be any tables yet. + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0], 0) + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[1], 0) + // Run the command twice to make sure it's idempotent. + for i := 0; i < 2; i++ { + err := clusterInstance.VtctlclientProcess.ExecuteCommand("CopySchemaShard", source, fmt.Sprintf("%s/%d", keyspaceName, shard)) + require.Nil(t, err) + } + // shard2 primary should look the same as the replica we copied from + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0], totalTableCount) + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[1], totalTableCount) + + matchSchema(t, clusterInstance.Keyspaces[0].Shards[0].Vttablets[0].VttabletProcess.TabletPath, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0].VttabletProcess.TabletPath) +} + +// testCopySchemaShardWithDifferentDB if we apply different schema to new shard, it should throw error +func testCopySchemaShardWithDifferentDB(t *testing.T, shard int) { + addNewShard(t, shard) + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0], 0) + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[1], 0) + source := fmt.Sprintf("%s/0", keyspaceName) + + tabletAlias := clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0].VttabletProcess.TabletPath + schema, err := clusterInstance.VtctldClientProcess.ExecuteCommandWithOutput("GetSchema", tabletAlias) + require.Nil(t, err) + + resultMap := make(map[string]any) + err = json.Unmarshal([]byte(schema), &resultMap) + require.Nil(t, err) + dbSchema := reflect.ValueOf(resultMap["database_schema"]) + assert.True(t, strings.Contains(dbSchema.String(), "utf8")) + + // Change the db charset on the destination shard from utf8 to latin1. + // This will make CopySchemaShard fail during its final diff. + // (The different charset won't be corrected on the destination shard + // because we use "CREATE DATABASE IF NOT EXISTS" and this doesn't fail if + // there are differences in the options e.g. the character set.) + err = clusterInstance.VtctldClientProcess.ExecuteCommand("ExecuteFetchAsDBA", "--json", tabletAlias, "ALTER DATABASE vt_ks CHARACTER SET latin1") + require.Nil(t, err) + + output, err := clusterInstance.VtctlclientProcess.ExecuteCommandWithOutput("CopySchemaShard", source, fmt.Sprintf("%s/%d", keyspaceName, shard)) + require.Error(t, err) + assert.True(t, strings.Contains(output, "schemas are different")) + + // shard2 primary should have the same number of tables. Only the db + // character set is different. + checkTablesCount(t, clusterInstance.Keyspaces[0].Shards[shard].Vttablets[0], totalTableCount) +} + // addNewShard adds a new shard dynamically func addNewShard(t *testing.T, shard int) { keyspace := &cluster.Keyspace{