Skip to content

Commit

Permalink
Merge branch 'mysql-checkconstraints-integration-test' into support-c…
Browse files Browse the repository at this point in the history
…heck-constraint-frontend
  • Loading branch information
akashthawaitcc authored and Vivek Yadav committed Dec 27, 2024
2 parents 4a2ca12 + a5067cd commit 099e79c
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 46 deletions.
4 changes: 3 additions & 1 deletion common/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"cloud.google.com/go/storage"
"github.com/GoogleCloudPlatform/spanner-migration-tool/common/constants"
"github.com/GoogleCloudPlatform/spanner-migration-tool/common/parse"
"github.com/GoogleCloudPlatform/spanner-migration-tool/expressions_api"
"github.com/GoogleCloudPlatform/spanner-migration-tool/internal"
"github.com/GoogleCloudPlatform/spanner-migration-tool/sources/common"
"github.com/GoogleCloudPlatform/spanner-migration-tool/sources/spanner"
Expand Down Expand Up @@ -445,7 +446,8 @@ func GetLegacyModeSupportedDrivers() []string {
func ReadSpannerSchema(ctx context.Context, conv *internal.Conv, client *sp.Client) error {
infoSchema := spanner.InfoSchemaImpl{Client: client, Ctx: ctx, SpDialect: conv.SpDialect}
processSchema := common.ProcessSchemaImpl{}
err := processSchema.ProcessSchema(conv, infoSchema, common.DefaultWorkers, internal.AdditionalSchemaAttributes{IsSharded: false}, &common.SchemaToSpannerImpl{}, &common.UtilsOrderImpl{}, &common.InfoSchemaImpl{})
expressionVerificationAccessor, _ := expressions_api.NewExpressionVerificationAccessorImpl(ctx, conv.SpProjectId, conv.SpInstanceId)
err := processSchema.ProcessSchema(conv, infoSchema, common.DefaultWorkers, internal.AdditionalSchemaAttributes{IsSharded: false}, &common.SchemaToSpannerImpl{ExpressionVerificationAccessor: expressionVerificationAccessor}, &common.UtilsOrderImpl{}, &common.InfoSchemaImpl{})
if err != nil {
return fmt.Errorf("error trying to read and convert spanner schema: %v", err)
}
Expand Down
210 changes: 205 additions & 5 deletions testing/mysql/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func TestIntegration_MYSQL_SchemaAndDataSubcommand(t *testing.T) {
filePrefix := filepath.Join(tmpdir, dbName)

host, user, srcDb, password := os.Getenv("MYSQLHOST"), os.Getenv("MYSQLUSER"), os.Getenv("MYSQLDATABASE"), os.Getenv("MYSQLPWD")
args := fmt.Sprintf("schema-and-data -source=%s -prefix=%s -source-profile='host=%s,user=%s,dbName=%s,password=%s' -target-profile='instance=%s,dbName=%s'", constants.MYSQL, filePrefix, host, user, srcDb, password, instanceID, dbName)
args := fmt.Sprintf("schema-and-data -source=%s -prefix=%s -source-profile='host=%s,user=%s,dbName=%s,password=%s' -target-profile='instance=%s,dbName=%s,project=%s'", constants.MYSQL, filePrefix, host, user, srcDb, password, instanceID, dbName, projectID)
err := common.RunCommand(args, projectID)
if err != nil {
t.Fatal(err)
Expand All @@ -134,23 +134,23 @@ func TestIntegration_MYSQL_SchemaAndDataSubcommand(t *testing.T) {
}

func runSchemaSubcommand(t *testing.T, dbName, filePrefix, sessionFile, dumpFilePath string) {
args := fmt.Sprintf("schema -prefix %s -source=mysql -target-profile='instance=%s,dbName=%s' < %s", filePrefix, instanceID, dbName, dumpFilePath)
args := fmt.Sprintf("schema -prefix %s -source=mysql -target-profile='instance=%s,dbName=%s,project=%s' < %s", filePrefix, instanceID, dbName, projectID, dumpFilePath)
err := common.RunCommand(args, projectID)
if err != nil {
t.Fatal(err)
}
}

func runDataSubcommand(t *testing.T, dbName, dbURI, filePrefix, sessionFile, dumpFilePath string) {
args := fmt.Sprintf("data -source=mysql -prefix %s -session %s -target-profile='instance=%s,dbName=%s' < %s", filePrefix, sessionFile, instanceID, dbName, dumpFilePath)
args := fmt.Sprintf("data -source=mysql -prefix %s -session %s -target-profile='instance=%s,dbName=%s,project=%s' < %s", filePrefix, sessionFile, instanceID, dbName, projectID, dumpFilePath)
err := common.RunCommand(args, projectID)
if err != nil {
t.Fatal(err)
}
}

func runSchemaAndDataSubcommand(t *testing.T, dbName, dbURI, filePrefix, dumpFilePath string) {
args := fmt.Sprintf("schema-and-data -source=mysql -prefix %s -target-profile='instance=%s,dbName=%s' < %s", filePrefix, instanceID, dbName, dumpFilePath)
args := fmt.Sprintf("schema-and-data -source=mysql -prefix %s -target-profile='instance=%s,dbName=%s,project=%s' < %s", filePrefix, instanceID, dbName, projectID, dumpFilePath)
err := common.RunCommand(args, projectID)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -228,7 +228,7 @@ func TestIntegration_MYSQL_ForeignKeyActionMigration(t *testing.T) {
filePrefix := filepath.Join(tmpdir, dbName)

host, user, srcDb, password := os.Getenv("MYSQLHOST"), os.Getenv("MYSQLUSER"), os.Getenv("MYSQLDB_FKACTION"), os.Getenv("MYSQLPWD")
args := fmt.Sprintf("schema-and-data -source=%s -prefix=%s -source-profile='host=%s,user=%s,dbName=%s,password=%s' -target-profile='instance=%s,dbName=%s'", constants.MYSQL, filePrefix, host, user, srcDb, password, instanceID, dbName)
args := fmt.Sprintf("schema-and-data -source=%s -prefix=%s -source-profile='host=%s,user=%s,dbName=%s,password=%s' -target-profile='instance=%s,dbName=%s,project=%s'", constants.MYSQL, filePrefix, host, user, srcDb, password, instanceID, dbName, projectID)
err := common.RunCommand(args, projectID)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -348,6 +348,206 @@ func checkForeignKeyActions(ctx context.Context, t *testing.T, dbURI string) {
assert.Equal(t, iterator.Done, err, "Expected rows in table 'cart' with productid 'zxi-631' to be deleted")
}

func TestIntegration_MySQLDUMP_CheckConstraintMigration(t *testing.T) {
onlyRunForEmulatorTest(t)
tmpdir := prepareIntegrationTest(t)
defer os.RemoveAll(tmpdir)

dbName := "test-check-constraint"
dumpFilePath := "../../test_data/mysql_checkconstraint_dump.test.out"
filePrefix := filepath.Join(tmpdir, dbName)
dbURI := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, dbName)
runSchemaAndDataSubcommand(t, dbName, dbURI, filePrefix, dumpFilePath)

defer dropDatabase(t, dbURI)
checkCheckConstraints(ctx, t, dbURI)
}

func checkCheckConstraints(ctx context.Context, t *testing.T, dbURI string) {
client, err := spanner.NewClient(ctx, dbURI)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
defer client.Close()

// Execute DDL statements
executeDDL := func(ddl string) {
op, err := databaseAdmin.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
Database: dbURI,
Statements: []string{ddl},
})
if err != nil {
t.Fatalf("Failed to execute DDL: %v", err)
}
if err := op.Wait(ctx); err != nil {
t.Fatalf("Failed to complete DDL operation: %v", err)
}
}

// Insert or update data
insertOrUpdateData := func(data map[string]interface{}) error {
_, err := client.Apply(ctx, []*spanner.Mutation{
spanner.InsertOrUpdateMap("TestTable", data),
})
return err
}

// Check if a constraint violation occurs
checkConstraintViolation := func(data map[string]interface{}, expectedErr string) {
err := insertOrUpdateData(data)
if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("Expected constraint violation for '%s' but got none or wrong error: %v", expectedErr, err)
}
}

// Create table in spanner with various check constraints
executeDDL(`CREATE TABLE TestTable (
ID INT64 NOT NULL,
Value INT64,
Flag BOOL,
Date TIMESTAMP,
Name STRING(MAX),
EnumValue STRING(MAX),
BooleanValue INT64,
CONSTRAINT chk_PositiveValue CHECK (Value >= 0),
CONSTRAINT chk_ComplexCondition CHECK ((Flag IS TRUE AND Value > 10) OR (Flag IS FALSE AND Value <= 10)),
CONSTRAINT chk_NullValue CHECK (Value IS NOT NULL),
CONSTRAINT chk_StringLength CHECK (LENGTH(Name) > 5),
CONSTRAINT chk_Enum CHECK (EnumValue IN ('OptionA', 'OptionB', 'OptionC')),
CONSTRAINT chk_Boolean CHECK (BooleanValue IN (0, 1))
) PRIMARY KEY (ID)`)

// Test Case 1: Valid Insert for chk_PositiveValue
err = insertOrUpdateData(map[string]interface{}{
"ID": 1,
"Value": 5,
"Flag": false,
"Date": time.Now(),
"Name": "ValidName",
"EnumValue": "OptionA",
"BooleanValue": 1,

})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_PositiveValue: %v", err)
}

// Test Case 2: Invalid Insert for chk_PositiveValue (Negative Value)
checkConstraintViolation(map[string]interface{}{
"ID": 2,
"Value": -1, // Value < 0
"Flag": false,
}, "chk_PositiveValue")

// Test Case 3: Valid Insert for chk_ComplexCondition (Flag TRUE, Value > 10)
err = insertOrUpdateData(map[string]interface{}{
"ID": 3,
"Value": 20, // Value > 10 because Flag is TRUE
"Flag": true,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_ComplexCondition (TRUE, Value > 10): %v", err)
}

// Test Case 4: Valid Insert for chk_ComplexCondition (Flag FALSE, Value <= 10)
err = insertOrUpdateData(map[string]interface{}{
"ID": 4,
"Value": 5, // Value <= 10 because Flag is FALSE
"Flag": false,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_ComplexCondition (FALSE, Value <= 10): %v", err)
}

// Test Case 5: Invalid Insert (Value is invalid for Flag = TRUE)
checkConstraintViolation(map[string]interface{}{
"ID": 5,
"Value": 5, // Value must be > 10 because Flag is TRUE
"Flag": true,
}, "chk_ComplexCondition")

// Test Case 6: Invalid Insert (Value is invalid for Flag = FALSE)
checkConstraintViolation(map[string]interface{}{
"ID": 6,
"Value": 15, // Value must be <= 10 because Flag is FALSE
"Flag": false,
}, "chk_ComplexCondition")

// Test Case 7: Valid Insert for chk_NullValue (Value is not NULL)
err = insertOrUpdateData(map[string]interface{}{
"ID": 7,
"Value": 10, // Value is not NULL
"Flag": false,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_NullValue: %v", err)
}

// Test Case 8: Invalid Insert for chk_NullValue (NULL Value)
checkConstraintViolation(map[string]interface{}{
"ID": 8,
"Value": nil, // NULL Value is not allowed
"Flag": false,
}, "chk_NullValue")

// Test Case 9: Valid Insert for chk_StringLength (Name length > 5)
err = insertOrUpdateData(map[string]interface{}{
"ID": 9,
"Name": "ValidName", // Name length > 5
"Flag": false,
"Value": 10,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_StringLength: %v", err)
}

// Test Case 10: Invalid Insert for chk_StringLength (Name length <= 5)
checkConstraintViolation(map[string]interface{}{
"ID": 10,
"Name": "Test", // Name length <= 5
"Flag": false,
"Value": 10,
}, "chk_StringLength")

// Test Case 11: Valid Insert for chk_Enum (Valid Enum)
err = insertOrUpdateData(map[string]interface{}{
"ID": 11,
"EnumValue": "OptionB", // Valid enum value
"Flag": false,
"Value": 10,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_Enum: %v", err)
}

// Test Case 12: Invalid Insert for chk_Enum (Invalid Enum)
checkConstraintViolation(map[string]interface{}{
"ID": 12,
"EnumValue": "InvalidOption", // Invalid enum value
"Flag": false,
"Value": 10,
}, "chk_Enum")

// Test Case 13: Valid Insert for chk_Boolean (Valid boolean 0 or 1)
err = insertOrUpdateData(map[string]interface{}{
"ID": 13,
"Value": 1, // Valid boolean value
"Flag": false,
"BooleanValue": 1,
})
if err != nil {
t.Fatalf("Failed to insert valid data for chk_Boolean: %v", err)
}

// Test Case 14: Invalid Insert for chk_Boolean (Invalid boolean value)
checkConstraintViolation(map[string]interface{}{
"ID": 14,
"Value": 2,
"Flag": false,
"BooleanValue": 2,// Invalid boolean representation
}, "chk_Boolean")
}

func onlyRunForEmulatorTest(t *testing.T) {
if os.Getenv("SPANNER_EMULATOR_HOST") == "" {
t.Skip("Skipping tests only running against the emulator.")
Expand Down
78 changes: 39 additions & 39 deletions ui/src/app/components/object-detail/object-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,15 @@ <h3 class="title">
<ng-container matColumnDef="srcDefaultValue" *ngIf="mySqlSource">
<th mat-header-cell class="table_header" *matHeaderCellDef>Default Value</th>
<td mat-cell *matCellDef="let element">
<div
class="trimmed-text"
matTooltip="{{ element.get('srcDefaultValue').value }}"
<div
class="trimmed-text"
matTooltip="{{ element.get('srcDefaultValue').value }}"
matTooltipPosition="above">
{{ element.get('srcDefaultValue').value }}
</div>
</td>
</ng-container>
</ng-container>

<tr mat-header-row *matHeaderRowDef="['srcDatabase']"></tr>
<tr mat-header-row *matHeaderRowDef="srcDisplayedColumns"></tr>
<tr mat-row [ngClass]="{ 'scr-column-data-edit-mode': isEditMode }"
Expand Down Expand Up @@ -719,6 +719,40 @@ <h3 class="title">
</div>
</mat-tab>

<mat-tab label="INTERLEAVE" *ngIf="
((interleaveStatus.tableInterleaveStatus &&
interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null) &&
currentObject.isSpannerNode
">
<div class="interleave-tab-container">
<button *ngIf="
interleaveStatus.tableInterleaveStatus &&
interleaveStatus.tableInterleaveStatus.Possible &&
this.interleaveParentName === null
" mat-raised-button color="primary" (click)="setInterleave()">
Convert to Interleave
</button>
<button *ngIf="
(interleaveStatus.tableInterleaveStatus &&
!interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null
" mat-raised-button color="primary" (click)="removeInterleave()">
Convert Back to Foreign Key
</button>
<br />
<div *ngIf="
(interleaveStatus.tableInterleaveStatus &&
!interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null
">
This table is interleaved with
<span class="interleave-parent-table">{{ interleaveParentName }}</span>. Click on the above button to convert
back to foreign key.
</div>
</div>
</mat-tab>

<mat-tab>
<ng-template mat-tab-label>
<span>CHECK CONSTRAINTS</span>
Expand Down Expand Up @@ -840,40 +874,6 @@ <h3 class="title">
</div>
</mat-tab>

<mat-tab label="INTERLEAVE" *ngIf="
((interleaveStatus.tableInterleaveStatus &&
interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null) &&
currentObject.isSpannerNode
">
<div class="interleave-tab-container">
<button *ngIf="
interleaveStatus.tableInterleaveStatus &&
interleaveStatus.tableInterleaveStatus.Possible &&
this.interleaveParentName === null
" mat-raised-button color="primary" (click)="setInterleave()">
Convert to Interleave
</button>
<button *ngIf="
(interleaveStatus.tableInterleaveStatus &&
!interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null
" mat-raised-button color="primary" (click)="removeInterleave()">
Convert Back to Foreign Key
</button>
<br />
<div *ngIf="
(interleaveStatus.tableInterleaveStatus &&
!interleaveStatus.tableInterleaveStatus.Possible) ||
this.interleaveParentName !== null
">
This table is interleaved with
<span class="interleave-parent-table">{{ interleaveParentName }}</span>. Click on the above button to convert
back to foreign key.
</div>
</div>
</mat-tab>

<mat-tab *ngIf="currentObject!.isSpannerNode && !currentObject!.isDeleted">
<ng-template mat-tab-label>
<span>SQL</span>
Expand Down
3 changes: 2 additions & 1 deletion ui/src/app/services/fetch/fetch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import ICreateSequence from 'src/app/model/auto-gen'
providedIn: 'root',
})
export class FetchService {
private url: string = window.location.origin
// private url: string = window.location.origin
private url: string = 'http://localhost:8080'
constructor(private http: HttpClient) {}

connectTodb(payload: IDbConfig, dialect: string) {
Expand Down

0 comments on commit 099e79c

Please sign in to comment.