Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,45 @@ type actionsWithDependencies =
export const nativeRequire =
typeof __webpack_require__ === "function" ? __non_webpack_require__ : require;

// Turns a selector pattern into an anchored RegExp in which "*" is a wildcard and
// everything else is literal text, e.g. "mrd*" -> /^mrd.*$/ and
// "*features*" -> /^.*features.*$/.
//
// Splitting on "*" first is what keeps the rest simple: every "*" in a pattern is a
// wildcard by definition, so the pieces between them are pure literal text and can be
// escaped wholesale, then rejoined with ".*".
//
// The escape is needed because an action name is not regex-safe. Names are
// dot-separated ("project.dataset.name"), and "." in a regex matches any character, so
// without escaping "schema.*" would also select "schemaXtable" - see the "literal dot"
// case in utils_test.ts. The set below is the usual list of JavaScript regex
// metacharacters with one deliberate omission: "*", which is left out because the split
// above has already consumed every "*", so none can reach here. ("]" and "\" carry
// backslashes for the character class's own syntax; "-" and "/" are not metacharacters
// outside a class and so need no escaping.)
function globToRegExp(pattern: string): RegExp {
const escapeLiteral = (literal: string) => literal.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^${pattern.split("*").map(escapeLiteral).join(".*")}$`);
}

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);
Comment thread
kolina marked this conversation as resolved.
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);
}
Expand Down
64 changes: 64 additions & 0 deletions core/utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getEffectiveTableFolderSubpath,
getFileFormatValueForIcebergTable,
getStorageUriForIcebergTable,
matchPatterns,
validateConnectionFormat,
validateNoMixedCompilationMode,
validateStorageUriFormat,
Expand Down Expand Up @@ -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']);
});
});
});
Loading