From 90a23adabb70a2da2d24f19201edaa51e2a8c2d0 Mon Sep 17 00:00:00 2001 From: "newton@alisx.com" Date: Thu, 15 Aug 2024 12:19:06 +0300 Subject: [PATCH] chore(spanner): refactor table replace on column attrs change --- .../alis_google_spanner_table_resource.go | 106 ++++++++-- internal/spanner/services/services.go | 130 ++++++++---- internal/spanner/services/services_test.go | 187 ++++++++++++++++-- internal/spanner/services/types.go | 44 ++++- internal/spanner/services/utils.go | 1 - 5 files changed, 391 insertions(+), 77 deletions(-) diff --git a/internal/spanner/alis_google_spanner_table_resource.go b/internal/spanner/alis_google_spanner_table_resource.go index 72fb4c9..037635c 100644 --- a/internal/spanner/alis_google_spanner_table_resource.go +++ b/internal/spanner/alis_google_spanner_table_resource.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -167,9 +167,6 @@ func (r *spannerTableResource) Schema(_ context.Context, _ resource.SchemaReques "Multiple columns can be specified as primary keys to create a composite primary key.\n" + "Primary key columns must be non-null.\n" + "**Changing this value will cause a table replace**.", - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, }, "is_computed": schema.BoolAttribute{ Optional: true, @@ -178,9 +175,6 @@ func (r *spannerTableResource) Schema(_ context.Context, _ resource.SchemaReques "A common use case is to generate a column from a PROTO column field.\n" + "This should be accompanied by a `computation_ddl` field.\n" + "**Changing this value will cause a table replace**.", - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, }, "computation_ddl": schema.StringAttribute{ Optional: true, @@ -189,9 +183,6 @@ func (r *spannerTableResource) Schema(_ context.Context, _ resource.SchemaReques "The expression must be a valid SQL expression that generates a value for the column.\n" + "Example: `column1 + column2`, or `proto_column.field`.\n" + "**Changing this value will cause a table replace**.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "auto_increment": schema.BoolAttribute{ Optional: true, @@ -210,9 +201,6 @@ func (r *spannerTableResource) Schema(_ context.Context, _ resource.SchemaReques Description: "The data type of the column.\n" + "Valid types are: `BOOL`, `INT64`, `FLOAT64`, `STRING`, `BYTES`, `DATE`, `TIMESTAMP`, `JSON`, `PROTO`, `ARRAY`, `ARRAY`, `ARRAY`, `ARRAY`.\n" + "**Changing this value will cause a table replace**.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "size": schema.Int64Attribute{ Optional: true, @@ -263,6 +251,98 @@ func (r *spannerTableResource) Schema(_ context.Context, _ resource.SchemaReques }, }, Description: "The columns of the table.", + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplaceIf(func(ctx context.Context, req planmodifier.ListRequest, resp *listplanmodifier.RequiresReplaceIfFuncResponse) { + // Create a map of the columns by name + type PriorAndCurrentColumns struct { + Prior *spannerTableColumn + Current *spannerTableColumn + } + columnsMap := make(map[string]*PriorAndCurrentColumns) + + // Get the columns prior to the plan + priorColumns := make([]spannerTableColumn, 0, len(req.StateValue.Elements())) + d := req.StateValue.ElementsAs(ctx, &priorColumns, false) + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + for _, column := range priorColumns { + if _, ok := columnsMap[column.Name.ValueString()]; !ok { + columnsMap[column.Name.ValueString()] = &PriorAndCurrentColumns{} + } + columnsMap[column.Name.ValueString()].Prior = &column + } + + // Get the columns after the plan + currentColumns := make([]spannerTableColumn, 0, len(req.PlanValue.Elements())) + d = req.PlanValue.ElementsAs(ctx, ¤tColumns, false) + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + for _, column := range currentColumns { + if _, ok := columnsMap[column.Name.ValueString()]; !ok { + columnsMap[column.Name.ValueString()] = &PriorAndCurrentColumns{} + } + columnsMap[column.Name.ValueString()].Current = &column + } + + // Check if the columns are the same. + // Columns that are new do not require a replace, unless a primary key is added. + // Columns that are removed do not require a replace, unless they are part of the primary key. + // Columns that are updated require a replace if: the column type is changed, + // the primary key status is changed, or the column's computation_ddl is changed. + for name, columns := range columnsMap { + // Column is new + if columns.Prior == nil && columns.Current != nil { + // Check if the column is a primary key + if !columns.Current.IsPrimaryKey.IsNull() && columns.Current.IsPrimaryKey.ValueBool() { + resp.RequiresReplace = true + resp.Diagnostics.AddWarning(fmt.Sprintf("Column %q requires a table replace", name), fmt.Sprintf("Column %q is a new primary key column and requires a table replace", name)) + } + continue + } + + // Column is removed + if columns.Current == nil && columns.Prior != nil { + // Check if the column is a primary key + if !columns.Prior.IsPrimaryKey.IsNull() && columns.Prior.IsPrimaryKey.ValueBool() { + resp.RequiresReplace = true + resp.Diagnostics.AddWarning(fmt.Sprintf("Column %q requires a table replace", name), fmt.Sprintf("Column %q is a removed primary key column and requires a table replace", name)) + } + continue + } + + // Column type is changed + // Type is required, so we can safely assume it is not null + if columns.Prior.Type.ValueString() != columns.Current.Type.ValueString() { + resp.RequiresReplace = true + resp.Diagnostics.AddWarning(fmt.Sprintf("Column %q requires a table replace", name), fmt.Sprintf("Column %q has a changed type and requires a table replace", name)) + } + + // Column primary key status is changed + // This is not required, so we also need to check if it is null + if (!columns.Prior.IsPrimaryKey.IsNull() && !columns.Current.IsPrimaryKey.IsNull() && columns.Prior.IsPrimaryKey.ValueBool() != columns.Current.IsPrimaryKey.ValueBool()) || + (columns.Prior.IsPrimaryKey.IsNull() && !columns.Current.IsPrimaryKey.IsNull() && columns.Current.IsPrimaryKey.ValueBool()) || + (!columns.Prior.IsPrimaryKey.IsNull() && columns.Prior.IsPrimaryKey.ValueBool() && columns.Current.IsPrimaryKey.IsNull()) { + resp.RequiresReplace = true + resp.Diagnostics.AddWarning(fmt.Sprintf("Column %q requires a table replace", name), fmt.Sprintf("Column %q has a changed primary key status and requires a table replace", name)) + } + + // Column is computed and computation_ddl is changed + // Both fields are required but only if at least one is set + if (!columns.Prior.IsComputed.IsNull() && columns.Prior.IsComputed.ValueBool() && !columns.Current.IsComputed.IsNull() && columns.Current.IsComputed.ValueBool() && + columns.Prior.ComputationDdl.ValueString() != columns.Current.ComputationDdl.ValueString()) || + (!columns.Prior.IsComputed.IsNull() && columns.Prior.IsComputed.ValueBool() && (columns.Current.IsComputed.IsNull() || !columns.Current.IsComputed.ValueBool())) { + resp.RequiresReplace = true + resp.Diagnostics.AddWarning(fmt.Sprintf("Column %q requires a table replace", name), fmt.Sprintf("Column %q has a changed computation_ddl or is_computed has been disabled and requires a table replace", name)) + } + } + + }, + "If certain values of any of the columns change, Terraform will destroy and recreate the table.", "If certain values of any of the columns change, Terraform will destroy and recreate the table."), + }, }, }, Description: "The schema of the table.", diff --git a/internal/spanner/services/services.go b/internal/spanner/services/services.go index ce5a0c4..ddf2557 100644 --- a/internal/spanner/services/services.go +++ b/internal/spanner/services/services.go @@ -2235,7 +2235,7 @@ func (s *SpannerService) DeleteSpannerTableIndex(ctx context.Context, parent str return &emptypb.Empty{}, nil } -func (s *SpannerService) CreateSpannerTableForeignKeysConstraint(ctx context.Context, parent string, constraint *SpannerTableForeignKeysConstraint) (*SpannerTableForeignKeysConstraint, error) { +func (s *SpannerService) CreateSpannerTableForeignKeyConstraint(ctx context.Context, parent string, constraint *SpannerTableForeignKeyConstraint) (*SpannerTableForeignKeyConstraint, error) { // Validate parent googleSqlParentValid := utils.ValidateArgument(parent, utils.SpannerGoogleSqlTableNameRegex) postgresSqlParentValid := utils.ValidateArgument(parent, utils.SpannerPostgresSqlTableNameRegex) @@ -2251,40 +2251,33 @@ func (s *SpannerService) CreateSpannerTableForeignKeysConstraint(ctx context.Con if !googleSqlConstraintIdValid && !postgresSqlConstraintIdValid { return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.name (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", constraint.Name, utils.SpannerGoogleSqlConstraintIdRegex, utils.SpannerPostgresSqlConstraintIdRegex) } - if constraint.ForeignKeys == nil || len(constraint.ForeignKeys) == 0 { - return nil, status.Error(codes.InvalidArgument, "Invalid argument constraint.foreign_keys, field is required but not provided") - } // Validate foreign key fields - for i, foreignKey := range constraint.ForeignKeys { - if foreignKey == nil { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d], field is required but not provided", i) - } - if foreignKey.ReferencedTable == "" { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].referenced_table, field is required but not provided", i) - } - googleSqlForeignKeyTableValid := utils.ValidateArgument(foreignKey.ReferencedTable, utils.SpannerGoogleSqlTableNameRegex) - postgresSqlForeignKeyTableValid := utils.ValidateArgument(foreignKey.ReferencedTable, utils.SpannerPostgresSqlTableNameRegex) - if !googleSqlForeignKeyTableValid && !postgresSqlForeignKeyTableValid { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].referenced_table (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", i, foreignKey.ReferencedTable, utils.SpannerGoogleSqlTableNameRegex, utils.SpannerPostgresSqlTableNameRegex) - } - if foreignKey.ReferencedColumn == "" { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].referenced_columns, field is required but not provided", i) - } - googleSqlForeignKeyColumnValid := utils.ValidateArgument(foreignKey.ReferencedColumn, utils.SpannerGoogleSqlColumnIdRegex) - postgresSqlForeignKeyColumnValid := utils.ValidateArgument(foreignKey.ReferencedColumn, utils.SpannerPostgresSqlColumnIdRegex) - if !googleSqlForeignKeyColumnValid && !postgresSqlForeignKeyColumnValid { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].referenced_columns (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", i, foreignKey.ReferencedColumn, utils.SpannerGoogleSqlColumnIdRegex, utils.SpannerPostgresSqlColumnIdRegex) - } + if constraint.ReferencedTable == "" { + return nil, status.Error(codes.InvalidArgument, "Invalid argument constraint.referenced_table, field is required but not provided") + } + googleSqlForeignKeyTableValid := utils.ValidateArgument(constraint.ReferencedTable, utils.SpannerGoogleSqlTableIdRegex) + postgresSqlForeignKeyTableValid := utils.ValidateArgument(constraint.ReferencedTable, utils.SpannerPostgresSqlTableIdRegex) + if !googleSqlForeignKeyTableValid && !postgresSqlForeignKeyTableValid { + return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.referenced_table (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", constraint.ReferencedTable, utils.SpannerGoogleSqlTableNameRegex, utils.SpannerPostgresSqlTableNameRegex) + } - if foreignKey.Column == "" { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].column, field is required but not provided", i) - } - googleSqlColumnValid := utils.ValidateArgument(foreignKey.Column, utils.SpannerGoogleSqlColumnIdRegex) - postgresSqlColumnValid := utils.ValidateArgument(foreignKey.Column, utils.SpannerPostgresSqlColumnIdRegex) - if !googleSqlColumnValid && !postgresSqlColumnValid { - return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.foreign_keys[%d].column (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", i, foreignKey.Column, utils.SpannerGoogleSqlColumnIdRegex, utils.SpannerPostgresSqlColumnIdRegex) - } + if constraint.ReferencedColumn == "" { + return nil, status.Error(codes.InvalidArgument, "Invalid argument constraint.referenced_column, field is required but not provided") + } + googleSqlForeignKeyColumnValid := utils.ValidateArgument(constraint.ReferencedColumn, utils.SpannerGoogleSqlColumnIdRegex) + postgresSqlForeignKeyColumnValid := utils.ValidateArgument(constraint.ReferencedColumn, utils.SpannerPostgresSqlColumnIdRegex) + if !googleSqlForeignKeyColumnValid && !postgresSqlForeignKeyColumnValid { + return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.referenced_column (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", constraint.ReferencedColumn, utils.SpannerGoogleSqlColumnIdRegex, utils.SpannerPostgresSqlColumnIdRegex) + } + + if constraint.Column == "" { + return nil, status.Error(codes.InvalidArgument, "Invalid argument constraint.column, field is required but not provided") + } + googleSqlColumnValid := utils.ValidateArgument(constraint.Column, utils.SpannerGoogleSqlColumnIdRegex) + postgresSqlColumnValid := utils.ValidateArgument(constraint.Column, utils.SpannerPostgresSqlColumnIdRegex) + if !googleSqlColumnValid && !postgresSqlColumnValid { + return nil, status.Errorf(codes.InvalidArgument, "Invalid argument constraint.column (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", constraint.Column, utils.SpannerGoogleSqlColumnIdRegex, utils.SpannerPostgresSqlColumnIdRegex) } // Deconstruct parent name to get project, instance, database and table @@ -2310,14 +2303,79 @@ func (s *SpannerService) CreateSpannerTableForeignKeysConstraint(ctx context.Con return nil, status.Errorf(codes.Internal, "Error connecting to database: %v", err) } - sqlStatement := fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s", tableId, constraint.Name) - for _, foreignKey := range constraint.ForeignKeys { - sqlStatement += fmt.Sprintf(" FOREIGN KEY (%s) REFERENCES %s(%s)", foreignKey.Column, foreignKey.ReferencedTable, foreignKey.ReferencedColumn) + sqlStatement := fmt.Sprintf("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)", tableId, constraint.Name, constraint.Column, constraint.ReferencedTable, constraint.ReferencedColumn) + if constraint.OnDelete != SpannerTableForeignKeyConstraintActionUnspecified { + sqlStatement += fmt.Sprintf(" ON DELETE %s", constraint.OnDelete.String()) } - if err := db.Exec(sqlStatement).Error; err != nil { return nil, status.Errorf(codes.Internal, "Error creating foreign key constraint: %v", err) } return constraint, nil } + +func (s *SpannerService) GetSpannerTableForeignKeyConstraint(ctx context.Context, parent string, name string) (*SpannerTableForeignKeyConstraint, error) { + // Validate parent + googleSqlParentValid := utils.ValidateArgument(parent, utils.SpannerGoogleSqlTableNameRegex) + postgresSqlParentValid := utils.ValidateArgument(parent, utils.SpannerPostgresSqlTableNameRegex) + if !googleSqlParentValid && !postgresSqlParentValid { + return nil, status.Errorf(codes.InvalidArgument, "Invalid argument parent (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", parent, utils.SpannerGoogleSqlTableNameRegex, utils.SpannerPostgresSqlTableNameRegex) + } + + // Validate name + googleSqlConstraintIdValid := utils.ValidateArgument(name, utils.SpannerGoogleSqlConstraintIdRegex) + postgresSqlConstraintIdValid := utils.ValidateArgument(name, utils.SpannerPostgresSqlConstraintIdRegex) + if !googleSqlConstraintIdValid && !postgresSqlConstraintIdValid { + return nil, status.Errorf(codes.InvalidArgument, "Invalid argument name (%s), must match `%s` for GoogleSql dialect or `%s` for PostgreSQL dialect", name, utils.SpannerGoogleSqlConstraintIdRegex, utils.SpannerPostgresSqlConstraintIdRegex) + } + + // Deconstruct parent name to get project, instance, database and table + parentNameParts := strings.Split(parent, "/") + project := parentNameParts[1] + instance := parentNameParts[3] + databaseId := parentNameParts[5] + tableId := parentNameParts[7] + + db, err := gorm.Open( + spannergorm.New( + spannergorm.Config{ + DriverName: "spanner", + DSN: fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, databaseId), + }, + ), + &gorm.Config{ + PrepareStmt: true, + Logger: tfLogger, + }, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, "Error connecting to database: %v", err) + } + + sqlStatement := ` + SELECT + TABLE_CONSTRAINTS.CONSTRAINT_NAME, + TABLE_CONSTRAINTS.TABLE_NAME, + TABLE_CONSTRAINTS.CONSTRAINT_TYPE, + REFERENTIAL_CONSTRAINTS.UPDATE_RULE, + REFERENTIAL_CONSTRAINTS.DELETE_RULE + FROM + INFORMATION_SCHEMA.TABLE_CONSTRAINTS + INNER JOIN + INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + ON + TABLE_CONSTRAINTS.CONSTRAINT_NAME = REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME + WHERE TABLE_CONSTRAINTS.TABLE_NAME = ? and TABLE_CONSTRAINTS.CONSTRAINT_NAME = ? AND TABLE_CONSTRAINTS.CONSTRAINT_TYPE = "FOREIGN KEY" + ` + + var result *Constraint + db = db.Raw(sqlStatement, tableId, name).Scan(&result) + if db.Error != nil { + return nil, status.Errorf(codes.Internal, "Error getting foreign key constraint: %v", db.Error) + } + if result == nil { + return nil, status.Errorf(codes.NotFound, "Foreign key constraint %s not found", name) + } + + return nil, nil +} diff --git a/internal/spanner/services/services_test.go b/internal/spanner/services/services_test.go index 04f25d4..b6a3a85 100644 --- a/internal/spanner/services/services_test.go +++ b/internal/spanner/services/services_test.go @@ -345,31 +345,92 @@ func TestCreateSpannerTable(t *testing.T) { args: args{ ctx: context.Background(), parent: fmt.Sprintf("projects/%s/instances/%s/databases/%s", TestProject, TestInstance, "alis_px_dev_cmk"), - tableId: "tftest", + tableId: "portfolios", table: &SpannerTable{ - Name: "tftest", + Name: "portfolios", Schema: &SpannerTableSchema{ Columns: []*SpannerTableColumn{ { - Name: "key", - IsPrimaryKey: wrapperspb.Bool(true), - Unique: wrapperspb.Bool(false), - Type: "INT64", - Size: wrapperspb.Int64(255), - Required: wrapperspb.Bool(true), + Name: "key", + IsPrimaryKey: wrapperspb.Bool(true), + Unique: wrapperspb.Bool(false), + Type: "INT64", + Size: wrapperspb.Int64(255), + Required: wrapperspb.Bool(true), + AutoIncrement: wrapperspb.Bool(true), }, { - Name: "test", - IsPrimaryKey: wrapperspb.Bool(false), - Unique: wrapperspb.Bool(false), - Type: "PROTO", - ProtoFileDescriptorSet: &ProtoFileDescriptorSet{ - ProtoPackage: wrapperspb.String("alis.px.services.data.v2.SpannerTest.NestedEnum"), - FileDescriptorSetPath: wrapperspb.String("gcs:gs://internal.descriptorset.alis-px-product-g51dmvo.alis.services/descriptorset.pb"), - FileDescriptorSetPathSource: ProtoFileDescriptorSetSourceGcs, - }, + Name: "portfolio_id", + Type: "STRING", + Size: wrapperspb.Int64(255), }, //{ + // Name: "test", + // IsPrimaryKey: wrapperspb.Bool(false), + // Unique: wrapperspb.Bool(false), + // Type: "PROTO", + // ProtoFileDescriptorSet: &ProtoFileDescriptorSet{ + // ProtoPackage: wrapperspb.String("alis.px.services.data.v2.SpannerTest.NestedEnum"), + // FileDescriptorSetPath: wrapperspb.String("gcs:gs://internal.descriptorset.alis-px-product-g51dmvo.alis.services/descriptorset.pb"), + // FileDescriptorSetPathSource: ProtoFileDescriptorSetSourceGcs, + // }, + //}, + //{ + // Name: "branch_test", + // IsPrimaryKey: wrapperspb.Bool(false), + // Unique: wrapperspb.Bool(false), + // Type: "STRING", + // //IsComputed: wrapperspb.Bool(true), + // //ComputationDdl: wrapperspb.String("branch.name"), + // Required: wrapperspb.Bool(false), + // Size: wrapperspb.Int64(255), + //}, + }, + }, + }, + }, + }, + { + name: "Test_CreateSpannerTable", + args: args{ + ctx: context.Background(), + parent: fmt.Sprintf("projects/%s/instances/%s/databases/%s", TestProject, TestInstance, "alis_px_dev_cmk"), + tableId: "branches", + table: &SpannerTable{ + Name: "branches", + Schema: &SpannerTableSchema{ + Columns: []*SpannerTableColumn{ + { + Name: "key", + IsPrimaryKey: wrapperspb.Bool(true), + Unique: wrapperspb.Bool(false), + Type: "INT64", + Size: wrapperspb.Int64(255), + Required: wrapperspb.Bool(true), + AutoIncrement: wrapperspb.Bool(true), + }, + { + Name: "parent", + Type: "STRING", + Size: wrapperspb.Int64(255), + }, + { + Name: "branch_id", + Type: "STRING", + Size: wrapperspb.Int64(255), + }, + //{ + // Name: "test", + // IsPrimaryKey: wrapperspb.Bool(false), + // Unique: wrapperspb.Bool(false), + // Type: "PROTO", + // ProtoFileDescriptorSet: &ProtoFileDescriptorSet{ + // ProtoPackage: wrapperspb.String("alis.px.services.data.v2.SpannerTest.NestedEnum"), + // FileDescriptorSetPath: wrapperspb.String("gcs:gs://internal.descriptorset.alis-px-product-g51dmvo.alis.services/descriptorset.pb"), + // FileDescriptorSetPathSource: ProtoFileDescriptorSetSourceGcs, + // }, + //}, + //{ // Name: "branch_test", // IsPrimaryKey: wrapperspb.Bool(false), // Unique: wrapperspb.Bool(false), @@ -413,7 +474,7 @@ func TestGetSpannerTable(t *testing.T) { name: "Test_GetSpannerTable", args: args{ ctx: context.Background(), - name: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "alis_px_dev_cmk", "tftest"), + name: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "mentenova-co", "mentenova_co_dev_62g_Maps"), }, want: &SpannerTable{}, wantErr: false, @@ -556,7 +617,7 @@ func TestDeleteSpannerTable(t *testing.T) { name: "Test_DeleteSpannerTable", args: args{ ctx: context.Background(), - name: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "tf-test", "tftest"), + name: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "mentenova-co", "mentenova_co_dev_62g_Tasks"), }, }, } @@ -1324,3 +1385,91 @@ func TestCreateProtoBundle(t *testing.T) { }) } } + +func TestSpannerService_CreateSpannerTableForeignKeyConstraint(t *testing.T) { + type fields struct { + GoogleCredentials *googleoauth.Credentials + } + type args struct { + ctx context.Context + parent string + constraint *SpannerTableForeignKeyConstraint + } + tests := []struct { + name string + fields fields + args args + want *SpannerTableForeignKeyConstraint + wantErr bool + }{ + { + name: "Test_CreateSpannerTableForeignKeyConstraint", + args: args{ + ctx: context.Background(), + parent: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "alis_px_dev_cmk", "branches"), + constraint: &SpannerTableForeignKeyConstraint{ + Name: "fk_branches_portfolio_id", + Column: "parent", + ReferencedTable: "portfolios", + ReferencedColumn: "portfolio_id", + OnDelete: SpannerTableForeignKeyConstraintActionCascade, + }, + }, + want: &SpannerTableForeignKeyConstraint{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := service.CreateSpannerTableForeignKeyConstraint(tt.args.ctx, tt.args.parent, tt.args.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("CreateSpannerTableForeignKeyConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateSpannerTableForeignKeyConstraint() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSpannerService_GetSpannerTableForeignKeyConstraint(t *testing.T) { + type fields struct { + GoogleCredentials *googleoauth.Credentials + } + type args struct { + ctx context.Context + parent string + name string + } + tests := []struct { + name string + fields fields + args args + want *SpannerTableForeignKeyConstraint + wantErr bool + }{ + { + name: "Test_GetSpannerTableForeignKeyConstraint", + args: args{ + ctx: context.Background(), + parent: fmt.Sprintf("projects/%s/instances/%s/databases/%s/tables/%s", TestProject, TestInstance, "alis_px_dev_cmk", "branches"), + name: "fk_branches_portfolio_id", + }, + want: &SpannerTableForeignKeyConstraint{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := service.GetSpannerTableForeignKeyConstraint(tt.args.ctx, tt.args.parent, tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("GetSpannerTableForeignKeyConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetSpannerTableForeignKeyConstraint() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/spanner/services/types.go b/internal/spanner/services/types.go index 102b289..3cc56aa 100644 --- a/internal/spanner/services/types.go +++ b/internal/spanner/services/types.go @@ -29,20 +29,17 @@ type SpannerTableIndex struct { Unique *wrapperspb.BoolValue } -type SpannerTableForeignKey struct { +type SpannerTableForeignKeyConstraint struct { + // The name of the constraint + Name string // Referenced table ReferencedTable string // Referenced column ReferencedColumn string // Referencing column Column string -} - -type SpannerTableForeignKeysConstraint struct { - // The name of the constraint - Name string - // Foreign keys - ForeignKeys []*SpannerTableForeignKey + // Referential actions on delete + OnDelete SpannerTableForeignKeyConstraintAction } // ProtoFileDescriptorSet represents a Proto File Descriptor Set. @@ -239,6 +236,14 @@ type Index struct { OrdinalPosition int } +type Constraint struct { + CONSTRAINT_NAME string + TABLE_NAME string + CONSTRAINT_TYPE string + UPDATE_RULE string + DELETE_RULE string +} + // SpannerTableDataType is a type for Spanner table column data types. type SpannerTableDataType int64 @@ -304,3 +309,26 @@ var SpannerTableIndexColumnOrders = []string{ SpannerTableIndexColumnOrder_ASC.String(), SpannerTableIndexColumnOrder_DESC.String(), } + +type SpannerTableForeignKeyConstraintAction int64 + +const ( + SpannerTableForeignKeyConstraintActionUnspecified SpannerTableForeignKeyConstraintAction = iota + SpannerTableForeignKeyConstraintActionCascade + SpannerTableForeignKeyConstraintActionRestrict + SpannerTableForeignKeyConstraintNoAction + SpannerTableForeignKeyConstraintSetNull + SpannerTableForeignKeyConstraintSetDefault +) + +func (a SpannerTableForeignKeyConstraintAction) String() string { + return [...]string{"", "CASCADE", "RESTRICT", "NO ACTION", "SET NULL", "SET DEFAULT"}[a] +} + +var SpannerTableForeignKeyConstraintActions = []string{ + SpannerTableForeignKeyConstraintActionCascade.String(), + SpannerTableForeignKeyConstraintActionRestrict.String(), + SpannerTableForeignKeyConstraintNoAction.String(), + SpannerTableForeignKeyConstraintSetNull.String(), + SpannerTableForeignKeyConstraintSetDefault.String(), +} diff --git a/internal/spanner/services/utils.go b/internal/spanner/services/utils.go index ce8ab9e..89d6ff7 100644 --- a/internal/spanner/services/utils.go +++ b/internal/spanner/services/utils.go @@ -251,7 +251,6 @@ func UpdateColumnMetadata(db *gorm.DB, tableName string, columns []*SpannerTable return nil } - func DeleteColumnMetadata(db *gorm.DB, tableName string, columns []*SpannerTableColumn) error { // Create or Update ColumnMetadata table if err := db.AutoMigrate(&ColumnMetadata{}); err != nil {