summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSlusarenko Igor <[email protected]>2026-07-15 14:08:44 +0300
committerGitHub <[email protected]>2026-07-15 14:08:44 +0300
commitf3db6838ad5e3bb3289849c554b35bc665f23bd7 (patch)
tree9710e07453ed14c7b7b520c984a327addc8181fb
parent9b22d24959121e1a56f8ca67c23b902d82a50842 (diff)
wm: make classifier predicates match the RFC (#46499)
-rw-r--r--ydb/core/kqp/workload_service/kqp_query_classifier.cpp6
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_app_name_ut.cpp51
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_full_scan_matcher_ut.cpp10
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_full_scan_ut.cpp4
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_path_ddl_ut.cpp2
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_path_matcher_ut.cpp16
-rw-r--r--ydb/core/kqp/workload_service/ut/kqp_has_path_ut.cpp4
-rw-r--r--ydb/core/resource_pools/regex_predicate.cpp44
-rw-r--r--ydb/core/resource_pools/regex_predicate.h10
-rw-r--r--ydb/core/resource_pools/regex_predicate_ut.cpp95
-rw-r--r--ydb/core/resource_pools/resource_pool_classifier_settings.cpp2
-rw-r--r--ydb/core/resource_pools/resource_pool_classifier_settings.h2
-rw-r--r--ydb/core/resource_pools/resource_pool_classifier_settings_ut.cpp44
13 files changed, 148 insertions, 142 deletions
diff --git a/ydb/core/kqp/workload_service/kqp_query_classifier.cpp b/ydb/core/kqp/workload_service/kqp_query_classifier.cpp
index 0ba7bf442bd..244cdab017c 100644
--- a/ydb/core/kqp/workload_service/kqp_query_classifier.cpp
+++ b/ydb/core/kqp/workload_service/kqp_query_classifier.cpp
@@ -154,10 +154,10 @@ private:
}
///
- /// Check Predicate HasAppName
+ /// Check Predicate HasAppName — strict string equality.
///
- static bool MatchesAppName(const std::optional<NResourcePool::TRegexPredicate>& predicate, const TString& appName) {
- return !predicate || predicate->Match(appName);
+ static bool MatchesAppName(const std::optional<TString>& expected, const TString& appName) {
+ return !expected || *expected == appName;
}
///
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_app_name_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_app_name_ut.cpp
index dd729034a13..a4ef96f6ac0 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_app_name_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_app_name_ut.cpp
@@ -32,21 +32,7 @@ Y_UNIT_TEST_SUITE(TQueryClassifierHasAppName) {
UNIT_ASSERT_VALUES_EQUAL(GetPoolId(tc.RunPreClassify()), "pool_target");
}
- Y_UNIT_TEST(ShouldMatchAppNameRegexSuffix) {
- TClassifyTestCase tc;
- tc.ClassifierHasAppName = "ydb-.*";
- tc.ContextAppName = "ydb-ui";
- UNIT_ASSERT_VALUES_EQUAL(GetPoolId(tc.RunPreClassify()), "pool_target");
- }
-
- Y_UNIT_TEST(ShouldMatchAppNameAlternation) {
- TClassifyTestCase tc;
- tc.ClassifierHasAppName = "ydb-ui|ydb-cli";
- tc.ContextAppName = "ydb-cli";
- UNIT_ASSERT_VALUES_EQUAL(GetPoolId(tc.RunPreClassify()), "pool_target");
- }
-
- Y_UNIT_TEST(ShouldRejectLiteralBeyondAnchor) {
+ Y_UNIT_TEST(ShouldRequireExactMatch) {
TClassifyTestCase tc;
tc.ClassifierHasAppName = "ydb-ui";
tc.ContextAppName = "ydb-ui-prod";
@@ -79,41 +65,6 @@ Y_UNIT_TEST_SUITE(HasAppNameDdl) {
WaitForClassifierSuccess(ydb, noMatchSettings);
}
- Y_UNIT_TEST(TestHasAppNameRegexClassifier) {
- auto ydb = TYdbSetupSettings().Create();
-
- const TString& poolId = "regex_pool";
- const TString& userSID = "test@user";
- ydb->ExecuteSchemeQuery(TStringBuilder() << R"(
- GRANT ALL ON `/)" << ydb->GetSettings().DomainName_ << R"(` TO `)" << userSID << R"(`;
- CREATE RESOURCE POOL )" << poolId << R"( WITH (
- CONCURRENT_QUERY_LIMIT=0
- );
- CREATE RESOURCE POOL CLASSIFIER regex_classifier WITH (
- RESOURCE_POOL=")" << poolId << R"(",
- HAS_APP_NAME="my_app.*"
- );
- )");
-
- auto matchSettings = TQueryRunnerSettings().PoolId("").UserSID(userSID).ApplicationName("my_app_v2");
- WaitForClassifierFail(ydb, matchSettings, poolId);
-
- auto noMatchSettings = TQueryRunnerSettings().PoolId("").UserSID(userSID).ApplicationName("other_app");
- WaitForClassifierSuccess(ydb, noMatchSettings);
- }
-
- Y_UNIT_TEST(TestHasAppNameInvalidRegex) {
- auto ydb = TYdbSetupSettings().Create();
-
- auto result = ydb->ExecuteQuery(TStringBuilder() << R"(
- CREATE RESOURCE POOL CLASSIFIER bad_regex_classifier WITH (
- RESOURCE_POOL=")" << NResourcePool::DEFAULT_POOL_ID << R"(",
- HAS_APP_NAME="[invalid"
- );
- )", TQueryRunnerSettings().PoolId(NResourcePool::DEFAULT_POOL_ID));
- UNIT_ASSERT_VALUES_UNEQUAL(result.GetStatus(), EStatus::SUCCESS);
- }
-
Y_UNIT_TEST(TestAlterHasAppName) {
auto ydb = TYdbSetupSettings().Create();
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_matcher_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_matcher_ut.cpp
index 9b872f8ee6d..28c135ff3e3 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_matcher_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_matcher_ut.cpp
@@ -36,7 +36,7 @@ TKqpReadRangesSource* AddSource(TKqpPhyQuery& phy, const TString& path) {
const auto Matches = [](auto builder, auto configure) {
TKqpPhyQuery phy;
configure(builder(phy, TString("/Root/t")));
- return NWorkload::MatchesFullScan(TRegexPredicate::Compile("/Root/t"), phy);
+ return NWorkload::MatchesFullScan(TRegexPredicate::FromGlob("/Root/t"), phy);
};
const auto AssertHasFullScan = [](auto c) { UNIT_ASSERT( Matches(AddOp, c)); };
@@ -55,21 +55,21 @@ Y_UNIT_TEST_SUITE(TFullScanMatcherPredicate) {
}
Y_UNIT_TEST(NonMatchingPathIsIgnored) {
- auto pred = TRegexPredicate::Compile("/Root/critical");
+ auto pred = TRegexPredicate::FromGlob("/Root/critical");
TKqpPhyQuery phy;
AddOp(phy, "/Root/other")->MutableReadRange()->MutableKeyRange();
UNIT_ASSERT(!NWorkload::MatchesFullScan(pred, phy));
}
- Y_UNIT_TEST(RegexPatternMatches) {
- auto pred = TRegexPredicate::Compile("/Root/db/orders_archive.*");
+ Y_UNIT_TEST(GlobPatternMatches) {
+ auto pred = TRegexPredicate::FromGlob("/Root/db/orders_archive*");
TKqpPhyQuery phy;
AddOp(phy, "/Root/db/orders_archive_2024")->MutableReadRange()->MutableKeyRange();
UNIT_ASSERT(NWorkload::MatchesFullScan(pred, phy));
}
Y_UNIT_TEST(MixedOpsReturnTrueOnAnyMatch) {
- auto pred = TRegexPredicate::Compile("/Root/t");
+ auto pred = TRegexPredicate::FromGlob("/Root/t");
TKqpPhyQuery phy;
AddOp(phy, "/Root/other")->MutableReadRange()->MutableKeyRange();
AddOp(phy, "/Root/t")->MutableReadRange()->MutableKeyRange();
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_ut.cpp
index 667755fdb83..ee69f7f5fa4 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_full_scan_ut.cpp
@@ -53,9 +53,9 @@ Y_UNIT_TEST_SUITE(TQueryClassifierHasFullScan) {
UNIT_ASSERT_VALUES_EQUAL(GetPostPoolId(result), "default");
}
- Y_UNIT_TEST(ShouldMatchRegexPattern) {
+ Y_UNIT_TEST(ShouldMatchGlobPattern) {
TClassifyTestCase tc;
- tc.ClassifierHasFullScan = "/Root/testdb/orders_.*";
+ tc.ClassifierHasFullScan = "/Root/testdb/orders_*";
auto result = tc.RunPostClassify("/Root/testdb/orders_2024", /*isFullScan=*/true);
UNIT_ASSERT_VALUES_EQUAL(GetPostPoolId(result), "pool_target");
}
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_path_ddl_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_path_ddl_ut.cpp
index 382b19bbaf8..114721f8651 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_path_ddl_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_path_ddl_ut.cpp
@@ -241,7 +241,7 @@ Y_UNIT_TEST_SUITE(HasPathSysView) {
SetupRowStoreTable(ydb, userSID);
ydb->ExecuteSchemeQuery(RejectClassifierDdl(
- poolId, "sysview_classifier", "/Root/.sys/.*"));
+ poolId, "sysview_classifier", "/Root/.sys/*"));
const TString query = "SELECT * FROM t;";
auto settings = NWorkload::TQueryRunnerSettings().PoolId("").UserSID(userSID);
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_path_matcher_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_path_matcher_ut.cpp
index 689f96443fe..55cd3c86ef8 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_path_matcher_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_path_matcher_ut.cpp
@@ -44,32 +44,32 @@ void AddPqTopicSink(TKqpPhyQuery& phy, const TString& topicPath) {
// build-and-check one-liners (mirrors kqp_has_full_scan_matcher_ut.cpp style).
// Runs the matcher against a phyQuery holding one tx-level table entry.
-// Predicate + path both vary so predicate-behaviour tests can exercise regex
+// Predicate + path both vary so predicate-behaviour tests can exercise glob
// semantics and canonicalization in isolation.
const auto MatchesTxTable = [](const TString& pattern, const TString& path) {
TKqpPhyQuery phy;
AddTxTable(phy, path);
- return NWorkload::MatchesPath(TRegexPredicate::Compile(pattern), {}, phy);
+ return NWorkload::MatchesPath(TRegexPredicate::FromGlob(pattern), {}, phy);
};
-// Runs the matcher with pattern `/Root/t.*` against the given queryTables list.
+// Runs the matcher with pattern `/Root/t*` against the given queryTables list.
const auto MatchesQueryTables = [](const TVector<TString>& queryTables) {
TKqpPhyQuery phy;
- return NWorkload::MatchesPath(TRegexPredicate::Compile("/Root/t.*"), queryTables, phy);
+ return NWorkload::MatchesPath(TRegexPredicate::FromGlob("/Root/t*"), queryTables, phy);
};
// Runs the matcher with pattern `/Root/db/target` against a phyQuery built by `build`.
const auto MatchesTx = [](auto build) {
TKqpPhyQuery phy;
build(phy);
- return NWorkload::MatchesPath(TRegexPredicate::Compile("/Root/db/target"), {}, phy);
+ return NWorkload::MatchesPath(TRegexPredicate::FromGlob("/Root/db/target"), {}, phy);
};
// Runs the matcher with pattern `/Root/db/topic` against a phyQuery built by `build`.
const auto MatchesTopics = [](auto build) {
TKqpPhyQuery phy;
build(phy);
- return NWorkload::MatchesPath(TRegexPredicate::Compile("/Root/db/topic"), {}, phy);
+ return NWorkload::MatchesPath(TRegexPredicate::FromGlob("/Root/db/topic"), {}, phy);
};
} // anonymous namespace
@@ -87,8 +87,8 @@ Y_UNIT_TEST_SUITE(TPathMatcherPredicate) {
UNIT_ASSERT(!MatchesTxTable("/Root/critical", "/Root/other"));
}
- Y_UNIT_TEST(RegexPatternMatches) {
- UNIT_ASSERT(MatchesTxTable("/Root/db/archive.*", "/Root/db/archive_2024"));
+ Y_UNIT_TEST(GlobPatternMatches) {
+ UNIT_ASSERT(MatchesTxTable("/Root/db/archive*", "/Root/db/archive_2024"));
}
Y_UNIT_TEST(CanonizesPathWithoutLeadingSlash) {
diff --git a/ydb/core/kqp/workload_service/ut/kqp_has_path_ut.cpp b/ydb/core/kqp/workload_service/ut/kqp_has_path_ut.cpp
index d6d140f507e..44f22c21661 100644
--- a/ydb/core/kqp/workload_service/ut/kqp_has_path_ut.cpp
+++ b/ydb/core/kqp/workload_service/ut/kqp_has_path_ut.cpp
@@ -45,9 +45,9 @@ Y_UNIT_TEST_SUITE(TQueryClassifierHasPath) {
UNIT_ASSERT_VALUES_EQUAL(GetPostPoolId(result), "default");
}
- Y_UNIT_TEST(ShouldMatchRegexPattern) {
+ Y_UNIT_TEST(ShouldMatchGlobPattern) {
TClassifyTestCase tc;
- tc.ClassifierHasPath = "/Root/testdb/archive/.*";
+ tc.ClassifierHasPath = "/Root/testdb/archive/*";
auto result = tc.RunPostClassifyForPath("/Root/testdb/archive/orders_2024");
UNIT_ASSERT_VALUES_EQUAL(GetPostPoolId(result), "pool_target");
}
diff --git a/ydb/core/resource_pools/regex_predicate.cpp b/ydb/core/resource_pools/regex_predicate.cpp
index 189f195d29e..9f1f84c7b31 100644
--- a/ydb/core/resource_pools/regex_predicate.cpp
+++ b/ydb/core/resource_pools/regex_predicate.cpp
@@ -8,18 +8,46 @@
namespace NKikimr::NResourcePool {
-TRegexPredicate TRegexPredicate::Compile(TStringBuf pattern) {
- if (pattern.empty()) {
- throw yexception() << "TRegexPredicate::Compile called with empty pattern; "
- << "caller must treat empty as nullopt (no filter)";
- }
- TString anchored = TStringBuilder() << "^(?:" << pattern << ")$";
+namespace {
+
+TRegexPredicate CompileAnchored(TStringBuf originalPattern, TStringBuf translated) {
+ TString anchored = TStringBuilder() << "^(?:" << translated << ")$";
try {
auto compiled = std::make_shared<TRegExMatch>(anchored, REG_EXTENDED | REG_NOSUB);
- return TRegexPredicate{TString(pattern), std::move(compiled)};
+ return TRegexPredicate{TString(originalPattern), std::move(compiled)};
} catch (...) {
- throw yexception() << "Invalid regex pattern '" << pattern << "': " << CurrentExceptionMessage();
+ throw yexception() << "Failed to compile pattern '" << originalPattern << "': " << CurrentExceptionMessage();
+ }
+}
+
+} // namespace
+
+TRegexPredicate TRegexPredicate::FromGlob(TStringBuf glob) {
+ if (glob.empty()) {
+ throw yexception() << "TRegexPredicate::FromGlob called with empty value; "
+ << "caller must treat empty as nullopt (no filter)";
+ }
+ TString translated;
+ translated.reserve(glob.size() * 2);
+ for (char c : glob) {
+ switch (c) {
+ case '*':
+ translated += ".*";
+ break;
+ case '?':
+ translated += '.';
+ break;
+ case '.': case '\\': case '+': case '(': case ')':
+ case '[': case ']': case '{': case '}':
+ case '|': case '^': case '$':
+ translated += '\\';
+ translated += c;
+ break;
+ default:
+ translated += c;
+ }
}
+ return CompileAnchored(glob, translated);
}
bool TRegexPredicate::Match(const TString& value) const {
diff --git a/ydb/core/resource_pools/regex_predicate.h b/ydb/core/resource_pools/regex_predicate.h
index 9603600b231..c9cc1ea59b0 100644
--- a/ydb/core/resource_pools/regex_predicate.h
+++ b/ydb/core/resource_pools/regex_predicate.h
@@ -12,17 +12,15 @@ class TRegExMatch;
namespace NKikimr::NResourcePool {
///
-/// A compiled regex predicate for classifier matching.
-///
-/// Stores both the original user pattern and the compiled regex
-/// (anchored as ^(?:pattern)$). Reusable for any regex-typed
-/// classifier field (HAS_APP_NAME and future matchers).
+/// A compiled predicate for classifier matching. Built from a user-facing
+/// glob (`*`, `?` wildcards; other characters — including regex metachars —
+/// are literal).
///
struct TRegexPredicate {
TString Pattern;
std::shared_ptr<TRegExMatch> Compiled;
- static TRegexPredicate Compile(TStringBuf pattern);
+ static TRegexPredicate FromGlob(TStringBuf glob);
bool Match(const TString& value) const;
};
diff --git a/ydb/core/resource_pools/regex_predicate_ut.cpp b/ydb/core/resource_pools/regex_predicate_ut.cpp
index c0722f81994..87194c7bcc4 100644
--- a/ydb/core/resource_pools/regex_predicate_ut.cpp
+++ b/ydb/core/resource_pools/regex_predicate_ut.cpp
@@ -8,49 +8,84 @@ namespace NKikimr {
using NResourcePool::TRegexPredicate;
-Y_UNIT_TEST_SUITE(TRegexPredicateTest) {
+Y_UNIT_TEST_SUITE(TRegexPredicateFromGlob) {
- Y_UNIT_TEST(LiteralIsAnchored) {
- auto pred = TRegexPredicate::Compile("ydb-ui");
- UNIT_ASSERT(pred.Match("ydb-ui"));
- UNIT_ASSERT(!pred.Match("ydb-ui-prod"));
- UNIT_ASSERT(!pred.Match("prefix-ydb-ui"));
+ Y_UNIT_TEST(LiteralGlob) {
+ auto pred = TRegexPredicate::FromGlob("/Root/db/orders");
+ UNIT_ASSERT(pred.Match("/Root/db/orders"));
+ UNIT_ASSERT(!pred.Match("/Root/db/orders_archive"));
+ UNIT_ASSERT(!pred.Match("/Root/db"));
}
- Y_UNIT_TEST(AlternationMatchesExactlyEachBranch) {
- auto pred = TRegexPredicate::Compile("ydb-ui|ydb-cli");
- UNIT_ASSERT(pred.Match("ydb-ui"));
- UNIT_ASSERT(pred.Match("ydb-cli"));
- UNIT_ASSERT(!pred.Match("ydb-ui-prod"));
- UNIT_ASSERT(!pred.Match("prefix-ydb-cli"));
+ Y_UNIT_TEST(StarMatchesAnyCount) {
+ auto pred = TRegexPredicate::FromGlob("/Root/db/*");
+ UNIT_ASSERT(pred.Match("/Root/db/"));
+ UNIT_ASSERT(pred.Match("/Root/db/orders"));
+ UNIT_ASSERT(pred.Match("/Root/db/archive/orders/2024"));
+ UNIT_ASSERT(!pred.Match("/Root/db"));
+ UNIT_ASSERT(!pred.Match("/Root/other/x"));
}
- Y_UNIT_TEST(WildcardSuffix) {
- auto pred = TRegexPredicate::Compile("ydb-.*");
- UNIT_ASSERT(pred.Match("ydb-ui"));
- UNIT_ASSERT(pred.Match("ydb-cli-v2"));
- UNIT_ASSERT(!pred.Match("ydb"));
- UNIT_ASSERT(!pred.Match("other-ydb-ui"));
+ Y_UNIT_TEST(QuestionMarkMatchesExactlyOne) {
+ auto pred = TRegexPredicate::FromGlob("/Root/t?");
+ UNIT_ASSERT(pred.Match("/Root/t1"));
+ UNIT_ASSERT(pred.Match("/Root/t_"));
+ UNIT_ASSERT(!pred.Match("/Root/t"));
+ UNIT_ASSERT(!pred.Match("/Root/t12"));
}
- Y_UNIT_TEST(EmptyInputDoesNotMatchNonEmptyPattern) {
- auto pred = TRegexPredicate::Compile("ydb-ui");
- UNIT_ASSERT(!pred.Match(""));
+ Y_UNIT_TEST(QuestionMarkMatchesSlash) {
+ auto pred = TRegexPredicate::FromGlob("/Root?db");
+ UNIT_ASSERT(pred.Match("/Root/db"));
+ UNIT_ASSERT(pred.Match("/RootXdb"));
+ UNIT_ASSERT(!pred.Match("/Rootdb"));
}
- Y_UNIT_TEST(EmptyPatternThrows) {
- UNIT_ASSERT_EXCEPTION(TRegexPredicate::Compile(""), yexception);
+ Y_UNIT_TEST(StarMatchesSlash) {
+ auto pred = TRegexPredicate::FromGlob("/Root/*/orders");
+ UNIT_ASSERT(pred.Match("/Root/db1/orders"));
+ UNIT_ASSERT(pred.Match("/Root/tenant/sub/orders"));
+ UNIT_ASSERT(!pred.Match("/Root/orders"));
}
- Y_UNIT_TEST(AdminProvidedAnchorsAreHarmless) {
- auto pred = TRegexPredicate::Compile("^ydb-ui$");
- UNIT_ASSERT(pred.Match("ydb-ui"));
- UNIT_ASSERT(!pred.Match("ydb-ui-prod"));
+ Y_UNIT_TEST(DoubleStarCollapses) {
+ auto both = TRegexPredicate::FromGlob("/Root/**");
+ auto one = TRegexPredicate::FromGlob("/Root/*");
+ UNIT_ASSERT(both.Match("/Root/"));
+ UNIT_ASSERT(both.Match("/Root/anything"));
+ UNIT_ASSERT(one.Match("/Root/"));
+ UNIT_ASSERT(one.Match("/Root/anything"));
}
- Y_UNIT_TEST(InvalidPatternThrows) {
- UNIT_ASSERT_EXCEPTION(TRegexPredicate::Compile("[unclosed"), yexception);
- UNIT_ASSERT_EXCEPTION(TRegexPredicate::Compile("(?<bad"), yexception);
+ Y_UNIT_TEST(MixedStarAndQuestion) {
+ auto pred = TRegexPredicate::FromGlob("/Root/t?/*");
+ UNIT_ASSERT(pred.Match("/Root/t1/anything"));
+ UNIT_ASSERT(pred.Match("/Root/tX/"));
+ UNIT_ASSERT(!pred.Match("/Root/t/anything"));
+ UNIT_ASSERT(!pred.Match("/Root/t12/anything"));
+ }
+
+ Y_UNIT_TEST(DotIsLiteral) {
+ auto pred = TRegexPredicate::FromGlob("/Root/foo.bar");
+ UNIT_ASSERT(pred.Match("/Root/foo.bar"));
+ UNIT_ASSERT(!pred.Match("/Root/fooxbar"));
+ }
+
+ Y_UNIT_TEST(RegexMetacharsAreLiteral) {
+ auto pred = TRegexPredicate::FromGlob("/Root/.+()[]{}|^$\\");
+ UNIT_ASSERT(pred.Match("/Root/.+()[]{}|^$\\"));
+ UNIT_ASSERT(!pred.Match("/Root/a"));
+ }
+
+ Y_UNIT_TEST(EmptyThrows) {
+ UNIT_ASSERT_EXCEPTION(TRegexPredicate::FromGlob(""), yexception);
+ }
+
+ Y_UNIT_TEST(StarOnlyMatchesAnything) {
+ auto pred = TRegexPredicate::FromGlob("*");
+ UNIT_ASSERT(pred.Match(""));
+ UNIT_ASSERT(pred.Match("anything"));
+ UNIT_ASSERT(pred.Match("/Root/db/orders"));
}
}
diff --git a/ydb/core/resource_pools/resource_pool_classifier_settings.cpp b/ydb/core/resource_pools/resource_pool_classifier_settings.cpp
index 6bbce7b4376..26df0373ded 100644
--- a/ydb/core/resource_pools/resource_pool_classifier_settings.cpp
+++ b/ydb/core/resource_pools/resource_pool_classifier_settings.cpp
@@ -30,7 +30,7 @@ void TClassifierSettings::TParser::operator()(std::optional<TRegexPredicate>* se
if (Value.empty()) {
setting->reset();
} else {
- *setting = TRegexPredicate::Compile(Value);
+ *setting = TRegexPredicate::FromGlob(Value);
}
}
diff --git a/ydb/core/resource_pools/resource_pool_classifier_settings.h b/ydb/core/resource_pools/resource_pool_classifier_settings.h
index 9de2bb39e08..71eff8f0ccf 100644
--- a/ydb/core/resource_pools/resource_pool_classifier_settings.h
+++ b/ydb/core/resource_pools/resource_pool_classifier_settings.h
@@ -43,7 +43,7 @@ struct TClassifierSettings : public TSettingsBase {
i64 Rank = -1; // -1 = max rank + CLASSIFIER_RANK_OFFSET
TString ResourcePool = DEFAULT_POOL_ID;
std::optional<TString> MemberName;
- std::optional<TRegexPredicate> HasAppName;
+ std::optional<TString> HasAppName;
std::optional<TRegexPredicate> HasFullScan;
std::optional<TRegexPredicate> HasPath;
std::optional<EClassifierAction> Action;
diff --git a/ydb/core/resource_pools/resource_pool_classifier_settings_ut.cpp b/ydb/core/resource_pools/resource_pool_classifier_settings_ut.cpp
index dca7debcfaa..be23cb7bb7c 100644
--- a/ydb/core/resource_pools/resource_pool_classifier_settings_ut.cpp
+++ b/ydb/core/resource_pools/resource_pool_classifier_settings_ut.cpp
@@ -37,30 +37,24 @@ Y_UNIT_TEST_SUITE(ResourcePoolClassifierTest) {
UNIT_ASSERT_VALUES_EQUAL(settings.MemberName, "test@user");
}
- Y_UNIT_TEST(RegexPredicateParsing) {
+ Y_UNIT_TEST(PredicateParsing) {
+ // Verify the parser dispatches to the right overload for each predicate's field type.
+ // Glob semantics themselves live in regex_predicate_ut.cpp.
TClassifierSettings settings;
auto propertiesMap = settings.GetPropertiesMap();
- std::visit(TClassifierSettings::TParser{"ydb-ui|ydb-cli"}, propertiesMap["has_app_name"]);
+ std::visit(TClassifierSettings::TParser{"my_app"}, propertiesMap["has_app_name"]);
+ std::visit(TClassifierSettings::TParser{"/Root/db/orders_*"}, propertiesMap["has_full_scan"]);
+ std::visit(TClassifierSettings::TParser{"/Root/db/archive/*"}, propertiesMap["has_path"]);
+
UNIT_ASSERT(settings.HasAppName.has_value());
- UNIT_ASSERT_VALUES_EQUAL(settings.HasAppName->Pattern, "ydb-ui|ydb-cli");
+ UNIT_ASSERT_VALUES_EQUAL(*settings.HasAppName, "my_app");
- std::visit(TClassifierSettings::TParser{"/Root/db/orders_.*"}, propertiesMap["has_full_scan"]);
UNIT_ASSERT(settings.HasFullScan.has_value());
- UNIT_ASSERT_VALUES_EQUAL(settings.HasFullScan->Pattern, "/Root/db/orders_.*");
+ UNIT_ASSERT_VALUES_EQUAL(settings.HasFullScan->Pattern, "/Root/db/orders_*");
- std::visit(TClassifierSettings::TParser{"/Root/db/archive/.*"}, propertiesMap["has_path"]);
UNIT_ASSERT(settings.HasPath.has_value());
- UNIT_ASSERT_VALUES_EQUAL(settings.HasPath->Pattern, "/Root/db/archive/.*");
- }
-
- Y_UNIT_TEST(RegexPredicateInvalidPattern) {
- TClassifierSettings settings;
- auto propertiesMap = settings.GetPropertiesMap();
-
- UNIT_ASSERT_EXCEPTION(std::visit(TClassifierSettings::TParser{"[invalid"}, propertiesMap["has_app_name"]), yexception);
- UNIT_ASSERT_EXCEPTION(std::visit(TClassifierSettings::TParser{"[invalid"}, propertiesMap["has_full_scan"]), yexception);
- UNIT_ASSERT_EXCEPTION(std::visit(TClassifierSettings::TParser{"[invalid"}, propertiesMap["has_path"]), yexception);
+ UNIT_ASSERT_VALUES_EQUAL(settings.HasPath->Pattern, "/Root/db/archive/*");
}
Y_UNIT_TEST(SettingsExtracting) {
@@ -76,22 +70,22 @@ Y_UNIT_TEST_SUITE(ResourcePoolClassifierTest) {
UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["member_name"]), "test@user");
}
- Y_UNIT_TEST(RegexPredicateExtracting) {
+ Y_UNIT_TEST(PredicateRoundTripExtracting) {
TClassifierSettings settings;
auto propertiesMap = settings.GetPropertiesMap();
- // Parse then extract — must round-trip the original pattern
- std::visit(TClassifierSettings::TParser{"ydb-.*"}, propertiesMap["has_app_name"]);
- std::visit(TClassifierSettings::TParser{"/Root/db/orders_.*"}, propertiesMap["has_full_scan"]);
- std::visit(TClassifierSettings::TParser{"/Root/db/archive/.*"}, propertiesMap["has_path"]);
+ // Parse then extract — must round-trip the raw user string (literal / glob).
+ std::visit(TClassifierSettings::TParser{"ydb-cli"}, propertiesMap["has_app_name"]);
+ std::visit(TClassifierSettings::TParser{"/Root/db/orders_*"}, propertiesMap["has_full_scan"]);
+ std::visit(TClassifierSettings::TParser{"/Root/db/archive/*"}, propertiesMap["has_path"]);
TClassifierSettings::TExtractor extractor;
- UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_app_name"]), "ydb-.*");
- UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_full_scan"]), "/Root/db/orders_.*");
- UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_path"]), "/Root/db/archive/.*");
+ UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_app_name"]), "ydb-cli");
+ UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_full_scan"]), "/Root/db/orders_*");
+ UNIT_ASSERT_VALUES_EQUAL(std::visit(extractor, propertiesMap["has_path"]), "/Root/db/archive/*");
}
- Y_UNIT_TEST(RegexPredicateExtractingEmpty) {
+ Y_UNIT_TEST(PredicateExtractingEmpty) {
TClassifierSettings settings;
auto propertiesMap = settings.GetPropertiesMap();