diff --git a/README.md b/README.md index cd2d100..46b1fd8 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ Application Options: -i, --instance= (required) Cloud Spanner Instance ID. [$SPANNER_INSTANCE_ID] -d, --database= (required) Cloud Spanner Database ID. [$SPANNER_DATABASE_ID] -q, --quiet Disable all interactive prompts. - -t, --tables= Comma separated table names to be truncated. Default to truncate all tables if not specified. - -e, --exclude-tables Comma separated table names to be exempted from truncating. 'tables' and 'exclude-tables' cannot co-exist. If interleaved tables are specified, the parent table is also excluded. + -t, --tables= Comma separated table names to be truncated. Default to truncate all tables if not specified. If an interleaved table is specified, its descendants tables are also truncated. + -e, --exclude-tables Comma separated table names to be exempted from truncating. 'tables' and 'exclude-tables' cannot co-exist. If an interleaved table is specified, its ancestors tables are also excluded. Help Options: -h, --help Show this help message ``` diff --git a/truncate/table_schema.go b/truncate/table_schema.go index e04cade..61111a5 100644 --- a/truncate/table_schema.go +++ b/truncate/table_schema.go @@ -153,6 +153,7 @@ func filterTableSchemas(tables []*tableSchema, targetTables, excludeTables []str // targetFilterTableSchemas filters tables with given targetTables. // If targetTables is empty, it returns all tables. +// When descendants tables of a target table are cascade deletable, they are also targeted to delete. func targetFilterTableSchemas(tables []*tableSchema, targetTables []string) []*tableSchema { if len(targetTables) == 0 { return tables @@ -163,7 +164,17 @@ func targetFilterTableSchemas(tables []*tableSchema, targetTables []string) []*t isTarget[t] = true } - // TODO: Add child tables that may be deleted in cascade (#18) + // Additionally include descendants tables that may be deleted in cascade + lineages := constructTableLineages(tables) + for _, l := range lineages { + if isTarget[l.tableSchema.tableName] { + for _, d := range l.descendants { + if d.isCascadeDeletable() { + isTarget[d.tableName] = true + } + } + } + } filtered := make([]*tableSchema, 0, len(tables)) for _, t := range tables { @@ -177,7 +188,7 @@ func targetFilterTableSchemas(tables []*tableSchema, targetTables []string) []*t // excludeFilterTableSchemas filters tables with given excludeTables. // If excludeTables is empty, it returns all tables. -// When an exclude table is cascade deletable, its parent table is also excluded. +// When an exclude table is cascade deletable, its ancestors tables are also excluded. func excludeFilterTableSchemas(tables []*tableSchema, excludeTableSchemas []string) []*tableSchema { if len(excludeTableSchemas) == 0 { return tables @@ -188,7 +199,7 @@ func excludeFilterTableSchemas(tables []*tableSchema, excludeTableSchemas []stri isExclude[t] = true } - // Additionally exclude parent tables that may delete the exclude tables in cascade + // Additionally exclude ancestors tables that may delete the exclude tables in cascade lineages := constructTableLineages(tables) for _, l := range lineages { if isExclude[l.tableSchema.tableName] && l.tableSchema.isCascadeDeletable() { diff --git a/truncate/table_schema_test.go b/truncate/table_schema_test.go index 08dfc7b..92e97fc 100644 --- a/truncate/table_schema_test.go +++ b/truncate/table_schema_test.go @@ -25,6 +25,27 @@ import ( func TestTargetFilterTableSchemas(t *testing.T) { var ( + // The following tables are hierarchical schemas and deleted in cascade. + // The table schemas are well known in Cloud Spanner document about 'schema and data model'. + singers = &tableSchema{ + tableName: "Singers", + parentTableName: "", + parentOnDeleteAction: deleteActionUndefined, + referencedBy: nil, + } + albums = &tableSchema{ + tableName: "Albums", + parentTableName: "Singers", + parentOnDeleteAction: deleteActionCascadeDelete, + referencedBy: nil, + } + songs = &tableSchema{ + tableName: "Songs", + parentTableName: "Albums", + parentOnDeleteAction: deleteActionCascadeDelete, + referencedBy: nil, + } + // The following tables are flat schemas and not related to each other. t1 = &tableSchema{ tableName: "t1", @@ -44,6 +65,26 @@ func TestTargetFilterTableSchemas(t *testing.T) { parentOnDeleteAction: deleteActionUndefined, referencedBy: nil, } + + // // The following tables are hierarchical schemas and not deleted in cascade. + t4 = &tableSchema{ + tableName: "t4", + parentTableName: "", + parentOnDeleteAction: deleteActionUndefined, + referencedBy: nil, + } + t5 = &tableSchema{ + tableName: "t5", + parentTableName: "t4", + parentOnDeleteAction: deleteActionNoAction, + referencedBy: nil, + } + t6 = &tableSchema{ + tableName: "t6", + parentTableName: "t5", + parentOnDeleteAction: deleteActionNoAction, + referencedBy: nil, + } ) opts := []cmp.Option{ @@ -60,19 +101,35 @@ func TestTargetFilterTableSchemas(t *testing.T) { want []*tableSchema }{ { - desc: "Include multiple tables", - schemas: []*tableSchema{t1, t2, t3}, - targetTables: []string{t1.tableName, t2.tableName}, - want: []*tableSchema{t1, t2}, + desc: "Include descendants tables by tracing down to the bottommost level.", + schemas: []*tableSchema{singers, albums, songs, t1, t2, t3}, + targetTables: []string{singers.tableName}, + want: []*tableSchema{singers, albums, songs}, + }, + { + desc: "Include only the lower levels without the higher levels.", + schemas: []*tableSchema{singers, albums, songs, t1, t2, t3}, + targetTables: []string{albums.tableName}, + want: []*tableSchema{albums, songs}, + }, + { + desc: "Include multiple tables.", + schemas: []*tableSchema{singers, albums, songs, t1, t2, t3}, + targetTables: []string{singers.tableName, t1.tableName, t2.tableName}, + want: []*tableSchema{singers, albums, songs, t1, t2}, }, { desc: "Do nothing when no target tables are passed.", - schemas: []*tableSchema{t1, t2, t3}, + schemas: []*tableSchema{singers, albums, songs, t1, t2, t3}, targetTables: nil, - want: []*tableSchema{t1, t2, t3}, + want: []*tableSchema{singers, albums, songs, t1, t2, t3}, + }, + { + desc: "Do not include descendants tables that will not be deleted in cascade.", + schemas: []*tableSchema{singers, albums, songs, t4, t5, t6}, + targetTables: []string{singers.tableName, t4.tableName}, + want: []*tableSchema{singers, albums, songs, t4}, }, - // TODO: Determine the specifications for parent-child relationships in hierarchical interleaved tables, and add corresponding tests and implementation. - // This includes defining the behavior of targetFilterTableSchemas for cases where tables not included in the target list are subject to cascade deletion. } { t.Run(test.desc, func(t *testing.T) { got := targetFilterTableSchemas(test.schemas, test.targetTables) @@ -166,7 +223,7 @@ func TestExcludeFilterTableSchemas(t *testing.T) { want []*tableSchema }{ { - desc: "Exclude the parent tables by tracing up to the topmost level.", + desc: "Exclude ancestors tables by tracing up to the topmost level.", schemas: []*tableSchema{singers, albums, songs, t1, t2, t3}, excludeTables: []string{songs.tableName}, want: []*tableSchema{t1, t2, t3}, @@ -190,7 +247,7 @@ func TestExcludeFilterTableSchemas(t *testing.T) { want: []*tableSchema{singers, albums, songs, t1, t2, t3}, }, { - desc: "Do not exclude the parent tables that are not deleted in cascade.", + desc: "Do not exclude ancestors tables that are not deleted in cascade.", schemas: []*tableSchema{singers, albums, songs, t4, t5, t6}, excludeTables: []string{songs.tableName, t6.tableName}, want: []*tableSchema{t4, t5},