diff --git a/go/vt/proto/vtadmin/vtadmin.pb.go b/go/vt/proto/vtadmin/vtadmin.pb.go
index e85385ec409..8b6a6997c8d 100644
--- a/go/vt/proto/vtadmin/vtadmin.pb.go
+++ b/go/vt/proto/vtadmin/vtadmin.pb.go
@@ -1203,8 +1203,12 @@ type ApplySchemaRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- ClusterId string `protobuf:"bytes,1,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
- Request *vtctldata.ApplySchemaRequest `protobuf:"bytes,2,opt,name=request,proto3" json:"request,omitempty"`
+ ClusterId string `protobuf:"bytes,1,opt,name=cluster_id,json=clusterId,proto3" json:"cluster_id,omitempty"`
+ // Request.Sql will be overriden by this Sql field.
+ Sql string `protobuf:"bytes,2,opt,name=sql,proto3" json:"sql,omitempty"`
+ // Request.CallerId will be overriden by this CallerId field.
+ CallerId string `protobuf:"bytes,3,opt,name=caller_id,json=callerId,proto3" json:"caller_id,omitempty"`
+ Request *vtctldata.ApplySchemaRequest `protobuf:"bytes,4,opt,name=request,proto3" json:"request,omitempty"`
}
func (x *ApplySchemaRequest) Reset() {
@@ -1244,6 +1248,20 @@ func (x *ApplySchemaRequest) GetClusterId() string {
return ""
}
+func (x *ApplySchemaRequest) GetSql() string {
+ if x != nil {
+ return x.Sql
+ }
+ return ""
+}
+
+func (x *ApplySchemaRequest) GetCallerId() string {
+ if x != nil {
+ return x.CallerId
+ }
+ return ""
+}
+
func (x *ApplySchemaRequest) GetRequest() *vtctldata.ApplySchemaRequest {
if x != nil {
return x.Request
@@ -7731,11 +7749,14 @@ var file_vtadmin_proto_rawDesc = []byte{
0x2e, 0x76, 0x74, 0x63, 0x74, 0x6c, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66,
0x6c, 0x6f, 0x77, 0x53, 0x77, 0x69, 0x74, 0x63, 0x68, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x22, 0x6c, 0x0a, 0x12, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65,
- 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73,
- 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x74, 0x63, 0x74, 0x6c, 0x64, 0x61,
+ 0x22, 0x9b, 0x01, 0x0a, 0x12, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74,
+ 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, 0x75,
+ 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x71, 0x6c, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x71, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x6c, 0x6c,
+ 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x6c,
+ 0x6c, 0x65, 0x72, 0x49, 0x64, 0x12, 0x37, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x76, 0x74, 0x63, 0x74, 0x6c, 0x64, 0x61,
0x74, 0x61, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x80,
0x01, 0x0a, 0x1c, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4d,
diff --git a/go/vt/proto/vtadmin/vtadmin_vtproto.pb.go b/go/vt/proto/vtadmin/vtadmin_vtproto.pb.go
index bc0746b7b8a..82cca2cea06 100644
--- a/go/vt/proto/vtadmin/vtadmin_vtproto.pb.go
+++ b/go/vt/proto/vtadmin/vtadmin_vtproto.pb.go
@@ -454,6 +454,8 @@ func (m *ApplySchemaRequest) CloneVT() *ApplySchemaRequest {
}
r := new(ApplySchemaRequest)
r.ClusterId = m.ClusterId
+ r.Sql = m.Sql
+ r.CallerId = m.CallerId
r.Request = m.Request.CloneVT()
if len(m.unknownFields) > 0 {
r.unknownFields = make([]byte, len(m.unknownFields))
@@ -4038,6 +4040,20 @@ func (m *ApplySchemaRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= size
i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
i--
+ dAtA[i] = 0x22
+ }
+ if len(m.CallerId) > 0 {
+ i -= len(m.CallerId)
+ copy(dAtA[i:], m.CallerId)
+ i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.CallerId)))
+ i--
+ dAtA[i] = 0x1a
+ }
+ if len(m.Sql) > 0 {
+ i -= len(m.Sql)
+ copy(dAtA[i:], m.Sql)
+ i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Sql)))
+ i--
dAtA[i] = 0x12
}
if len(m.ClusterId) > 0 {
@@ -10321,6 +10337,14 @@ func (m *ApplySchemaRequest) SizeVT() (n int) {
if l > 0 {
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
}
+ l = len(m.Sql)
+ if l > 0 {
+ n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+ }
+ l = len(m.CallerId)
+ if l > 0 {
+ n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+ }
if m.Request != nil {
l = m.Request.SizeVT()
n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
@@ -15873,6 +15897,70 @@ func (m *ApplySchemaRequest) UnmarshalVT(dAtA []byte) error {
m.ClusterId = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field Sql", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return protohelpers.ErrIntOverflow
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return protohelpers.ErrInvalidLength
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return protohelpers.ErrInvalidLength
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.Sql = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 3:
+ if wireType != 2 {
+ return fmt.Errorf("proto: wrong wireType = %d for field CallerId", wireType)
+ }
+ var stringLen uint64
+ for shift := uint(0); ; shift += 7 {
+ if shift >= 64 {
+ return protohelpers.ErrIntOverflow
+ }
+ if iNdEx >= l {
+ return io.ErrUnexpectedEOF
+ }
+ b := dAtA[iNdEx]
+ iNdEx++
+ stringLen |= uint64(b&0x7F) << shift
+ if b < 0x80 {
+ break
+ }
+ }
+ intStringLen := int(stringLen)
+ if intStringLen < 0 {
+ return protohelpers.ErrInvalidLength
+ }
+ postIndex := iNdEx + intStringLen
+ if postIndex < 0 {
+ return protohelpers.ErrInvalidLength
+ }
+ if postIndex > l {
+ return io.ErrUnexpectedEOF
+ }
+ m.CallerId = string(dAtA[iNdEx:postIndex])
+ iNdEx = postIndex
+ case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Request", wireType)
}
diff --git a/go/vt/vtadmin/api.go b/go/vt/vtadmin/api.go
index cef8816504a..4f91459d9ed 100644
--- a/go/vt/vtadmin/api.go
+++ b/go/vt/vtadmin/api.go
@@ -59,6 +59,7 @@ import (
"vitess.io/vitess/go/vt/vtadmin/rbac"
"vitess.io/vitess/go/vt/vtadmin/sort"
"vitess.io/vitess/go/vt/vtadmin/vtadminproto"
+ "vitess.io/vitess/go/vt/vtctl/grpcvtctldserver"
"vitess.io/vitess/go/vt/vtctl/workflow"
"vitess.io/vitess/go/vt/vtenv"
"vitess.io/vitess/go/vt/vterrors"
@@ -488,6 +489,31 @@ func (api *API) ApplySchema(ctx context.Context, req *vtadminpb.ApplySchemaReque
return nil, err
}
+ // Parser with default options. New() itself initializes with default MySQL version.
+ parser, err := sqlparser.New(sqlparser.Options{
+ TruncateUILen: 512,
+ TruncateErrLen: 0,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Split the sql statement received from request.
+ sqlParts, err := parser.SplitStatementToPieces(req.Sql)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Request.Sql = sqlParts
+
+ // Set the callerID if not empty.
+ if req.CallerId != "" {
+ req.Request.CallerId = &vtrpcpb.CallerID{Principal: req.CallerId}
+ }
+
+ // Set the default wait replicas timeout.
+ req.Request.WaitReplicasTimeout = protoutil.DurationToProto(grpcvtctldserver.DefaultWaitReplicasTimeout)
+
return c.ApplySchema(ctx, req.Request)
}
diff --git a/go/vt/vtadmin/http/schema_migrations.go b/go/vt/vtadmin/http/schema_migrations.go
index e0207989648..3da6026fe9f 100644
--- a/go/vt/vtadmin/http/schema_migrations.go
+++ b/go/vt/vtadmin/http/schema_migrations.go
@@ -34,19 +34,26 @@ func ApplySchema(ctx context.Context, r Request, api *API) *JSONResponse {
decoder := json.NewDecoder(r.Body)
defer r.Body.Close()
- var req vtctldatapb.ApplySchemaRequest
- if err := decoder.Decode(&req); err != nil {
+ var body struct {
+ Sql string `json:"sql"`
+ CallerId string `json:"caller_id"`
+ Request vtctldatapb.ApplySchemaRequest `json:"request"`
+ }
+
+ if err := decoder.Decode(&body); err != nil {
return NewJSONResponse(nil, &errors.BadRequest{
Err: err,
})
}
vars := mux.Vars(r.Request)
- req.Keyspace = vars["keyspace"]
+ body.Request.Keyspace = vars["keyspace"]
resp, err := api.server.ApplySchema(ctx, &vtadminpb.ApplySchemaRequest{
ClusterId: vars["cluster_id"],
- Request: &req,
+ Sql: body.Sql,
+ CallerId: body.CallerId,
+ Request: &body.Request,
})
return NewJSONResponse(resp, err)
diff --git a/proto/vtadmin.proto b/proto/vtadmin.proto
index 78f086ec345..963d1fa5779 100644
--- a/proto/vtadmin.proto
+++ b/proto/vtadmin.proto
@@ -388,7 +388,11 @@ message WorkflowSwitchTrafficRequest {
message ApplySchemaRequest {
string cluster_id = 1;
- vtctldata.ApplySchemaRequest request = 2;
+ // Request.Sql will be overriden by this Sql field.
+ string sql = 2;
+ // Request.CallerId will be overriden by this CallerId field.
+ string caller_id = 3;
+ vtctldata.ApplySchemaRequest request = 4;
}
message CancelSchemaMigrationRequest {
diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts
index 3f75330d240..674df961ef0 100644
--- a/web/vtadmin/src/api/http.ts
+++ b/web/vtadmin/src/api/http.ts
@@ -1068,3 +1068,41 @@ export const showVDiff = async ({ clusterID, request }: ShowVDiffParams) => {
return vtadmin.VDiffShowResponse.create(result);
};
+
+export const fetchSchemaMigrations = async (request: vtadmin.IGetSchemaMigrationsRequest) => {
+ const { result } = await vtfetch(`/api/migrations/`, {
+ body: JSON.stringify(request),
+ method: 'post',
+ });
+
+ const err = vtadmin.GetSchemaMigrationsResponse.verify(result);
+ if (err) throw Error(err);
+
+ return vtadmin.GetSchemaMigrationsResponse.create(result);
+};
+
+export interface ApplySchemaParams {
+ clusterID: string;
+ keyspace: string;
+ callerID: string;
+ sql: string;
+ request: vtctldata.IApplySchemaRequest;
+}
+
+export const applySchema = async ({ clusterID, keyspace, callerID, sql, request }: ApplySchemaParams) => {
+ const body = {
+ sql,
+ caller_id: callerID,
+ request,
+ };
+
+ const { result } = await vtfetch(`/api/migration/${clusterID}/${keyspace}`, {
+ body: JSON.stringify(body),
+ method: 'post',
+ });
+
+ const err = vtctldata.ApplySchemaResponse.verify(result);
+ if (err) throw Error(err);
+
+ return vtctldata.ApplySchemaResponse.create(result);
+};
diff --git a/web/vtadmin/src/components/App.tsx b/web/vtadmin/src/components/App.tsx
index ef27a35dc95..3bb41ea35f0 100644
--- a/web/vtadmin/src/components/App.tsx
+++ b/web/vtadmin/src/components/App.tsx
@@ -45,6 +45,8 @@ import { Transactions } from './routes/Transactions';
import { Transaction } from './routes/transaction/Transaction';
import { CreateReshard } from './routes/createWorkflow/CreateReshard';
import { CreateMaterialize } from './routes/createWorkflow/CreateMaterialize';
+import { SchemaMigrations } from './routes/SchemaMigrations';
+import { CreateSchemaMigration } from './routes/createSchemaMigration/CreateSchemaMigration';
export const App = () => {
return (
@@ -140,6 +142,16 @@ export const App = () => {
+
+
+
+
+ {!isReadOnlyMode() && (
+
+
+
+ )}
+
diff --git a/web/vtadmin/src/components/NavRail.tsx b/web/vtadmin/src/components/NavRail.tsx
index 9f9e1bf1681..b30cd165684 100644
--- a/web/vtadmin/src/components/NavRail.tsx
+++ b/web/vtadmin/src/components/NavRail.tsx
@@ -65,6 +65,9 @@ export const NavRail = () => {
+ -
+
+
-
diff --git a/web/vtadmin/src/components/routes/createWorkflow/ErrorDialog.tsx b/web/vtadmin/src/components/dialog/ErrorDialog.tsx
similarity index 94%
rename from web/vtadmin/src/components/routes/createWorkflow/ErrorDialog.tsx
rename to web/vtadmin/src/components/dialog/ErrorDialog.tsx
index 25ac5dedb0b..087876e4cd2 100644
--- a/web/vtadmin/src/components/routes/createWorkflow/ErrorDialog.tsx
+++ b/web/vtadmin/src/components/dialog/ErrorDialog.tsx
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import React from 'react';
-import Dialog from '../../dialog/Dialog';
-import { Icon, Icons } from '../../Icon';
+import Dialog from './Dialog';
+import { Icon, Icons } from '../Icon';
export interface ErrorDialogProps {
errorTitle?: string;
diff --git a/web/vtadmin/src/components/routes/SchemaMigrations.tsx b/web/vtadmin/src/components/routes/SchemaMigrations.tsx
new file mode 100644
index 00000000000..1761d26de49
--- /dev/null
+++ b/web/vtadmin/src/components/routes/SchemaMigrations.tsx
@@ -0,0 +1,195 @@
+/**
+ * Copyright 2024 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useEffect, useState } from 'react';
+import { useKeyspaces, useSchemaMigrations } from '../../hooks/api';
+import { DataCell } from '../dataTable/DataCell';
+import { DataTable } from '../dataTable/DataTable';
+import { ContentContainer } from '../layout/ContentContainer';
+import { WorkspaceHeader } from '../layout/WorkspaceHeader';
+import { WorkspaceTitle } from '../layout/WorkspaceTitle';
+import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
+import { useDocumentTitle } from '../../hooks/useDocumentTitle';
+import { vtadmin } from '../../proto/vtadmin';
+import { Select } from '../inputs/Select';
+import { ShardLink } from '../links/ShardLink';
+import { formatDateTime } from '../../util/time';
+import { ReadOnlyGate } from '../ReadOnlyGate';
+import { formatSchemaMigrationStatus } from '../../util/schemaMigrations';
+import { Link } from 'react-router-dom';
+import { TabletLink } from '../links/TabletLink';
+import { formatAlias } from '../../util/tablets';
+import { useURLQuery } from '../../hooks/useURLQuery';
+
+const COLUMNS = ['UUID', 'Status', 'DDL Action', 'Timestamps', 'Stage', 'Progress'];
+
+export const SchemaMigrations = () => {
+ useDocumentTitle('Schema Migrations');
+
+ const { query, replaceQuery } = useURLQuery();
+ const urlKeyspace = query['keyspace'];
+ const urlCluster = query['cluster'];
+
+ const keyspacesQuery = useKeyspaces();
+ const { data: keyspaces = [], ...ksQuery } = keyspacesQuery;
+
+ const [selectedKeyspace, setSelectedKeypsace] = useState();
+
+ const request: vtadmin.IGetSchemaMigrationsRequest = {
+ cluster_requests: [
+ {
+ cluster_id: selectedKeyspace && selectedKeyspace.cluster?.id,
+ request: {
+ keyspace: selectedKeyspace && selectedKeyspace.keyspace?.name,
+ },
+ },
+ ],
+ };
+
+ const schemaMigrationsQuery = useSchemaMigrations(request, {
+ enabled: !!selectedKeyspace,
+ });
+
+ const schemaMigrations = schemaMigrationsQuery.data ? schemaMigrationsQuery.data.schema_migrations : [];
+
+ const handleKeyspaceChange = (ks: vtadmin.Keyspace | null | undefined) => {
+ setSelectedKeypsace(ks);
+
+ if (ks) {
+ replaceQuery({ keyspace: ks.keyspace?.name, cluster: ks.cluster?.id });
+ } else {
+ replaceQuery({ keyspace: undefined, cluster: undefined });
+ }
+ };
+
+ useEffect(() => {
+ if (urlKeyspace && urlCluster) {
+ const keyspace = keyspaces.find(
+ (ks) => ks.cluster?.id === String(urlCluster) && ks.keyspace?.name === String(urlKeyspace)
+ );
+
+ if (keyspace) {
+ setSelectedKeypsace(keyspace);
+ } else if (!ksQuery.isLoading) {
+ replaceQuery({ keyspace: undefined, cluster: undefined });
+ }
+ } else {
+ setSelectedKeypsace(undefined);
+ }
+ }, [urlKeyspace, urlCluster, keyspaces, ksQuery.isLoading, replaceQuery]);
+
+ const renderRows = (rows: vtadmin.ISchemaMigration[]) => {
+ return rows.map((row) => {
+ const migrationInfo = row.schema_migration;
+
+ if (!migrationInfo) return <>>;
+
+ return (
+
+
+ {migrationInfo.uuid}
+
+ Tablet{' '}
+
+ {formatAlias(migrationInfo.tablet)}
+
+
+
+ Shard{' '}
+
+ {`${migrationInfo.keyspace}/${migrationInfo.shard}`}
+
+
+
+
+ {formatSchemaMigrationStatus(migrationInfo)}
+
+ {migrationInfo.ddl_action ? migrationInfo.ddl_action : '-'}
+
+ {migrationInfo.added_at && (
+
+ Added
+ {formatDateTime(migrationInfo.added_at?.seconds)}
+
+ )}
+ {migrationInfo.requested_at && (
+
+ Requested
+ {formatDateTime(migrationInfo.requested_at?.seconds)}
+
+ )}
+ {migrationInfo.started_at && (
+
+ Started
+ {formatDateTime(migrationInfo.started_at?.seconds)}
+
+ )}
+ {migrationInfo.completed_at && (
+
+ Completed
+ {formatDateTime(migrationInfo.completed_at?.seconds)}
+
+ )}
+
+ {migrationInfo.stage ? migrationInfo.stage : '-'}
+ {migrationInfo.progress ? `${migrationInfo.progress}%` : '-'}
+
+ );
+ });
+ };
+
+ return (
+
+
+
+
Schema Migrations
+
+
+
+ Create Schema Migration Request
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.module.scss b/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.module.scss
new file mode 100644
index 00000000000..51f5fdca04e
--- /dev/null
+++ b/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.module.scss
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2024 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.sqlInput {
+ border: solid 2px var(--colorDisabled);
+ border-radius: 6px;
+ display: block;
+ font-family: var(--fontFamilyMonospace);
+ line-height: var(--lineHeightBody);
+ padding: 0.8rem;
+ resize: vertical;
+ width: 100%;
+}
+
+.sqlInput:focus {
+ border-color: var(--colorPrimary);
+ outline: none;
+}
diff --git a/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.tsx b/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.tsx
new file mode 100644
index 00000000000..0f7326d2ae1
--- /dev/null
+++ b/web/vtadmin/src/components/routes/createSchemaMigration/CreateSchemaMigration.tsx
@@ -0,0 +1,270 @@
+/**
+ * Copyright 2024 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useEffect, useState } from 'react';
+import { Link, useHistory } from 'react-router-dom';
+
+import style from './CreateSchemaMigration.module.scss';
+import { useApplySchema, useClusters, useKeyspaces } from '../../../hooks/api';
+import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
+import { Label } from '../../inputs/Label';
+import { Select } from '../../inputs/Select';
+import { ContentContainer } from '../../layout/ContentContainer';
+import { NavCrumbs } from '../../layout/NavCrumbs';
+import { WorkspaceHeader } from '../../layout/WorkspaceHeader';
+import { WorkspaceTitle } from '../../layout/WorkspaceTitle';
+import { TextInput } from '../../TextInput';
+import { success } from '../../Snackbar';
+import { FormError } from '../../forms/FormError';
+import { vtadmin } from '../../../proto/vtadmin';
+import ErrorDialog from '../../dialog/ErrorDialog';
+
+interface FormData {
+ clusterID: string;
+ keyspace: string;
+ ddlStrategy: string;
+ sql: string;
+ batchSize: number;
+ callerID: string;
+ migrationContext: string;
+ uuidList: string;
+}
+
+const DEFAULT_FORM_DATA: FormData = {
+ clusterID: '',
+ keyspace: '',
+ // Default DDL Strategy set to "vitess".
+ ddlStrategy: 'vitess',
+ sql: '',
+ batchSize: 0,
+ callerID: '',
+ migrationContext: '',
+ uuidList: '',
+};
+
+const DDL_STRATEGY_HELP_TEXT = `Online DDL strategy, compatible with @@ddl_strategy session variable (default "vitess")`;
+
+const MIGRATION_CONTEXT_HELP_TEXT =
+ 'For Online DDL, optionally supply a custom unique string used as context for the migration(s) in this command. By default a unique context is auto-generated by Vitess.';
+
+const CALLER_ID_HELP_TEXT =
+ 'Effective caller ID used for the operation and should map to an ACL name which grants this identity the necessary permissions to perform the operation (this is only necessary when strict table ACLs are used)';
+
+export const CreateSchemaMigration = () => {
+ useDocumentTitle('Create Schema Migration Request');
+
+ const history = useHistory();
+
+ const [formData, setFormData] = useState(DEFAULT_FORM_DATA);
+
+ const [clusterKeyspaces, setClusterKeyspaces] = useState([]);
+
+ const [errorDialogOpen, setErrorDialogOpen] = useState(false);
+
+ const { data: clusters = [], ...clustersQuery } = useClusters();
+
+ const { data: keyspaces = [], ...keyspacesQuery } = useKeyspaces();
+
+ const mutation = useApplySchema(
+ {
+ clusterID: formData.clusterID,
+ keyspace: formData.keyspace,
+ callerID: formData.callerID,
+ sql: formData.sql,
+ request: {
+ ddl_strategy: formData.ddlStrategy,
+ batch_size: formData.batchSize,
+ migration_context: formData.migrationContext,
+ uuid_list: (formData.uuidList && formData.uuidList.split(',').map((uuid) => uuid.trim())) || undefined,
+ },
+ },
+ {
+ onSuccess: () => {
+ success(`Successfully created schema migration request.`, { autoClose: 1600 });
+
+ history.push({
+ pathname: `/migrations`,
+ search: `?keyspace=${formData.keyspace}&cluster=${formData.clusterID}`,
+ });
+ },
+ onError: () => {
+ setErrorDialogOpen(true);
+ },
+ }
+ );
+
+ let selectedCluster = null;
+ if (!!formData.clusterID) {
+ selectedCluster = clusters.find((c) => c.id === formData.clusterID);
+ }
+
+ let selectedKeyspace = null;
+ if (!!formData.keyspace) {
+ selectedKeyspace = keyspaces.find((ks) => ks.keyspace?.name === formData.keyspace);
+ }
+
+ const isValid = !!selectedCluster && !!formData.keyspace && !!formData.sql && !!formData.ddlStrategy;
+
+ const isDisabled = !isValid || mutation.isLoading;
+
+ const onSubmit: React.FormEventHandler = (e) => {
+ e.preventDefault();
+ mutation.mutate();
+ };
+
+ useEffect(() => {
+ // Clear out the selected keyspaces if selected cluster is changed.
+ setFormData((prevFormData) => ({ ...prevFormData, keyspace: '' }));
+ setClusterKeyspaces(keyspaces.filter((ks) => ks.cluster?.id === formData.clusterID));
+ }, [formData.clusterID, keyspaces]);
+
+ useEffect(() => {
+ if (clusters.length === 1) {
+ setFormData((prevFormData) => ({ ...prevFormData, clusterID: clusters[0].id }));
+ }
+ }, [clusters]);
+
+ return (
+
+
+
+ Schema Migrations
+
+
+ Create Schema Migration Request
+
+
+
+
+
+ {mutation.isError && !mutation.isLoading && (
+ {
+ setErrorDialogOpen(false);
+ }}
+ />
+ )}
+
+
+ );
+};
diff --git a/web/vtadmin/src/components/routes/createWorkflow/CreateMaterialize.tsx b/web/vtadmin/src/components/routes/createWorkflow/CreateMaterialize.tsx
index c5d688a1fb7..81447cd0e6d 100644
--- a/web/vtadmin/src/components/routes/createWorkflow/CreateMaterialize.tsx
+++ b/web/vtadmin/src/components/routes/createWorkflow/CreateMaterialize.tsx
@@ -31,7 +31,7 @@ import Toggle from '../../toggle/Toggle';
import { tabletmanagerdata, vtadmin, vtctldata } from '../../../proto/vtadmin';
import { MultiSelect } from '../../inputs/MultiSelect';
import { TABLET_TYPES } from '../../../util/tablets';
-import ErrorDialog from './ErrorDialog';
+import ErrorDialog from '../../dialog/ErrorDialog';
interface FormData {
clusterID: string;
diff --git a/web/vtadmin/src/components/routes/createWorkflow/CreateMoveTables.tsx b/web/vtadmin/src/components/routes/createWorkflow/CreateMoveTables.tsx
index bca84cda4fa..1852d85b848 100644
--- a/web/vtadmin/src/components/routes/createWorkflow/CreateMoveTables.tsx
+++ b/web/vtadmin/src/components/routes/createWorkflow/CreateMoveTables.tsx
@@ -31,7 +31,7 @@ import Toggle from '../../toggle/Toggle';
import { vtadmin } from '../../../proto/vtadmin';
import { MultiSelect } from '../../inputs/MultiSelect';
import { TABLET_TYPES } from '../../../util/tablets';
-import ErrorDialog from './ErrorDialog';
+import ErrorDialog from '../../dialog/ErrorDialog';
interface FormData {
clusterID: string;
diff --git a/web/vtadmin/src/components/routes/createWorkflow/CreateReshard.tsx b/web/vtadmin/src/components/routes/createWorkflow/CreateReshard.tsx
index 4977c59e46b..05a33825174 100644
--- a/web/vtadmin/src/components/routes/createWorkflow/CreateReshard.tsx
+++ b/web/vtadmin/src/components/routes/createWorkflow/CreateReshard.tsx
@@ -31,7 +31,7 @@ import Toggle from '../../toggle/Toggle';
import { tabletmanagerdata, vtadmin } from '../../../proto/vtadmin';
import { MultiSelect } from '../../inputs/MultiSelect';
import { TABLET_TYPES } from '../../../util/tablets';
-import ErrorDialog from './ErrorDialog';
+import ErrorDialog from '../../dialog/ErrorDialog';
interface FormData {
clusterID: string;
diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts
index 9261f4f0eb0..18ab3b60a53 100644
--- a/web/vtadmin/src/hooks/api.ts
+++ b/web/vtadmin/src/hooks/api.ts
@@ -95,6 +95,8 @@ import {
showVDiff,
ShowVDiffParams,
createMaterialize,
+ fetchSchemaMigrations,
+ applySchema,
} from '../api/http';
import { vtadmin as pb, vtctldata } from '../proto/vtadmin';
import { formatAlias } from '../util/tablets';
@@ -796,3 +798,25 @@ export const useShowVDiff = (
) => {
return useQuery(['vdiff_show', params], () => showVDiff(params), { ...options });
};
+
+/**
+ * useSchemaMigrations is a query hook that fetches schema migrations.
+ */
+export const useSchemaMigrations = (
+ request: pb.IGetSchemaMigrationsRequest,
+ options?: UseQueryOptions | undefined
+) => {
+ return useQuery(['migrations', request], () => fetchSchemaMigrations(request), { ...options });
+};
+
+/**
+ * useApplySchema is a mutation query hook that creates ApplySchema request.
+ */
+export const useApplySchema = (
+ params: Parameters[0],
+ options: UseMutationOptions>, Error>
+) => {
+ return useMutation>, Error>(() => {
+ return applySchema(params);
+ }, options);
+};
diff --git a/web/vtadmin/src/proto/vtadmin.d.ts b/web/vtadmin/src/proto/vtadmin.d.ts
index 95b1448acef..410aaa644ff 100644
--- a/web/vtadmin/src/proto/vtadmin.d.ts
+++ b/web/vtadmin/src/proto/vtadmin.d.ts
@@ -3720,6 +3720,12 @@ export namespace vtadmin {
/** ApplySchemaRequest cluster_id */
cluster_id?: (string|null);
+ /** ApplySchemaRequest sql */
+ sql?: (string|null);
+
+ /** ApplySchemaRequest caller_id */
+ caller_id?: (string|null);
+
/** ApplySchemaRequest request */
request?: (vtctldata.IApplySchemaRequest|null);
}
@@ -3736,6 +3742,12 @@ export namespace vtadmin {
/** ApplySchemaRequest cluster_id. */
public cluster_id: string;
+ /** ApplySchemaRequest sql. */
+ public sql: string;
+
+ /** ApplySchemaRequest caller_id. */
+ public caller_id: string;
+
/** ApplySchemaRequest request. */
public request?: (vtctldata.IApplySchemaRequest|null);
diff --git a/web/vtadmin/src/proto/vtadmin.js b/web/vtadmin/src/proto/vtadmin.js
index f179934c800..b8ab0c1186a 100644
--- a/web/vtadmin/src/proto/vtadmin.js
+++ b/web/vtadmin/src/proto/vtadmin.js
@@ -7838,6 +7838,8 @@ export const vtadmin = $root.vtadmin = (() => {
* @memberof vtadmin
* @interface IApplySchemaRequest
* @property {string|null} [cluster_id] ApplySchemaRequest cluster_id
+ * @property {string|null} [sql] ApplySchemaRequest sql
+ * @property {string|null} [caller_id] ApplySchemaRequest caller_id
* @property {vtctldata.IApplySchemaRequest|null} [request] ApplySchemaRequest request
*/
@@ -7864,6 +7866,22 @@ export const vtadmin = $root.vtadmin = (() => {
*/
ApplySchemaRequest.prototype.cluster_id = "";
+ /**
+ * ApplySchemaRequest sql.
+ * @member {string} sql
+ * @memberof vtadmin.ApplySchemaRequest
+ * @instance
+ */
+ ApplySchemaRequest.prototype.sql = "";
+
+ /**
+ * ApplySchemaRequest caller_id.
+ * @member {string} caller_id
+ * @memberof vtadmin.ApplySchemaRequest
+ * @instance
+ */
+ ApplySchemaRequest.prototype.caller_id = "";
+
/**
* ApplySchemaRequest request.
* @member {vtctldata.IApplySchemaRequest|null|undefined} request
@@ -7898,8 +7916,12 @@ export const vtadmin = $root.vtadmin = (() => {
writer = $Writer.create();
if (message.cluster_id != null && Object.hasOwnProperty.call(message, "cluster_id"))
writer.uint32(/* id 1, wireType 2 =*/10).string(message.cluster_id);
+ if (message.sql != null && Object.hasOwnProperty.call(message, "sql"))
+ writer.uint32(/* id 2, wireType 2 =*/18).string(message.sql);
+ if (message.caller_id != null && Object.hasOwnProperty.call(message, "caller_id"))
+ writer.uint32(/* id 3, wireType 2 =*/26).string(message.caller_id);
if (message.request != null && Object.hasOwnProperty.call(message, "request"))
- $root.vtctldata.ApplySchemaRequest.encode(message.request, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim();
+ $root.vtctldata.ApplySchemaRequest.encode(message.request, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim();
return writer;
};
@@ -7939,6 +7961,14 @@ export const vtadmin = $root.vtadmin = (() => {
break;
}
case 2: {
+ message.sql = reader.string();
+ break;
+ }
+ case 3: {
+ message.caller_id = reader.string();
+ break;
+ }
+ case 4: {
message.request = $root.vtctldata.ApplySchemaRequest.decode(reader, reader.uint32());
break;
}
@@ -7980,6 +8010,12 @@ export const vtadmin = $root.vtadmin = (() => {
if (message.cluster_id != null && message.hasOwnProperty("cluster_id"))
if (!$util.isString(message.cluster_id))
return "cluster_id: string expected";
+ if (message.sql != null && message.hasOwnProperty("sql"))
+ if (!$util.isString(message.sql))
+ return "sql: string expected";
+ if (message.caller_id != null && message.hasOwnProperty("caller_id"))
+ if (!$util.isString(message.caller_id))
+ return "caller_id: string expected";
if (message.request != null && message.hasOwnProperty("request")) {
let error = $root.vtctldata.ApplySchemaRequest.verify(message.request);
if (error)
@@ -8002,6 +8038,10 @@ export const vtadmin = $root.vtadmin = (() => {
let message = new $root.vtadmin.ApplySchemaRequest();
if (object.cluster_id != null)
message.cluster_id = String(object.cluster_id);
+ if (object.sql != null)
+ message.sql = String(object.sql);
+ if (object.caller_id != null)
+ message.caller_id = String(object.caller_id);
if (object.request != null) {
if (typeof object.request !== "object")
throw TypeError(".vtadmin.ApplySchemaRequest.request: object expected");
@@ -8025,10 +8065,16 @@ export const vtadmin = $root.vtadmin = (() => {
let object = {};
if (options.defaults) {
object.cluster_id = "";
+ object.sql = "";
+ object.caller_id = "";
object.request = null;
}
if (message.cluster_id != null && message.hasOwnProperty("cluster_id"))
object.cluster_id = message.cluster_id;
+ if (message.sql != null && message.hasOwnProperty("sql"))
+ object.sql = message.sql;
+ if (message.caller_id != null && message.hasOwnProperty("caller_id"))
+ object.caller_id = message.caller_id;
if (message.request != null && message.hasOwnProperty("request"))
object.request = $root.vtctldata.ApplySchemaRequest.toObject(message.request, options);
return object;
diff --git a/web/vtadmin/src/util/schemaMigrations.ts b/web/vtadmin/src/util/schemaMigrations.ts
new file mode 100644
index 00000000000..c405c4dbecf
--- /dev/null
+++ b/web/vtadmin/src/util/schemaMigrations.ts
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2024 The Vitess Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { invertBy } from 'lodash-es';
+import { vtctldata } from '../proto/vtadmin';
+
+/**
+ * SCHEMA_MIGRATION_STATUS maps numeric schema migration status back to human readable strings.
+ */
+export const SCHEMA_MIGRATION_STATUS = Object.entries(invertBy(vtctldata.SchemaMigration.Status)).reduce(
+ (acc, [k, vs]) => {
+ acc[k] = vs[0];
+ return acc;
+ },
+ {} as { [k: string]: string }
+);
+
+export const formatSchemaMigrationStatus = (schemaMigration: vtctldata.ISchemaMigration) =>
+ schemaMigration.status && SCHEMA_MIGRATION_STATUS[schemaMigration.status];