Repository: ad-freiburg/qlever
Pull request #1889: Allow REGEX expressions with a dynamically computed regular expression
Author: login: RobinTF
Created: 2025-03-19T18:21:23Z | Merged: 2025-03-26T08:36:25Z

Add support for the `REGEX` function in the case where the regular expression is dynamically computed by the query, e.g.
`REGEX(?input, ?regex)` or `REGEX(?input, CONCAT("bla", "*"))`. Previously, QLever only supported hardcoded regular expressions like `REGEX(?input, "bla*")`.
The implementation optimizes for the case, that the regex is stored in a variable with a low multiplicity, so for example for the case `REGEX(?input, ?regex)`, where the `?regex` only binds to a few distinct values by using a simple LRU cache in the `RegexValueGetter`.
Also clean up the code for the `Regex`, `Replace` and `Prefix` expressions.
Fixes #1821


Linked issue #1821: REGEX argument must be an explicit regular expression
As far as I can tell concat often returns values of type simple literal.  Therefore it can generally be used to construct regular expressions.  However the first query is accepted by QLever and the second one is not:

https://qlever.cs.uni-freiburg.de/wikidata/Qaw3dN
```
SELECT ?x WHERE {
  BIND ( regex("foo-x", "^foo-") AS ?x )
  }
```

https://qlever.cs.uni-freiburg.de/wikidata/SA5D31
```
SELECT ?x WHERE {
  BIND ( regex("hi", concat("^", "foo", "-")) AS ?x )
  }
```
Linked issue comments: user: login: hannahbast
created_at: 2025-02-20T01:18:42Z
body: @pfps Yes, that is one of the smaller missing features on our TODO list. The regular expression currently has to be an explicit literal and must not be the result of a function or a variable. We should leave this issue open until this is resolved.

@RobinTF If you want to take a shot at this, that would be great.
 
author_association: MEMBER
user: login: RobinTF
created_at: 2025-02-20T09:14:14Z
body: @hannahbast Had a quick look at it and it's doable, but more effort than I anticipated because currently the regex is compiled whenever the the function is created, not when it's evaluated. So I need to make sure that I don't break things in the process (there is some code that depends on this behaviour)
author_association: COLLABORATOR
user: login: hannahbast
created_at: 2025-02-20T20:18:18Z
body: @RobinTF Thanks for looking into this. Three comments:

1. We already have a `RegexValueGetter`: https://github.com/ad-freiburg/qlever/blob/master/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h#L272-L284 . It is used for the implementation of `REPLACE`, which takes a regular expressions as second argument: https://github.com/ad-freiburg/qlever/blob/master/src/engine/sparqlExpressions/StringExpressions.cpp#L382-L384

2. In most use cases of `REGEX`, the regular expression is a constant (even when it's not a fixed literal). It's important to compile the regular expression only once in those cases. In the more general case, one could maintain a hash map that maps the regular expressions already seen in an expression to their compiled version in order to avoid recompilation. Once the number of different regular expressions becomes too large, it is OK to recompile for every row, but that is a very rare case.

3. Here is a nice example of a query, where the replacement pattern is a variable but still the same for every row. It's a mechanism used by Wikidata to turn IDs stored in literals into the corresponding IRIs.

```
PREFIX wd: <http://www.wikidata.org/entity/>
PREFIX wdt: <http://www.wikidata.org/prop/direct/>
SELECT * WHERE {
  wd:P12047 wdt:P1630 ?formatterURL .
  ?wikidataitem wdt:P12047 ?mimotextid ;
  BIND (IRI(REPLACE(?mimotextid, '^(.+)$', ?formatterURL)) AS ?mimotextitem) .
}
```
https://qlever.cs.uni-freiburg.de/wikidata/hNSN4p

author_association: MEMBER
user: login: RobinTF
created_at: 2025-02-20T20:29:24Z
body: @hannahbast I haven't looked into the differences of replace and regex yet, but if we can unify the implementations that would be good.
author_association: COLLABORATOR

Diff:
diff --git a/src/engine/sparqlExpressions/RegexExpression.cpp b/src/engine/sparqlExpressions/RegexExpression.cpp
index 84b1e4c4e3..720750b2c2 100644
--- a/src/engine/sparqlExpressions/RegexExpression.cpp
+++ b/src/engine/sparqlExpressions/RegexExpression.cpp
@@ -4,22 +4,91 @@
 // Authors: Johannes Kalmbach <kalmbach@cs.uni-freiburg.de>
 //          Hannah Bast <bast@cs.uni-freiburg.de>
 
-#include "./RegexExpression.h"
+#include "engine/sparqlExpressions/RegexExpression.h"
 
 #include <re2/re2.h>
 
+#include "NaryExpressionImpl.h"
 #include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/NaryExpression.h"
 #include "engine/sparqlExpressions/SparqlExpressionGenerators.h"
 #include "engine/sparqlExpressions/SparqlExpressionValueGetters.h"
+#include "engine/sparqlExpressions/StringExpressionsHelper.h"
 #include "global/ValueIdComparators.h"
 
 using namespace std::literals;
 
 namespace sparqlExpression::detail {
 
-// ____________________________________________________________________________
-std::optional<std::string> getPrefixRegex(std::string regex) {
+void ensureIsSimpleLiteral(
+    const ad_utility::triple_component::Literal& literal) {
+  if (literal.hasDatatype() || literal.hasLanguageTag()) {
+    throw std::runtime_error{
+        "The REGEX function only accepts simple literals (literals without a "
+        "language tag or a datatype)"};
+  }
+}
+
+// _____________________________________________________________________________
+void ensureIsValidRegexIfConstant(const SparqlExpression& expression) {
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&expression);
+  if (stringLiteralExpression) {
+    const auto& literal = stringLiteralExpression->value();
+    const auto& string = asStringViewUnsafe(literal.getContent());
+    RE2 regex{string, RE2::Quiet};
+    if (!regex.ok()) {
+      throw std::runtime_error{absl::StrCat(
+          "The regex \"", string,
+          "\" is not supported by QLever (which uses Google's RE2 library); "
+          "the error from RE2 is: ",
+          regex.error())};
+    }
+  }
+}
+
+// _____________________________________________________________________________
+void ensureIsValidFlagIfConstant(const SparqlExpression& expression) {
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&expression);
+  if (stringLiteralExpression) {
+    const auto& literal = stringLiteralExpression->value();
+    const auto& string = asStringViewUnsafe(literal.getContent());
+    auto firstInvalidFlag = string.find_first_not_of("imsU");
+    if (firstInvalidFlag != std::string::npos) {
+      throw std::runtime_error{absl::StrCat(
+          "Invalid regex flag '", string.substr(firstInvalidFlag, 1),
+          "' found in \"", string,
+          "\". The only supported flags are 'i', 'm', 's', 'U', and any "
+          "combination of them")};
+    }
+  }
+}
+
+// _____________________________________________________________________________
+[[maybe_unused]] auto regexImpl = [](const std::optional<std::string>& input,
+                                     const std::shared_ptr<RE2>& pattern) {
+  if (!input.has_value() || !pattern) {
+    return Id::makeUndefined();
+  }
+  // Check for invalid regexes.
+  if (!pattern->ok()) {
+    return Id::makeUndefined();
+  }
+  return Id::makeFromBool(RE2::PartialMatch(input.value(), *pattern));
+};
+
+using RegexExpression =
+    string_expressions::StringExpressionImpl<2, decltype(regexImpl),
+                                             RegexValueGetter>;
+
+}  // namespace sparqlExpression::detail
+
+namespace sparqlExpression {
+
+// _____________________________________________________________________________
+std::optional<std::string> PrefixRegexExpression::getPrefixRegex(
+    std::string regex) {
   if (!regex.starts_with('^')) {
     return std::nullopt;
   }
@@ -69,107 +138,37 @@ std::optional<std::string> getPrefixRegex(std::string regex) {
   return regex;
 }
 
-}  // namespace sparqlExpression::detail
-
-namespace sparqlExpression {
-
-// ___________________________________________________________________________
-RegexExpression::RegexExpression(
-    SparqlExpression::Ptr child, SparqlExpression::Ptr regex,
-    std::optional<SparqlExpression::Ptr> optionalFlags)
-    : child_{std::move(child)} {
+// _____________________________________________________________________________
+PrefixRegexExpression::PrefixRegexExpression(Ptr child, std::string prefixRegex,
+                                             Variable variable)
+    : child_{std::move(child)},
+      prefixRegex_{std::move(prefixRegex)},
+      variable_{std::move(variable)} {
   // If we have a `STR()` expression, remove the `STR()` and remember that it
   // was there.
   if (child_->isStrExpression()) {
     child_ = std::move(std::move(*child_).moveChildrenOut().at(0));
     childIsStrExpression_ = true;
   }
-
-  // Get the regex string, which must be a string literal without a datatype or
-  // language tag.
-  std::string regexString;
-  if (auto regexPtr =
-          dynamic_cast<const StringLiteralExpression*>(regex.get())) {
-    const auto& regexLiteral = regexPtr->value();
-    regexString = asStringViewUnsafe(regexLiteral.getContent());
-    if (regexLiteral.hasDatatype() || regexLiteral.hasLanguageTag()) {
-      throw std::runtime_error(
-          "The second argument to the REGEX function (which contains the "
-          "regular expression) must not contain a language tag or a datatype");
-    }
-  } else {
-    throw std::runtime_error(
-        "The second argument to the REGEX function must be a "
-        "string literal (which contains the regular expression)");
-  }
-
-  // Parse the flags. The optional argument for that must, again, be a
-  // string literal without a datatype or language tag.
-  if (optionalFlags.has_value()) {
-    if (auto flagsPtr = dynamic_cast<const StringLiteralExpression*>(
-            optionalFlags.value().get())) {
-      const auto& flagsLiteral = flagsPtr->value();
-      std::string_view flags = asStringViewUnsafe(flagsLiteral.getContent());
-      if (flagsLiteral.hasDatatype() || flagsLiteral.hasLanguageTag()) {
-        throw std::runtime_error(
-            "The third argument to the REGEX function (which contains optional "
-            "flags to configure the evaluation) must not contain a language "
-            "tag or a datatype");
-      }
-      auto firstInvalidFlag = flags.find_first_not_of("imsu");
-      if (firstInvalidFlag != std::string::npos) {
-        throw std::runtime_error{absl::StrCat(
-            "Invalid regex flag '", std::string{flags[firstInvalidFlag]},
-            "' found in \"", flags,
-            "\". The only supported flags are 'i', 's', 'm', 's', 'u', and any "
-            "combination of them")};
-      }
-
-      // In Google RE2 the flags are directly part of the regex.
-      if (!flags.empty()) {
-        regexString = absl::StrCat("(?", flags, ":", regexString + ")");
-      }
-    } else {
-      throw std::runtime_error(
-          "The optional third argument to the REGEX function must be a string "
-          "literal (which contains the configuration flags)");
-    }
-  }
-
-  // Create RE2 object from the regex string. If it is a simple prefix regex,
-  // store the prefix in `prefixRegex_` (otherwise that becomes `std::nullopt`).
-  regexAsString_ = regexString;
-  prefixRegex_ = detail::getPrefixRegex(regexString);
-  regex_.emplace(regexString, RE2::Quiet);
-  const auto& r = regex_.value();
-  if (r.error_code() != RE2::NoError) {
-    throw std::runtime_error{absl::StrCat(
-        "The regex \"", regexString,
-        "\" is not supported by QLever (which uses Google's RE2 library); "
-        "the error from RE2 is: ",
-        r.error())};
-  }
 }
 
-// ___________________________________________________________________________
-string RegexExpression::getCacheKey(
+// _____________________________________________________________________________
+string PrefixRegexExpression::getCacheKey(
     const VariableToColumnMap& varColMap) const {
-  return absl::StrCat("REGEX expression ", child_->getCacheKey(varColMap),
-                      " with ", regexAsString_, "str:", childIsStrExpression_);
+  return absl::StrCat("Prefix REGEX expression: ", prefixRegex_,
+                      " child:", child_->getCacheKey(varColMap),
+                      " str:", childIsStrExpression_);
 }
 
-// ___________________________________________________________________________
-std::span<SparqlExpression::Ptr> RegexExpression::childrenImpl() {
+// _____________________________________________________________________________
+std::span<SparqlExpression::Ptr> PrefixRegexExpression::childrenImpl() {
   return {&child_, 1};
 }
 
-// ___________________________________________________________________________
-ExpressionResult RegexExpression::evaluatePrefixRegex(
-    const Variable& variable,
-    sparqlExpression::EvaluationContext* context) const {
+// _____________________________________________________________________________
+ExpressionResult PrefixRegexExpression::evaluate(
+    EvaluationContext* context) const {
   // This function must only be called if we have a simple prefix regex.
-  AD_CORRECTNESS_CHECK(prefixRegex_.has_value());
-  std::string prefixRegex = prefixRegex_.value();
 
   // If the expression is enclosed in `STR()`, we have two ranges: for the
   // prefix with and without leading "<".
@@ -182,9 +181,9 @@ ExpressionResult RegexExpression::evaluatePrefixRegex(
   // TODO<joka921> prefix filters currently never find numbers or other
   // datatypes that are encoded directly inside the IDs.
   std::vector<std::string> actualPrefixes;
-  actualPrefixes.push_back("\"" + prefixRegex);
+  actualPrefixes.push_back("\"" + prefixRegex_);
   if (childIsStrExpression_) {
-    actualPrefixes.push_back("<" + prefixRegex);
+    actualPrefixes.push_back("<" + prefixRegex_);
   }
 
   // Compute the (one or two) ranges.
@@ -208,9 +207,9 @@ ExpressionResult RegexExpression::evaluatePrefixRegex(
   // In this function, the expression is a simple variable. If the input is
   // sorted by that variable, the result can be computed by a constant number
   // of binary searches and the result is a set of intervals.
-  std::vector<ad_utility::SetOfIntervals> resultSetOfIntervals;
-  if (context->isResultSortedBy(variable)) {
-    auto optColumn = context->getColumnIndexForVariable(variable);
+  if (context->isResultSortedBy(variable_)) {
+    std::vector<ad_utility::SetOfIntervals> resultSetOfIntervals;
+    auto optColumn = context->getColumnIndexForVariable(variable_);
     AD_CORRECTNESS_CHECK(optColumn.has_value(),
                          "We have previously asserted that the input is sorted "
                          "by the variable, so we expect it to exist");
@@ -245,7 +244,7 @@ ExpressionResult RegexExpression::evaluatePrefixRegex(
   auto resultSize = context->size();
   VectorWithMemoryLimit<Id> result{context->_allocator};
   result.reserve(resultSize);
-  for (auto id : detail::makeGenerator(variable, resultSize, context)) {
+  for (auto id : detail::makeGenerator(variable_, resultSize, context)) {
     result.push_back(Id::makeFromBool(
         ql::ranges::any_of(lowerAndUpperIds, [&](const auto& lowerUpper) {
           return !valueIdComparators::compareByBits(id, lowerUpper.first) &&
@@ -256,107 +255,78 @@ ExpressionResult RegexExpression::evaluatePrefixRegex(
   return result;
 }
 
-// ___________________________________________________________________________
-CPP_template_def(typename T)(requires SingleExpressionResult<T>)
-    ExpressionResult RegexExpression::evaluateGeneralCase(
-        T&& input, sparqlExpression::EvaluationContext* context) const {
-  // We have one result for each row of the input.
-  auto resultSize = context->size();
-  VectorWithMemoryLimit<Id> result{context->_allocator};
-  result.reserve(resultSize);
-  AD_CORRECTNESS_CHECK(regex_.has_value());
-
-  // Compute the result using the given value getter. If the getter returns
-  // `std::nullopt` for a row, the result is `UNDEF`. Otherwise, we have a
-  // string and evaluate the regex on it.
-  auto computeResult = [&]<typename ValueGetter>(const ValueGetter& getter) {
-    ql::ranges::for_each(
-        detail::makeGenerator(AD_FWD(input), resultSize, context),
-        [&getter, &context, &result, this](const auto& id) {
-          auto str = getter(id, context);
-          if (!str.has_value()) {
-            result.push_back(Id::makeUndefined());
-          } else {
-            result.push_back(Id::makeFromBool(
-                RE2::PartialMatch(str.value(), regex_.value())));
-          }
-          checkCancellation(context);
-        });
-  };
-
-  // Compute the result with the correct value getter (depending on whether the
-  // expression is enclosed in `STR()` or not), and return it.
-  if (childIsStrExpression_) {
-    computeResult(detail::StringValueGetter{});
-  } else {
-    computeResult(detail::LiteralFromIdGetter{});
-  }
-  return result;
-}
-
-// ___________________________________________________________________________
-ExpressionResult RegexExpression::evaluate(
-    sparqlExpression::EvaluationContext* context) const {
-  auto resultAsVariant = child_->evaluate(context);
-  auto variablePtr = std::get_if<Variable>(&resultAsVariant);
+// _____________________________________________________________________________
+auto PrefixRegexExpression::getEstimatesForFilterExpression(
+    uint64_t inputSize,
+    const std::optional<Variable>& firstSortedVariable) const -> Estimates {
+  // Assume that only 10^-k entries remain, where k is the length of the prefix
+  // and cap to reasonable maximal values to prevent numerical stability
+  // problems.
+  double reductionFactor =
+      std::pow(10, std::min<size_t>(8, prefixRegex_.size()));
+  size_t sizeEstimate = inputSize / static_cast<size_t>(reductionFactor);
+  size_t costEstimate = firstSortedVariable == variable_
+                            ? sizeEstimate
+                            : sizeEstimate + inputSize;
 
-  if (prefixRegex_.has_value() && variablePtr != nullptr) {
-    return evaluatePrefixRegex(*variablePtr, context);
-  } else {
-    return std::visit(
-        [this, context](auto&& input) {
-          return evaluateGeneralCase(AD_FWD(input), context);
-        },
-        std::move(resultAsVariant));
-  }
+  return {sizeEstimate, costEstimate};
 }
 
-// ____________________________________________________________________________
-bool RegexExpression::isPrefixExpression() const {
-  return prefixRegex_.has_value();
+// _____________________________________________________________________________
+void PrefixRegexExpression::checkCancellation(
+    const EvaluationContext* context, ad_utility::source_location location) {
+  context->cancellationHandle_->throwIfCancelled(location);
 }
 
-// ____________________________________________________________________________
-auto RegexExpression::getEstimatesForFilterExpression(
-    uint64_t inputSize,
-    const std::optional<Variable>& firstSortedVariable) const -> Estimates {
-  // If we have a simple prefix regex, assume that only 10^-k entries remain,
-  // where k is the length of the prefix.
-  if (isPrefixExpression()) {
-    double reductionFactor = std::pow(
-        10, std::max(0, static_cast<int>(prefixRegex_.value().size())));
-    // Cap to reasonable minimal and maximal values to prevent numerical
-    // stability problems.
-    reductionFactor = std::min(100000000.0, reductionFactor);
-    reductionFactor = std::max(1.0, reductionFactor);
-    size_t sizeEstimate = inputSize / static_cast<size_t>(reductionFactor);
-    auto varPtr = dynamic_cast<VariableExpression*>(child_.get());
-    size_t costEstimate = (varPtr && firstSortedVariable == varPtr->value())
-                              ? sizeEstimate
-                              : sizeEstimate + inputSize;
-
-    return {sizeEstimate, costEstimate};
+// _____________________________________________________________________________
+std::optional<PrefixRegexExpression>
+PrefixRegexExpression::makePrefixRegexExpressionIfPossible(
+    Ptr& string, const SparqlExpression& regex) {
+  detail::ensureIsValidRegexIfConstant(regex);
+  const auto* variableExpression = dynamic_cast<const VariableExpression*>(
+      string->isStrExpression() ? string->children()[0].get() : string.get());
+  if (!variableExpression) {
+    return std::nullopt;
   }
-
-  // For the general case, we make two assumptions.
-  //
-  // 1. Half of the entries remain after the filter. This is a very simple
-  // and arbitrary heuristic.
-  //
-  // 2. Checking a REGEX for an element is 10 times more expensive than a
-  // "simple" filter check. This is reasonable because regex evaluations are
-  // expensive, but the fixed factor disregard that it depends on the
-  // complexity of the regex how expensive it is.
-  size_t sizeEstimate = inputSize / 2;
-  size_t costEstimate = sizeEstimate + 10 * inputSize;
-  return {sizeEstimate, costEstimate};
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&regex);
+  if (!stringLiteralExpression) {
+    return std::nullopt;
+  }
+  const auto& stringLiteral = stringLiteralExpression->value();
+  detail::ensureIsSimpleLiteral(stringLiteral);
+  if (std::optional<std::string> prefixRegex = getPrefixRegex(
+          std::string{asStringViewUnsafe(stringLiteral.getContent())})) {
+    return PrefixRegexExpression{std::move(string),
+                                 std::move(prefixRegex.value()),
+                                 variableExpression->value()};
+  }
+  return std::nullopt;
 }
 
-// ____________________________________________________________________________
-void RegexExpression::checkCancellation(
-    const sparqlExpression::EvaluationContext* context,
-    ad_utility::source_location location) {
-  context->cancellationHandle_->throwIfCancelled(location);
+// _____________________________________________________________________________
+SparqlExpression::Ptr makeRegexExpression(SparqlExpression::Ptr string,
+                                          SparqlExpression::Ptr regex,
+                                          SparqlExpression::Ptr flags) {
+  if (flags) {
+    if (auto* stringLiteralExpression =
+            dynamic_cast<const StringLiteralExpression*>(flags.get())) {
+      detail::ensureIsSimpleLiteral(stringLiteralExpression->value());
+    }
+    detail::ensureIsValidRegexIfConstant(*regex);
+    detail::ensureIsValidFlagIfConstant(*flags);
+    regex = makeMergeRegexPatternAndFlagsExpression(std::move(regex),
+                                                    std::move(flags));
+  } else if (auto prefixExpression =
+                 PrefixRegexExpression::makePrefixRegexExpressionIfPossible(
+                     string, *regex)) {
+    return std::make_unique<PrefixRegexExpression>(
+        std::move(prefixExpression.value()));
+  } else {
+    detail::ensureIsValidRegexIfConstant(*regex);
+  }
+  return std::make_unique<detail::RegexExpression>(std::move(string),
+                                                   std::move(regex));
 }
 
 }  // namespace sparqlExpression
diff --git a/src/engine/sparqlExpressions/RegexExpression.h b/src/engine/sparqlExpressions/RegexExpression.h
index ec5e25bfa9..b282e11555 100644
--- a/src/engine/sparqlExpressions/RegexExpression.h
+++ b/src/engine/sparqlExpressions/RegexExpression.h
@@ -4,78 +4,77 @@
 
 #pragma once
 
+#include <gtest/gtest_prod.h>
+#include <re2/re2.h>
+
 #include <string>
 
-#include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/SparqlExpression.h"
-#include "re2/re2.h"
 
 namespace sparqlExpression {
-// Class implementing the REGEX function, which takes two mandatory arguments
-// (an expression and a regex) and one optional argument (a string of flags).
-class RegexExpression : public SparqlExpression {
+// Class implementing a specialization of the REGEX function. This optimization
+// is possible if the regex is known in advance and is a simple prefix regex.
+class PrefixRegexExpression : public SparqlExpression {
  private:
-  SparqlExpression::Ptr child_;
-  // The reguar expression. It needs to be a `std::optional` because `RE2`
-  // objects do not have a default constructor.
-  std::optional<RE2> regex_;
-  // If this `std::optional` holds a string, we have a simple prefix regex
-  // (which translates to a range search) and this string holds the prefix.
-  std::optional<std::string> prefixRegex_;
-  // The regex as a string, used for the cache key.
-  std::string regexAsString_;
-
-  // True iff the expression is enclosed in `STR()`.
+  Ptr child_;
+  // A simple prefix regex (which translates to a range search) and this string
+  // holds the prefix.
+  std::string prefixRegex_;
+  // Holds the variable over which the regex is evaluated.
+  Variable variable_;
+  // If the variable is wrapped inside a `STR()` function, this is set to true.
   bool childIsStrExpression_ = false;
 
- public:
   // The `child` must be a `VariableExpression` and `regex` must be a
   // `LiteralExpression` that stores a string, otherwise an exception will be
   // thrown.
-  RegexExpression(SparqlExpression::Ptr child, SparqlExpression::Ptr regex,
-                  std::optional<SparqlExpression::Ptr> optionalFlags);
+  PrefixRegexExpression(Ptr child, std::string prefixRegex, Variable variable);
+
+ public:
+  PrefixRegexExpression(PrefixRegexExpression&&) noexcept = default;
+  PrefixRegexExpression& operator=(PrefixRegexExpression&&) noexcept = default;
+  PrefixRegexExpression(const PrefixRegexExpression&) noexcept = delete;
+  PrefixRegexExpression& operator=(const PrefixRegexExpression&) noexcept =
+      delete;
+
+  // Check if the children of this expression allow for the prefix regex
+  // optimization. If this is the case, a `PrefixRegexExpression` is returned,
+  // otherwise `std::nullopt`.
+  static std::optional<PrefixRegexExpression>
+  makePrefixRegexExpressionIfPossible(Ptr& string,
+                                      const SparqlExpression& regex);
 
+  // ___________________________________________________________________________
   ExpressionResult evaluate(EvaluationContext* context) const override;
 
-  // _________________________________________________________________________
-  [[nodiscard]] string getCacheKey(
+  // ___________________________________________________________________________
+  [[nodiscard]] std::string getCacheKey(
       const VariableToColumnMap& varColMap) const override;
 
-  // _________________________________________________________________________
-  [[nodiscard]] bool isPrefixExpression() const;
-
-  // _________________________________________________________________________
+  // ___________________________________________________________________________
   Estimates getEstimatesForFilterExpression(
       uint64_t inputSize,
       const std::optional<Variable>& firstSortedVariable) const override;
 
  private:
-  std::span<SparqlExpression::Ptr> childrenImpl() override;
-
-  // Evaluate for the special case, where the expression is a variable and we
-  // have a simple prefix regex (in which case the regex match translates to a
-  // simple range check).
-  ExpressionResult evaluatePrefixRegex(
-      const Variable& variable,
-      sparqlExpression::EvaluationContext* context) const;
-
-  // Evaluate for the general case.
-  CPP_template(typename T)(requires SingleExpressionResult<T>) ExpressionResult
-      evaluateGeneralCase(T&& input,
-                          sparqlExpression::EvaluationContext* context) const;
+  std::span<Ptr> childrenImpl() override;
 
   // Check if the `CancellationHandle` of `context` has been cancelled and throw
   // an exception if this is the case.
-  static void checkCancellation(
-      const sparqlExpression::EvaluationContext* context,
-      ad_utility::source_location location =
-          ad_utility::source_location::current());
+  static void checkCancellation(const EvaluationContext* context,
+                                ad_utility::source_location location =
+                                    ad_utility::source_location::current());
+
+  // Check if `regex` is a prefix regex which means that it starts with `^` and
+  // contains no other "special" regex characters like `*` or `.`. If this check
+  // succeeds, the prefix is returned without the leading `^` and with all
+  // escaping undone. Else, `std::nullopt` is returned.
+  static std::optional<std::string> getPrefixRegex(std::string regex);
+
+  FRIEND_TEST(RegexExpression, getPrefixRegex);
 };
-namespace detail {
-// Check if `regex` is a prefix regex which means that it starts with `^` and
-// contains no other "special" regex characters like `*` or `.`. If this check
-// succeeds, the prefix is returned without the leading `^` and with all
-// escaping undone. Else, `std::nullopt` is returned.
-std::optional<std::string> getPrefixRegex(std::string regex);
-}  // namespace detail
+
+SparqlExpression::Ptr makeRegexExpression(SparqlExpression::Ptr string,
+                                          SparqlExpression::Ptr regex,
+                                          SparqlExpression::Ptr flags);
 }  // namespace sparqlExpression
diff --git a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp
index d597011b37..0a03e2198b 100644
--- a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp
+++ b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp
@@ -124,7 +124,7 @@ LiteralValueGetterWithoutStrFunction::operator()(
 std::optional<std::string> ReplacementStringGetter::operator()(
     Id id, const EvaluationContext* context) const {
   std::optional<std::string> originalString =
-      StringValueGetter::operator()(id, context);
+      LiteralFromIdGetter{}(id, context);
   if (!originalString.has_value()) {
     return originalString;
   }
diff --git a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h
index 95cae2653c..1b86c7cf61 100644
--- a/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h
+++ b/src/engine/sparqlExpressions/SparqlExpressionValueGetters.h
@@ -9,11 +9,11 @@
 #include <re2/re2.h>
 
 #include "engine/ExportQueryExecutionTrees.h"
-#include "engine/Result.h"
 #include "engine/sparqlExpressions/SparqlExpressionTypes.h"
 #include "global/Id.h"
 #include "parser/GeoPoint.h"
 #include "util/ConstexprSmallString.h"
+#include "util/LruCache.h"
 #include "util/TypeTraits.h"
 
 /// Several classes that can be used as the `ValueGetter` template
@@ -144,22 +144,6 @@ struct StringValueGetter : Mixin<StringValueGetter> {
     return std::string(asStringViewUnsafe(s.getContent()));
   }
 };
-// Similar to `StringValueGetter`, but correctly preprocesses strings so that
-// they can be used by re2 as replacement strings. So '$1 \abc \$' becomes
-// '\1 \\abc $', where the former variant is valid in the SPARQL standard and
-// the latter represents the format that re2 expects.
-struct ReplacementStringGetter : StringValueGetter,
-                                 Mixin<ReplacementStringGetter> {
-  using Mixin<ReplacementStringGetter>::operator();
-  std::optional<std::string> operator()(ValueId,
-                                        const EvaluationContext*) const;
-
-  std::optional<std::string> operator()(const LiteralOrIri& s,
-                                        const EvaluationContext*) const;
-
- private:
-  static std::string convertToReplacementString(std::string_view view);
-};
 
 // This class can be used as the `ValueGetter` argument of Expression
 // templates. It implicitly applies the STR() function. In particular,
@@ -295,19 +279,39 @@ struct LiteralFromIdGetter : Mixin<LiteralFromIdGetter> {
   }
 };
 
-// Convert the input into a `unique_ptr<RE2>`. Return nullptr if the input is
+// Similar to `LiteralFromIdGetter`, but correctly preprocesses strings so that
+// they can be used by re2 as replacement strings. So '$1 \abc \$' becomes
+// '\1 \\abc $', where the former variant is valid in the SPARQL standard and
+// the latter represents the format that re2 expects.
+struct ReplacementStringGetter : Mixin<ReplacementStringGetter> {
+  using Mixin<ReplacementStringGetter>::operator();
+  std::optional<std::string> operator()(ValueId,
+                                        const EvaluationContext*) const;
+
+  std::optional<std::string> operator()(const LiteralOrIri& s,
+                                        const EvaluationContext*) const;
+
+ private:
+  static std::string convertToReplacementString(std::string_view view);
+};
+
+// Convert the input into a `shared_ptr<RE2>`. Return nullptr if the input is
 // not convertible to a string.
 struct RegexValueGetter {
+  mutable ad_utility::util::LRUCache<std::string, std::shared_ptr<RE2>> cache_{
+      100};
   template <typename S>
-  auto operator()(S&& input, const EvaluationContext* context) const -> CPP_ret(
-      std::unique_ptr<re2::RE2>)(
-      requires SingleExpressionResult<S>&&
-          ranges::invocable<StringValueGetter, S&&, const EvaluationContext*>) {
-    auto str = StringValueGetter{}(AD_FWD(input), context);
+  auto operator()(S&& input, const EvaluationContext* context) const
+      -> CPP_ret(std::shared_ptr<RE2>)(
+          requires SingleExpressionResult<S>&& ranges::invocable<
+              LiteralFromIdGetter, S&&, const EvaluationContext*>) {
+    auto str = LiteralFromIdGetter{}(AD_FWD(input), context);
     if (!str.has_value()) {
       return nullptr;
     }
-    return std::make_unique<re2::RE2>(str.value(), re2::RE2::Quiet);
+    return cache_.getOrCompute(str.value(), [](const std::string& value) {
+      return std::make_shared<RE2>(value, RE2::Quiet);
+    });
   }
 };
 
diff --git a/src/engine/sparqlExpressions/StringExpressions.cpp b/src/engine/sparqlExpressions/StringExpressions.cpp
index 0e60120b69..de3c64bd1b 100644
--- a/src/engine/sparqlExpressions/StringExpressions.cpp
+++ b/src/engine/sparqlExpressions/StringExpressions.cpp
@@ -6,6 +6,7 @@
 
 #include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/NaryExpressionImpl.h"
+#include "engine/sparqlExpressions/StringExpressionsHelper.h"
 #include "engine/sparqlExpressions/VariadicExpression.h"
 
 namespace sparqlExpression {
@@ -66,70 +67,6 @@ class StrExpression : public StrExpressionImpl {
   bool isStrExpression() const override { return true; }
 };
 
-// Template for an expression that works on string literals. The arguments are
-// the same as those to `NaryExpression` with the difference that the value
-// getter is deduced automatically. If the child of the expression is the
-// `STR()` expression, then the `StringValueGetter` will be used (which also
-// returns string values for IRIs, numeric literals, etc.), otherwise the
-// `LiteralFromIdGetter` is used (which returns `std::nullopt` for these cases).
-template <typename ValueGetterWithStr, typename ValueGetterWithoutStr, size_t N,
-          typename Function, typename... AdditionalNonStringValueGetters>
-class StringExpressionImplImpl : public SparqlExpression {
- private:
-  using ExpressionWithStr = NARY<
-      N, FV<Function, ValueGetterWithStr, AdditionalNonStringValueGetters...>>;
-  using ExpressionWithoutStr = NARY<N, FV<Function, ValueGetterWithoutStr,
-                                          AdditionalNonStringValueGetters...>>;
-
-  SparqlExpression::Ptr impl_;
-
- public:
-  CPP_template(typename... C)(
-      requires(concepts::same_as<C, SparqlExpression::Ptr>&&...) CPP_and(
-          sizeof...(C) + 1 ==
-          N)) explicit StringExpressionImplImpl(SparqlExpression::Ptr child,
-                                                C... children) {
-    AD_CORRECTNESS_CHECK(child != nullptr);
-    if (child->isStrExpression()) {
-      auto childrenOfStr = std::move(*child).moveChildrenOut();
-      AD_CORRECTNESS_CHECK(childrenOfStr.size() == 1);
-      impl_ = std::make_unique<ExpressionWithStr>(
-          std::move(childrenOfStr.at(0)), std::move(children)...);
-    } else {
-      impl_ = std::make_unique<ExpressionWithoutStr>(std::move(child),
-                                                     std::move(children)...);
-    }
-  }
-
-  ExpressionResult evaluate(EvaluationContext* context) const override {
-    return impl_->evaluate(context);
-  }
-  std::string getCacheKey(const VariableToColumnMap& varColMap) const override {
-    return impl_->getCacheKey(varColMap);
-  }
-
- private:
-  std::span<SparqlExpression::Ptr> childrenImpl() override {
-    return impl_->children();
-  }
-};
-
-// Impl class for expressions that work on plain strings.
-template <size_t N, typename Function,
-          typename... AdditionalNonStringValueGetters>
-using StringExpressionImpl =
-    StringExpressionImplImpl<StringValueGetter, LiteralFromIdGetter, N,
-                             Function, AdditionalNonStringValueGetters...>;
-
-// Impl class for expressions that work on literals with datatypes and language
-// tags.
-template <size_t N, typename Function,
-          typename... AdditionalNonStringValueGetters>
-using LiteralExpressionImpl =
-    StringExpressionImplImpl<LiteralValueGetterWithStrFunction,
-                             LiteralValueGetterWithoutStrFunction, N, Function,
-                             AdditionalNonStringValueGetters...>;
-
 // Lift a `Function` that takes one or multiple `std::string`s (possibly via
 // references) and returns an `Id` or `std::string` to a function that takes the
 // same number of `std::optional<std::string>` and returns `Id` or
@@ -409,7 +346,7 @@ using StrBeforeExpression =
   if (!flags.has_value() || !regex.has_value()) {
     return Id::makeUndefined();
   }
-  auto firstInvalidFlag = flags.value().find_first_not_of("imsu");
+  auto firstInvalidFlag = flags.value().find_first_not_of("imsU");
   if (firstInvalidFlag != std::string::npos) {
     return Id::makeUndefined();
   }
@@ -423,11 +360,10 @@ using StrBeforeExpression =
 };
 
 using MergeRegexPatternAndFlagsExpression =
-    StringExpressionImpl<2, decltype(mergeFlagsIntoRegex), StringValueGetter>;
+    StringExpressionImpl<2, decltype(mergeFlagsIntoRegex), LiteralFromIdGetter>;
 
 [[maybe_unused]] auto replaceImpl =
-    [](std::optional<std::string> input,
-       const std::unique_ptr<re2::RE2>& pattern,
+    [](std::optional<std::string> input, const std::shared_ptr<RE2>& pattern,
        const std::optional<std::string>& replacement) -> IdOrLiteralOrIri {
   if (!input.has_value() || !pattern || !replacement.has_value()) {
     return Id::makeUndefined();
@@ -439,7 +375,7 @@ using MergeRegexPatternAndFlagsExpression =
     return Id::makeUndefined();
   }
   const auto& repl = replacement.value();
-  re2::RE2::GlobalReplace(&in, pat, repl);
+  RE2::GlobalReplace(&in, pat, repl);
   return toLiteral(in);
 };
 
diff --git a/src/engine/sparqlExpressions/StringExpressionsHelper.h b/src/engine/sparqlExpressions/StringExpressionsHelper.h
new file mode 100644
index 0000000000..5baaf7b4f0
--- /dev/null
+++ b/src/engine/sparqlExpressions/StringExpressionsHelper.h
@@ -0,0 +1,72 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include "engine/sparqlExpressions/NaryExpressionImpl.h"
+
+namespace sparqlExpression::detail::string_expressions {
+
+// Template for an expression that works on string literals. The arguments are
+// the same as those to `NaryExpression` with the difference that the value
+// getter is deduced automatically. If the child of the expression is the
+// `STR()` expression, then the `StringValueGetter` will be used (which also
+// returns string values for IRIs, numeric literals, etc.), otherwise the
+// `LiteralFromIdGetter` is used (which returns `std::nullopt` for these cases).
+template <typename ValueGetterWithStr, typename ValueGetterWithoutStr, size_t N,
+          typename Function, typename... AdditionalNonStringValueGetters>
+class StringExpressionImplImpl : public SparqlExpression {
+ private:
+  using ExpressionWithStr = NARY<
+      N, FV<Function, ValueGetterWithStr, AdditionalNonStringValueGetters...>>;
+  using ExpressionWithoutStr = NARY<N, FV<Function, ValueGetterWithoutStr,
+                                          AdditionalNonStringValueGetters...>>;
+
+  Ptr impl_;
+
+ public:
+  CPP_template(typename... C)(
+      requires(concepts::same_as<C, SparqlExpression::Ptr>&&...)
+          CPP_and(sizeof...(C) + 1 ==
+                  N)) explicit StringExpressionImplImpl(Ptr child,
+                                                        C... children) {
+    AD_CORRECTNESS_CHECK(child != nullptr);
+    if (child->isStrExpression()) {
+      auto childrenOfStr = std::move(*child).moveChildrenOut();
+      AD_CORRECTNESS_CHECK(childrenOfStr.size() == 1);
+      impl_ = std::make_unique<ExpressionWithStr>(
+          std::move(childrenOfStr.at(0)), std::move(children)...);
+    } else {
+      impl_ = std::make_unique<ExpressionWithoutStr>(std::move(child),
+                                                     std::move(children)...);
+    }
+  }
+
+  ExpressionResult evaluate(EvaluationContext* context) const override {
+    return impl_->evaluate(context);
+  }
+  std::string getCacheKey(const VariableToColumnMap& varColMap) const override {
+    return impl_->getCacheKey(varColMap);
+  }
+
+ private:
+  std::span<Ptr> childrenImpl() override { return impl_->children(); }
+};
+
+// Impl class for expressions that work on plain strings.
+template <size_t N, typename Function,
+          typename... AdditionalNonStringValueGetters>
+using StringExpressionImpl =
+    StringExpressionImplImpl<StringValueGetter, LiteralFromIdGetter, N,
+                             Function, AdditionalNonStringValueGetters...>;
+
+// Impl class for expressions that work on literals with datatypes and language
+// tags.
+template <size_t N, typename Function,
+          typename... AdditionalNonStringValueGetters>
+using LiteralExpressionImpl =
+    StringExpressionImplImpl<LiteralValueGetterWithStrFunction,
+                             LiteralValueGetterWithoutStrFunction, N, Function,
+                             AdditionalNonStringValueGetters...>;
+}  // namespace sparqlExpression::detail::string_expressions
diff --git a/src/parser/sparqlParser/SparqlQleverVisitor.cpp b/src/parser/sparqlParser/SparqlQleverVisitor.cpp
index 9156733981..e71a42179c 100644
--- a/src/parser/sparqlParser/SparqlQleverVisitor.cpp
+++ b/src/parser/sparqlParser/SparqlQleverVisitor.cpp
@@ -2544,8 +2544,8 @@ ExpressionPtr Visitor::visit(Parser::RegexExpressionContext* ctx) {
   AD_CONTRACT_CHECK(numArgs >= 2 && numArgs <= 3);
   auto flags = numArgs == 3 ? visitOptional(exp[2]) : std::nullopt;
   try {
-    return std::make_unique<sparqlExpression::RegexExpression>(
-        visit(exp[0]), visit(exp[1]), std::move(flags));
+    return makeRegexExpression(visit(exp[0]), visit(exp[1]),
+                               std::move(flags).value_or(nullptr));
   } catch (const std::exception& e) {
     reportError(ctx, e.what());
   }
diff --git a/src/util/LruCache.h b/src/util/LruCache.h
new file mode 100644
index 0000000000..7f83f035c0
--- /dev/null
+++ b/src/util/LruCache.h
@@ -0,0 +1,66 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include <absl/container/flat_hash_map.h>
+
+#include <cstdint>
+#include <list>
+
+#include "backports/concepts.h"
+#include "util/Exception.h"
+
+namespace ad_utility::util {
+
+// Simple LRU cache implementation that stores key-value pairs up to a
+// configurable threshold and discards the least recently used element when the
+// threshold is exceeded.
+template <typename K, typename V>
+class LRUCache {
+ private:
+  size_t capacity_;
+  // Stores keys in order of usage (MRU at front)
+  std::list<K> keys_;
+  absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
+
+ public:
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {
+    AD_CONTRACT_CHECK(capacity > 0, "Capacity must be greater than 0");
+  }
+
+  // Check if `key` is in the cache and return a reference to the value if it is
+  // found. Otherwise, compute the value using `computeFunction` and store it in
+  // the cache. If the cache is already at maximum capacity, evict the least
+  // recently used element.
+  CPP_template(typename Func)(
+      requires ad_utility::InvocableWithConvertibleReturnType<
+          Func, V, const K&>) const V& getOrCompute(const K& key,
+                                                    Func computeFunction) {
+    auto it = cache_.find(key);
+    if (it != cache_.end()) {
+      const auto& [value, listIterator] = it->second;
+      // Move accessed key to front (most recently used)
+      keys_.splice(keys_.begin(), keys_, listIterator);
+
+      return value;
+    }
+    // Evict LRU if cache is full
+    if (cache_.size() >= capacity_) {
+      K& lruKey = keys_.back();
+      cache_.erase(lruKey);
+      // Reuse allocated memory by moving node and reassigning key
+      keys_.splice(keys_.begin(), keys_, std::prev(keys_.end()));
+      lruKey = key;
+    } else {
+      // Push new element if not full
+      keys_.push_front(key);
+    }
+    auto result = cache_.try_emplace(key, computeFunction(key), keys_.begin());
+    AD_CORRECTNESS_CHECK(result.second);
+    return result.first->second.first;
+  }
+};
+
+}  // namespace ad_utility::util
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 8f288ec1a3..2a4935832c 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -460,3 +460,5 @@ addLinkAndDiscoverTest(GraphStoreProtocolTest engine)
 addLinkAndDiscoverTest(SPARQLProtocolTest engine)
 
 addLinkAndDiscoverTest(ParsedRequestBuilderTest engine)
+
+addLinkAndDiscoverTest(LruCacheTest)
diff --git a/test/LruCacheTest.cpp b/test/LruCacheTest.cpp
new file mode 100644
index 0000000000..ece1928aae
--- /dev/null
+++ b/test/LruCacheTest.cpp
@@ -0,0 +1,45 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#include <gtest/gtest.h>
+
+#include "util/LruCache.h"
+
+TEST(LRUCache, testLruCache) {
+  ad_utility::util::LRUCache<int, int> cache{2};
+  // Type-erase the lambdas to get a better coverage report.
+  using F = std::function<int(int)>;
+
+  EXPECT_EQ(1, cache.getOrCompute<F>(1, [](int i) {
+    EXPECT_EQ(i, 1);
+    return 1;
+  }));
+  EXPECT_EQ(2, cache.getOrCompute<F>(2, [](int i) {
+    EXPECT_EQ(i, 2);
+    return 2;
+  }));
+
+  EXPECT_EQ(1, cache.getOrCompute<F>(1, [](int) {
+    ADD_FAILURE();
+    return 0;
+  }));
+  EXPECT_EQ(3, cache.getOrCompute<F>(3, [](int i) {
+    EXPECT_EQ(i, 3);
+    return 3;
+  }));
+  EXPECT_EQ(20, cache.getOrCompute<F>(2, [](int i) {
+    EXPECT_EQ(i, 2);
+    return 20;
+  }));
+  EXPECT_EQ(10, cache.getOrCompute<F>(1, [](int i) {
+    EXPECT_EQ(i, 1);
+    return 10;
+  }));
+}
+
+// _____________________________________________________________________________
+TEST(LRUCache, testEmptyCapacityForbidden) {
+  EXPECT_THROW((ad_utility::util::LRUCache<int, int>{0}),
+               ad_utility::Exception);
+}
diff --git a/test/RegexExpressionTest.cpp b/test/RegexExpressionTest.cpp
index 0e94d3603d..2b3295a7a2 100644
--- a/test/RegexExpressionTest.cpp
+++ b/test/RegexExpressionTest.cpp
@@ -2,6 +2,8 @@
 // Chair of Algorithms and Data Structures
 // Author: Johannes Kalmbach <kalmbacj@cs.uni-freiburg.de>
 
+#include <gmock/gmock.h>
+
 #include <optional>
 #include <string>
 
@@ -11,7 +13,6 @@
 #include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/NaryExpression.h"
 #include "engine/sparqlExpressions/RegexExpression.h"
-#include "gtest/gtest.h"
 
 using namespace sparqlExpression;
 using ad_utility::source_location;
@@ -23,71 +24,125 @@ constexpr auto T = Id::makeFromBool(true);
 constexpr auto F = Id::makeFromBool(false);
 constexpr Id U = Id::makeUndefined();
 
+// _____________________________________________________________________________
+auto literal(const std::string& literal,
+             std::string_view langtagOrDatatype = "") {
+  return std::make_unique<StringLiteralExpression>(
+      lit(literal, langtagOrDatatype));
+}
+
+// _____________________________________________________________________________
+auto variable(std::string literal) {
+  return std::make_unique<VariableExpression>(Variable{std::move(literal)});
+}
+
+// _____________________________________________________________________________
+bool isPrefixExpression(const SparqlExpression::Ptr& expression) {
+  return dynamic_cast<PrefixRegexExpression*>(expression.get());
+}
+
 // Make `RegexExpression` from given the `child` (the expression on which to
 // apply the regex), `regex`, and optional `flags`. The argument `childAsStr` is
 // true iff the expression is enclosed in a `STR()` function.
-RegexExpression makeRegexExpression(
-    SparqlExpression::Ptr child, std::string regex,
-    std::optional<std::string> flags = std::nullopt, bool childAsStr = false) {
-  // The regex and the flags both have to be enquoted. This is normally ensured
-  // by the SPARQL parser. For easier readability of the tests we add those
-  // quotes here.
-  regex = absl::StrCat("\"", regex, "\"");
-  if (flags.has_value()) {
-    flags.value() = absl::StrCat("\"", flags.value(), "\"");
-  }
+SparqlExpression::Ptr makeTestRegexExpression(
+    SparqlExpression::Ptr child, SparqlExpression::Ptr regex,
+    SparqlExpression::Ptr flags = nullptr, bool childAsStr = false) {
   if (childAsStr) {
     child = makeStrExpression(std::move(child));
   }
-  auto regexExpression = std::make_unique<StringLiteralExpression>(lit(regex));
-  std::optional<SparqlExpression::Ptr> flagsExpression = std::nullopt;
-  if (flags.has_value()) {
-    flagsExpression = SparqlExpression::Ptr{
-        std::make_unique<StringLiteralExpression>(lit(flags.value()))};
-  }
 
-  return {std::move(child), std::move(regexExpression),
-          std::move(flagsExpression)};
+  return makeRegexExpression(std::move(child), std::move(regex),
+                             std::move(flags));
 }
 
 // Special case of the `makeRegexExpression` above, where the `child`
 // expression is a variable.
-RegexExpression makeRegexExpression(
+SparqlExpression::Ptr makeRegexExpression(
     std::string variable, std::string regex,
     std::optional<std::string> flags = std::nullopt, bool childAsStr = false) {
   SparqlExpression::Ptr variableExpression =
       std::make_unique<VariableExpression>(Variable{std::move(variable)});
-  return makeRegexExpression(std::move(variableExpression), std::move(regex),
-                             std::move(flags), childAsStr);
+  // The regex and the flags both have to be enquoted. This is normally ensured
+  // by the SPARQL parser. For easier readability of the tests we add those
+  // quotes here.
+  regex = absl::StrCat("\"", regex, "\"");
+  SparqlExpression::Ptr regexExpression =
+      std::make_unique<StringLiteralExpression>(lit(std::move(regex)));
+  SparqlExpression::Ptr flagsExpression = nullptr;
+  if (flags.has_value()) {
+    flagsExpression =
+        SparqlExpression::Ptr{std::make_unique<StringLiteralExpression>(
+            lit(absl::StrCat("\"", flags.value(), "\"")))};
+  }
+  return makeTestRegexExpression(std::move(variableExpression),
+                                 std::move(regexExpression),
+                                 std::move(flagsExpression), childAsStr);
 }
 }  // namespace
 
 // Test that the expression `leftValue Comparator rightValue`, when evaluated on
 // the `TestContext` (see above), yields the `expected` result.
-void testWithExplicitResult(const SparqlExpression& expression,
-                            std::vector<Id> expected,
-                            std::optional<size_t> numInputs = std::nullopt,
-                            source_location l = source_location::current()) {
+void testWithExplicitResult(
+    const SparqlExpression& expression, const std::vector<Id>& expected,
+    const std::optional<size_t>& numInputs = std::nullopt,
+    source_location l = source_location::current()) {
   TestContext ctx;
-  auto trace = generateLocationTrace(l, "testWithExplicitResult");
+  auto trace = generateLocationTrace(std::move(l), "testWithExplicitResult");
   if (numInputs.has_value()) {
     ctx.context._endIndex = numInputs.value();
   }
   auto resultAsVariant = expression.evaluate(&ctx.context);
+  ASSERT_TRUE(
+      std::holds_alternative<VectorWithMemoryLimit<Id>>(resultAsVariant));
+  const auto& result = std::get<VectorWithMemoryLimit<Id>>(resultAsVariant);
+
+  EXPECT_THAT(result, ::testing::ElementsAreArray(expected));
+}
+
+// Test that the expression works with variables which cannot be evaluated
+// during creation of the expression.
+void testValuesInVariables(
+    const std::vector<std::array<std::string, 3>>& inputValues,
+    const std::vector<Id>& expected, bool flagsUsed,
+    source_location l = source_location::current()) {
+  TestContext ctx;
+  auto trace = generateLocationTrace(std::move(l), "testWithExplicitResult");
+  ctx.varToColMap.clear();
+  ctx.varToColMap[Variable{"?string"}] = makeAlwaysDefinedColumn(0);
+  ctx.varToColMap[Variable{"?regex"}] = makeAlwaysDefinedColumn(1);
+  ctx.varToColMap[Variable{"?flags"}] = makeAlwaysDefinedColumn(2);
+  ctx.table.clear();
+  ctx.table.setNumColumns(3);
+  auto toLiteralId = [&ctx](const std::string& value) {
+    return Id::makeFromLocalVocabIndex(
+        ctx.localVocab.getIndexAndAddIfNotContained(
+            ad_utility::triple_component::LiteralOrIri::literalWithoutQuotes(
+                value)));
+  };
+  for (const auto& value : inputValues) {
+    ctx.table.push_back({toLiteralId(value.at(0)), toLiteralId(value.at(1)),
+                         toLiteralId(value.at(2))});
+  }
+  ctx.context._endIndex = inputValues.size();
+  auto resultAsVariant =
+      makeRegexExpression(variable("?string"), variable("?regex"),
+                          flagsUsed ? variable("?flags") : nullptr)
+          ->evaluate(&ctx.context);
   const auto& result = std::get<VectorWithMemoryLimit<Id>>(resultAsVariant);
 
   EXPECT_THAT(result, ::testing::ElementsAreArray(expected));
 }
 
+// _____________________________________________________________________________
 auto testNonPrefixRegex = [](std::string variable, std::string regex,
                              const std::vector<Id>& expectedResult,
                              bool childAsStr = false,
                              source_location l = source_location::current()) {
-  auto trace = generateLocationTrace(l, "testNonPrefixRegex");
+  auto trace = generateLocationTrace(std::move(l), "testNonPrefixRegex");
   auto expr = makeRegexExpression(std::move(variable), std::move(regex),
                                   std::nullopt, childAsStr);
-  ASSERT_FALSE(expr.isPrefixExpression());
-  testWithExplicitResult(expr, expectedResult);
+  EXPECT_FALSE(isPrefixExpression(expr));
+  testWithExplicitResult(*expr, expectedResult);
 };
 
 // Tests where the expression is a variable and the regex is not a simple prefix
@@ -118,6 +173,28 @@ TEST(RegexExpression, nonPrefixRegex) {
 
   // The IRI is only considered when testing with a STR expression
   test("?localVocab", "Vocab[AD]", {T, F, T}, true);
+
+  {
+    auto expr = makeTestRegexExpression(variable("?vocab"), variable("?vocab"));
+    EXPECT_FALSE(isPrefixExpression(expr));
+    testWithExplicitResult(*expr, {T, T, T});
+  }
+
+  // Values that are not simple literals should result in undefined
+  {
+    auto expr =
+        makeTestRegexExpression(variable("?vocab"), variable("?doubles"));
+    EXPECT_FALSE(isPrefixExpression(expr));
+    testWithExplicitResult(*expr, {U, U, U});
+  }
+
+  testValuesInVariables({{"Abc", "[A-Z]", ""},
+                         {"abc", "[A-Z]", ""},
+                         {"aBc", "[A-Z]", ""},
+                         {"abC", "[A-Z]", ""},
+                         {"", "", ""},
+                         {"", "(invalid", ""}},
+                        {T, F, T, T, T, U}, false);
 }
 
 // Test where the expression is not simply a variable.
@@ -126,16 +203,33 @@ TEST(RegexExpression, inputNotVariable) {
   VectorWithMemoryLimit<IdOrLiteralOrIri> input{
       ad_utility::testing::getQec()->getAllocator()};
   input.push_back(ad_utility::triple_component::LiteralOrIri(lit("\"hallo\"")));
-  auto child = std::make_unique<sparqlExpression::detail::SingleUseExpression>(
-      input.clone());
-
-  // "hallo" matches the regex "ha".
-  auto expr = makeRegexExpression(std::move(child), "ha", "");
-  std::vector<Id> expected;
-  expected.push_back(Id::makeFromBool(true));
-  testWithExplicitResult(expr, expected, input.size());
+
+  {
+    auto child =
+        std::make_unique<sparqlExpression::detail::SingleUseExpression>(
+            input.clone());
+
+    // "hallo" matches the regex "ha".
+    auto expr = makeRegexExpression(std::move(child), literal("\"ha\""),
+                                    literal("\"\""));
+    std::vector<Id> expected;
+    expected.push_back(Id::makeFromBool(true));
+    testWithExplicitResult(*expr, expected, input.size());
+  }
+
+  // Without variable it cannot be a prefix regex expression.
+  {
+    auto child =
+        std::make_unique<sparqlExpression::detail::SingleUseExpression>(
+            input.clone());
+
+    auto expr =
+        makeTestRegexExpression(std::move(child), literal("\"^ha\""), nullptr);
+    EXPECT_FALSE(isPrefixExpression(expr));
+  }
 }
 
+// _____________________________________________________________________________
 auto testNonPrefixRegexWithFlags =
     [](std::string variable, std::string regex, std::string flags,
        const std::vector<Id>& expectedResult,
@@ -143,8 +237,8 @@ auto testNonPrefixRegexWithFlags =
       auto trace = generateLocationTrace(l, "testNonPrefixRegexWithFlags");
       auto expr = makeRegexExpression(std::move(variable), std::move(regex),
                                       std::move(flags));
-      ASSERT_FALSE(expr.isPrefixExpression());
-      testWithExplicitResult(expr, expectedResult);
+      EXPECT_FALSE(isPrefixExpression(expr));
+      testWithExplicitResult(*expr, expectedResult);
     };
 
 // Fun with flags.
@@ -176,23 +270,57 @@ TEST(RegexExpression, nonPrefixRegexWithFlags) {
   test("?vocab", "^alp", "i", {F, T, F});
 
   // TODO<joka921>  Add tests for other flags (maybe the non-greedy one?)
+
+  // Undefined variable should result in undefined results
+  {
+    auto expr = makeRegexExpression(variable("?vocab"), literal("\"b\""),
+                                    variable("?b"));
+    EXPECT_FALSE(isPrefixExpression(expr));
+    testWithExplicitResult(*expr, {U, U, U});
+  }
+  // Values that are not simple literals should result in undefined
+  {
+    auto expr = makeRegexExpression(variable("?vocab"), literal("\"b\""),
+                                    variable("?ints"));
+    EXPECT_FALSE(isPrefixExpression(expr));
+    testWithExplicitResult(*expr, {U, U, U});
+  }
+  {
+    auto expr = makeRegexExpression(variable("?vocab"), variable("?ints"),
+                                    literal("i"));
+    EXPECT_FALSE(isPrefixExpression(expr));
+    testWithExplicitResult(*expr, {U, U, U});
+  }
+
+  testValuesInVariables({{"Abc", "[A-Z]", ""},
+                         {"abc", "[A-Z]", ""},
+                         {"abc", "[A-Z]", "i"},
+                         {"abc", "[A-Z]", "imsU"},
+                         {"", "", ""},
+                         {"", "(invalid", ""},
+                         {"", "", "invalid"}},
+                        {T, F, T, T, T, U, U}, true);
 }
 
+namespace sparqlExpression {
 // Test the `getPrefixRegex` function (which returns `std::nullopt` if the regex
 // is not a simple prefix regex).
 TEST(RegexExpression, getPrefixRegex) {
-  using namespace sparqlExpression::detail;
-  ASSERT_EQ(std::nullopt, getPrefixRegex("alpha"));
-  ASSERT_EQ(std::nullopt, getPrefixRegex("^al.ha"));
-  ASSERT_EQ(std::nullopt, getPrefixRegex("^alh*"));
-  ASSERT_EQ(std::nullopt, getPrefixRegex("^a(lh)"));
-
-  ASSERT_EQ("alpha", getPrefixRegex("^alpha"));
-  ASSERT_EQ(R"(\al*ph.a()", getPrefixRegex(R"(^\\al\*ph\.a\()"));
+  ASSERT_EQ(std::nullopt, PrefixRegexExpression::getPrefixRegex("alpha"));
+  ASSERT_EQ(std::nullopt, PrefixRegexExpression::getPrefixRegex("^al.ha"));
+  ASSERT_EQ(std::nullopt, PrefixRegexExpression::getPrefixRegex("^alh*"));
+  ASSERT_EQ(std::nullopt, PrefixRegexExpression::getPrefixRegex("^a(lh)"));
+
+  ASSERT_EQ("alpha", PrefixRegexExpression::getPrefixRegex("^alpha"));
+  ASSERT_EQ(R"(\al*ph.a()",
+            PrefixRegexExpression::getPrefixRegex(R"(^\\al\*ph\.a\()"));
   // Invalid escaping of `"` (no need to escape it).
-  ASSERT_THROW(getPrefixRegex(R"(^\")"), std::runtime_error);
+  ASSERT_THROW(PrefixRegexExpression::getPrefixRegex(R"(^\")"),
+               std::runtime_error);
 }
+}  // namespace sparqlExpression
 
+// _____________________________________________________________________________
 auto testPrefixRegexUnorderedColumn =
     [](std::string variable, std::string regex,
        const std::vector<Id>& expectedResult, bool childAsStr = false,
@@ -200,10 +328,11 @@ auto testPrefixRegexUnorderedColumn =
       auto trace = generateLocationTrace(l, "testUnorderedPrefix");
       auto expr = makeRegexExpression(std::move(variable), std::move(regex),
                                       std::nullopt, childAsStr);
-      ASSERT_TRUE(expr.isPrefixExpression());
-      testWithExplicitResult(expr, expectedResult);
+      EXPECT_TRUE(isPrefixExpression(expr));
+      testWithExplicitResult(*expr, expectedResult);
     };
 
+// _____________________________________________________________________________
 TEST(RegexExpression, unorderedPrefixRegexUnorderedColumn) {
   auto test = testPrefixRegexUnorderedColumn;
   // ?vocab column is `"Beta", "alpha", "älpha"
@@ -226,6 +355,7 @@ TEST(RegexExpression, unorderedPrefixRegexUnorderedColumn) {
   // TODO<joka921> Prefix filters on numbers do not yet work.
 }
 
+// _____________________________________________________________________________
 auto testPrefixRegexOrderedColumn =
     [](std::string variableAsString, std::string regex,
        ad_utility::SetOfIntervals expected, bool childAsStr = false,
@@ -235,13 +365,14 @@ auto testPrefixRegexOrderedColumn =
       TestContext ctx = TestContext::sortedBy(variable);
       auto expression = makeRegexExpression(variableAsString, regex,
                                             std::nullopt, childAsStr);
-      ASSERT_TRUE(expression.isPrefixExpression());
-      auto resultAsVariant = expression.evaluate(&ctx.context);
+      EXPECT_TRUE(isPrefixExpression(expression));
+      auto resultAsVariant = expression->evaluate(&ctx.context);
       const auto& result =
           std::get<ad_utility::SetOfIntervals>(resultAsVariant);
       ASSERT_EQ(result, expected);
     };
 
+// _____________________________________________________________________________
 TEST(RegexExpression, prefixRegexOrderedColumn) {
   auto test = testPrefixRegexOrderedColumn;
   // Sorted order (by bits of the valueIds):
@@ -258,83 +389,153 @@ TEST(RegexExpression, prefixRegexOrderedColumn) {
   test("?mixed", "^x", {{{2, 3}}}, true);
 }
 
+// _____________________________________________________________________________
 TEST(RegexExpression, getCacheKey) {
-  const auto exp1 = makeRegexExpression("?first", "alp");
+  using namespace ::testing;
+  auto exp0 = makeRegexExpression("?first", "^alp");
+  auto exp1 = makeRegexExpression("?first", "alp");
   auto exp2 = makeRegexExpression("?first", "alp");
 
   VariableToColumnMap map;
   map[Variable{"?first"}] = makeAlwaysDefinedColumn(0);
   map[Variable{"?second"}] = makeAlwaysDefinedColumn(1);
-  ASSERT_EQ(exp1.getCacheKey(map), exp2.getCacheKey(map));
+  EXPECT_TRUE(isPrefixExpression(exp0));
+  EXPECT_THAT(
+      exp0->getCacheKey(map),
+      AllOf(StartsWith("Prefix REGEX"), HasSubstr("str:0"), HasSubstr("alp"),
+            HasSubstr(exp0->children()[0]->getCacheKey(map))));
+  EXPECT_NE(exp0->getCacheKey(map), exp1->getCacheKey(map));
+  ASSERT_EQ(exp1->getCacheKey(map), exp2->getCacheKey(map));
 
   // Different regex, different cache key.
   auto exp3 = makeRegexExpression("?first", "alk");
-  ASSERT_NE(exp1.getCacheKey(map), exp3.getCacheKey(map));
+  ASSERT_NE(exp1->getCacheKey(map), exp3->getCacheKey(map));
 
   // Different variable, different cache key.
   auto exp4 = makeRegexExpression("?second", "alp");
-  ASSERT_NE(exp1.getCacheKey(map), exp4.getCacheKey(map));
+  ASSERT_NE(exp1->getCacheKey(map), exp4->getCacheKey(map));
 
   // Different flags, different cache key.
   auto exp5 = makeRegexExpression("?first", "alp", "im");
-  ASSERT_NE(exp1.getCacheKey(map), exp5.getCacheKey(map));
+  ASSERT_NE(exp1->getCacheKey(map), exp5->getCacheKey(map));
 
   // Different variable name, but the variable is stored in the same column ->
   // same cache key.
   auto map2 = map;
   map2[Variable{"?otherFirst"}] = makeAlwaysDefinedColumn(0);
   auto exp6 = makeRegexExpression("?otherFirst", "alp");
-  ASSERT_EQ(exp1.getCacheKey(map), exp6.getCacheKey(map2));
+  ASSERT_EQ(exp1->getCacheKey(map), exp6->getCacheKey(map2));
+
+  auto exp7 = makeRegexExpression(variable("?first"), literal("alp"),
+                                  variable("?second"));
+  EXPECT_NE(exp6->getCacheKey(map), exp7->getCacheKey(map));
+
+  auto exp8 = makeRegexExpression(variable("?first"), variable("?second"),
+                                  literal("i"));
+  EXPECT_NE(exp7->getCacheKey(map), exp8->getCacheKey(map));
+
+  map[Variable{"?third"}] = makeAlwaysDefinedColumn(1);
+  auto exp9 = makeRegexExpression(variable("?first"), variable("?second"),
+                                  variable("?third"));
+  EXPECT_NE(exp8->getCacheKey(map), exp9->getCacheKey(map));
+
+  auto exp10 = makeRegexExpression("?first", "^alp", std::nullopt, true);
+  EXPECT_THAT(exp10->getCacheKey(map),
+              AllOf(HasSubstr("str:1"), HasSubstr("alp"),
+                    HasSubstr(exp10->children()[0]->getCacheKey(map))));
+  EXPECT_NE(exp0->getCacheKey(map), exp10->getCacheKey(map));
 }
 
+// _____________________________________________________________________________
 TEST(RegexExpression, getChildren) {
-  auto expression = makeRegexExpression("?a", "someRegex");
-  auto vars = expression.containedVariables();
-  ASSERT_EQ(vars.size(), 1);
-  ASSERT_EQ(*vars[0], Variable{"?a"});
+  using namespace ::testing;
+  EXPECT_THAT(makeRegexExpression("?a", "someRegex")->containedVariables(),
+              ElementsAre(Pointee(Variable{"?a"})));
+  EXPECT_THAT(makeRegexExpression("?a", "^someRegex")->containedVariables(),
+              ElementsAre(Pointee(Variable{"?a"})));
+  EXPECT_THAT(
+      makeRegexExpression("?a", "someRegex", "imsU")->containedVariables(),
+      ElementsAre(Pointee(Variable{"?a"})));
+  EXPECT_THAT(makeTestRegexExpression(variable("?a"), literal("someRegex"),
+                                      variable("?c"))
+                  ->containedVariables(),
+              ElementsAre(Pointee(Variable{"?a"}), Pointee(Variable{"?c"})));
+  EXPECT_THAT(makeTestRegexExpression(variable("?a"), variable("?b"))
+                  ->containedVariables(),
+              ElementsAre(Pointee(Variable{"?a"}), Pointee(Variable{"?b"})));
+  EXPECT_THAT(
+      makeTestRegexExpression(variable("?a"), variable("?b"), variable("?c"))
+          ->containedVariables(),
+      ElementsAre(Pointee(Variable{"?a"}), Pointee(Variable{"?b"}),
+                  Pointee(Variable{"?c"})));
 }
 
+// _____________________________________________________________________________
 TEST(RegexExpression, invalidConstruction) {
-  auto literal = [](const std::string& literal,
-                    std::string_view langtagOrDatatype = "") {
-    return std::make_unique<StringLiteralExpression>(
-        lit(literal, langtagOrDatatype));
-  };
-  auto variable = [](std::string literal) {
-    return std::make_unique<VariableExpression>(Variable{std::move(literal)});
-  };
-
-  // The second argument must be a string literal.
-  EXPECT_THROW(RegexExpression(variable("?a"), variable("?b"), std::nullopt),
-               std::runtime_error);
-
   // The second argument must not have a datatype or langtag
-  EXPECT_THROW(
-      RegexExpression(variable("?a"), literal("\"b\"", "@en"), std::nullopt),
-      std::runtime_error);
-  EXPECT_THROW(RegexExpression(variable("?a"), literal("\"b\"", "^^<someType>"),
-                               std::nullopt),
+  EXPECT_THROW(makeTestRegexExpression(variable("?a"), literal("\"b\"", "@en")),
                std::runtime_error);
-
-  // The third argument must be a string literal.
   EXPECT_THROW(
-      RegexExpression(variable("?a"), literal("\"b\""), variable("?b")),
+      makeTestRegexExpression(variable("?a"), literal("\"b\"", "^^<someType>")),
       std::runtime_error);
   // The third argument must not have a language tag or datatype
-  EXPECT_THROW(RegexExpression(variable("?a"), literal("\"b\""),
-                               literal("\"i\"", "@en")),
+  EXPECT_THROW(makeTestRegexExpression(variable("?a"), literal("\"b\""),
+                                       literal("\"i\"", "@en")),
                std::runtime_error);
-  EXPECT_THROW(RegexExpression(variable("?a"), literal("\"b\""),
-                               literal("\"i\"", "^^<someType>")),
+  EXPECT_THROW(makeTestRegexExpression(variable("?a"), literal("\"b\""),
+                                       literal("\"i\"", "^^<someType>")),
                std::runtime_error);
 
   // Invalid regex (parentheses that are never closed).
-  EXPECT_THROW(
-      RegexExpression(variable("?a"), literal("\"(open\""), std::nullopt),
-      std::runtime_error);
+  EXPECT_THROW(makeTestRegexExpression(variable("?a"), literal("\"(open\"")),
+               std::runtime_error);
 
   // Invalid option flag.
-  EXPECT_THROW(
-      RegexExpression(variable("?a"), literal("\"a\""), literal("\"x\"")),
-      std::runtime_error);
+  EXPECT_THROW(makeTestRegexExpression(variable("?a"), literal("\"a\""),
+                                       literal("\"x\"")),
+               std::runtime_error);
+}
+
+// _____________________________________________________________________________
+TEST(RegexExpression, getEstimatesForFilterExpression) {
+  using Estimates = SparqlExpressionPimpl::Estimates;
+  auto hasEstimate = [](size_t sizeEstimate, size_t costEstimate) {
+    using namespace ::testing;
+    return AllOf(AD_FIELD(Estimates, sizeEstimate, Eq(sizeEstimate)),
+                 AD_FIELD(Estimates, costEstimate, Eq(costEstimate)));
+  };
+  auto expression = makeRegexExpression("?a", "^abc");
+  EXPECT_THAT(expression->getEstimatesForFilterExpression(10000, std::nullopt),
+              hasEstimate(10, 10010));
+  EXPECT_THAT(expression->getEstimatesForFilterExpression(100000, std::nullopt),
+              hasEstimate(100, 100100));
+  EXPECT_THAT(
+      expression->getEstimatesForFilterExpression(10000, Variable{"?b"}),
+      hasEstimate(10, 10010));
+  EXPECT_THAT(
+      expression->getEstimatesForFilterExpression(100000, Variable{"?b"}),
+      hasEstimate(100, 100100));
+  EXPECT_THAT(
+      expression->getEstimatesForFilterExpression(10000, Variable{"?a"}),
+      hasEstimate(10, 10));
+  EXPECT_THAT(
+      expression->getEstimatesForFilterExpression(100000, Variable{"?a"}),
+      hasEstimate(100, 100));
+
+  auto longRegexExpression = makeRegexExpression("?a", "^thisisverylong");
+  EXPECT_THAT(longRegexExpression->getEstimatesForFilterExpression(
+                  1000000000, std::nullopt),
+              hasEstimate(10, 1000000010));
+  EXPECT_THAT(longRegexExpression->getEstimatesForFilterExpression(
+                  1000000000, Variable{"?a"}),
+              hasEstimate(10, 10));
+
+  auto zeroLengthExpression = makeRegexExpression("?a", "^");
+  ASSERT_TRUE(isPrefixExpression(zeroLengthExpression));
+  EXPECT_THAT(
+      zeroLengthExpression->getEstimatesForFilterExpression(100, std::nullopt),
+      hasEstimate(100, 200));
+  EXPECT_THAT(zeroLengthExpression->getEstimatesForFilterExpression(
+                  1000000000, Variable{"?a"}),
+              hasEstimate(1000000000, 1000000000));
 }
diff --git a/test/SparqlAntlrParserTest.cpp b/test/SparqlAntlrParserTest.cpp
index 2595914f60..7094066b84 100644
--- a/test/SparqlAntlrParserTest.cpp
+++ b/test/SparqlAntlrParserTest.cpp
@@ -1752,13 +1752,13 @@ TEST(SparqlParser, builtInCall) {
                     matchNary(makeReplaceExpressionThreeArgs, Var{"?x"},
                               Var{"?y"}, Var{"?z"}));
   expectBuiltInCall(
-      "replace(?x, ?y, ?z, \"imsu\")",
+      "replace(?x, ?y, ?z, \"imsU\")",
       matchNaryWithChildrenMatchers(
           makeReplaceExpressionThreeArgs, variableExpressionMatcher(Var{"?x"}),
           matchNaryWithChildrenMatchers(
               &makeMergeRegexPatternAndFlagsExpression,
               variableExpressionMatcher(Var{"?y"}),
-              matchLiteralExpression(lit("imsu"))),
+              matchLiteralExpression(lit("imsU"))),
           variableExpressionMatcher(Var{"?z"})));
   expectBuiltInCall("IF(?a, ?h, ?c)", matchNary(&makeIfExpression, Var{"?a"},
                                                 Var{"?h"}, Var{"?c"}));
@@ -1791,7 +1791,22 @@ TEST(SparqlParser, builtInCall) {
   // The following three cases delegate to a separate parsing function, so we
   // only perform rather simple checks.
   expectBuiltInCall("COUNT(?x)", matchPtr<CountExpression>());
-  expectBuiltInCall("regex(?x, \"ab\")", matchPtr<RegexExpression>());
+  auto makeRegexExpressionTwoArgs = [](auto&& arg0, auto&& arg1) {
+    return makeRegexExpression(AD_FWD(arg0), AD_FWD(arg1), nullptr);
+  };
+  expectBuiltInCall(
+      "regex(?x, \"ab\")",
+      matchNaryWithChildrenMatchers(makeRegexExpressionTwoArgs,
+                                    variableExpressionMatcher(Var{"?x"}),
+                                    matchLiteralExpression(lit("ab"))));
+  expectBuiltInCall(
+      "regex(?x, \"ab\", \"imsU\")",
+      matchNaryWithChildrenMatchers(
+          makeRegexExpressionTwoArgs, variableExpressionMatcher(Var{"?x"}),
+          matchNaryWithChildrenMatchers(
+              &makeMergeRegexPatternAndFlagsExpression,
+              matchLiteralExpression(lit("ab")),
+              matchLiteralExpression(lit("imsU")))));
 
   expectBuiltInCall("MD5(?x)", matchUnary(&makeMD5Expression));
   expectBuiltInCall("SHA1(?x)", matchUnary(&makeSHA1Expression));
diff --git a/test/SparqlExpressionTest.cpp b/test/SparqlExpressionTest.cpp
index 864ca3f1a9..b746315b0a 100644
--- a/test/SparqlExpressionTest.cpp
+++ b/test/SparqlExpressionTest.cpp
@@ -1376,7 +1376,8 @@ TEST(SparqlExpression, ReplaceExpression) {
                           IdOrLiteralOrIri{lit("([A-Z]+)")},
                           IdOrLiteralOrIri{lit(R"("\\$1 \\2 $1 \\")")}});
 
-  checkReplace(idOrLitOrStringVec({"truebc", "truef"}),
+  // Only simple literals should be replaced.
+  checkReplace(idOrLitOrStringVec({U, U}),
                std::tuple{idOrLitOrStringVec({"Abc", "DEf"}),
                           IdOrLiteralOrIri{lit("([A-Z]+)")},
                           IdOrLiteralOrIri{Id::makeFromBool(true)}});
@@ -1393,6 +1394,13 @@ TEST(SparqlExpression, ReplaceExpression) {
       std::tuple{idOrLitOrStringVec({"null", "eIns", "zwEi", "drei"}),
                  IdOrLiteralOrIri{lit("[ei]")}, IdOrLiteralOrIri{lit("x")},
                  IdOrLiteralOrIri{lit("i")}});
+
+  // Matching using the all the flags
+  checkReplaceWithFlags(
+      idOrLitOrStringVec({"null", "xxns", "zwxx", "drxx"}),
+      std::tuple{idOrLitOrStringVec({"null", "eIns", "zwEi", "drei"}),
+                 IdOrLiteralOrIri{lit("[ei]")}, IdOrLiteralOrIri{lit("x")},
+                 IdOrLiteralOrIri{lit("imsU")}});
   // Empty flag
   checkReplaceWithFlags(
       idOrLitOrStringVec({"null", "xIns", "zwEx", "drxx"}),
diff --git a/test/SparqlExpressionTestHelpers.h b/test/SparqlExpressionTestHelpers.h
index 55a6734b51..4c85fde858 100644
--- a/test/SparqlExpressionTestHelpers.h
+++ b/test/SparqlExpressionTestHelpers.h
@@ -2,17 +2,13 @@
 //                  Chair of Algorithms and Data Structures.
 //  Author: Johannes Kalmbach <kalmbacj@cs.uni-freiburg.de>
 
+#pragma once
+
 #include "./util/IdTestHelpers.h"
-#include "./util/TripleComponentTestHelpers.h"
-#include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/SparqlExpression.h"
 #include "global/ValueIdComparators.h"
-#include "gtest/gtest.h"
-#include "index/ConstantsIndexBuilding.h"
 #include "util/IndexTestHelpers.h"
 
-#pragma once
-
 namespace sparqlExpression {
 
 // Make a `ValueId` from an int/ a double. Shorter name, as it will be used


Reviews:
user: login: RobinTF
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T20:02:19Z
user: login: joka921
body: A first round of reviews (only skimmed over the tests so far)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-24T10:42:12Z
user: login: RobinTF
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T10:58:22Z
user: login: joka921
body: Two small comments.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T08:17:51Z
user: login: joka921
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T08:23:12Z
user: login: joka921
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T08:23:40Z

Review comments:
diff_hunk: @@ -130,46 +165,56 @@ RegexExpression::RegexExpression(
         regexString = absl::StrCat("(?", flags, ":", regexString + ")");
       }
     } else {
-      throw std::runtime_error(
-          "The optional third argument to the REGEX function must be a string "
-          "literal (which contains the configuration flags)");
+      hasUnappliedFlags = true;
     }
   }
 
   // Create RE2 object from the regex string. If it is a simple prefix regex,
   // store the prefix in `prefixRegex_` (otherwise that becomes `std::nullopt`).
-  regexAsString_ = regexString;
   prefixRegex_ = detail::getPrefixRegex(regexString);
-  regex_.emplace(regexString, RE2::Quiet);
-  const auto& r = regex_.value();
-  if (r.error_code() != RE2::NoError) {
-    throw std::runtime_error{absl::StrCat(
-        "The regex \"", regexString,
-        "\" is not supported by QLever (which uses Google's RE2 library); "
-        "the error from RE2 is: ",
-        r.error())};
+  if (!regexString.empty() && !hasUnappliedFlags) {
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: RobinTF
body: Note to self: The empty regex should also get precomputed.
created_at: 2025-03-19T20:02:19Z
author_association: COLLABORATOR
diff_hunk: @@ -4,78 +4,75 @@
 
 #pragma once
 
+#include <gtest/gtest_prod.h>
+#include <re2/re2.h>
+
 #include <string>
 
-#include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/SparqlExpression.h"
-#include "re2/re2.h"
 
 namespace sparqlExpression {
 // Class implementing the REGEX function, which takes two mandatory arguments
 // (an expression and a regex) and one optional argument (a string of flags).
-class RegexExpression : public SparqlExpression {
+class PrefixRegexExpression : public SparqlExpression {
path: src/engine/sparqlExpressions/RegexExpression.h
user: login: joka921
body: The comment is out of sync now.
created_at: 2025-03-24T09:53:52Z
author_association: MEMBER
diff_hunk: @@ -4,78 +4,75 @@
 
 #pragma once
 
+#include <gtest/gtest_prod.h>
+#include <re2/re2.h>
+
 #include <string>
 
-#include "engine/sparqlExpressions/LiteralExpression.h"
 #include "engine/sparqlExpressions/SparqlExpression.h"
-#include "re2/re2.h"
 
 namespace sparqlExpression {
 // Class implementing the REGEX function, which takes two mandatory arguments
 // (an expression and a regex) and one optional argument (a string of flags).
-class RegexExpression : public SparqlExpression {
+class PrefixRegexExpression : public SparqlExpression {
  private:
-  SparqlExpression::Ptr child_;
-  // The reguar expression. It needs to be a `std::optional` because `RE2`
-  // objects do not have a default constructor.
-  std::optional<RE2> regex_;
-  // If this `std::optional` holds a string, we have a simple prefix regex
-  // (which translates to a range search) and this string holds the prefix.
-  std::optional<std::string> prefixRegex_;
-  // The regex as a string, used for the cache key.
-  std::string regexAsString_;
-
-  // True iff the expression is enclosed in `STR()`.
+  Ptr child_;
+  // A simple prefix regex (which translates to a range search) and this string
+  // holds the prefix.
+  std::string prefixRegex_;
+  // Holds the variable over which the regex is evaluated.
+  Variable variable_;
+  // If the variable is wrapped inside a `STR()` function, this is set to true.
   bool childIsStrExpression_ = false;
 
- public:
   // The `child` must be a `VariableExpression` and `regex` must be a
   // `LiteralExpression` that stores a string, otherwise an exception will be
   // thrown.
-  RegexExpression(SparqlExpression::Ptr child, SparqlExpression::Ptr regex,
-                  std::optional<SparqlExpression::Ptr> optionalFlags);
+  PrefixRegexExpression(Ptr child, std::string prefixRegex, Variable variable);
+
+ public:
+  PrefixRegexExpression(PrefixRegexExpression&&) noexcept = default;
+  PrefixRegexExpression& operator=(PrefixRegexExpression&&) noexcept = default;
+  PrefixRegexExpression(const PrefixRegexExpression&) noexcept = delete;
+  PrefixRegexExpression& operator=(const PrefixRegexExpression&) noexcept =
+      delete;
+
+  // ___________________________________________________________________________
path: src/engine/sparqlExpressions/RegexExpression.h
user: login: joka921
body: Document member functions that are not inherited.
created_at: 2025-03-24T09:54:30Z
author_association: MEMBER
diff_hunk: @@ -18,8 +19,74 @@ using namespace std::literals;
 
 namespace sparqlExpression::detail {
 
-// ____________________________________________________________________________
-std::optional<std::string> getPrefixRegex(std::string regex) {
+void ensureIsSimpleLiteral(
+    const ad_utility::triple_component::Literal& literal) {
+  if (literal.hasDatatype() || literal.hasLanguageTag()) {
+    throw std::runtime_error{
+        "The REGEX function only accepts simple literals (literals without a "
+        "language tag or a datatype)"};
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: joka921
body: Is there ar "for the second and third argument"  or "for the rexex and the flags" missing in the error message?
created_at: 2025-03-24T09:59:46Z
author_association: MEMBER
diff_hunk: @@ -18,8 +19,74 @@ using namespace std::literals;
 
 namespace sparqlExpression::detail {
 
-// ____________________________________________________________________________
-std::optional<std::string> getPrefixRegex(std::string regex) {
+void ensureIsSimpleLiteral(
+    const ad_utility::triple_component::Literal& literal) {
+  if (literal.hasDatatype() || literal.hasLanguageTag()) {
+    throw std::runtime_error{
+        "The REGEX function only accepts simple literals (literals without a "
+        "language tag or a datatype)"};
+  }
+}
+
+// _____________________________________________________________________________
+void ensureIsValidRegexIfConstant(const SparqlExpression& expression) {
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&expression);
+  if (stringLiteralExpression) {
+    const auto& literal = stringLiteralExpression->value();
+    const auto& string = asStringViewUnsafe(literal.getContent());
+    RE2 regex{string, RE2::Quiet};
+    if (!regex.ok()) {
+      throw std::runtime_error{absl::StrCat(
+          "The regex \"", string,
+          "\" is not supported by QLever (which uses Google's RE2 library); "
+          "the error from RE2 is: ",
+          regex.error())};
+    }
+  }
+}
+
+// _____________________________________________________________________________
+void ensureIsValidFlagIfConstant(const SparqlExpression& expression) {
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&expression);
+  if (stringLiteralExpression) {
+    const auto& literal = stringLiteralExpression->value();
+    const auto& string = asStringViewUnsafe(literal.getContent());
+    auto firstInvalidFlag = string.find_first_not_of("imsu");
+    if (firstInvalidFlag != std::string::npos) {
+      throw std::runtime_error{absl::StrCat(
+          "Invalid regex flag '", string.substr(firstInvalidFlag, 1),
+          "' found in \"", string,
+          "\". The only supported flags are 'i', 'm', 's', 'u', and any "
+          "combination of them")};
+    }
+  }
+}
+
+// _____________________________________________________________________________
+[[maybe_unused]] auto regexImpl = [](const std::optional<std::string>& input,
+                                     const std::shared_ptr<RE2>& pattern) {
+  if (!input.has_value() || !pattern) {
+    return Id::makeUndefined();
+  }
+  // Check for invalid regexes.
+  if (!pattern->ok()) {
+    return Id::makeUndefined();
+  }
+  return Id::makeFromBool(RE2::PartialMatch(input.value(), *pattern));
+};
+
+using RegexExpression =
+    NARY<2, FV<decltype(regexImpl), LiteralFromIdGetter, RegexValueGetter>>;
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: joka921
body: The regex implementation could also benefit from the direct application of the 
 `STR()` function in the child, we have a certain machinery at the  beginning of `StringExpressions.cpp` for these things, which could be extracted into a separate header to reuse them here.
created_at: 2025-03-24T10:03:34Z
author_association: MEMBER
diff_hunk: @@ -69,107 +136,37 @@ std::optional<std::string> getPrefixRegex(std::string regex) {
   return regex;
 }
 
-}  // namespace sparqlExpression::detail
-
-namespace sparqlExpression {
-
-// ___________________________________________________________________________
-RegexExpression::RegexExpression(
-    SparqlExpression::Ptr child, SparqlExpression::Ptr regex,
-    std::optional<SparqlExpression::Ptr> optionalFlags)
-    : child_{std::move(child)} {
+// _____________________________________________________________________________
+PrefixRegexExpression::PrefixRegexExpression(Ptr child, std::string prefixRegex,
+                                             Variable variable)
+    : child_{std::move(child)},
+      prefixRegex_{std::move(prefixRegex)},
+      variable_{std::move(variable)} {
   // If we have a `STR()` expression, remove the `STR()` and remember that it
   // was there.
   if (child_->isStrExpression()) {
     child_ = std::move(std::move(*child_).moveChildrenOut().at(0));
     childIsStrExpression_ = true;
   }
-
-  // Get the regex string, which must be a string literal without a datatype or
-  // language tag.
-  std::string regexString;
-  if (auto regexPtr =
-          dynamic_cast<const StringLiteralExpression*>(regex.get())) {
-    const auto& regexLiteral = regexPtr->value();
-    regexString = asStringViewUnsafe(regexLiteral.getContent());
-    if (regexLiteral.hasDatatype() || regexLiteral.hasLanguageTag()) {
-      throw std::runtime_error(
-          "The second argument to the REGEX function (which contains the "
-          "regular expression) must not contain a language tag or a datatype");
-    }
-  } else {
-    throw std::runtime_error(
-        "The second argument to the REGEX function must be a "
-        "string literal (which contains the regular expression)");
-  }
-
-  // Parse the flags. The optional argument for that must, again, be a
-  // string literal without a datatype or language tag.
-  if (optionalFlags.has_value()) {
-    if (auto flagsPtr = dynamic_cast<const StringLiteralExpression*>(
-            optionalFlags.value().get())) {
-      const auto& flagsLiteral = flagsPtr->value();
-      std::string_view flags = asStringViewUnsafe(flagsLiteral.getContent());
-      if (flagsLiteral.hasDatatype() || flagsLiteral.hasLanguageTag()) {
-        throw std::runtime_error(
-            "The third argument to the REGEX function (which contains optional "
-            "flags to configure the evaluation) must not contain a language "
-            "tag or a datatype");
-      }
-      auto firstInvalidFlag = flags.find_first_not_of("imsu");
-      if (firstInvalidFlag != std::string::npos) {
-        throw std::runtime_error{absl::StrCat(
-            "Invalid regex flag '", std::string{flags[firstInvalidFlag]},
-            "' found in \"", flags,
-            "\". The only supported flags are 'i', 's', 'm', 's', 'u', and any "
-            "combination of them")};
-      }
-
-      // In Google RE2 the flags are directly part of the regex.
-      if (!flags.empty()) {
-        regexString = absl::StrCat("(?", flags, ":", regexString + ")");
-      }
-    } else {
-      throw std::runtime_error(
-          "The optional third argument to the REGEX function must be a string "
-          "literal (which contains the configuration flags)");
-    }
-  }
-
-  // Create RE2 object from the regex string. If it is a simple prefix regex,
-  // store the prefix in `prefixRegex_` (otherwise that becomes `std::nullopt`).
-  regexAsString_ = regexString;
-  prefixRegex_ = detail::getPrefixRegex(regexString);
-  regex_.emplace(regexString, RE2::Quiet);
-  const auto& r = regex_.value();
-  if (r.error_code() != RE2::NoError) {
-    throw std::runtime_error{absl::StrCat(
-        "The regex \"", regexString,
-        "\" is not supported by QLever (which uses Google's RE2 library); "
-        "the error from RE2 is: ",
-        r.error())};
-  }
 }
 
-// ___________________________________________________________________________
-string RegexExpression::getCacheKey(
+// _____________________________________________________________________________
+string PrefixRegexExpression::getCacheKey(
     const VariableToColumnMap& varColMap) const {
-  return absl::StrCat("REGEX expression ", child_->getCacheKey(varColMap),
-                      " with ", regexAsString_, "str:", childIsStrExpression_);
+  return absl::StrCat("Prefix REGEX expression ",
+                      child_->getCacheKey(varColMap),
+                      " str:", childIsStrExpression_);
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: joka921
body: The prefix has to be part of the cache key, otherwise this is very wrong.
Please also add a check for this case.
created_at: 2025-03-24T10:04:46Z
author_association: MEMBER
diff_hunk: @@ -256,107 +253,76 @@ ExpressionResult RegexExpression::evaluatePrefixRegex(
   return result;
 }
 
-// ___________________________________________________________________________
-CPP_template_def(typename T)(requires SingleExpressionResult<T>)
-    ExpressionResult RegexExpression::evaluateGeneralCase(
-        T&& input, sparqlExpression::EvaluationContext* context) const {
-  // We have one result for each row of the input.
-  auto resultSize = context->size();
-  VectorWithMemoryLimit<Id> result{context->_allocator};
-  result.reserve(resultSize);
-  AD_CORRECTNESS_CHECK(regex_.has_value());
-
-  // Compute the result using the given value getter. If the getter returns
-  // `std::nullopt` for a row, the result is `UNDEF`. Otherwise, we have a
-  // string and evaluate the regex on it.
-  auto computeResult = [&]<typename ValueGetter>(const ValueGetter& getter) {
-    ql::ranges::for_each(
-        detail::makeGenerator(AD_FWD(input), resultSize, context),
-        [&getter, &context, &result, this](const auto& id) {
-          auto str = getter(id, context);
-          if (!str.has_value()) {
-            result.push_back(Id::makeUndefined());
-          } else {
-            result.push_back(Id::makeFromBool(
-                RE2::PartialMatch(str.value(), regex_.value())));
-          }
-          checkCancellation(context);
-        });
-  };
-
-  // Compute the result with the correct value getter (depending on whether the
-  // expression is enclosed in `STR()` or not), and return it.
-  if (childIsStrExpression_) {
-    computeResult(detail::StringValueGetter{});
-  } else {
-    computeResult(detail::LiteralFromIdGetter{});
-  }
-  return result;
-}
-
-// ___________________________________________________________________________
-ExpressionResult RegexExpression::evaluate(
-    sparqlExpression::EvaluationContext* context) const {
-  auto resultAsVariant = child_->evaluate(context);
-  auto variablePtr = std::get_if<Variable>(&resultAsVariant);
+// _____________________________________________________________________________
+auto PrefixRegexExpression::getEstimatesForFilterExpression(
+    uint64_t inputSize,
+    const std::optional<Variable>& firstSortedVariable) const -> Estimates {
+  // Assume that only 10^-k entries remain, where k is the length of the prefix
+  // and cap to reasonable maximal values to prevent numerical stability
+  // problems.
+  double reductionFactor =
+      std::pow(10, std::min<size_t>(8, prefixRegex_.size()));
+  size_t sizeEstimate = inputSize / static_cast<size_t>(reductionFactor);
+  size_t costEstimate = firstSortedVariable == variable_
+                            ? sizeEstimate
+                            : sizeEstimate + inputSize;
 
-  if (prefixRegex_.has_value() && variablePtr != nullptr) {
-    return evaluatePrefixRegex(*variablePtr, context);
-  } else {
-    return std::visit(
-        [this, context](auto&& input) {
-          return evaluateGeneralCase(AD_FWD(input), context);
-        },
-        std::move(resultAsVariant));
-  }
+  return {sizeEstimate, costEstimate};
 }
 
-// ____________________________________________________________________________
-bool RegexExpression::isPrefixExpression() const {
-  return prefixRegex_.has_value();
+// _____________________________________________________________________________
+void PrefixRegexExpression::checkCancellation(
+    const EvaluationContext* context, ad_utility::source_location location) {
+  context->cancellationHandle_->throwIfCancelled(location);
 }
 
-// ____________________________________________________________________________
-auto RegexExpression::getEstimatesForFilterExpression(
-    uint64_t inputSize,
-    const std::optional<Variable>& firstSortedVariable) const -> Estimates {
-  // If we have a simple prefix regex, assume that only 10^-k entries remain,
-  // where k is the length of the prefix.
-  if (isPrefixExpression()) {
-    double reductionFactor = std::pow(
-        10, std::max(0, static_cast<int>(prefixRegex_.value().size())));
-    // Cap to reasonable minimal and maximal values to prevent numerical
-    // stability problems.
-    reductionFactor = std::min(100000000.0, reductionFactor);
-    reductionFactor = std::max(1.0, reductionFactor);
-    size_t sizeEstimate = inputSize / static_cast<size_t>(reductionFactor);
-    auto varPtr = dynamic_cast<VariableExpression*>(child_.get());
-    size_t costEstimate = (varPtr && firstSortedVariable == varPtr->value())
-                              ? sizeEstimate
-                              : sizeEstimate + inputSize;
-
-    return {sizeEstimate, costEstimate};
+// _____________________________________________________________________________
+std::optional<PrefixRegexExpression>
+PrefixRegexExpression::makePrefixRegexExpressionIfPossible(
+    Ptr& string, const SparqlExpression& regex) {
+  detail::ensureIsValidRegexIfConstant(regex);
+  const auto* variableExpression = dynamic_cast<const VariableExpression*>(
+      string->isStrExpression() ? string->children()[0].get() : string.get());
+  const auto* stringLiteralExpression =
+      dynamic_cast<const StringLiteralExpression*>(&regex);
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: joka921
body: isn't `if ! variableExpression then immediately return nullopt` the much cleaner control flow here?
created_at: 2025-03-24T10:07:09Z
author_association: MEMBER
diff_hunk: @@ -267,19 +251,40 @@ struct LiteralFromIdGetter : Mixin<LiteralFromIdGetter> {
   }
 };
 
-// Convert the input into a `unique_ptr<RE2>`. Return nullptr if the input is
+// Similar to `LiteralFromIdGetter`, but correctly preprocesses strings so that
+// they can be used by re2 as replacement strings. So '$1 \abc \$' becomes
+// '\1 \\abc $', where the former variant is valid in the SPARQL standard and
+// the latter represents the format that re2 expects.
+struct ReplacementStringGetter : LiteralFromIdGetter,
+                                 Mixin<ReplacementStringGetter> {
path: src/engine/sparqlExpressions/SparqlExpressionValueGetters.h
user: login: joka921
body: Do you really need multiple inheritance, or can the (stateless!) LiteralFromIdGetter be something local inside the class.
created_at: 2025-03-24T10:18:03Z
author_association: MEMBER
diff_hunk: @@ -94,7 +94,7 @@ std::optional<std::string> StringValueGetter::operator()(
 std::optional<std::string> ReplacementStringGetter::operator()(
     Id id, const EvaluationContext* context) const {
   std::optional<std::string> originalString =
-      StringValueGetter::operator()(id, context);
+      LiteralFromIdGetter::operator()(id, context);
path: src/engine/sparqlExpressions/SparqlExpressionValueGetters.cpp
user: login: joka921
body: ```suggestion
      LiteralFromIdGetter{}()(id, context);
```
-> No inheritance necessary.
created_at: 2025-03-24T10:20:40Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include <absl/container/flat_hash_map.h>
+
+#include <cstdint>
+#include <list>
+
+#include "util/Exception.h"
+
+namespace ad_utility::util {
+
path: src/util/LruCache.h
user: login: joka921
body: Do Cu Ment You Re Co De
created_at: 2025-03-24T10:23:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include <absl/container/flat_hash_map.h>
+
+#include <cstdint>
+#include <list>
+
+#include "util/Exception.h"
+
+namespace ad_utility::util {
+
+template <typename K, typename V>
+class LRUCache {
+ private:
+  size_t capacity_;
+  // Stores keys in order of usage (MRU at front)
+  std::list<K> keys_;
+  absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
+
+ public:
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+
+  template <typename Func>
path: src/util/LruCache.h
user: login: joka921
body: Add a constraint (InvocableWithExact/ConvertibleReturnType)

created_at: 2025-03-24T10:25:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include <absl/container/flat_hash_map.h>
+
+#include <cstdint>
+#include <list>
+
+#include "util/Exception.h"
+
+namespace ad_utility::util {
+
+template <typename K, typename V>
+class LRUCache {
+ private:
+  size_t capacity_;
+  // Stores keys in order of usage (MRU at front)
+  std::list<K> keys_;
+  absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
+
+ public:
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+
+  template <typename Func>
+  const V& getOrCompute(const K& key, Func computeFunction) {
+    auto it = cache_.find(key);
+    if (it != cache_.end()) {
+      // Move accessed key to front (most recently used)
path: src/util/LruCache.h
user: login: joka921
body: `auto& [values, listIt] = it->second;`.
created_at: 2025-03-24T10:27:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+//   Copyright 2025, University of Freiburg,
+//   Chair of Algorithms and Data Structures.
+//   Author: Robin Textor-Falconi <textorr@informatik.uni-freiburg.de>
+
+#pragma once
+
+#include <absl/container/flat_hash_map.h>
+
+#include <cstdint>
+#include <list>
+
+#include "util/Exception.h"
+
+namespace ad_utility::util {
+
+template <typename K, typename V>
+class LRUCache {
+ private:
+  size_t capacity_;
+  // Stores keys in order of usage (MRU at front)
+  std::list<K> keys_;
+  absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
+
+ public:
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+
+  template <typename Func>
+  const V& getOrCompute(const K& key, Func computeFunction) {
+    auto it = cache_.find(key);
+    if (it != cache_.end()) {
+      // Move accessed key to front (most recently used)
+      keys_.erase(it->second.second);
+      keys_.push_front(key);
+      it->second.second = keys_.begin();
path: src/util/LruCache.h
user: login: joka921
body: Each access to an existing element triggers a reallocation as I see it, 
as `std::list` doesn't give you the interface to efficiently reuse the allocated memory.
That is really inefficient here and shouldn't be like this. The easiest way is to write
an allocator (e.g. using the facilities from `std::pmr` namespace) to accomodate your specific usage pattern here (which basically is "never free memory, as it will be reused".
created_at: 2025-03-24T10:35:56Z
author_association: MEMBER
diff_hunk: @@ -18,8 +19,74 @@ using namespace std::literals;
 
 namespace sparqlExpression::detail {
 
-// ____________________________________________________________________________
-std::optional<std::string> getPrefixRegex(std::string regex) {
+void ensureIsSimpleLiteral(
+    const ad_utility::triple_component::Literal& literal) {
+  if (literal.hasDatatype() || literal.hasLanguageTag()) {
+    throw std::runtime_error{
+        "The REGEX function only accepts simple literals (literals without a "
+        "language tag or a datatype)"};
path: src/engine/sparqlExpressions/RegexExpression.cpp
user: login: RobinTF
body: It was there previously, but this error message is only for the case where you hard-code the strings in the SPARQL query like this:
```SPARQL
SELECT * {
 ?a ?b ?c .
FILTER(REGEX(?a, "myRegex", "flags"))
}
```

So if you write `"myRegex"@en` or `"flags"^^my:type` instead, well you should be smart enough to figure that out on your own.
created_at: 2025-03-24T10:58:22Z
author_association: COLLABORATOR
diff_hunk: @@ -22,27 +26,37 @@ class LRUCache {
   absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
 
  public:
-  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {
+    AD_CONTRACT_CHECK(capacity > 0, "Capacity must be greater than 0");
+  }
 
-  template <typename Func>
-  const V& getOrCompute(const K& key, Func computeFunction) {
+  // Check if `key` is in the cache and return a reference to the value if it is
+  // found. Otherwise, compute the value using `computeFunction` and store it in
+  // the cache. If the cache is already at maximum capacity, evict the least
+  // recently used element.
+  CPP_template(typename Func)(
+      requires ad_utility::InvocableWithConvertibleReturnType<
+          Func, V, const K&>) const V& getOrCompute(const K& key,
+                                                    Func computeFunction) {
     auto it = cache_.find(key);
     if (it != cache_.end()) {
+      const auto& [value, listIterator] = it->second;
       // Move accessed key to front (most recently used)
-      keys_.erase(it->second.second);
-      keys_.push_front(key);
-      it->second.second = keys_.begin();
+      keys_.splice(keys_.begin(), keys_, listIterator);
 
-      return it->second.first;
+      return value;
     }
     // Evict LRU if cache is full
     if (cache_.size() >= capacity_) {
-      K lruKey = keys_.back();
-      keys_.pop_back();
+      K& lruKey = keys_.back();
       cache_.erase(lruKey);
+      // Reuse allocated memory by moving node and reassigning key
+      keys_.splice(keys_.begin(), keys_, std::prev(keys_.end()));
path: src/util/LruCache.h
user: login: joka921
body: The splicing is nice, but I would still like a `monotic_buffer...` to make the list even less allocation-heavy.
created_at: 2025-03-25T12:40:07Z
author_association: MEMBER
diff_hunk: @@ -22,27 +26,37 @@ class LRUCache {
   absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
 
  public:
-  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {
+    AD_CONTRACT_CHECK(capacity > 0, "Capacity must be greater than 0");
+  }
 
-  template <typename Func>
-  const V& getOrCompute(const K& key, Func computeFunction) {
+  // Check if `key` is in the cache and return a reference to the value if it is
+  // found. Otherwise, compute the value using `computeFunction` and store it in
+  // the cache. If the cache is already at maximum capacity, evict the least
+  // recently used element.
+  CPP_template(typename Func)(
+      requires ad_utility::InvocableWithConvertibleReturnType<
+          Func, V, const K&>) const V& getOrCompute(const K& key,
+                                                    Func computeFunction) {
     auto it = cache_.find(key);
     if (it != cache_.end()) {
+      const auto& [value, listIterator] = it->second;
       // Move accessed key to front (most recently used)
-      keys_.erase(it->second.second);
-      keys_.push_front(key);
-      it->second.second = keys_.begin();
+      keys_.splice(keys_.begin(), keys_, listIterator);
 
-      return it->second.first;
+      return value;
     }
     // Evict LRU if cache is full
     if (cache_.size() >= capacity_) {
-      K lruKey = keys_.back();
-      keys_.pop_back();
+      K& lruKey = keys_.back();
       cache_.erase(lruKey);
+      // Reuse allocated memory by moving node and reassigning key
+      keys_.splice(keys_.begin(), keys_, std::prev(keys_.end()));
path: src/util/LruCache.h
user: login: joka921
body: Maybe I can also do this myself when I merge the conflicts that will soon appear
created_at: 2025-03-25T12:40:26Z
author_association: MEMBER
diff_hunk: @@ -22,27 +26,37 @@ class LRUCache {
   absl::flat_hash_map<K, std::pair<V, typename std::list<K>::iterator>> cache_;
 
  public:
-  explicit LRUCache(size_t capacity) : capacity_{capacity} {}
+  explicit LRUCache(size_t capacity) : capacity_{capacity} {
+    AD_CONTRACT_CHECK(capacity > 0, "Capacity must be greater than 0");
+  }
 
-  template <typename Func>
-  const V& getOrCompute(const K& key, Func computeFunction) {
+  // Check if `key` is in the cache and return a reference to the value if it is
+  // found. Otherwise, compute the value using `computeFunction` and store it in
+  // the cache. If the cache is already at maximum capacity, evict the least
+  // recently used element.
+  CPP_template(typename Func)(
+      requires ad_utility::InvocableWithConvertibleReturnType<
+          Func, V, const K&>) const V& getOrCompute(const K& key,
+                                                    Func computeFunction) {
     auto it = cache_.find(key);
     if (it != cache_.end()) {
+      const auto& [value, listIterator] = it->second;
       // Move accessed key to front (most recently used)
-      keys_.erase(it->second.second);
-      keys_.push_front(key);
-      it->second.second = keys_.begin();
+      keys_.splice(keys_.begin(), keys_, listIterator);
 
-      return it->second.first;
+      return value;
     }
     // Evict LRU if cache is full
     if (cache_.size() >= capacity_) {
-      K lruKey = keys_.back();
-      keys_.pop_back();
+      K& lruKey = keys_.back();
       cache_.erase(lruKey);
+      // Reuse allocated memory by moving node and reassigning key
+      keys_.splice(keys_.begin(), keys_, std::prev(keys_.end()));
path: src/util/LruCache.h
user: login: joka921
body: After some trying out, this is easier said then done, 
because the valuegetters currently have to be copyable, and this is really difficult to achive with the 
PMR allocators., So this is fine for now, and we can still optimize this later should someone ever ask for this in production.
created_at: 2025-03-26T08:23:12Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-19T19:08:41Z
body: ## [Codecov](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) Report
Attention: Patch coverage is `97.54601%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 90.65%. Comparing base [(`707f5e7`)](https://app.codecov.io/gh/ad-freiburg/qlever/commit/707f5e7826fd40d00ce520d17d3d59e960f2cfdb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) to head [(`df1cc2f`)](https://app.codecov.io/gh/ad-freiburg/qlever/commit/df1cc2feee4dbb23c04e5063b8fe574fa5be5922?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) | Patch % | Lines |
|---|---|---|
| [src/util/LruCache.h](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?src=pr&el=tree&filepath=src%2Futil%2FLruCache.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg#diff-c3JjL3V0aWwvTHJ1Q2FjaGUuaA==) | 86.36% | [0 Missing and 3 partials :warning: ](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) |
| [...engine/sparqlExpressions/StringExpressionsHelper.h](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?src=pr&el=tree&filepath=src%2Fengine%2FsparqlExpressions%2FStringExpressionsHelper.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg#diff-c3JjL2VuZ2luZS9zcGFycWxFeHByZXNzaW9ucy9TdHJpbmdFeHByZXNzaW9uc0hlbHBlci5o) | 94.73% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1889      +/-   ##
==========================================
+ Coverage   90.62%   90.65%   +0.03%     
==========================================
  Files         407      411       +4     
  Lines       40100    40111      +11     
  Branches     4534     4539       +5     
==========================================
+ Hits        36339    36363      +24     
+ Misses       2414     2401      -13     
  Partials     1347     1347              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1889?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sparql-conformance[bot]
created_at: 2025-03-25T17:57:25Z
body: 
### Conformance check passed ✅


#### Test Status Changes 📊

| Number of Tests | Previous Status | Current Status |
| --------------- | --------------- | -------------- |
| 2 | Failed | Intended |

Details: https://qlever.cs.uni-freiburg.de/sparql-conformance-ui?cur=df1cc2feee4dbb23c04e5063b8fe574fa5be5922&prev=19c59c135b5f6d0b661ed754dd3739b07abf3eca
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-25T18:47:30Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=ad-freiburg_qlever&pullRequest=1889) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&pullRequest=1889&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=ad-freiburg_qlever&pullRequest=1889&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=ad-freiburg_qlever&pullRequest=1889&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=ad-freiburg_qlever&pullRequest=1889&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=ad-freiburg_qlever&pullRequest=1889&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ad-freiburg_qlever&pullRequest=1889)


author_association: NONE