diff --git a/.github/workflows/raiden.yaml b/.github/workflows/raiden.yaml index d6b09a61..394b97ee 100644 --- a/.github/workflows/raiden.yaml +++ b/.github/workflows/raiden.yaml @@ -26,7 +26,7 @@ jobs: - name: Setup golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.58.1 + version: v1.60 args: --verbose test: needs: lint @@ -79,4 +79,4 @@ jobs: - name: Format if: matrix.go-version == '1.22.x' - run: diff -u <(echo -n) <(gofmt -d .) \ No newline at end of file + run: diff -u <(echo -n) <(gofmt -d .) diff --git a/model.go b/model.go index df6fad84..d3ba7625 100644 --- a/model.go +++ b/model.go @@ -22,6 +22,7 @@ type ( Nullable bool Default any Unique bool + Index bool } // definition of join tag, example: diff --git a/pkg/generator/model.go b/pkg/generator/model.go index c88bbf22..60391662 100644 --- a/pkg/generator/model.go +++ b/pkg/generator/model.go @@ -266,7 +266,7 @@ func containsRelation(relations []state.Relation, r state.Relation) bool { return false } -func BuildJoinTag(r *state.Relation) string { +func BuildRelationTag(r *state.Relation) string { var tags []string var joinTags []string @@ -274,6 +274,27 @@ func BuildJoinTag(r *state.Relation) string { jsonTag := fmt.Sprintf("json:%q", utils.ToSnakeCase(r.Table)+",omitempty") tags = append(tags, jsonTag) + if r.Action != nil { + + onUpdate, onDelete := objects.RelationActionDefaultLabel, objects.RelationActionDefaultLabel + if r.Action.UpdateAction != "" { + code := strings.ToLower(r.Action.UpdateAction) + if v, ok := objects.RelationActionMapLabel[objects.RelationAction(code)]; ok { + onUpdate = v + } + } + + if r.Action.DeletionAction != "" { + code := strings.ToLower(r.Action.DeletionAction) + if v, ok := objects.RelationActionMapLabel[objects.RelationAction(code)]; ok { + onDelete = v + } + } + + tags = append(tags, fmt.Sprintf("onUpdate:%q", onUpdate)) + tags = append(tags, fmt.Sprintf("onDelete:%q", onDelete)) + } + // append relation type tag relTypeTag := fmt.Sprintf("joinType:%s", r.RelationType) joinTags = append(joinTags, relTypeTag) @@ -350,7 +371,7 @@ func BuildRelationFields(table objects.Table, relations []state.Relation) (mappe r.Table = fmt.Sprintf("%sThrough%s", inflection.Plural(r.Table), utils.SnakeCaseToPascalCase(inflection.Singular(throughSuffix))) } - r.Tag = BuildJoinTag(&r) + r.Tag = BuildRelationTag(&r) if !containsRelation(mappedRelations, r) { mappedRelations = append(mappedRelations, r) diff --git a/pkg/generator/model_test.go b/pkg/generator/model_test.go index 159155f9..fcd9374e 100644 --- a/pkg/generator/model_test.go +++ b/pkg/generator/model_test.go @@ -21,6 +21,11 @@ func TestGenerateModels(t *testing.T) { err1 := utils.CreateFolder(modelPath) assert.NoError(t, err1) + relationshipAction := objects.TablesRelationshipAction{ + UpdateAction: "c", + DeletionAction: "c", + } + tables := []*generator.GenerateModelInput{ { Table: objects.Table{ @@ -42,6 +47,7 @@ func TestGenerateModels(t *testing.T) { Table: "related_table", ForeignKey: "test_table_id", PrimaryKey: "id", + Action: &relationshipAction, }, }, Policies: objects.Policies{}, diff --git a/pkg/generator/storage.go b/pkg/generator/storage.go index cf13be2e..45e3de71 100644 --- a/pkg/generator/storage.go +++ b/pkg/generator/storage.go @@ -121,7 +121,7 @@ func GenerateStorage(folderPath string, storage *GenerateStorageInput, generateF } var allowedMimeTypes = "" - if storage.Bucket.AllowedMimeTypes != nil && len(storage.Bucket.AllowedMimeTypes) > 0 { + if len(storage.Bucket.AllowedMimeTypes) > 0 { allowedMimeTypes = GenerateArrayDeclaration(reflect.ValueOf(storage.Bucket.AllowedMimeTypes), false) } diff --git a/pkg/resource/apply.go b/pkg/resource/apply.go index adc4010e..bda4cc4e 100644 --- a/pkg/resource/apply.go +++ b/pkg/resource/apply.go @@ -35,7 +35,7 @@ type MigrateData struct { // Migrate resource : // -// [ ] migrate table +// [x] migrate table // // [x] create table name, schema and columns // [x] create table rls enable @@ -84,8 +84,8 @@ type MigrateData struct { // [x] create new storage // [x] update storage // [x] delete storage -// [ ] add storage acl -// [ ] update storage acl +// [x] add storage acl +// [x] update storage acl func Apply(flags *Flags, config *raiden.Config) error { // declare default variable var migrateData MigrateData @@ -165,6 +165,7 @@ func Apply(flags *Flags, config *raiden.Config) error { } if flags.All() || flags.ModelsOnly { + resource.Tables = tables.AttachIndexAndAction(resource.Tables, resource.Indexes, resource.RelationActions) if data, err := tables.BuildMigrateData(appTables, resource.Tables); err != nil { return err } else { @@ -243,7 +244,6 @@ func Migrate(config *raiden.Config, importState *state.LocalState, projectPath s ForceCreateRelation: true, }, }) - resource.Tables[i].MigrationItems.ChangeRelationItems = make([]objects.UpdateRelationItem, 0) } else { updateTableRelation = append(updateTableRelation, tables.MigrateItem{ Type: t.Type, @@ -254,7 +254,6 @@ func Migrate(config *raiden.Config, importState *state.LocalState, projectPath s ChangeRelationItems: t.MigrationItems.ChangeRelationItems, }, }) - resource.Tables[i].MigrationItems.ChangeRelationItems = make([]objects.UpdateRelationItem, 0) } } } diff --git a/pkg/resource/import.go b/pkg/resource/import.go index d7b97c5b..47b06b8b 100644 --- a/pkg/resource/import.go +++ b/pkg/resource/import.go @@ -44,6 +44,7 @@ func Import(flags *Flags, config *raiden.Config) error { if err != nil { return err } + spResource.Tables = tables.AttachIndexAndAction(spResource.Tables, spResource.Indexes, spResource.RelationActions) // create import state ImportLogger.Debug("get native roles") diff --git a/pkg/resource/load.go b/pkg/resource/load.go index c4e6e25e..90563f9b 100644 --- a/pkg/resource/load.go +++ b/pkg/resource/load.go @@ -15,11 +15,13 @@ import ( var LoadLogger hclog.Logger = logger.HcLog().Named("import.load") type Resource struct { - Tables []objects.Table - Policies objects.Policies - Roles []objects.Role - Functions []objects.Function - Storages []objects.Bucket + Tables []objects.Table + Policies objects.Policies + Roles []objects.Role + Functions []objects.Function + Storages []objects.Bucket + Indexes []objects.Index + RelationActions []objects.TablesRelationshipAction } // The Load function loads resources based on the provided flags and project ID, and returns a resource @@ -47,6 +49,12 @@ func Load(flags *Flags, cfg *raiden.Config) (*Resource, error) { case []objects.Bucket: resource.Storages = rs LoadLogger.Debug("Finish Get Bucket From Supabase") + case []objects.Index: + resource.Indexes = rs + LoadLogger.Debug("Finish Get Indexes From Supabase") + case []objects.TablesRelationshipAction: + resource.RelationActions = rs + LoadLogger.Debug("Finish Get Relation Action From Supabase") case error: return nil, rs } @@ -99,6 +107,17 @@ func loadResource(cfg *raiden.Config, flags *Flags) <-chan any { return supabase.GetTables(cfg, supabase.DefaultIncludedSchema) }) + wg.Add(1) + LoadLogger.Debug("Get Index From Supabase") + go loadSupabaseResource(&wg, cfg, outChan, func(cfg *raiden.Config) ([]objects.Index, error) { + return supabase.GetIndexes(cfg, supabase.DefaultIncludedSchema[0]) + }) + + wg.Add(1) + LoadLogger.Debug("Get Table Relation Actions From Supabase") + go loadSupabaseResource(&wg, cfg, outChan, func(cfg *raiden.Config) ([]objects.TablesRelationshipAction, error) { + return supabase.GetTableRelationshipActions(cfg, supabase.DefaultIncludedSchema[0]) + }) } if flags.All() || flags.RolesOnly { diff --git a/pkg/resource/tables/compare.go b/pkg/resource/tables/compare.go index 6a864875..ad7a43db 100644 --- a/pkg/resource/tables/compare.go +++ b/pkg/resource/tables/compare.go @@ -238,6 +238,43 @@ func compareRelations(table *objects.Table, source, target []objects.TablesRelat continue } + if t.Index == nil && sc.Index == nil { + updateItems = append(updateItems, objects.UpdateRelationItem{ + Data: sc, + Type: objects.UpdateRelationCreateIndex, + }) + Logger.Debug("create new index", "constrain-name", sc.ConstraintName) + } + + if t.Action != nil && sc.Action != nil { + if t.Action.UpdateAction != sc.Action.UpdateAction { + updateItems = append(updateItems, objects.UpdateRelationItem{ + Data: sc, + Type: objects.UpdateRelationActionOnUpdate, + }) + Logger.Debug("check on update", "t-on-update", t.Action.UpdateAction, "sc-on-delete", sc.Action.UpdateAction, "same", t.Action.UpdateAction == sc.Action.UpdateAction) + } + + if t.Action.DeletionAction != sc.Action.DeletionAction { + updateItems = append(updateItems, objects.UpdateRelationItem{ + Data: sc, + Type: objects.UpdateRelationActionOnDelete, + }) + Logger.Debug("check on delete", "t-on-delete", t.Action.DeletionAction, "sc-on-delete", sc.Action.DeletionAction, "same", t.Action.DeletionAction == sc.Action.DeletionAction) + } + } else if t.Action != nil && sc.Action == nil { + updateItems = append(updateItems, objects.UpdateRelationItem{ + Data: sc, + Type: objects.UpdateRelationActionOnUpdate, + }) + + updateItems = append(updateItems, objects.UpdateRelationItem{ + Data: sc, + Type: objects.UpdateRelationActionOnDelete, + }) + Logger.Debug("create relation new action", "on-update", t.Action.UpdateAction, "on-delete", t.Action.DeletionAction) + } + delete(mapTargetRelation, sc.ConstraintName) if (sc.SourceSchema != t.SourceSchema) || (sc.SourceTableName != t.SourceTableName) || (sc.SourceColumnName != t.SourceColumnName) { diff --git a/pkg/resource/tables/compare_test.go b/pkg/resource/tables/compare_test.go index dc26ec59..d5e52815 100644 --- a/pkg/resource/tables/compare_test.go +++ b/pkg/resource/tables/compare_test.go @@ -60,6 +60,102 @@ func TestCompareList(t *testing.T) { } func TestCompareItem(t *testing.T) { + + sourceRelationshipAction := objects.TablesRelationshipAction{ + UpdateAction: "c", + DeletionAction: "c", + } + + targetRelationshipAction := objects.TablesRelationshipAction{ + UpdateAction: "a", + DeletionAction: "a", + } + + source := objects.Table{ + ID: 1, + Name: "table1", + Schema: "public", + RLSEnabled: true, + RLSForced: true, + PrimaryKeys: []objects.PrimaryKey{{Name: "id", Schema: "public", TableName: "table1"}}, + Columns: []objects.Column{ + {Name: "id", DataType: "int", IsNullable: false}, + {Name: "name", DataType: "varchar", IsNullable: true}, + {Name: "nullable", DataType: "varchar", IsNullable: true}, + {Name: "changeable", DataType: "varchar", IsNullable: true}, + {Name: "uniqueness", DataType: "varchar", IsNullable: false, IsUnique: true}, + {Name: "identity", DataType: "varchar", IsNullable: false, IsIdentity: true}, + }, + Relationships: []objects.TablesRelationship{ + { + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, + Action: &sourceRelationshipAction, + }, + }, + } + + target := objects.Table{ + ID: 1, + Name: "table1_updated", + Schema: "private", + RLSEnabled: false, + RLSForced: false, + PrimaryKeys: []objects.PrimaryKey{{Name: "id", Schema: "public", TableName: "table1"}}, + Columns: []objects.Column{ + {Name: "id", DataType: "int", IsNullable: false}, + {Name: "name", DataType: "varchar", IsNullable: false}, + {Name: "description", DataType: "text", IsNullable: true}, + {Name: "nullable", DataType: "varchar", IsNullable: false}, + {Name: "changeable", DataType: "json", IsNullable: true}, + {Name: "uniqueness", DataType: "varchar", IsNullable: false, IsUnique: false}, + {Name: "identity", DataType: "varchar", IsNullable: false, IsIdentity: false}, + }, + Relationships: []objects.TablesRelationship{ + { + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, + Action: &targetRelationshipAction, + }, + { + ConstraintName: "constraint2", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "name", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "name", + Action: &targetRelationshipAction, + }, + }, + } + + diffResult := tables.CompareItem(source, target) + assert.True(t, diffResult.IsConflict) + assert.Equal(t, "table1", diffResult.SourceResource.Name) + assert.Equal(t, "table1_updated", diffResult.TargetResource.Name) + assert.Equal(t, []objects.UpdateColumnType{objects.UpdateColumnNullable}, diffResult.DiffItems.ChangeColumnItems[0].UpdateItems) + assert.Equal(t, []objects.UpdateColumnType{objects.UpdateColumnNullable}, diffResult.DiffItems.ChangeColumnItems[1].UpdateItems) +} + +func TestCompareItemWithoutIndex(t *testing.T) { + targetRelationshipAction := objects.TablesRelationshipAction{ + UpdateAction: "c", + DeletionAction: "c", + } + source := objects.Table{ ID: 1, Name: "table1", @@ -84,6 +180,7 @@ func TestCompareItem(t *testing.T) { TargetTableSchema: "public", TargetTableName: "table2", TargetColumnName: "id", + Action: nil, }, }, } @@ -113,6 +210,7 @@ func TestCompareItem(t *testing.T) { TargetTableSchema: "public", TargetTableName: "table2", TargetColumnName: "id", + Action: &targetRelationshipAction, }, { ConstraintName: "constraint2", diff --git a/pkg/resource/tables/generate.go b/pkg/resource/tables/generate.go index f551fd03..9aa6ca3c 100644 --- a/pkg/resource/tables/generate.go +++ b/pkg/resource/tables/generate.go @@ -107,6 +107,8 @@ func scanGenerateTableRelation(table *objects.Table) (relations []*state.Relatio RelationType: relationType, PrimaryKey: primaryKey, ForeignKey: foreignKey, + Action: r.Action, + Index: r.Index, } relations = append(relations, &relation) @@ -165,6 +167,57 @@ func mergeGenerateManyToManyCandidate(candidates []*ManyToManyTable, mapRelation } } +// --- attach index and action to relation +func AttachIndexAndAction(allTable []objects.Table, allIndex []objects.Index, allAction []objects.TablesRelationshipAction) []objects.Table { + // build map index + mapIndex := make(map[string]objects.Index) + for _, v := range allIndex { + mapIndex[v.Name] = v + } + + // build map action + mapAction := make(map[string]objects.TablesRelationshipAction) + for _, v := range allAction { + key := fmt.Sprintf("%s_%s", v.SourceSchema, v.ConstraintName) + mapAction[key] = v + } + + for iTable := range allTable { + table := allTable[iTable] + for i := range table.Relationships { + r := table.Relationships[i] + + // check index + indexKey := fmt.Sprintf("ix_%s_%s", r.SourceTableName, r.SourceColumnName) + if fIndex, exist := mapIndex[indexKey]; exist && fIndex.Name != "" { + r.Index = &fIndex + } else { + indexKey := fmt.Sprintf("ix_%s_%s_%s", r.SourceSchema, r.SourceTableName, r.SourceColumnName) + if fIndex2, exist := mapIndex[indexKey]; exist && fIndex2.Name != "" { + r.Index = &fIndex2 + } + } + + // check action + if action, exist := mapAction[r.ConstraintName]; exist { + r.Action = &action + } else { + actionKey := fmt.Sprintf("%s_%s", r.SourceSchema, r.ConstraintName) + if action2, exist := mapAction[actionKey]; exist && action2.ConstraintName != "" { + r.Action = &action2 + } + } + + // replace with new value + table.Relationships[i] = r + } + + allTable[iTable] = table + } + + return allTable +} + // --- attach relation to table func buildGenerateModelInput(mapTable MapTable, mapRelations MapRelations, policies objects.Policies, mapModelValidationTags map[string]state.ModelValidationTag) []*generator.GenerateModelInput { generateInputs := make([]*generator.GenerateModelInput, 0) diff --git a/pkg/resource/tables/generate_test.go b/pkg/resource/tables/generate_test.go index c111ba96..62ba04dc 100644 --- a/pkg/resource/tables/generate_test.go +++ b/pkg/resource/tables/generate_test.go @@ -10,7 +10,7 @@ import ( ) func TestBuildGenerateModelInputs(t *testing.T) { - jsonStrData := `[{"id":29072,"schema":"public","name":"candidate","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":32768,"size":"32 kB","live_rows_estimate":2,"dead_rows_estimate":0,"comment":"list of candidate","columns":[{"table_id":29072,"schema":"public","table":"candidate","id":"29072.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.3","ordinal_position":3,"name":"batch","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"candidate","name":"id","table_id":29072}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id"}]},{"id":29079,"schema":"public","name":"scouter","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":"scouter list","columns":[{"table_id":29079,"schema":"public","table":"scouter","id":"29079.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.3","ordinal_position":3,"name":"email","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"scouter","name":"id","table_id":29079}],"relationships":[{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]},{"id":29086,"schema":"public","name":"submission","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":null,"columns":[{"table_id":29086,"schema":"public","table":"submission","id":"29086.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.2","ordinal_position":2,"name":"scouter_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.3","ordinal_position":3,"name":"candidate_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.4","ordinal_position":4,"name":"score","default_value":null,"data_type":"real","format":"float4","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.5","ordinal_position":5,"name":"note","default_value":null,"data_type":"text","format":"text","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.6","ordinal_position":6,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"submission","name":"id","table_id":29086}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id"},{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]}]` + jsonStrData := `[{"id":29072,"schema":"public","name":"candidate","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":32768,"size":"32 kB","live_rows_estimate":2,"dead_rows_estimate":0,"comment":"list of candidate","columns":[{"table_id":29072,"schema":"public","table":"candidate","id":"29072.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.3","ordinal_position":3,"name":"batch","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"candidate","name":"id","table_id":29072}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id"}]},{"id":29079,"schema":"public","name":"scouter","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":"scouter list","columns":[{"table_id":29079,"schema":"public","table":"scouter","id":"29079.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.3","ordinal_position":3,"name":"email","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"scouter","name":"id","table_id":29079}],"relationships":[{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]},{"id":29086,"schema":"public","name":"submission","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":null,"columns":[{"table_id":29086,"schema":"public","table":"submission","id":"29086.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.2","ordinal_position":2,"name":"scouter_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.3","ordinal_position":3,"name":"candidate_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.4","ordinal_position":4,"name":"score","default_value":null,"data_type":"real","format":"float4","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.5","ordinal_position":5,"name":"note","default_value":null,"data_type":"text","format":"text","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.6","ordinal_position":6,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"submission","name":"id","table_id":29086}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id","action":{"id":29242,"constraint_name":"submission_candidate_id_fkey","deletion_action":"NO ACTION","update_action":"NO ACTION","source_id":29086,"source_schema":"public","source_table":"submission","source_columns":"candidate_id","target_id":29072,"target_schema":"public","target_table":"candidate","target_columns":"id"},"index":{"schema":"public","table_name":"submission","name":"submission_candidate_id_fkey","definition":"FOREIGN KEY (candidate_id) REFERENCES public.candidate(id) MATCH SIMPLE"}},{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]}]` var sourceTables []objects.Table err := json.Unmarshal([]byte(jsonStrData), &sourceTables) @@ -22,3 +22,31 @@ func TestBuildGenerateModelInputs(t *testing.T) { assert.Equal(t, 2, len(r.Relations)) } } + +func TestAttachActionAndIndex(t *testing.T) { + jsonStrData := `[{"id":29072,"schema":"public","name":"candidate","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":32768,"size":"32 kB","live_rows_estimate":2,"dead_rows_estimate":0,"comment":"list of candidate","columns":[{"table_id":29072,"schema":"public","table":"candidate","id":"29072.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.3","ordinal_position":3,"name":"batch","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29072,"schema":"public","table":"candidate","id":"29072.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"candidate","name":"id","table_id":29072}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id"}]},{"id":29079,"schema":"public","name":"scouter","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":"scouter list","columns":[{"table_id":29079,"schema":"public","table":"scouter","id":"29079.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.2","ordinal_position":2,"name":"name","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.3","ordinal_position":3,"name":"email","default_value":null,"data_type":"character varying","format":"varchar","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29079,"schema":"public","table":"scouter","id":"29079.4","ordinal_position":4,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"scouter","name":"id","table_id":29079}],"relationships":[{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]},{"id":29086,"schema":"public","name":"submission","rls_enabled":true,"rls_forced":false,"replica_identity":"DEFAULT","bytes":16384,"size":"16 kB","live_rows_estimate":0,"dead_rows_estimate":0,"comment":null,"columns":[{"table_id":29086,"schema":"public","table":"submission","id":"29086.1","ordinal_position":1,"name":"id","default_value":null,"data_type":"bigint","format":"int8","is_identity":true,"identity_generation":"BY DEFAULT","is_generated":false,"is_nullable":false,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.2","ordinal_position":2,"name":"scouter_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.3","ordinal_position":3,"name":"candidate_id","default_value":null,"data_type":"bigint","format":"int8","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.4","ordinal_position":4,"name":"score","default_value":null,"data_type":"real","format":"float4","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.5","ordinal_position":5,"name":"note","default_value":null,"data_type":"text","format":"text","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null},{"table_id":29086,"schema":"public","table":"submission","id":"29086.6","ordinal_position":6,"name":"created_at","default_value":"now()","data_type":"timestamp with time zone","format":"timestamptz","is_identity":false,"identity_generation":null,"is_generated":false,"is_nullable":true,"is_updatable":true,"is_unique":false,"enums":[],"check":null,"comment":null}],"primary_keys":[{"schema":"public","table_name":"submission","name":"id","table_id":29086}],"relationships":[{"id":29242,"constraint_name":"submission_candidate_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"candidate_id","target_table_schema":"public","target_table_name":"candidate","target_column_name":"id","action":{"id":29242,"constraint_name":"submission_candidate_id_fkey","deletion_action":"NO ACTION","update_action":"NO ACTION","source_id":29086,"source_schema":"public","source_table":"submission","source_columns":"candidate_id","target_id":29072,"target_schema":"public","target_table":"candidate","target_columns":"id"},"index":{"schema":"public","table_name":"submission","name":"submission_candidate_id_fkey","definition":"FOREIGN KEY (candidate_id) REFERENCES public.candidate(id) MATCH SIMPLE"}},{"id":30078,"constraint_name":"submission_scouter_id_fkey","source_schema":"public","source_table_name":"submission","source_column_name":"scouter_id","target_table_schema":"public","target_table_name":"scouter","target_column_name":"id"}]}]` + + var sourceTables []objects.Table + err := json.Unmarshal([]byte(jsonStrData), &sourceTables) + assert.NoError(t, err) + + rs := tables.BuildGenerateModelInputs(sourceTables, nil, nil) + tbls := make([]objects.Table, 0) + indexes := make([]objects.Index, 0) + actions := make([]objects.TablesRelationshipAction, 0) + for _, r := range rs { + tbls = append(tbls, r.Table) + + for _, rel := range r.Relations { + if rel.Action != nil { + actions = append(actions, *rel.Action) + } + + if rel.Index != nil { + indexes = append(indexes, *rel.Index) + } + } + } + + tables.AttachIndexAndAction(tbls, indexes, actions) +} diff --git a/pkg/resource/tables/print_diff.go b/pkg/resource/tables/print_diff.go index 5134f9ab..901c89e8 100644 --- a/pkg/resource/tables/print_diff.go +++ b/pkg/resource/tables/print_diff.go @@ -69,16 +69,12 @@ func GetDiffChangeMessage(items []MigrateItem) string { case migrator.MigrateTypeCreate: newTable = append(newTable, fmt.Sprintf("- %s", name)) case migrator.MigrateTypeUpdate: - // if Logger.GetLevel() == hclog.Trace { diffMessage, err := GenerateDiffChangeUpdateMessage(name, item) if err != nil { Logger.Error("print change table error", "msg", err.Error()) continue } updateTable = append(updateTable, diffMessage) - // } else { - // updateTable = append(updateTable, fmt.Sprintf("- %s", name)) - // } case migrator.MigrateTypeDelete: deleteTable = append(deleteTable, fmt.Sprintf("- %s", name)) } @@ -228,7 +224,7 @@ func GenerateDiffMessage(diffData CompareDiffResult, sRelation MapRelations, tRe } } - r.Tag = generator.BuildJoinTag(r) + r.Tag = generator.BuildRelationTag(r) relations = append(relations, r) } sFoundRelations = relations @@ -254,7 +250,7 @@ func GenerateDiffMessage(diffData CompareDiffResult, sRelation MapRelations, tRe } } - r.Tag = generator.BuildJoinTag(r) + r.Tag = generator.BuildRelationTag(r) relations = append(relations, r) } tFoundRelations = relations @@ -313,8 +309,8 @@ func GenerateDiffMessage(diffData CompareDiffResult, sRelation MapRelations, tRe if fSource != nil { sRelationArr = append(sRelationArr, fmt.Sprintf( - "%s *%s `json:\"%s,omitempty\" %s", - symbol, utils.SnakeCaseToPascalCase(fSource.Table), + "%s *%s `%s`", + symbol, fSource.Type, fSource.Tag, )) } else { @@ -325,8 +321,8 @@ func GenerateDiffMessage(diffData CompareDiffResult, sRelation MapRelations, tRe if fTarget != nil { tRelationArr = append(tRelationArr, fmt.Sprintf( - "%s *%s `json:\"%s,omitempty\" %s", - symbol, utils.SnakeCaseToPascalCase(fTarget.Table), + "%s *%s `%s`", + symbol, fTarget.Type, fTarget.Tag, )) } else { @@ -519,6 +515,37 @@ func GenerateDiffChangeUpdateMessage(name string, item MigrateItem) (string, err changeRelationArr = append(changeRelationArr, fmt.Sprintf("- %s : %s", "update relation", c.Data.ConstraintName)) case objects.UpdateRelationDelete: changeRelationArr = append(changeRelationArr, fmt.Sprintf("- %s : %s", "delete relation", c.Data.ConstraintName)) + case objects.UpdateRelationCreateIndex: + changeRelationArr = append(changeRelationArr, fmt.Sprintf("- %s : %s", "create new index", c.Data.ConstraintName)) + case objects.UpdateRelationActionOnUpdate, objects.UpdateRelationActionOnDelete: + var oldRelation, newRelation objects.TablesRelationship + + // find old column detail + for ii := range item.OldData.Relationships { + or := item.OldData.Relationships[ii] + if or.ConstraintName == c.Data.Action.ConstraintName { + oldRelation = or + } + } + + for ii := range item.NewData.Relationships { + or := item.NewData.Relationships[ii] + if or.ConstraintName == c.Data.Action.ConstraintName { + newRelation = or + } + } + + if c.Type == objects.UpdateRelationActionOnUpdate { + oldLabel := objects.RelationActionMapLabel[objects.RelationAction(oldRelation.Action.UpdateAction)] + newLabel := objects.RelationActionMapLabel[objects.RelationAction(newRelation.Action.UpdateAction)] + changeRelationArr = append(changeRelationArr, fmt.Sprintf("- %s %s : %s >>> %s", oldRelation.SourceColumnName, "on update", oldLabel, newLabel)) + } + + if c.Type == objects.UpdateRelationActionOnDelete { + oldLabel := objects.RelationActionMapLabel[objects.RelationAction(oldRelation.Action.DeletionAction)] + newLable := objects.RelationActionMapLabel[objects.RelationAction(newRelation.Action.DeletionAction)] + changeRelationArr = append(changeRelationArr, fmt.Sprintf("- %s %s : %s >>> %s", oldRelation.SourceColumnName, "on delete", oldLabel, newLable)) + } } } diff --git a/pkg/resource/tables/print_diff_test.go b/pkg/resource/tables/print_diff_test.go index 55371ed4..e0e2a31d 100644 --- a/pkg/resource/tables/print_diff_test.go +++ b/pkg/resource/tables/print_diff_test.go @@ -18,6 +18,11 @@ import ( ) var ( + relationAction = objects.TablesRelationshipAction{ + ConstraintName: "constraint1", + UpdateAction: "c", + DeletionAction: "c", + } MigratedItems = objects.UpdateTableParam{ OldData: objects.Table{Name: "old_table"}, ChangeItems: []objects.UpdateTableType{ @@ -65,6 +70,45 @@ var ( TargetColumnName: "id", }, }, + { + Type: objects.UpdateRelationCreateIndex, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, + }, + }, + { + Type: objects.UpdateRelationActionOnUpdate, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Action: &relationAction, + }, + }, + { + Type: objects.UpdateRelationActionOnDelete, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Action: &relationAction, + }, + }, }, ChangeColumnItems: []objects.UpdateColumnItem{ { @@ -138,6 +182,8 @@ var ( TargetTableSchema: "public", TargetTableName: "table2", TargetColumnName: "id", + Action: &relationAction, + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, }, }, } @@ -167,6 +213,7 @@ var ( TargetTableSchema: "public", TargetTableName: "table2", TargetColumnName: "id", + Action: &relationAction, }, { ConstraintName: "constraint2", @@ -176,6 +223,7 @@ var ( TargetTableSchema: "public", TargetTableName: "table2", TargetColumnName: "name", + Action: &relationAction, }, }, } @@ -420,8 +468,8 @@ func TestGenerateDiffChangeUpdateMessage(t *testing.T) { assert.Contains(t, diffMessage, fmt.Sprintf("- %s : %s >>> %s", "replica identity", item.OldData.ReplicaIdentity, item.NewData.ReplicaIdentity)) item = tables.MigrateItem{ - NewData: objects.Table{ReplicaIdentity: "FULL"}, - OldData: objects.Table{ReplicaIdentity: "NOTHING"}, + NewData: SourceTable, + OldData: TargetTable, MigrationItems: objects.UpdateTableParam{ ChangeColumnItems: MigratedItems.ChangeColumnItems, ChangeRelationItems: MigratedItems.ChangeRelationItems, diff --git a/pkg/state/state.go b/pkg/state/state.go index b37e3b00..fec40830 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -65,6 +65,9 @@ type ( ForeignKey string Tag string *JoinRelation + + Action *objects.TablesRelationshipAction + Index *objects.Index } JoinRelation struct { diff --git a/pkg/state/table.go b/pkg/state/table.go index a832c162..e295d55f 100644 --- a/pkg/state/table.go +++ b/pkg/state/table.go @@ -40,6 +40,7 @@ func ExtractTable(tableStates []TableState, appTable []any) (result ExtractTable for _, t := range appTable { tableName := raiden.GetTableName(t) ts, isExist := mapTableState[tableName] + if !isExist { nt := buildTableFromModel(t) result.New = append(result.New, nt) @@ -135,6 +136,42 @@ func buildTableFromModel(model any) (ei ExtractTableItem) { rel.TargetTableSchema = ei.Table.Schema rel.TargetColumnName = jt.PrimaryKey + // check action field + onUpdate := field.Tag.Get("onUpdate") + onDelete := field.Tag.Get("onDelete") + if len(onUpdate) > 0 || len(onDelete) > 0 { + if len(onUpdate) == 0 { + onUpdate = string(objects.RelationActionDefault) + } else { + v, ok := objects.RelationActionMapCode[objects.RelationActionLabel(onUpdate)] + if ok { + onUpdate = string(v) + } + } + + if len(onDelete) == 0 { + onDelete = string(objects.RelationActionDefault) + } else { + v, ok := objects.RelationActionMapCode[objects.RelationActionLabel(onDelete)] + if ok { + onDelete = string(v) + } + } + + rel.Action = &objects.TablesRelationshipAction{ + ConstraintName: fmt.Sprintf("%s_%s_fkey", rel.SourceTableName, rel.SourceColumnName), + UpdateAction: onUpdate, + DeletionAction: onDelete, + SourceSchema: rel.SourceSchema, + SourceTable: rel.SourceTableName, + SourceColumns: fmt.Sprintf("{%s}", rel.SourceColumnName), + + TargetSchema: rel.TargetTableSchema, + TargetTable: rel.SourceTableName, + TargetColumns: fmt.Sprintf("{%s}", rel.TargetColumnName), + } + } + ei.Table.Relationships = append(ei.Table.Relationships, rel) } } @@ -251,9 +288,26 @@ func buildTableFromState(model any, state TableState) (ei ExtractTableItem) { tableName := findTypeName(field.Type, reflect.Struct, 4) if tableName != "" { if r := buildTableRelation(ei.Table.Name, tableName, ei.Table.Schema, mapRelation, joinTag); r.ConstraintName != "" { + if onUpdate := field.Tag.Get("onUpdate"); onUpdate != "" { + if r.Action == nil { + r.Action = &objects.TablesRelationshipAction{} + } + + r.Action.UpdateAction = string(objects.RelationActionMapCode[objects.RelationActionLabel(strings.ToLower(onUpdate))]) + } + + if onDelete := field.Tag.Get("onDelete"); onDelete != "" { + if r.Action == nil { + r.Action = &objects.TablesRelationshipAction{} + } + + r.Action.DeletionAction = string(objects.RelationActionMapCode[objects.RelationActionLabel(strings.ToLower(onDelete))]) + } + relations = append(relations, r) } } + } } } @@ -467,6 +521,10 @@ func buildTableRelation(tableName, fieldName, schema string, mapRelations map[st relation.ConstraintName = getRelationConstrainName(schema, sourceTableName, foreignKey) if r, ok := mapRelations[relation.ConstraintName]; ok { relation = r + } else { + if r, ok := mapRelations[getRelationConstrainNameWithoutSchema(sourceTableName, foreignKey)]; ok { + relation = r + } } relation.SourceSchema = schema @@ -485,6 +543,10 @@ func getRelationConstrainName(schema, table, foreignKey string) string { return fmt.Sprintf("%s_%s_%s_fkey", schema, table, foreignKey) } +func getRelationConstrainNameWithoutSchema(table, foreignKey string) string { + return fmt.Sprintf("%s_%s_fkey", table, foreignKey) +} + func (f ExtractTableItems) ToFlatTable() (tables []objects.Table) { for i := range f { t := f[i] diff --git a/pkg/state/table_test.go b/pkg/state/table_test.go index 8f95f2c6..5f12e7be 100644 --- a/pkg/state/table_test.go +++ b/pkg/state/table_test.go @@ -24,7 +24,7 @@ type Submission struct { Acl string `json:"-" read:"anon" write:"anon"` // Relations - Candidate *Candidate `json:"candidate,omitempty" join:"joinType:hasOne;primaryKey:id;foreignKey:candidate_id"` + Candidate *Candidate `json:"candidate,omitempty" join:"joinType:hasOne;primaryKey:id;foreignKey:candidate_id" onUpdate:"cascade" onDelete:"cascade"` } type Candidate struct { @@ -40,7 +40,7 @@ type Candidate struct { Acl string `json:"-" read:"anon" write:"authenticated"` // Relations - Submission []*Submission `json:"submission,omitempty" join:"joinType:hasMany;primaryKey:id;foreignKey:candidate_id"` + Submission []*Submission `json:"submission,omitempty" join:"joinType:hasMany;primaryKey:id;foreignKey:candidate_id" onUpdate:"cascade" onDelete:"cascade"` } func TestExtractTable_NoRelation(t *testing.T) { @@ -111,6 +111,11 @@ func TestExtractTable_WithRelation(t *testing.T) { } func TestExtractTable(t *testing.T) { + relationAction := objects.TablesRelationshipAction{ + UpdateAction: "cascade", + DeletionAction: "cascade", + } + tableStates := []state.TableState{ { Table: objects.Table{ @@ -123,6 +128,7 @@ func TestExtractTable(t *testing.T) { TargetTableSchema: "public", TargetTableName: "candidate", TargetColumnName: "id", + Action: &relationAction, }, }, PrimaryKeys: []objects.PrimaryKey{ diff --git a/pkg/supabase/drivers/cloud/indexes.go b/pkg/supabase/drivers/cloud/indexes.go new file mode 100644 index 00000000..817f57ce --- /dev/null +++ b/pkg/supabase/drivers/cloud/indexes.go @@ -0,0 +1,19 @@ +package cloud + +import ( + "fmt" + + "github.com/sev-2/raiden" + "github.com/sev-2/raiden/pkg/supabase/objects" + "github.com/sev-2/raiden/pkg/supabase/query/sql" +) + +func GetIndexes(cfg *raiden.Config, schema string) ([]objects.Index, error) { + CloudLogger.Trace("start fetching index from supabase") + rs, err := ExecuteQuery[[]objects.Index](cfg.SupabaseApiUrl, cfg.ProjectId, sql.GenerateGetIndexQuery(schema), DefaultAuthInterceptor(cfg.AccessToken), getRoleResponseInterceptor) + if err != nil { + err = fmt.Errorf("get index error : %s", err) + } + CloudLogger.Trace("finish fetching index from supabase") + return rs, err +} diff --git a/pkg/supabase/drivers/cloud/table.go b/pkg/supabase/drivers/cloud/table.go index d089bec2..39c16efd 100644 --- a/pkg/supabase/drivers/cloud/table.go +++ b/pkg/supabase/drivers/cloud/table.go @@ -1,6 +1,7 @@ package cloud import ( + "errors" "fmt" "strings" "sync" @@ -81,25 +82,25 @@ func UpdateTable(cfg *raiden.Config, newTable objects.Table, updateItem objects. // execute update column if len(updateItem.ChangeColumnItems) > 0 { - errors := updateColumnFromTable(cfg, updateItem.ChangeColumnItems, newTable.Columns, updateItem.OldData.Columns, newTable.PrimaryKeys) - if len(errors) > 0 { + errorsUpdate := updateColumnFromTable(cfg, updateItem.ChangeColumnItems, newTable.Columns, updateItem.OldData.Columns, newTable.PrimaryKeys) + if len(errorsUpdate) > 0 { var errMsg []string - for _, e := range errors { + for _, e := range errorsUpdate { errMsg = append(errMsg, e.Error()) } - return fmt.Errorf(strings.Join(errMsg, ";")) + return errors.New(strings.Join(errMsg, ";")) } } if len(updateItem.ChangeRelationItems) > 0 || updateItem.ForceCreateRelation { - errors := updateRelations(cfg, updateItem.ChangeRelationItems, newTable.Relationships, updateItem.ForceCreateRelation) - if len(errors) > 0 { + errorsUpdate := updateRelations(cfg, updateItem.ChangeRelationItems, newTable.Relationships, updateItem.ForceCreateRelation) + if len(errorsUpdate) > 0 { var errMsg []string - for _, e := range errors { + for _, e := range errorsUpdate { errMsg = append(errMsg, e.Error()) } - return fmt.Errorf(strings.Join(errMsg, ";")) + return errors.New(strings.Join(errMsg, ";")) } } CloudLogger.Trace("finish update table", "name", newTable.Name) @@ -117,6 +118,20 @@ func DeleteTable(cfg *raiden.Config, table objects.Table, cascade bool) error { return nil } +// ----- relationship action ----- +func GetTableRelationshipActions(cfg *raiden.Config, schema string) ([]objects.TablesRelationshipAction, error) { + CloudLogger.Trace("start fetching table relationships from supabase") + q := sql.GenerateGetTableRelationshipActionsQuery(schema) + + rs, err := ExecuteQuery[[]objects.TablesRelationshipAction](cfg.SupabaseApiUrl, cfg.ProjectId, q, DefaultAuthInterceptor(cfg.AccessToken), nil) + if err != nil { + return rs, fmt.Errorf("get tables error : %s", err) + } + + CloudLogger.Trace("finish fetching table relationships from supabase") + return rs, nil +} + // ----- update column ----- func updateColumnFromTable( cfg *raiden.Config, updateColumns []objects.UpdateColumnItem, @@ -295,10 +310,15 @@ func updateRelations(cfg *raiden.Config, items []objects.UpdateRelationItem, rel } eChan <- nil }(&wg, cfg, rel, errChan) - case objects.UpdateRelationUpdate: + case objects.UpdateRelationUpdate, objects.UpdateRelationActionOnDelete, objects.UpdateRelationActionOnUpdate, objects.UpdateRelationCreateIndex: rel, exist := relationMap[i.Data.ConstraintName] if !exist { - continue + actionKey := fmt.Sprintf("%s_%s_fkey", i.Data.SourceTableName, i.Data.SourceColumnName) + rel2, exist2 := relationMap[actionKey] + if !exist2 { + continue + } + rel = rel2 } wg.Add(1) @@ -339,7 +359,6 @@ func updateRelations(cfg *raiden.Config, items []objects.UpdateRelationItem, rel func createForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) error { CloudLogger.Trace("start create foreign key", "table", relation.TargetTableName, "constrain-name", relation.ConstraintName) - sql, err := query.BuildFkQuery(objects.UpdateRelationCreate, relation) if err != nil { return err @@ -350,6 +369,16 @@ func createForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) return fmt.Errorf("create foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationCreate, relation); err != nil { + return err + } else if len(indexSql) > 0 { + _, err = ExecuteQuery[any](cfg.SupabaseApiUrl, cfg.ProjectId, indexSql, DefaultAuthInterceptor(cfg.AccessToken), nil) + if err != nil { + return fmt.Errorf("create foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + CloudLogger.Trace("finish create foreign key", "table", relation.TargetTableName, "constrain-name", relation.ConstraintName) return nil } @@ -373,6 +402,18 @@ func updateForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) return fmt.Errorf("update foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + if relation.Index == nil { + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationCreate, relation); err != nil { + return err + } else if len(indexSql) > 0 { + _, err = ExecuteQuery[any](cfg.SupabaseApiUrl, cfg.ProjectId, indexSql, DefaultAuthInterceptor(cfg.AccessToken), nil) + if err != nil { + return fmt.Errorf("create foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + } + CloudLogger.Trace("finish update foreign key", "table", relation.SourceTableName, "constrain-name", relation.ConstraintName) return nil } @@ -390,6 +431,19 @@ func deleteForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) return fmt.Errorf("delete foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + if relation.Index != nil { + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationDelete, relation); err != nil { + return err + } else if len(indexSql) > 0 { + CloudLogger.Info("deleteForeignKey", "sql", indexSql) + _, err = ExecuteQuery[any](cfg.SupabaseApiUrl, cfg.ProjectId, indexSql, DefaultAuthInterceptor(cfg.AccessToken), nil) + if err != nil { + return fmt.Errorf("delete foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + } + CloudLogger.Trace("finish delete foreign key", "table", relation.SourceTableName, "constrain-name", relation.ConstraintName) return nil } diff --git a/pkg/supabase/drivers/local/meta/indexes.go b/pkg/supabase/drivers/local/meta/indexes.go new file mode 100644 index 00000000..c5a3439b --- /dev/null +++ b/pkg/supabase/drivers/local/meta/indexes.go @@ -0,0 +1,20 @@ +package meta + +import ( + "fmt" + + "github.com/sev-2/raiden" + "github.com/sev-2/raiden/pkg/supabase/objects" + "github.com/sev-2/raiden/pkg/supabase/query/sql" +) + +func GetIndexes(cfg *raiden.Config, schema string) ([]objects.Index, error) { + MetaLogger.Trace("start fetching indexes from meta") + rs, err := ExecuteQuery[[]objects.Index](getBaseUrl(cfg), sql.GenerateGetIndexQuery(schema), nil, nil, nil) + if err != nil { + err = fmt.Errorf("get indexes error : %s", err) + return []objects.Index{}, err + } + MetaLogger.Trace("finish fetching policy by name from meta") + return rs, nil +} diff --git a/pkg/supabase/drivers/local/meta/table.go b/pkg/supabase/drivers/local/meta/table.go index 43fd8dcf..cf186048 100644 --- a/pkg/supabase/drivers/local/meta/table.go +++ b/pkg/supabase/drivers/local/meta/table.go @@ -1,6 +1,7 @@ package meta import ( + "errors" "fmt" "net/http" "strconv" @@ -91,25 +92,25 @@ func UpdateTable(cfg *raiden.Config, newTable objects.Table, updateItem objects. // execute update column if len(updateItem.ChangeColumnItems) > 0 { - errors := updateColumnFromTable(cfg, updateItem.ChangeColumnItems, newTable.Columns, updateItem.OldData.Columns, newTable.PrimaryKeys) - if len(errors) > 0 { + errorsMessage := updateColumnFromTable(cfg, updateItem.ChangeColumnItems, newTable.Columns, updateItem.OldData.Columns, newTable.PrimaryKeys) + if len(errorsMessage) > 0 { var errMsg []string - for _, e := range errors { + for _, e := range errorsMessage { errMsg = append(errMsg, e.Error()) } - return fmt.Errorf(strings.Join(errMsg, ";")) + return errors.New(strings.Join(errMsg, ";")) } } if len(updateItem.ChangeRelationItems) > 0 || updateItem.ForceCreateRelation { - errors := updateRelations(cfg, updateItem.ChangeRelationItems, newTable.Relationships, updateItem.ForceCreateRelation) - if len(errors) > 0 { + errorsUpdate := updateRelations(cfg, updateItem.ChangeRelationItems, newTable.Relationships, updateItem.ForceCreateRelation) + if len(errorsUpdate) > 0 { var errMsg []string - for _, e := range errors { + for _, e := range errorsUpdate { errMsg = append(errMsg, e.Error()) } - return fmt.Errorf(strings.Join(errMsg, ";")) + return errors.New(strings.Join(errMsg, ";")) } } MetaLogger.Trace("finish update table", "name", newTable.Name) @@ -128,6 +129,20 @@ func DeleteTable(cfg *raiden.Config, table objects.Table, cascade bool) error { return nil } +// ----- relationship action ----- +func GetTableRelationshipActions(cfg *raiden.Config, schema string) ([]objects.TablesRelationshipAction, error) { + MetaLogger.Trace("start fetching table relationships from supabase") + q := sql.GenerateGetTableRelationshipActionsQuery(schema) + + rs, err := ExecuteQuery[[]objects.TablesRelationshipAction](getBaseUrl(cfg), q, nil, nil, nil) + if err != nil { + return rs, fmt.Errorf("get tables error : %s", err) + } + + MetaLogger.Trace("finish fetching table relationships from supabase") + return rs, nil +} + // ----- update column ----- func updateColumnFromTable( cfg *raiden.Config, updateColumns []objects.UpdateColumnItem, @@ -308,10 +323,15 @@ func updateRelations(cfg *raiden.Config, items []objects.UpdateRelationItem, rel } eChan <- nil }(&wg, cfg, rel, errChan) - case objects.UpdateRelationUpdate: + case objects.UpdateRelationUpdate, objects.UpdateRelationActionOnDelete, objects.UpdateRelationActionOnUpdate, objects.UpdateRelationCreateIndex: rel, exist := relationMap[i.Data.ConstraintName] if !exist { - continue + actionKey := fmt.Sprintf("%s_%s_fkey", i.Data.SourceTableName, i.Data.SourceColumnName) + rel2, exist2 := relationMap[actionKey] + if !exist2 { + continue + } + rel = rel2 } wg.Add(1) @@ -360,6 +380,17 @@ func createForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) if err != nil { return fmt.Errorf("create foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationCreate, relation); err != nil { + return err + } else if len(indexSql) > 0 { + _, err = ExecuteQuery[any](getBaseUrl(cfg), indexSql, nil, nil, nil) + if err != nil { + return fmt.Errorf("create foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + MetaLogger.Trace("finish create foreign key", "table", relation.TargetTableName, "constrain-name", relation.ConstraintName) return nil } @@ -381,6 +412,19 @@ func updateForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) if err != nil { return fmt.Errorf("update foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + + if relation.Action == nil { + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationCreate, relation); err != nil { + return err + } else if len(indexSql) > 0 { + _, err = ExecuteQuery[any](getBaseUrl(cfg), indexSql, nil, nil, nil) + if err != nil { + return fmt.Errorf("create foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + } + MetaLogger.Trace("finish update foreign key", "table", relation.TargetTableName, "constrain-name", relation.ConstraintName) return nil } @@ -397,6 +441,19 @@ func deleteForeignKey(cfg *raiden.Config, relation *objects.TablesRelationship) if err != nil { return fmt.Errorf("delete foreign key %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) } + + if relation.Index != nil { + // create FK index + if indexSql, err := query.BuildFKIndexQuery(objects.UpdateRelationDelete, relation); err != nil { + return err + } else if len(indexSql) > 0 { + _, err = ExecuteQuery[any](getBaseUrl(cfg), indexSql, nil, nil, nil) + if err != nil { + return fmt.Errorf("delete foreign index %s.%s error : %s", relation.SourceTableName, relation.SourceColumnName, err) + } + } + } + MetaLogger.Trace("start delete foreign key", "table", relation.TargetTableName, "constrain-name", relation.ConstraintName) return nil } diff --git a/pkg/supabase/objects/index.go b/pkg/supabase/objects/index.go new file mode 100644 index 00000000..d6fc81d5 --- /dev/null +++ b/pkg/supabase/objects/index.go @@ -0,0 +1,8 @@ +package objects + +type Index struct { + Schema string `json:"schema"` + Table string `json:"table"` + Name string `json:"name"` + Definition string `json:"definition"` +} diff --git a/pkg/supabase/objects/table.go b/pkg/supabase/objects/table.go index 477e99b5..159f3d13 100644 --- a/pkg/supabase/objects/table.go +++ b/pkg/supabase/objects/table.go @@ -18,6 +18,67 @@ type TablesRelationship struct { TargetTableSchema string `json:"target_table_schema"` TargetTableName string `json:"target_table_name"` TargetColumnName string `json:"target_column_name"` + + // Preload data when import or apply + Action *TablesRelationshipAction `json:"action"` + Index *Index `json:"index"` +} + +// ----- relation action + +// ----- relation action map +// a: No action +// r: Restrict +// c: Cascade +// n: Set null +// d: Set default + +type RelationAction string +type RelationActionLabel string + +const ( + RelationActionNoAction RelationAction = "a" + RelationActionRestrict RelationAction = "r" + RelationActionCascade RelationAction = "c" + RelationActionSetNull RelationAction = "n" + RelationActionDefault RelationAction = "d" + + RelationActionNoActionLabel RelationActionLabel = "no action" + RelationActionRestrictLabel RelationActionLabel = "restrict" + RelationActionCascadeLabel RelationActionLabel = "cascade" + RelationActionSetNullLabel RelationActionLabel = "set null" + RelationActionDefaultLabel RelationActionLabel = "set default" +) + +var RelationActionMapLabel = map[RelationAction]RelationActionLabel{ + RelationActionNoAction: RelationActionNoActionLabel, + RelationActionRestrict: RelationActionRestrictLabel, + RelationActionCascade: RelationActionCascadeLabel, + RelationActionSetNull: RelationActionSetNullLabel, + RelationActionDefault: RelationActionDefaultLabel, +} + +var RelationActionMapCode = map[RelationActionLabel]RelationAction{ + RelationActionNoActionLabel: RelationActionNoAction, + RelationActionRestrictLabel: RelationActionRestrict, + RelationActionCascadeLabel: RelationActionCascade, + RelationActionSetNullLabel: RelationActionSetNull, + RelationActionDefaultLabel: RelationActionDefault, +} + +type TablesRelationshipAction struct { + ID int `json:"id"` + ConstraintName string `json:"constraint_name"` + DeletionAction string `json:"deletion_action"` + UpdateAction string `json:"update_action"` + SourceID int `json:"source_id"` + SourceSchema string `json:"source_schema"` + SourceTable string `json:"source_table"` + SourceColumns string `json:"source_columns"` + TargetID int `json:"target_id"` + TargetSchema string `json:"target_schema"` + TargetTable string `json:"target_table"` + TargetColumns string `json:"target_columns"` } type Column struct { @@ -93,9 +154,12 @@ const ( ) const ( - UpdateRelationCreate UpdateRelationType = "create" - UpdateRelationUpdate UpdateRelationType = "update" - UpdateRelationDelete UpdateRelationType = "delete" + UpdateRelationCreate UpdateRelationType = "create" + UpdateRelationUpdate UpdateRelationType = "update" + UpdateRelationDelete UpdateRelationType = "delete" + UpdateRelationActionOnUpdate UpdateRelationType = "on_update_action" + UpdateRelationActionOnDelete UpdateRelationType = "on_delete_action" + UpdateRelationCreateIndex UpdateRelationType = "index" ) type UpdateColumnItem struct { diff --git a/pkg/supabase/query/sql/index.go b/pkg/supabase/query/sql/index.go new file mode 100644 index 00000000..544f2635 --- /dev/null +++ b/pkg/supabase/query/sql/index.go @@ -0,0 +1,24 @@ +package sql + +import "fmt" + +var GetIndexesQuery = ` +SELECT + schemaname AS "schema", + tablename AS "table", + indexname AS "name", + indexdef AS "definition" +FROM + pg_indexes +` + +func GenerateGetIndexQuery(schema string) string { + if len(schema) == 0 { + schema = "public" + } + + filteredSql := GetIndexesQuery + " WHERE schemaname = %s" + schemaFilter := fmt.Sprintf("'%s'", schema) + + return fmt.Sprintf(filteredSql, schemaFilter) +} diff --git a/pkg/supabase/query/sql/table_relationships.go b/pkg/supabase/query/sql/table_relationships.go index 8a898c8b..c7c7c60c 100644 --- a/pkg/supabase/query/sql/table_relationships.go +++ b/pkg/supabase/query/sql/table_relationships.go @@ -1,5 +1,7 @@ package sql +import "fmt" + var GetTableRelationshipsQuery = ` -- Adapted from -- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L722 @@ -46,3 +48,62 @@ LEFT JOIN pks_uniques_cols pks_uqs ON pks_uqs.connamespace = traint.connamespace WHERE traint.contype = 'f' AND traint.conparentid = 0 ` + +var GetTableRelationshipActionsQuery = ` +SELECT + con.oid as id, + con.conname as constraint_name, + con.confdeltype as deletion_action, + con.confupdtype as update_action, + rel.oid as source_id, + nsp.nspname as source_schema, + rel.relname as source_table, + ( + SELECT + array_agg( + att.attname + ORDER BY + un.ord + ) + FROM + unnest(con.conkey) WITH ORDINALITY un (attnum, ord) + INNER JOIN pg_attribute att ON att.attnum = un.attnum + WHERE + att.attrelid = rel.oid + ) source_columns, + frel.oid as target_id, + fnsp.nspname as target_schema, + frel.relname as target_table, + ( + SELECT + array_agg( + att.attname + ORDER BY + un.ord + ) + FROM + unnest(con.confkey) WITH ORDINALITY un (attnum, ord) + INNER JOIN pg_attribute att ON att.attnum = un.attnum + WHERE + att.attrelid = frel.oid + ) target_columns +FROM + pg_constraint con + INNER JOIN pg_class rel ON rel.oid = con.conrelid + INNER JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace + INNER JOIN pg_class frel ON frel.oid = con.confrelid + INNER JOIN pg_namespace fnsp ON fnsp.oid = frel.relnamespace +WHERE + con.contype = 'f' +` + +func GenerateGetTableRelationshipActionsQuery(schema string) string { + if len(schema) == 0 { + schema = "public" + } + + filteredSql := GetTableRelationshipActionsQuery + " AND nsp.nspname = %s" + schemaFilter := fmt.Sprintf("'%s'", schema) + + return fmt.Sprintf(filteredSql, schemaFilter) +} diff --git a/pkg/supabase/query/table.go b/pkg/supabase/query/table.go index a7460f58..6858c86b 100644 --- a/pkg/supabase/query/table.go +++ b/pkg/supabase/query/table.go @@ -361,14 +361,36 @@ func BuildFkQuery(updateType objects.UpdateRelationType, relation *objects.Table do $$ BEGIN IF NOT EXISTS (SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '%s' AND TABLE_NAME = '%s') THEN - %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s.%s (%s); + %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s.%s (%s) %s %s; END IF; END $$; ` + var onUpdate, onDelete string + + if relation.Action != nil { + if relation.Action.UpdateAction != "" { + action := relation.Action.UpdateAction + if len(action) == 1 { + action = string(objects.RelationActionMapLabel[objects.RelationAction(action)]) + } + + onUpdate = fmt.Sprintf("ON UPDATE %s", strings.ToUpper(action)) + } + + if relation.Action.DeletionAction != "" { + action := relation.Action.DeletionAction + if len(action) == 1 { + action = string(objects.RelationActionMapLabel[objects.RelationAction(action)]) + } + + onDelete = fmt.Sprintf("ON DELETE %s", strings.ToUpper(action)) + } + } + return fmt.Sprintf(tmp, relation.ConstraintName, relation.SourceTableName, alter, relation.ConstraintName, relation.SourceColumnName, - relation.TargetTableSchema, relation.TargetTableName, relation.TargetColumnName, + relation.TargetTableSchema, relation.TargetTableName, relation.TargetColumnName, onUpdate, onDelete, ), nil case objects.UpdateRelationDelete: return fmt.Sprintf("%s DROP CONSTRAINT IF EXISTS %s;", alter, relation.ConstraintName), nil @@ -376,3 +398,52 @@ func BuildFkQuery(updateType objects.UpdateRelationType, relation *objects.Table return "", fmt.Errorf("update relation with type '%s' is not available", updateType) } } + +func BuildFKIndexQuery(updateType objects.UpdateRelationType, relation *objects.TablesRelationship) (string, error) { + if relation == nil { + return "", nil + } + + indexName := fmt.Sprintf("ix_%s_%s", relation.SourceTableName, relation.SourceColumnName) + + switch updateType { + case objects.UpdateRelationCreate: + tmp := ` + DO $$ + BEGIN + -- Check if the index already exists + IF NOT EXISTS ( + SELECT 1 + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = '%s' -- Replace with your index name + AND n.nspname = '%s' -- Replace with the schema name if necessary + ) THEN + -- Create the index if it does not exist + CREATE INDEX %s ON %s.%s (%s); + END IF; + END $$; + ` + return fmt.Sprintf(tmp, indexName, relation.SourceSchema, indexName, relation.SourceSchema, relation.SourceTableName, relation.SourceColumnName), nil + case objects.UpdateRelationDelete: + tmp := ` + DO $$ + BEGIN + -- Check if the index already exists + IF NOT EXISTS ( + SELECT 1 + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = '%s' -- Replace with your index name + AND n.nspname = '%s' -- Replace with the schema name if necessary + ) THEN + -- Drop the index if it exists + EXECUTE 'DROP INDEX %s.%s'; -- Ensure to specify the correct schema + END IF; + END $$; + ` + return fmt.Sprintf(tmp, indexName, relation.SourceSchema, relation.SourceSchema, indexName), nil + default: + return "", fmt.Errorf("update index with type '%s' is not available", updateType) + } +} diff --git a/pkg/supabase/supabase.go b/pkg/supabase/supabase.go index 34c1ab0a..3cd188ef 100644 --- a/pkg/supabase/supabase.go +++ b/pkg/supabase/supabase.go @@ -95,6 +95,20 @@ func GetTableByName(cfg *raiden.Config, name string, schema string, includeColum }) } +func GetTableRelationshipActions(cfg *raiden.Config, schema string) (result []objects.TablesRelationshipAction, err error) { + if cfg.DeploymentTarget == raiden.DeploymentTargetCloud { + SupabaseLogger.Debug("Get table by name from supabase cloud", "project-id", cfg.ProjectId) + return decorateActionWithDataErr("Fetch", "table", func() ([]objects.TablesRelationshipAction, error) { + return cloud.GetTableRelationshipActions(cfg, schema) + }) + } + + SupabaseLogger.Debug("Get table by name from supabase pg-meta") + return decorateActionWithDataErr("Fetch", "table", func() ([]objects.TablesRelationshipAction, error) { + return meta.GetTableRelationshipActions(cfg, schema) + }) +} + func CreateTable(cfg *raiden.Config, table objects.Table) (rs objects.Table, err error) { if cfg.DeploymentTarget == raiden.DeploymentTargetCloud { SupabaseLogger.Debug("Create new table to supabase cloud", "table", table.Name, "project-id", cfg.ProjectId) @@ -110,6 +124,7 @@ func CreateTable(cfg *raiden.Config, table objects.Table) (rs objects.Table, err } func UpdateTable(cfg *raiden.Config, newTable objects.Table, updateItems objects.UpdateTableParam) (err error) { + if cfg.DeploymentTarget == raiden.DeploymentTargetCloud { SupabaseLogger.Debug("Update table in supabase cloud", "name", updateItems.OldData.Name, "project-id", cfg.ProjectId) return decorateActionErr("update", "table", func() error { @@ -330,6 +345,19 @@ func DeleteFunction(cfg *raiden.Config, fn objects.Function) (err error) { }) } +func GetIndexes(cfg *raiden.Config, schema string) ([]objects.Index, error) { + if cfg.DeploymentTarget == raiden.DeploymentTargetCloud { + SupabaseLogger.Debug("Get all index from supabase cloud", "project-id", cfg.ProjectId) + return decorateActionWithDataErr("fetch", "index", func() ([]objects.Index, error) { + return cloud.GetIndexes(cfg, schema) + }) + } + SupabaseLogger.Debug("Get all index from supabase pg-meta") + return decorateActionWithDataErr("fetch", "index", func() ([]objects.Index, error) { + return meta.GetIndexes(cfg, schema) + }) +} + func AdminUpdateUserData(cfg *raiden.Config, userId string, data objects.User) (objects.User, error) { if cfg.DeploymentTarget == raiden.DeploymentTargetCloud { SupabaseLogger.Debug("Update user data in supabase cloud", "user-id", userId, "project-id", cfg.ProjectId) diff --git a/pkg/supabase/supabase_test.go b/pkg/supabase/supabase_test.go index efef7cee..41a6a6b0 100644 --- a/pkg/supabase/supabase_test.go +++ b/pkg/supabase/supabase_test.go @@ -13,6 +13,11 @@ import ( ) var ( + relationAction = objects.TablesRelationshipAction{ + ConstraintName: "constraint1", + UpdateAction: "c", + DeletionAction: "c", + } sampleUpdateNewTable = objects.Table{ Schema: "some-schema", Name: "some-table", @@ -38,6 +43,8 @@ var ( SourceSchema: "some-schema", SourceColumnName: "some-column", TargetTableSchema: "other-schema", + Action: &relationAction, + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, }, }, RLSEnabled: true, @@ -452,6 +459,77 @@ func TestUpdateTable_Cloud(t *testing.T) { ForceCreateRelation: false, } + relationAction := objects.TablesRelationshipAction{ + ConstraintName: "constraint1", + UpdateAction: "c", + DeletionAction: "c", + } + updateParam4 := objects.UpdateTableParam{ + OldData: sampleUpdateOldTable, + ChangeColumnItems: []objects.UpdateColumnItem{ + { + Name: "some-column", + UpdateItems: []objects.UpdateColumnType{ + objects.UpdateColumnNew, + }, + }, + }, + ChangeItems: []objects.UpdateTableType{ + objects.UpdateTableName, + }, + ChangeRelationItems: []objects.UpdateRelationItem{ + { + Data: objects.TablesRelationship{ + ConstraintName: "", + SourceSchema: "some-schema", + SourceColumnName: "some-column", + TargetTableSchema: "other-schema", + }, + Type: objects.UpdateRelationDelete, + }, + { + Type: objects.UpdateRelationCreateIndex, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Index: &objects.Index{Schema: "public", Table: "table1", Name: "index1", Definition: "index1"}, + }, + }, + { + Type: objects.UpdateRelationActionOnUpdate, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Action: &relationAction, + }, + }, + { + Type: objects.UpdateRelationActionOnDelete, + Data: objects.TablesRelationship{ + ConstraintName: "constraint1", + SourceSchema: "public", + SourceTableName: "table1", + SourceColumnName: "id", + TargetTableSchema: "public", + TargetTableName: "table2", + TargetColumnName: "id", + Action: &relationAction, + }, + }, + }, + ForceCreateRelation: false, + } + mock := mock.MockSupabase{Cfg: cfg} mock.Activate() defer mock.Deactivate() @@ -476,6 +554,9 @@ func TestUpdateTable_Cloud(t *testing.T) { err4 := supabase.UpdateTable(cfg, sampleUpdateOldTable, updateParam3) assert.NoError(t, err4) + + err5 := supabase.UpdateTable(cfg, sampleUpdateOldTable, updateParam4) + assert.NoError(t, err5) } func TestUpdateTable_SelfHosted(t *testing.T) { @@ -1497,6 +1578,46 @@ func TestDeleteFunction_SelfHosted(t *testing.T) { assert.NoError(t, err1) } +func TestGetIndexes_Cloud(t *testing.T) { + cfg := loadCloudConfig() + + _, err0 := supabase.GetIndexes(cfg, "") + assert.Error(t, err0) + + _, err1 := supabase.GetIndexes(cfg, "public") + assert.Error(t, err1) +} + +func TestGetIndexes_SelfHosted(t *testing.T) { + cfg := loadSelfHostedConfig() + + _, err0 := supabase.GetIndexes(cfg, "") + assert.Error(t, err0) + + _, err1 := supabase.GetIndexes(cfg, "public") + assert.Error(t, err1) +} + +func TestGetActions_Cloud(t *testing.T) { + cfg := loadCloudConfig() + + _, err0 := supabase.GetTableRelationshipActions(cfg, "") + assert.Error(t, err0) + + _, err1 := supabase.GetTableRelationshipActions(cfg, "public") + assert.Error(t, err1) +} + +func TestGetActions_SelfHosted(t *testing.T) { + cfg := loadSelfHostedConfig() + + _, err0 := supabase.GetTableRelationshipActions(cfg, "") + assert.Error(t, err0) + + _, err1 := supabase.GetTableRelationshipActions(cfg, "public") + assert.Error(t, err1) +} + func TestAdminUpdateUser_Cloud(t *testing.T) { cfg := loadCloudConfig()