diff --git a/cli/api/commands/prune.ts b/cli/api/commands/prune.ts index 71358ea0b..0388ec613 100644 --- a/cli/api/commands/prune.ts +++ b/cli/api/commands/prune.ts @@ -44,6 +44,7 @@ function computeIncludedActionNames( const hasActionSelector = runConfig.actions?.length > 0; const hasTagSelector = runConfig.tags?.length > 0; + const hasFlterTagsAsIntersectionSelector = runConfig.filterTagsAsIntersection?.valueOf(); // If no selectors, return all actions. if (!hasActionSelector && !hasTagSelector) { @@ -61,9 +62,18 @@ function computeIncludedActionNames( // Determine actions selected with --tag option and update applicable actions if (hasTagSelector) { - allActions - .filter(action => action.tags.some(tag => runConfig.tags.includes(tag))) - .forEach(action => includedActionNames.add(targetAsReadableString(action.target))); + // Select actions which match the intersection of the set of tags. + if (hasFlterTagsAsIntersectionSelector) { + allActions + .filter(action => runConfig.tags.every(tag => action.tags.includes(tag))) + .forEach(action => includedActionNames.add(targetAsReadableString(action.target))); + } + // Select actions which match the union of the set of tags. + else { + allActions + .filter(action => action.tags.some(tag => runConfig.tags.includes(tag))) + .forEach(action => includedActionNames.add(targetAsReadableString(action.target))); + } } // Compute all transitive dependencies. diff --git a/cli/index.ts b/cli/index.ts index 3107471e6..4aa4ad039 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -91,6 +91,15 @@ const tagsOption: INamedOption = { } }; +const flterTagsAsIntersectionSelectorOption: INamedOption = { + name: "filter-tags-as-intersection", + option: { + describe: "If set, only actions that include every user-specified tags will be run", + type: "boolean", + default: false + } +} + const includeDepsOption: INamedOption = { name: "include-deps", option: { @@ -469,6 +478,7 @@ export function runCli() { credentialsOption, fullRefreshOption, includeDepsOption, + flterTagsAsIntersectionSelectorOption, includeDependentsOption, credentialsOption, jsonOutputOption, @@ -511,7 +521,9 @@ export function runCli() { actions: argv[actionsOption.name], includeDependencies: argv[includeDepsOption.name], includeDependents: argv[includeDependentsOption.name], - tags: argv[tagsOption.name] + tags: argv[tagsOption.name], + flterTagsAsIntersectionSelector: argv[flterTagsAsIntersectionSelectorOption.name] + }, dbadapter ); diff --git a/protos/execution.proto b/protos/execution.proto index 17f883719..616f0c98f 100644 --- a/protos/execution.proto +++ b/protos/execution.proto @@ -10,6 +10,7 @@ option go_package = "github.com/dataform-co/dataform/protos/dataform"; message RunConfig { repeated string actions = 1; repeated string tags = 5; + bool filter_tags_as_intersection = 10; bool include_dependencies = 3; bool include_dependents = 11; bool full_refresh = 2; diff --git a/tests/api/api.spec.ts b/tests/api/api.spec.ts index 8fd7bbae7..120009948 100644 --- a/tests/api/api.spec.ts +++ b/tests/api/api.spec.ts @@ -287,6 +287,11 @@ suite("@dataform/api", () => { target: { schema: "schema", name: "op_d" }, tags: ["tag3"], queries: ["create or replace view schema.someview as select 1 as test"] + }, + { + target: { schema: "schema", name: "op_e" }, + tags: ["tag1", "tag2"], + queries: ["create or replace view schema.someview as select 1 as test"] } ], tables: [ @@ -362,6 +367,22 @@ suite("@dataform/api", () => { expect(actionNames).includes("schema.tab_a"); }); + test("prune actions with --tags (with include all tags)", () => { + const prunedGraph = prune(TEST_GRAPH_WITH_TAGS, { + tags: ["tag1", "tag2"], + includeAllTags: true + }); + const actionNames = [ + ...prunedGraph.tables.map(action => targetAsReadableString(action.target)), + ...prunedGraph.operations.map(action => targetAsReadableString(action.target)) + ]; + expect(actionNames).deep.equals(["schema.op_e", "schema.op_a"]); + expect(actionNames).not.includes("schema.op_a"); + expect(actionNames).not.includes("schema.op_b"); + expect(actionNames).not.includes("schema.op_c"); + expect(actionNames).not.includes("schema.op_d"); + }); + test("prune actions with --actions with dependencies", () => { const prunedGraph = prune(TEST_GRAPH, { actions: ["schema.a"], includeDependencies: true }); const actionNames = [