From 18e4fb3b9099b793dfc2e9d7366e6733bab3b16b Mon Sep 17 00:00:00 2001 From: Ivan Histand Date: Mon, 27 Jul 2026 22:05:12 -0500 Subject: [PATCH] Support '*' wildcards in --actions selectors as documented (#2224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matchPatterns did plain string equality only, so the '*' wildcards the --actions help text (run + compile) advertises never matched anything — `run --actions "*"` / "mrd*" reported "No actions to run." Compile wildcard patterns to an anchored RegExp: non-'*' characters match literally (regex metacharacters escaped, so '.' stays a literal dot), each '*' becomes '.*'. A pattern containing '.' matches the fully-qualified action name, otherwise the unqualified last segment — mirroring the existing exact-match branches. Wildcards bypass the ambiguous-name error since matching many actions is the intent; exact selection is unchanged. Adds a matchPatterns test suite (previously untested). Fixes #2224 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171FwKo8gRQQ35VoYDtSHNU --- core/utils.ts | 25 +++++++++++++++++- core/utils_test.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/core/utils.ts b/core/utils.ts index 9068ad669..03234ada0 100644 --- a/core/utils.ts +++ b/core/utils.ts @@ -26,10 +26,33 @@ type actionsWithDependencies = export const nativeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require; +// Turn a selector pattern containing '*' wildcards into an anchored RegExp. +// Every character except '*' is matched literally (regex metacharacters are +// escaped); each '*' matches any run of characters, so "mrd*" -> /^mrd.*$/ and +// "*features*" -> /^.*features.*$/. +function globToRegExp(pattern: string): RegExp { + const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*"); + return new RegExp(`^${escaped}$`); +} + export function matchPatterns(patterns: string[], values: string[]) { const fullyQualifiedActions: string[] = []; patterns.forEach(pattern => { - if (pattern.includes(".")) { + if (pattern.includes("*")) { + // Wildcard selector. A pattern that contains "." matches against the + // fully-qualified action name; otherwise it matches against the unqualified + // name (last segment), mirroring the exact-match branches below. Wildcards + // are expected to select many actions, so no ambiguity error applies here. + const regExp = globToRegExp(pattern); + const scope = pattern.includes(".") + ? values + : values.map(value => value.split(".").slice(-1)[0]); + values.forEach((value, i) => { + if (regExp.test(scope[i])) { + fullyQualifiedActions.push(value); + } + }); + } else if (pattern.includes(".")) { if (values.includes(pattern)) { fullyQualifiedActions.push(pattern); } diff --git a/core/utils_test.ts b/core/utils_test.ts index 88c281f92..b23d82e17 100644 --- a/core/utils_test.ts +++ b/core/utils_test.ts @@ -6,6 +6,7 @@ import { getEffectiveTableFolderSubpath, getFileFormatValueForIcebergTable, getStorageUriForIcebergTable, + matchPatterns, validateConnectionFormat, validateNoMixedCompilationMode, validateStorageUriFormat, @@ -299,4 +300,67 @@ suite('Dataform Utility Validations', () => { ); }); }); + + suite('matchPatterns', () => { + const values = [ + 'schema.mrd_features_inference', + 'schema.mrd_features_training', + 'other.customer_orders', + 'analytics.mrd_summary', + ]; + + test('exact unqualified name selects the single matching action', () => { + expect(matchPatterns(['mrd_features_inference'], values)).to.deep.equal([ + 'schema.mrd_features_inference', + ]); + }); + + test('exact fully-qualified name selects that action', () => { + expect(matchPatterns(['other.customer_orders'], values)).to.deep.equal([ + 'other.customer_orders', + ]); + }); + + test('ambiguous unqualified exact name still throws', () => { + // Two schemas, same unqualified name. + const dupes = ['a.dup', 'b.dup']; + expect(() => matchPatterns(['dup'], dupes)).to.throw(); + }); + + test('bare "*" matches every action', () => { + expect(matchPatterns(['*'], values)).to.deep.equal(values); + }); + + test('prefix wildcard matches on the unqualified name', () => { + expect(matchPatterns(['mrd*'], values)).to.deep.equal([ + 'schema.mrd_features_inference', + 'schema.mrd_features_training', + 'analytics.mrd_summary', + ]); + }); + + test('surrounding wildcards match a substring of the unqualified name', () => { + expect(matchPatterns(['*features*'], values)).to.deep.equal([ + 'schema.mrd_features_inference', + 'schema.mrd_features_training', + ]); + }); + + test('qualified wildcard matches against the fully-qualified name', () => { + expect(matchPatterns(['schema.*'], values)).to.deep.equal([ + 'schema.mrd_features_inference', + 'schema.mrd_features_training', + ]); + }); + + test('wildcard with no matches returns empty (no ambiguity error)', () => { + expect(matchPatterns(['nope*'], values)).to.deep.equal([]); + }); + + test('literal dot in a qualified wildcard is not a regex wildcard', () => { + // "schemaXmrd..." must NOT match "schema.*" — the "." is literal. + const tricky = ['schema.mrd_a', 'schemaXmrd_b']; + expect(matchPatterns(['schema.*'], tricky)).to.deep.equal(['schema.mrd_a']); + }); + }); });