Repository: ad-freiburg/qlever
Pull request #1924: Let `CONCAT` preserve datatype or language tag when identical for all arguments
Author: login: DuDaAG
Created: 2025-04-01T21:58:17Z | Merged: 2025-04-15T20:55:58Z

So far, the result of a `CONCAT` never had a datatype or language tag. Now, if all arguments have the same language tag or datatype, it is preserved, see https://www.w3.org/TR/sparql11-query/#func-concat. Continuation of #1905 and #1906.


Diff:
diff --git a/e2e/scientists_queries.yaml b/e2e/scientists_queries.yaml
index 7ccd07e6cf..7aa3e9604c 100644
--- a/e2e/scientists_queries.yaml
+++ b/e2e/scientists_queries.yaml
@@ -1885,7 +1885,7 @@ queries:
   - query: CONCAT
     type: no-text
     sparql: |
-      SELECT ?x (CONCAT("Name: ", ?x, " ", "Year: ", YEAR(?date)) as ?f) WHERE {
+      SELECT ?x (CONCAT("Name: ", STR(?x), " ", "Year: ", STR(YEAR(?date))) as ?f) WHERE {
           ?x <is-a> <Scientist> .
           ?x <Gender> <Female>  .
           ?x <Date_of_birth> ?date
@@ -1900,7 +1900,7 @@ queries:
   - query: CONCAT_WITH_GROUP_BY
     type: no-text
     sparql: |
-      SELECT ?x (CONCAT("Name: ", ?x, " ", "Year: ", SAMPLE(YEAR(?date))) as ?f) WHERE {
+      SELECT ?x (CONCAT("Name: ", STR(?x), " ", "Year: ", SAMPLE(STR(YEAR(?date)))) as ?f) WHERE {
           ?x <is-a> <Scientist> .
           ?x <Gender> <Female>  .
           ?x <Date_of_birth> ?date
diff --git a/src/engine/sparqlExpressions/StringExpressions.cpp b/src/engine/sparqlExpressions/StringExpressions.cpp
index dcee1e892b..3ee9a226c0 100644
--- a/src/engine/sparqlExpressions/StringExpressions.cpp
+++ b/src/engine/sparqlExpressions/StringExpressions.cpp
@@ -41,6 +41,23 @@ static std::size_t utf8Length(std::string_view s) {
   return ql::ranges::count_if(s, &isUtf8CodepointStart);
 }
 
+// Initialize or append a Literal. If both literals are valid and initialized,
+// concatenate nextLiteral into literalSoFar. If not initialized yet, set
+// literalSoFar to nextLiteral. If either is UNDEF, set literalSoFar to nullopt
+// to indicate an undefined result.
+void concatOrSetLiteral(
+    std::optional<ad_utility::triple_component::Literal>& literalSoFarOpt,
+    const std::optional<ad_utility::triple_component::Literal>& nextLiteral,
+    const bool isFirstLiteral) {
+  if (!nextLiteral.has_value() || !literalSoFarOpt.has_value()) {
+    literalSoFarOpt = std::nullopt;  // UNDEF
+  } else if (isFirstLiteral) {
+    literalSoFarOpt = nextLiteral.value();
+  } else {
+    literalSoFarOpt.value().concat(nextLiteral.value());
+  }
+}
+
 // Convert UTF-8 position to byte offset. If utf8Pos exceeds the
 // string length, the byte offset will be set to the size of the string.
 static std::size_t utf8ToByteOffset(std::string_view str, int64_t utf8Pos) {
@@ -409,7 +426,8 @@ class ConcatExpression : public detail::VariadicExpression {
 
   // _________________________________________________________________
   ExpressionResult evaluate(EvaluationContext* ctx) const override {
-    using StringVec = VectorWithMemoryLimit<std::string>;
+    using Literal = ad_utility::triple_component::Literal;
+    using LiteralVec = VectorWithMemoryLimit<std::optional<Literal>>;
     // We evaluate one child after the other and append the strings from child i
     // to the strings already constructed for children 0, …, i - 1. The
     // seemingly more natural row-by-row approach has two problems. First, the
@@ -419,39 +437,73 @@ class ConcatExpression : public detail::VariadicExpression {
     // have constant results (in which case, we need to evaluate the whole
     // expression only once).
 
-    // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // We store the (intermediate) result either as single literal or a vector.
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    auto valueGetter =
+        sparqlExpression::detail::LiteralValueGetterWithoutStrFunction{};
+    std::variant<std::optional<Literal>, LiteralVec> result =
+        Literal::literalWithNormalizedContent(asNormalizedStringViewUnsafe(""));
+    bool isFirstLiteral = true;
+
+    auto moveLiteralToResult =
+        [](std::optional<Literal>& literal) -> IdOrLiteralOrIri {
+      if (!literal.has_value()) {
+        return Id::makeUndefined();
+      }
+      return LiteralOrIri(std::move(literal.value()));
+    };
+
     auto visitSingleExpressionResult = CPP_template_lambda(
-        &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
-                                           std::is_rvalue_reference_v<T&&>) {
+        &ctx, &result, &isFirstLiteral, &valueGetter)(typename T)(T && s)(
+        requires SingleExpressionResult<T> && std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
-          // All previous children were constants, and the current child also is
-          // a constant.
-          std::get<std::string>(result).append(strFromConstant);
-        } else {
-          // One of the previous children was not a constant, so we already
-          // store a vector.
-          auto& resultAsVector = std::get<StringVec>(result);
-          ql::ranges::for_each(resultAsVector, [&](std::string& target) {
-            target.append(strFromConstant);
+        auto literalFromConstant = valueGetter(std::forward<T>(s), ctx);
+
+        auto concatOrSetLitFromConst =
+            [&](std::optional<Literal>& literalSoFar) {
+              concatOrSetLiteral(literalSoFar, literalFromConstant,
+                                 isFirstLiteral);
+            };
+
+        // All previous children were constants, and the current child also is
+        // a constant.
+        auto visitLiteralConcat =
+            [&concatOrSetLitFromConst](std::optional<Literal>&& literalSoFar) {
+              concatOrSetLitFromConst(literalSoFar);
+            };
+
+        // One of the previous children was not a constant, so we already
+        // store a vector.
+        auto visitLiteralVecConcat = [&concatOrSetLitFromConst](
+                                         LiteralVec&& literalVec) {
+          ql::ranges::for_each(std::move(literalVec), [&](auto& literalSoFar) {
+            concatOrSetLitFromConst(literalSoFar);
           });
-        }
+        };
+
+        std::visit(ad_utility::OverloadCallOperator{visitLiteralConcat,
+                                                    visitLiteralVecConcat},
+                   std::move(result));
       } else {
         auto gen = sparqlExpression::detail::makeGenerator(AD_FWD(s),
                                                            ctx->size(), ctx);
 
-        if (std::holds_alternative<std::string>(result)) {
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, but now we have a
           // non-constant child, so we have to expand the `result` from a single
           // string to a vector.
-          std::string constantResultSoFar =
-              std::move(std::get<std::string>(result));
-          result.emplace<StringVec>(ctx->_allocator);
-          auto& resultAsVec = std::get<StringVec>(result);
+          std::optional<Literal> constantResultSoFar =
+              std::move(std::get<std::optional<Literal>>(result));
+          result.emplace<LiteralVec>(ctx->_allocator);
+          auto& resultAsVec = std::get<LiteralVec>(result);
           resultAsVec.reserve(ctx->size());
           std::fill_n(std::back_inserter(resultAsVec), ctx->size(),
                       constantResultSoFar);
@@ -459,37 +511,45 @@ class ConcatExpression : public detail::VariadicExpression {
 
         // The `result` already is a vector, and the current child also returns
         // multiple results, so we do the `natural` way.
-        auto& resultAsVec = std::get<StringVec>(result);
+        auto& resultAsVec = std::get<LiteralVec>(result);
         // TODO<C++23> Use `ql::views::zip` or `enumerate`.
         size_t i = 0;
         for (auto& el : gen) {
-          if (auto str = StringValueGetter{}(std::move(el), ctx);
-              str.has_value()) {
-            resultAsVec[i].append(str.value());
-          }
+          auto literal = valueGetter(std::move(el), ctx);
+          concatOrSetLiteral(resultAsVec[i], literal, isFirstLiteral);
           ctx->cancellationHandle_->throwIfCancelled();
           ++i;
         }
       }
       ctx->cancellationHandle_->throwIfCancelled();
     };
-    ql::ranges::for_each(
-        childrenVec(), [&ctx, &visitSingleExpressionResult](const auto& child) {
-          std::visit(visitSingleExpressionResult, child->evaluate(ctx));
-        });
+    ql::ranges::for_each(childrenVec(), [&ctx, &visitSingleExpressionResult,
+                                         &isFirstLiteral](const auto& child) {
+      std::visit(visitSingleExpressionResult, child->evaluate(ctx));
+      isFirstLiteral = false;
+    });
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
-    } else {
-      auto& stringVec = std::get<StringVec>(result);
+
+    auto visitLiteralResult =
+        [&moveLiteralToResult](
+            std::optional<Literal>& literalSoFar) -> ExpressionResult {
+      return moveLiteralToResult(literalSoFar);
+    };
+
+    auto visitLiteralVecResult =
+        [&ctx,
+         &moveLiteralToResult](LiteralVec& literalVec) -> ExpressionResult {
       VectorWithMemoryLimit<IdOrLiteralOrIri> resultAsVec(ctx->_allocator);
-      resultAsVec.reserve(stringVec.size());
-      ql::ranges::copy(stringVec | ql::views::transform(toLiteral),
+      resultAsVec.reserve(literalVec.size());
+      ql::ranges::copy(literalVec | ql::views::transform(moveLiteralToResult),
                        std::back_inserter(resultAsVec));
       return resultAsVec;
-    }
+    };
+    return std::visit(ad_utility::OverloadCallOperator{visitLiteralResult,
+                                                       visitLiteralVecResult},
+                      result);
   }
 };
 
diff --git a/src/parser/Literal.cpp b/src/parser/Literal.cpp
index 9fd190075d..fc781aab14 100644
--- a/src/parser/Literal.cpp
+++ b/src/parser/Literal.cpp
@@ -163,4 +163,18 @@ void Literal::replaceContent(std::string_view newContent) {
   }
   beginOfSuffix_ = newContent.size() + 2;
 }
+
+// __________________________________________
+void Literal::concat(const Literal& other) {
+  if (!((hasLanguageTag() && other.hasLanguageTag() &&
+         getLanguageTag() == other.getLanguageTag()) ||
+        (hasDatatype() && other.hasDatatype() &&
+         getDatatype() == other.getDatatype()))) {
+    removeDatatypeOrLanguageTag();
+  }
+  const auto& otherContent = asStringViewUnsafe(other.getContent());
+  content_.insert(beginOfSuffix_ - 1, otherContent);
+  beginOfSuffix_ += otherContent.size();
+}
+
 }  // namespace ad_utility::triple_component
diff --git a/src/parser/Literal.h b/src/parser/Literal.h
index 29495928e6..797e0bd108 100644
--- a/src/parser/Literal.h
+++ b/src/parser/Literal.h
@@ -107,6 +107,12 @@ class Literal {
   // It truncates or extends the content based on the length of newContent
   // Used in UCASE/LCASE functions in StringExpressions.cpp.
   void replaceContent(std::string_view newContent);
+
+  // Concatenates the content of the current literal with another literal.
+  // If the language tag or datatype of the literals differ, the existing
+  // language tag or datatype is removed from the current literal. Used in the
+  // CONCAT function in StringExpressions.cpp.
+  void concat(const Literal& other);
 };
 }  // namespace ad_utility::triple_component
 
diff --git a/test/GroupByTest.cpp b/test/GroupByTest.cpp
index 431f4d005b..7bfa453b27 100644
--- a/test/GroupByTest.cpp
+++ b/test/GroupByTest.cpp
@@ -2248,8 +2248,8 @@ TEST_P(GroupByLazyFixture, groupsOfSize1AreAggregatedCorrectly) {
 
 // _____________________________________________________________________________
 TEST_P(GroupByLazyFixture, nestedAggregateFunctionsWork) {
-  // Test queries of the class `SELECT (CONCAT(SUM(?x), "---") as ?result) ...`
-  // where the aggregate function is not on top of the expression tree.
+  // Test queries of the class `SELECT (CONCAT(STR(SUM(?x)), "---") as ?result)
+  // ...` where the aggregate function is not on top of the expression tree.
   using L = TripleComponent::Literal;
   std::vector<IdTable> idTables;
   idTables.push_back(makeIntTable({{1, 0}}));
@@ -2259,7 +2259,7 @@ TEST_P(GroupByLazyFixture, nestedAggregateFunctionsWork) {
       std::vector<ColumnIndex>{1});
 
   std::vector<std::unique_ptr<SparqlExpression>> children;
-  children.push_back(makeSum("?x"));
+  children.push_back(makeStrExpression(makeSum("?x")));
   children.push_back(std::make_unique<StringLiteralExpression>(
       L::fromStringRepresentation("\"---\"")));
   Alias alias{SparqlExpressionPimpl{makeConcatExpression(std::move(children)),
diff --git a/test/SparqlExpressionTest.cpp b/test/SparqlExpressionTest.cpp
index f2ac2f111a..e0e50f2b6b 100644
--- a/test/SparqlExpressionTest.cpp
+++ b/test/SparqlExpressionTest.cpp
@@ -1370,26 +1370,35 @@ TEST(SparqlExpression, concatExpression) {
   auto checkConcat = testNaryExpressionVec<makeConcatExpressionVariadic>;
 
   const auto T = Id::makeFromBool(true);
+
+  checkConcat(idOrLitOrStringVec({U, U, U}),
+              std::tuple{Ids{U, I(0), U}, Ids{U, U, I(0)}});
+
+  //  CONCAT function explicitly takes string literals according to the sparql
+  //  standard.
   checkConcat(
-      idOrLitOrStringVec({"0null", "eins", "2zwei", "3drei", "", "5.35.2"}),
-      // UNDEF evaluates to an empty string..
-      std::tuple{Ids{I(0), U, I(2), I(3), U, D(5.3)},
-                 idOrLitOrStringVec({"null", "eins", "zwei", "drei", U, U}),
-                 Ids{U, U, U, U, U, D(5.2)}});
+      idOrLitOrStringVec({"0null0", U, U, U, U}),
+      std::tuple{idOrLitOrStringVec({"0", I(1), "zwei", T, D(5.3)}),
+                 idOrLitOrStringVec({"null", "eins", I(2), "true", ""}),
+                 idOrLitOrStringVec({"0", "EINS", "ZWEI", "TRUE", D(5.2)})});
   // Example with some constants in the middle.
   checkConcat(
-      idOrLitOrStringVec({"0trueeins", "trueeins", "2trueeins", "3trueeins",
-                          "trueeins", "12.3trueeins-2.1"}),
-      // UNDEF and the empty string are considered to be `false`.
-      std::tuple{Ids{I(0), U, I(2), I(3), U, D(12.3)}, T,
-                 IdOrLiteralOrIri{lit("eins")}, Ids{U, U, U, U, U, D(-2.1)}});
+      idOrLitOrStringVec(
+          {"0trueeins", U, "2trueeins", "3trueeins", U, "12.3trueeins-2.1"}),
+      std::tuple{idOrLitOrStringVec({"0", U, "2", "3", "", "12.3"}),
+                 IdOrLiteralOrIri{lit("true")}, IdOrLiteralOrIri{lit("eins")},
+                 idOrLitOrStringVec({"", "", "", "", I(4), "-2.1"})});
 
   // Only constants
-  checkConcat(IdOrLiteralOrIri{lit("trueMe1")},
-              std::tuple{T, IdOrLiteralOrIri{lit("Me")}, I(1)});
+  checkConcat(
+      IdOrLiteralOrIri{lit("trueMe1")},
+      std::tuple{IdOrLiteralOrIri{lit("true")}, IdOrLiteralOrIri{lit("Me")},
+                 IdOrLiteralOrIri{lit("1")}});
   // Constants at the beginning.
-  checkConcat(IdOrLiteralOrIriVec{lit("trueMe1"), lit("trueMe2")},
-              std::tuple{T, IdOrLiteralOrIri{lit("Me")}, Ids{I(1), I(2)}});
+  checkConcat(
+      IdOrLiteralOrIriVec{lit("trueMe1"), lit("trueMe2")},
+      std::tuple{IdOrLiteralOrIri{lit("true")}, IdOrLiteralOrIri{lit("Me")},
+                 idOrLitOrStringVec({"1", "2"})});
 
   checkConcat(IdOrLiteralOrIri{lit("")}, std::tuple{});
   auto coalesceExpr = makeConcatExpressionVariadic(
@@ -1399,6 +1408,72 @@ TEST(SparqlExpression, concatExpression) {
               testing::AllOf(::testing::ContainsRegex("ConcatExpression"),
                              ::testing::HasSubstr("<bim>"),
                              ::testing::HasSubstr("<bam>")));
+  // Only constants with datatypes or language tags.
+  checkConcat(
+      IdOrLiteralOrIri{
+          lit("HelloWorld", "^^<http://www.w3.org/2001/XMLSchema#string>")},
+      std::tuple{IdOrLiteralOrIri{lit(
+                     "Hello", "^^<http://www.w3.org/2001/XMLSchema#string>")},
+                 IdOrLiteralOrIri{lit(
+                     "World", "^^<http://www.w3.org/2001/XMLSchema#string>")}});
+  checkConcat(IdOrLiteralOrIri{lit("HelloWorld", "@en")},
+              std::tuple{IdOrLiteralOrIri{lit("Hello", "@en")},
+                         IdOrLiteralOrIri{lit("World", "@en")}});
+  checkConcat(
+      IdOrLiteralOrIri{lit("HelloWorld")},
+      std::tuple{IdOrLiteralOrIri{lit(
+                     "Hello", "^^<http://www.w3.org/2001/XMLSchema#string>")},
+                 IdOrLiteralOrIri{lit("World")}});
+  checkConcat(IdOrLiteralOrIri{lit("HelloWorld")},
+              std::tuple{IdOrLiteralOrIri{lit("Hello", "@de")},
+                         IdOrLiteralOrIri{lit("World")}});
+  checkConcat(IdOrLiteralOrIri{lit("HelloWorld")},
+              std::tuple{IdOrLiteralOrIri{lit("Hello", "@de")},
+                         IdOrLiteralOrIri{lit("World", "@en")}});
+  checkConcat(
+      IdOrLiteralOrIri{lit("HelloWorld")},
+      std::tuple{IdOrLiteralOrIri{lit(
+                     "Hello", "^^<http://www.w3.org/2001/XMLSchema#string>")},
+                 IdOrLiteralOrIri{lit("World", "@en")}});
+
+  // Constants at beginning with datatypes or language tags.
+  checkConcat(IdOrLiteralOrIriVec{lit("MeHello", "@en"), lit("MeWorld"),
+                                  lit("MeHallo")},
+              std::tuple{IdOrLiteralOrIri{lit("Me", "@en")},
+                         IdOrLiteralOrIriVec{lit("Hello", "@en"), lit("World"),
+                                             lit("Hallo", "@de")}});
+  checkConcat(
+      IdOrLiteralOrIriVec{
+          lit("MeHello", "^^<http://www.w3.org/2001/XMLSchema#string>"),
+          lit("MeWorld"), lit("MeHallo")},
+      std::tuple{
+          IdOrLiteralOrIri{
+              lit("Me", "^^<http://www.w3.org/2001/XMLSchema#string>")},
+          IdOrLiteralOrIriVec{
+              lit("Hello", "^^<http://www.w3.org/2001/XMLSchema#string>"),
+              lit("World"), lit("Hallo", "@de")}});
+
+  // Vector at the beginning with datatypes or language tags.
+  checkConcat(
+      IdOrLiteralOrIriVec{
+          lit("HelloWorld", "^^<http://www.w3.org/2001/XMLSchema#string>"),
+          lit("HiWorld"), lit("HalloWorld")},
+      std::tuple{
+          IdOrLiteralOrIriVec{
+              lit("Hello", "^^<http://www.w3.org/2001/XMLSchema#string>"),
+              lit("Hi"), lit("Hallo", "@de")},
+          IdOrLiteralOrIri{
+              lit("World", "^^<http://www.w3.org/2001/XMLSchema#string>")}});
+  checkConcat(IdOrLiteralOrIriVec{lit("HelloWorld", "@en"), lit("HiWorld"),
+                                  lit("HalloWorld")},
+              std::tuple{IdOrLiteralOrIriVec{lit("Hello", "@en"), lit("Hi"),
+                                             lit("Hallo", "@de")},
+                         IdOrLiteralOrIri{lit("World", "@en")}});
+  checkConcat(
+      IdOrLiteralOrIriVec{lit("HelloWorld!"), lit("HalloWorld!")},
+      std::tuple{IdOrLiteralOrIriVec{lit("Hello", "@en"), lit("Hallo", "@de")},
+                 IdOrLiteralOrIri{lit("World", "@en")},
+                 IdOrLiteralOrIri{lit("!", "@fr")}});
 }
 
 // ______________________________________________________________________________
diff --git a/test/parser/LiteralOrIriTest.cpp b/test/parser/LiteralOrIriTest.cpp
index 3dce9c08c9..a23ea664c2 100644
--- a/test/parser/LiteralOrIriTest.cpp
+++ b/test/parser/LiteralOrIriTest.cpp
@@ -362,3 +362,48 @@ TEST(LiteralTest, replaceContent) {
   literal.getLiteral().replaceContent("Hello World!");
   EXPECT_THAT("Hello World!", asStringViewUnsafe(literal.getContent()));
 }
+// _______________________________________________________________________
+TEST(LiteralTest, concat) {
+  LiteralOrIri literal = LiteralOrIri::literalWithoutQuotes("Hello ");
+  LiteralOrIri literalOther = LiteralOrIri::literalWithoutQuotes("World!");
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_FALSE(literal.hasLanguageTag());
+  EXPECT_FALSE(literal.hasDatatype());
+  literal = LiteralOrIri::literalWithoutQuotes(
+      "Hello ", Iri::fromIriref("<http://www.w3.org/2001/XMLSchema#string>"));
+  literalOther = LiteralOrIri::literalWithoutQuotes(
+      "World!", Iri::fromIriref("<http://www.w3.org/2001/XMLSchema#string>"));
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_FALSE(literal.hasLanguageTag());
+  EXPECT_TRUE(literal.hasDatatype());
+  EXPECT_THAT("http://www.w3.org/2001/XMLSchema#string",
+              asStringViewUnsafe(literal.getDatatype()));
+  literal = LiteralOrIri::literalWithoutQuotes("Hello World!", "@en");
+  literalOther = LiteralOrIri::literalWithoutQuotes("Bye!", "@en");
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!Bye!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_TRUE(literal.hasLanguageTag());
+  EXPECT_FALSE(literal.hasDatatype());
+  EXPECT_THAT("en", asStringViewUnsafe(literal.getLanguageTag()));
+  literal = LiteralOrIri::literalWithoutQuotes(
+      "Hello ", Iri::fromIriref("<http://www.w3.org/2001/XMLSchema#string>"));
+  literalOther = LiteralOrIri::literalWithoutQuotes("World!");
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_FALSE(literal.hasLanguageTag());
+  EXPECT_FALSE(literal.hasDatatype());
+  literal = LiteralOrIri::literalWithoutQuotes("Hello World!", "@en");
+  literalOther = LiteralOrIri::literalWithoutQuotes("Bye!");
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!Bye!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_FALSE(literal.hasLanguageTag());
+  EXPECT_FALSE(literal.hasDatatype());
+  literal = LiteralOrIri::literalWithoutQuotes("Hello World!", "@en");
+  literalOther = LiteralOrIri::literalWithoutQuotes("Thüss!", "@de");
+  literal.getLiteral().concat(literalOther.getLiteral());
+  EXPECT_THAT("Hello World!Thüss!", asStringViewUnsafe(literal.getContent()));
+  EXPECT_FALSE(literal.hasLanguageTag());
+  EXPECT_FALSE(literal.hasDatatype());
+}


Reviews:
user: login: joka921
body: An additional round of reviews.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-02T10:45:59Z
user: login: joka921
body: I have written something about the correct behavior, please implement it and write a unit test for it.
Also merge in the current master, you have conflicts.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-04T10:31:43Z
user: login: joka921
body: I have a suggestion for a much simpler interface.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-07T06:02:03Z
user: login: joka921
body: This is getting much cleaner, Just some code duplication and a semantic bug.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-09T11:08:58Z
user: login: DuDaAG
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T06:09:24Z
user: login: DuDaAG
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T06:15:52Z
user: login: joka921
body: Two answers to two questions.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T08:03:52Z
user: login: DuDaAG
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:38:34Z
user: login: joka921
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T08:42:16Z
user: login: joka921
body: What I wrote here 
+ Adapt the E2e tests in `e2e/scientists_queries.yaml`.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-14T08:05:54Z
user: login: DuDaAG
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T10:09:55Z
user: login: joka921
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T10:29:40Z

Review comments:
diff_hunk: @@ -454,7 +454,9 @@ class ConcatExpression : public detail::VariadicExpression {
 
   // _________________________________________________________________
   ExpressionResult evaluate(EvaluationContext* ctx) const override {
-    using StringVec = VectorWithMemoryLimit<std::string>;
+    using LiteralVec =
+        VectorWithMemoryLimit<ad_utility::triple_component::Literal>;
+    using Literal = ad_utility::triple_component::Literal;
     // We evaluate one child after the other and append the strings from child i
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: I haven't looked at all the details yet, but have a question in principle:

Wouldn't it be appropraite to leave it as a `vector<string> ` where we just concatenate the contents, and then have another vector, where we store the `datatypeOrLanguageTag` that the results will have.
In particular find out, what is the correct behavior when you concat different language tags, or `@en` with `xsd:string` etc, to find out what the correct behavior has to be.
Maybe start with writing unit tests for such cases that mimic the conformance tests.
created_at: 2025-04-02T10:45:48Z
author_association: MEMBER
diff_hunk: @@ -467,12 +470,54 @@ class ConcatExpression : public detail::VariadicExpression {
     // We store the (intermediate) result either as single string or a vector.
     // If the result is a string, then all the previously evaluated children
     // were constants (see above).
+
     std::variant<std::string, StringVec> result{std::string{""}};
+    StringVec dataTypesOrLangTags(ctx->_allocator);
+
+    auto storeDatatypeOrLanguageTag = [&](const auto& literal) {
+      if (literal.hasDatatype()) {
+        dataTypesOrLangTags.push_back(
+            std::string{asStringViewUnsafe(literal.getDatatype())});
+      } else if (literal.hasLanguageTag()) {
+        dataTypesOrLangTags.push_back(
+            std::string{asStringViewUnsafe(literal.getLanguageTag())});
+      } else {
+        dataTypesOrLangTags.push_back("");
+      }
+    };
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: This doesn't quite work.
You currently store the datatypes and language tags in a flat vector for all columns AND rows.
You have to do this per row, because for example consider

```
?x -> "en"@en und "fr"@fr
then 
CONCAT(?x, ?x)
should yield
"enen"@en und "frfr"@fr, as the language tags are equal in eacht row.
```
You could for each row store a separate vector of language tag or datatype, but i would only store
one entry which either stores the matching suffix so far, or something empty (because no suffix was found yet), or the information that there already was a mismatch and the suffix will be empty.
created_at: 2025-04-04T10:31:10Z
author_association: MEMBER
diff_hunk: @@ -388,7 +391,8 @@ class ConcatExpression : public detail::VariadicExpression {
 
   // _________________________________________________________________
   ExpressionResult evaluate(EvaluationContext* ctx) const override {
-    using StringVec = VectorWithMemoryLimit<std::string>;
+    using StringVec = VectorWithMemoryLimit<
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: Add documentation what the elements mean.
created_at: 2025-04-07T05:51:58Z
author_association: MEMBER
diff_hunk: @@ -401,22 +405,74 @@ class ConcatExpression : public detail::VariadicExpression {
     // We store the (intermediate) result either as single string or a vector.
     // If the result is a string, then all the previously evaluated children
     // were constants (see above).
+
     std::variant<std::string, StringVec> result{std::string{""}};
+    std::optional<std::string> datatypeOrLangTag;
+
+    // Extracts the datatype or language tag from a literal.
+    // Language tags are returned as "@lang",datatypes as IRIs without angle
+    // brackets.
+    auto getDatatypeOrLanguageTag = [&](const auto& literal) {
+      if (literal.hasDatatype()) {
+        return std::string{asStringViewUnsafe(literal.getDatatype())};
+      } else if (literal.hasLanguageTag()) {
+        return "@" + std::string{asStringViewUnsafe(literal.getLanguageTag())};
+      }
+      return std::string{};
+    };
+
+    auto getCommonDescriptor = [&](const std::optional<std::string>& str1,
+                                   const std::string& str2) {
+      if (!str1.has_value() ||
+          (!str1.value().empty() && str1.value() == str2)) {
+        return str2;
+      }
+      return std::string{};
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: Make sure to have unit tests with more than two inputs, s.t. e.g. en, fr, en  correctly yields the empty tag and so on, as the logic of this function is a bit compilcated.
Also this lambda (same as the above lambda) don't need any captures, so you can make them free functions in the anonymous namespace with proper documentation.
created_at: 2025-04-07T05:58:01Z
author_association: MEMBER
diff_hunk: @@ -401,22 +405,74 @@ class ConcatExpression : public detail::VariadicExpression {
     // We store the (intermediate) result either as single string or a vector.
     // If the result is a string, then all the previously evaluated children
     // were constants (see above).
+
     std::variant<std::string, StringVec> result{std::string{""}};
+    std::optional<std::string> datatypeOrLangTag;
+
+    // Extracts the datatype or language tag from a literal.
+    // Language tags are returned as "@lang",datatypes as IRIs without angle
+    // brackets.
+    auto getDatatypeOrLanguageTag = [&](const auto& literal) {
+      if (literal.hasDatatype()) {
+        return std::string{asStringViewUnsafe(literal.getDatatype())};
+      } else if (literal.hasLanguageTag()) {
+        return "@" + std::string{asStringViewUnsafe(literal.getLanguageTag())};
+      }
+      return std::string{};
+    };
+
+    auto getCommonDescriptor = [&](const std::optional<std::string>& str1,
+                                   const std::string& str2) {
+      if (!str1.has_value() ||
+          (!str1.value().empty() && str1.value() == str2)) {
+        return str2;
+      }
+      return std::string{};
+    };
+
+    // Builds a literal from the given string content and descriptor.
+    // If descriptor starts with "@", it's a language tag. Otherwise it's either
+    // the datatype std:string or empty.
+    auto toLiteralFromDescriptor =
+        [](const std::string& content,
+           const std::optional<std::string>& descriptor) {
+          if (descriptor.has_value() && descriptor.value().starts_with("@")) {
+            return toLiteral(content, descriptor);
+          } else if (descriptor.has_value() && !descriptor.value().empty()) {
+            return toLiteral(
+                content, Iri::fromIrirefWithoutBrackets(descriptor.value()));
+          }
+          return toLiteral(content);
+        };
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: I think the most efficient way, and also the 
easiest for code quality is to simply add a `concat` function to the `Literal` class, that implements this, so you can simply write `literalSoFar.concat(nextLiteral)`.
This function can then do all the adjustments of the datatypes and language tags, but never has to do unnecessary allocations (the one for concating strings is somewhat unavoidable).
created_at: 2025-04-07T06:01:12Z
author_association: MEMBER
diff_hunk: @@ -401,22 +405,74 @@ class ConcatExpression : public detail::VariadicExpression {
     // We store the (intermediate) result either as single string or a vector.
     // If the result is a string, then all the previously evaluated children
     // were constants (see above).
+
     std::variant<std::string, StringVec> result{std::string{""}};
+    std::optional<std::string> datatypeOrLangTag;
+
+    // Extracts the datatype or language tag from a literal.
+    // Language tags are returned as "@lang",datatypes as IRIs without angle
+    // brackets.
+    auto getDatatypeOrLanguageTag = [&](const auto& literal) {
+      if (literal.hasDatatype()) {
+        return std::string{asStringViewUnsafe(literal.getDatatype())};
+      } else if (literal.hasLanguageTag()) {
+        return "@" + std::string{asStringViewUnsafe(literal.getLanguageTag())};
+      }
+      return std::string{};
+    };
+
+    auto getCommonDescriptor = [&](const std::optional<std::string>& str1,
+                                   const std::string& str2) {
+      if (!str1.has_value() ||
+          (!str1.value().empty() && str1.value() == str2)) {
+        return str2;
+      }
+      return std::string{};
+    };
+
+    // Builds a literal from the given string content and descriptor.
+    // If descriptor starts with "@", it's a language tag. Otherwise it's either
+    // the datatype std:string or empty.
+    auto toLiteralFromDescriptor =
+        [](const std::string& content,
+           const std::optional<std::string>& descriptor) {
+          if (descriptor.has_value() && descriptor.value().starts_with("@")) {
+            return toLiteral(content, descriptor);
+          } else if (descriptor.has_value() && !descriptor.value().empty()) {
+            return toLiteral(
+                content, Iri::fromIrirefWithoutBrackets(descriptor.value()));
+          }
+          return toLiteral(content);
+        };
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: With this change, the code in this evaluate function is again really simple and similar to the original one.
created_at: 2025-04-07T06:01:41Z
author_association: MEMBER
diff_hunk: @@ -71,6 +71,7 @@ class Literal {
 
   // Return the datatype of the literal, if available, without leading ^^
   // prefix. Throws an exception if the literal has no datatype.
+
path: src/parser/Literal.h
user: login: joka921
body: ```suggestion
```
Seems unrelated.
created_at: 2025-04-09T09:07:35Z
author_association: MEMBER
diff_hunk: @@ -163,4 +163,18 @@ void Literal::replaceContent(std::string_view newContent) {
   }
   beginOfSuffix_ = newContent.size() + 2;
 }
+
+// __________________________________________
+void Literal::concat(const Literal& other) {
+  std::string newContent = std::string{asStringViewUnsafe(getContent())} +
+                           std::string{asStringViewUnsafe(other.getContent())};
+  replaceContent(newContent);
path: src/parser/Literal.cpp
user: login: joka921
body: This has a completely unnecessary string allocation.
Just insert into the content at the correct position (at the end of the content), and adjust the beginning of suffix.
created_at: 2025-04-09T09:09:12Z
author_association: MEMBER
diff_hunk: @@ -163,4 +163,18 @@ void Literal::replaceContent(std::string_view newContent) {
   }
   beginOfSuffix_ = newContent.size() + 2;
 }
+
+// __________________________________________
+void Literal::concat(const Literal& other) {
+  std::string newContent = std::string{asStringViewUnsafe(getContent())} +
+                           std::string{asStringViewUnsafe(other.getContent())};
+  replaceContent(newContent);
+  if (!((hasLanguageTag() && other.hasLanguageTag() &&
+         getLanguageTag() == other.getLanguageTag()) ||
+        (hasDatatype() && other.hasDatatype() &&
+         getDatatype() == other.getDatatype()))) {
+    removeDatatypeOrLanguageTag();
+  }
path: src/parser/Literal.cpp
user: login: joka921
body: Do this first before inserting, then you might save an allocation in this case (the language is deleted, but its memory can be reused for the insertion. not so relevant as the one before, but the efficiency mindset is important.
created_at: 2025-04-09T09:10:12Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: I don't think that always using the `WithStrFunction` is correct. 
And also, if the result of the value getter is `nullopt`, then the whole concatenation will be `Id::makeUndefined`. You therefore need different encodings for "not set yet", 
maybe initialize the result with an empty literal, or do something special for the first row. 
Then after this `nullopt` means "there was an error, don't do anything else for this row, we will finally convert this to UNDEF".
(Maybe start with unit tests for this configuration s.t. you have understood the semantics and then can fix it).
created_at: 2025-04-09T11:01:12Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, and the current child also is
           // a constant.
-          std::get<std::string>(result).append(strFromConstant);
+          concatOrSetLiteral(std::get<std::optional<Literal>>(result),
+                             literalFromConstant);
         } else {
           // One of the previous children was not a constant, so we already
           // store a vector.
-          auto& resultAsVector = std::get<StringVec>(result);
-          ql::ranges::for_each(resultAsVector, [&](std::string& target) {
-            target.append(strFromConstant);
+          auto& resultAsVector = std::get<LiteralVec>(result);
+          ql::ranges::for_each(resultAsVector, [&](auto& literalSoFar) {
+            concatOrSetLiteral(literalSoFar, literalFromConstant);
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: Use a `std::visit` with `ad_utility::OverloadCallOperator`. 
You can even first implement a lambda that does `concatOrSet(arg, literalFromConstant)`, which you then can use in both the vector and the single argument case .
created_at: 2025-04-09T11:03:37Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, and the current child also is
           // a constant.
-          std::get<std::string>(result).append(strFromConstant);
+          concatOrSetLiteral(std::get<std::optional<Literal>>(result),
+                             literalFromConstant);
         } else {
           // One of the previous children was not a constant, so we already
           // store a vector.
-          auto& resultAsVector = std::get<StringVec>(result);
-          ql::ranges::for_each(resultAsVector, [&](std::string& target) {
-            target.append(strFromConstant);
+          auto& resultAsVector = std::get<LiteralVec>(result);
+          ql::ranges::for_each(resultAsVector, [&](auto& literalSoFar) {
+            concatOrSetLiteral(literalSoFar, literalFromConstant);
           });
         }
       } else {
         auto gen = sparqlExpression::detail::makeGenerator(AD_FWD(s),
                                                            ctx->size(), ctx);
 
-        if (std::holds_alternative<std::string>(result)) {
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, but now we have a
           // non-constant child, so we have to expand the `result` from a single
           // string to a vector.
-          std::string constantResultSoFar =
-              std::move(std::get<std::string>(result));
-          result.emplace<StringVec>(ctx->_allocator);
-          auto& resultAsVec = std::get<StringVec>(result);
+          std::optional<Literal> constantResultSoFar =
+              std::move(std::get<std::optional<Literal>>(result));
+          result.emplace<LiteralVec>(ctx->_allocator);
+          auto& resultAsVec = std::get<LiteralVec>(result);
           resultAsVec.reserve(ctx->size());
+
           std::fill_n(std::back_inserter(resultAsVec), ctx->size(),
                       constantResultSoFar);
         }
 
         // The `result` already is a vector, and the current child also returns
         // multiple results, so we do the `natural` way.
-        auto& resultAsVec = std::get<StringVec>(result);
+        auto& resultAsVec = std::get<LiteralVec>(result);
         // TODO<C++23> Use `ql::views::zip` or `enumerate`.
         size_t i = 0;
         for (auto& el : gen) {
-          if (auto str = StringValueGetter{}(std::move(el), ctx);
-              str.has_value()) {
-            resultAsVec[i].append(str.value());
+          if (auto literal =
+                  sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: The Type of the value getter is duplicated, 
initially bind it to a variable  `auto valueGetter = LiteralValueGetter...{}` , and then consistently use it in all cases. You can even write a `getValue` lambda, s.t. you don't have to redundantly repeat the second `ctx` argument.
created_at: 2025-04-09T11:05:45Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, and the current child also is
           // a constant.
-          std::get<std::string>(result).append(strFromConstant);
+          concatOrSetLiteral(std::get<std::optional<Literal>>(result),
+                             literalFromConstant);
         } else {
           // One of the previous children was not a constant, so we already
           // store a vector.
-          auto& resultAsVector = std::get<StringVec>(result);
-          ql::ranges::for_each(resultAsVector, [&](std::string& target) {
-            target.append(strFromConstant);
+          auto& resultAsVector = std::get<LiteralVec>(result);
+          ql::ranges::for_each(resultAsVector, [&](auto& literalSoFar) {
+            concatOrSetLiteral(literalSoFar, literalFromConstant);
           });
         }
       } else {
         auto gen = sparqlExpression::detail::makeGenerator(AD_FWD(s),
                                                            ctx->size(), ctx);
 
-        if (std::holds_alternative<std::string>(result)) {
+        if (std::holds_alternative<std::optional<Literal>>(result)) {
           // All previous children were constants, but now we have a
           // non-constant child, so we have to expand the `result` from a single
           // string to a vector.
-          std::string constantResultSoFar =
-              std::move(std::get<std::string>(result));
-          result.emplace<StringVec>(ctx->_allocator);
-          auto& resultAsVec = std::get<StringVec>(result);
+          std::optional<Literal> constantResultSoFar =
+              std::move(std::get<std::optional<Literal>>(result));
+          result.emplace<LiteralVec>(ctx->_allocator);
+          auto& resultAsVec = std::get<LiteralVec>(result);
           resultAsVec.reserve(ctx->size());
+
           std::fill_n(std::back_inserter(resultAsVec), ctx->size(),
                       constantResultSoFar);
         }
 
         // The `result` already is a vector, and the current child also returns
         // multiple results, so we do the `natural` way.
-        auto& resultAsVec = std::get<StringVec>(result);
+        auto& resultAsVec = std::get<LiteralVec>(result);
         // TODO<C++23> Use `ql::views::zip` or `enumerate`.
         size_t i = 0;
         for (auto& el : gen) {
-          if (auto str = StringValueGetter{}(std::move(el), ctx);
-              str.has_value()) {
-            resultAsVec[i].append(str.value());
+          if (auto literal =
+                  sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(
+                      std::move(el), ctx);
+              literal.has_value()) {
+            concatOrSetLiteral(resultAsVec[i], literal.value());
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: Also here: If there's no value, then you have to set the result to an error (nullopt which then becomes Undefined at the end) (add unit tests)
created_at: 2025-04-09T11:06:40Z
author_association: MEMBER
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: 1. use std::visit
created_at: 2025-04-09T11:07:22Z
author_association: MEMBER
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
+      return IdOrLiteralOrIri(
+          LiteralOrIri{std::get<std::optional<Literal>>(result).value_or(
+              Literal::literalWithNormalizedContent(
+                  asNormalizedStringViewUnsafe("")))});
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: This pattern (which is wrong) is repeated multiple times, reduce code duplication wherever possible and fix the semantics.
created_at: 2025-04-09T11:07:56Z
author_association: MEMBER
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
+      return IdOrLiteralOrIri(
+          LiteralOrIri{std::get<std::optional<Literal>>(result).value_or(
+              Literal::literalWithNormalizedContent(
+                  asNormalizedStringViewUnsafe("")))});
     } else {
-      auto& stringVec = std::get<StringVec>(result);
+      auto& literalVec = std::get<LiteralVec>(result);
       VectorWithMemoryLimit<IdOrLiteralOrIri> resultAsVec(ctx->_allocator);
-      resultAsVec.reserve(stringVec.size());
-      ql::ranges::copy(stringVec | ql::views::transform(toLiteral),
-                       std::back_inserter(resultAsVec));
+      resultAsVec.reserve(literalVec.size());
+      ql::ranges::copy(
+          literalVec |
+              ql::views::transform([](const std::optional<Literal>& literal) {
+                return IdOrLiteralOrIri(LiteralOrIri(
+                    literal.value_or(Literal::literalWithNormalizedContent(
+                        asNormalizedStringViewUnsafe("")))));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: This is exactly the same logic as for the constant case, just with `ranges::copy` around it, factor out the common functionality.
created_at: 2025-04-09T11:08:31Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: DuDaAG
body: I think I do need the LiteralValueGetterWithStrFunction so it also handles IDs like doubles, ... Do you mean I should differentiate between the two LiteralValueGetters depending on the type to make it more efficient, or what exactly do you mean?
created_at: 2025-04-10T06:09:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: DuDaAG
body: I tried to implement this using std::visit, which distinguishes between optional<Literal> and LiteralVec. The problem is that in one case, an IdOrLiteralOrIri is returned, and in the other, a VectorWithMemoryLimit<IdOrLiteralOrIri>. std::visit expects the same return types. What's the best way to solve this?
created_at: 2025-04-10T06:15:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: That is a correct observation. 
The fix is to explicitly set the return type of the lambda to the common type `ExpressionResult` which is a variant. so something like
```
auto handleSingleLiteral = [&](Literal& literal) -> ExpressionResult { 
  return IdOrLiteralOrIri{LiteralOrIri{....}}; // the actual code as it is written.
}
```
same for the vector case. 
then you can do simply `return std::visit(OverloadCallOperator{lambda1, lambda2}, std::move(seVariant))`.
created_at: 2025-04-10T07:58:39Z
author_association: MEMBER
diff_hunk: @@ -399,53 +415,70 @@ class ConcatExpression : public detail::VariadicExpression {
     // expression only once).
 
     // We store the (intermediate) result either as single string or a vector.
-    // If the result is a string, then all the previously evaluated children
+    // If the result is a Literal, then all the previously evaluated children
     // were constants (see above).
-    std::variant<std::string, StringVec> result{std::string{""}};
+
+    // `LiteralVec` stores literals whose string contents are the result of
+    // concatenation. Each literal also carries a suffix (language tag or
+    // datatype) that is determined by the previously processed children. For
+    // each row, the suffix reflects either the current matching suffix or a
+    // mismatch (in which case the final suffix will be empty).
+
+    std::variant<std::optional<Literal>, LiteralVec> result;
+
     auto visitSingleExpressionResult = CPP_template_lambda(
         &ctx, &result)(typename T)(T && s)(requires SingleExpressionResult<T> &&
                                            std::is_rvalue_reference_v<T&&>) {
       if constexpr (isConstantResult<T>) {
-        std::string strFromConstant = StringValueGetter{}(s, ctx).value_or("");
-        if (std::holds_alternative<std::string>(result)) {
+        auto literalFromConstant =
+            sparqlExpression::detail::LiteralValueGetterWithStrFunction{}(s,
+                                                                          ctx)
+                .value_or(Literal::literalWithNormalizedContent(
+                    asNormalizedStringViewUnsafe("")));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: The `CONCAT` function explicitly takes string literals according to the sparql standard. 
so 
`CONCAT ("hallo", 3)` gives an expression error, so Undefined (because `3` is not a string).
You have to explicitly write `CONCAT("hallo", STR(3))` to achieve this behavior.
If we want, we can write an interface that uses the other value getter for an argument *IF* this argument is wrapped in the `STR()` function, but that is nothing for this PR, because it is relatively orthogonal.
created_at: 2025-04-10T08:03:34Z
author_association: MEMBER
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: DuDaAG
body: Yes, I've already gotten that far.But the entire Concat expression must not return a `variant<VectorWithMemoryLimit<std::variant<ValueId, LocalVocabEntry>>, std::variant<ValueId, LocalVocabEntry>>.` So I need to return the specific element of the variant.
created_at: 2025-04-10T08:38:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -459,14 +492,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::string>(result)) {
-      return IdOrLiteralOrIri{toLiteral(std::get<std::string>(result))};
+    if (std::holds_alternative<std::optional<Literal>>(result)) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: I don't understand.
I explicitly wrote `ExpressionResult` in my example, which is
1. The return type that you need and want. and
2. A variant, that can store those two types (but also some other types).
created_at: 2025-04-10T08:42:16Z
author_association: MEMBER
diff_hunk: @@ -166,15 +166,14 @@ void Literal::replaceContent(std::string_view newContent) {
 
 // __________________________________________
 void Literal::concat(const Literal& other) {
-  std::string newContent = std::string{asStringViewUnsafe(getContent())} +
-                           std::string{asStringViewUnsafe(other.getContent())};
-  replaceContent(newContent);
   if (!((hasLanguageTag() && other.hasLanguageTag() &&
          getLanguageTag() == other.getLanguageTag()) ||
         (hasDatatype() && other.hasDatatype() &&
          getDatatype() == other.getDatatype()))) {
     removeDatatypeOrLanguageTag();
   }
+  content_.insert(beginOfSuffix_ - 1, asStringViewUnsafe(other.getContent()));
+  beginOfSuffix_ += other.beginOfSuffix_ - 2;
path: src/parser/Literal.cpp
user: login: joka921
body: ```suggestion
 const auto& otherContent = asStringViewUnsafe(other.getContent());
  content_.insert(beginOfSuffix_ - 1, otherContent);
  beginOfSuffix_ += otherContent.size()
```
Much easier to understand here.
created_at: 2025-04-14T07:42:10Z
author_association: MEMBER
diff_hunk: @@ -39,15 +39,21 @@ static std::size_t utf8Length(std::string_view s) {
   return ql::ranges::count_if(s, &isUtf8CodepointStart);
 }
 
-// Initialize or concat a literal. If `literalSoFarOpt` is set, concat
-// `nextLiteral` to it; otherwise, initialize it with `nextLiteral`
+// Initialize or append a Literal. If both literals are valid and initialized,
+// concatenate nextLiteral into literalSoFar. If not initialized yet, set
+// literalSoFar to nextLiteral. If either is UNDEF, set literalSoFar to nullopt
+// to indicate an undefined result.
 void concatOrSetLiteral(
     std::optional<ad_utility::triple_component::Literal>& literalSoFarOpt,
-    const ad_utility::triple_component::Literal& nextLiteral) {
-  if (literalSoFarOpt.has_value()) {
-    literalSoFarOpt.value().concat(nextLiteral);
+    const std::optional<ad_utility::triple_component::Literal>& nextLiteral,
+    char& isInitializedFlag) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: first argument can be called `literalSoFar`. 
and the `char&` can be `literalSoFarIsInitialized`.
created_at: 2025-04-14T07:45:32Z
author_association: MEMBER
diff_hunk: @@ -39,15 +39,21 @@ static std::size_t utf8Length(std::string_view s) {
   return ql::ranges::count_if(s, &isUtf8CodepointStart);
 }
 
-// Initialize or concat a literal. If `literalSoFarOpt` is set, concat
-// `nextLiteral` to it; otherwise, initialize it with `nextLiteral`
+// Initialize or append a Literal. If both literals are valid and initialized,
+// concatenate nextLiteral into literalSoFar. If not initialized yet, set
+// literalSoFar to nextLiteral. If either is UNDEF, set literalSoFar to nullopt
+// to indicate an undefined result.
 void concatOrSetLiteral(
     std::optional<ad_utility::triple_component::Literal>& literalSoFarOpt,
-    const ad_utility::triple_component::Literal& nextLiteral) {
-  if (literalSoFarOpt.has_value()) {
-    literalSoFarOpt.value().concat(nextLiteral);
+    const std::optional<ad_utility::triple_component::Literal>& nextLiteral,
+    char& isInitializedFlag) {
+  if (!nextLiteral.has_value() || !literalSoFarOpt.has_value()) {
+    literalSoFarOpt = std::nullopt;  // UNDEF
+  } else if (literalSoFarOpt.has_value() && isInitializedFlag) {
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: ```suggestion
  } else if (static_cast<bool>(isInitializedFlag)) {
```
redundant condition + I am sure sonarcloud and I both don't like the implicit char as bool thing:)
created_at: 2025-04-14T07:46:15Z
author_association: MEMBER
diff_hunk: @@ -39,15 +39,21 @@ static std::size_t utf8Length(std::string_view s) {
   return ql::ranges::count_if(s, &isUtf8CodepointStart);
 }
 
-// Initialize or concat a literal. If `literalSoFarOpt` is set, concat
-// `nextLiteral` to it; otherwise, initialize it with `nextLiteral`
+// Initialize or append a Literal. If both literals are valid and initialized,
+// concatenate nextLiteral into literalSoFar. If not initialized yet, set
+// literalSoFar to nextLiteral. If either is UNDEF, set literalSoFar to nullopt
+// to indicate an undefined result.
 void concatOrSetLiteral(
     std::optional<ad_utility::triple_component::Literal>& literalSoFarOpt,
-    const ad_utility::triple_component::Literal& nextLiteral) {
-  if (literalSoFarOpt.has_value()) {
-    literalSoFarOpt.value().concat(nextLiteral);
+    const std::optional<ad_utility::triple_component::Literal>& nextLiteral,
+    char& isInitializedFlag) {
+  if (!nextLiteral.has_value() || !literalSoFarOpt.has_value()) {
+    literalSoFarOpt = std::nullopt;  // UNDEF
+  } else if (literalSoFarOpt.has_value() && isInitializedFlag) {
+    literalSoFarOpt.value().concat(nextLiteral.value());
   } else {
-    literalSoFarOpt = nextLiteral;
+    literalSoFarOpt = nextLiteral.value();
+    isInitializedFlag = true;
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: ```suggestion
    isInitializedFlag = static_cast<char>(true);
```
created_at: 2025-04-14T07:46:59Z
author_association: MEMBER
diff_hunk: @@ -426,30 +432,51 @@ class ConcatExpression : public detail::VariadicExpression {
     // each row, the suffix reflects either the current matching suffix or a
     // mismatch (in which case the final suffix will be empty).
 
-    std::variant<std::optional<Literal>, LiteralVec> result;
+    auto valueGetter =
+        sparqlExpression::detail::LiteralValueGetterWithoutStrFunction{};
+    std::variant<std::optional<Literal>, LiteralVec> result =
+        Literal::literalWithNormalizedContent(asNormalizedStringViewUnsafe(""));
+    char isInitializedFlag = 0;
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: Okay, if you only need a single flag for everything, then it can also be 
1. a bool instead of a char, a
2. set by the ouytside code (after each child (so in particular after the first)) set it to `true`, then the concat function only has to read it.
3. Can be called `isFirstArgument`. or `isFirstLiteral`.
created_at: 2025-04-14T07:49:16Z
author_association: MEMBER
diff_hunk: @@ -494,25 +515,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::optional<Literal>>(result)) {
-      return IdOrLiteralOrIri(
-          LiteralOrIri{std::get<std::optional<Literal>>(result).value_or(
-              Literal::literalWithNormalizedContent(
-                  asNormalizedStringViewUnsafe("")))});
-    } else {
-      auto& literalVec = std::get<LiteralVec>(result);
+
+    auto visitLiteralResult =
+        [&](const std::optional<Literal>& literalSoFar) -> ExpressionResult {
+      return convertLiteral(literalSoFar);
+    };
+
+    auto visitLiteralVecResult =
+        [&](const LiteralVec& literalVec) -> ExpressionResult {
       VectorWithMemoryLimit<IdOrLiteralOrIri> resultAsVec(ctx->_allocator);
       resultAsVec.reserve(literalVec.size());
-      ql::ranges::copy(
-          literalVec |
-              ql::views::transform([](const std::optional<Literal>& literal) {
-                return IdOrLiteralOrIri(LiteralOrIri(
-                    literal.value_or(Literal::literalWithNormalizedContent(
-                        asNormalizedStringViewUnsafe("")))));
-              }),
-          std::back_inserter(resultAsVec));
+      ql::ranges::copy(literalVec | ql::views::transform(convertLiteral),
+                       std::back_inserter(resultAsVec));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: As you are finished here, you can move out
the resulting literals (especially important for the vector case).
So call your function `moveLiteralToResult` (instead of convertLiteral),
let it take a mutable `optional<Literal>&` (not const)` which moves the literal.
Then of course your two lambdas here for the overload also have to take mutable references.

created_at: 2025-04-14T08:04:28Z
author_association: MEMBER
diff_hunk: @@ -494,25 +515,23 @@ class ConcatExpression : public detail::VariadicExpression {
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
-    if (std::holds_alternative<std::optional<Literal>>(result)) {
-      return IdOrLiteralOrIri(
-          LiteralOrIri{std::get<std::optional<Literal>>(result).value_or(
-              Literal::literalWithNormalizedContent(
-                  asNormalizedStringViewUnsafe("")))});
-    } else {
-      auto& literalVec = std::get<LiteralVec>(result);
+
+    auto visitLiteralResult =
+        [&](const std::optional<Literal>& literalSoFar) -> ExpressionResult {
+      return convertLiteral(literalSoFar);
+    };
+
+    auto visitLiteralVecResult =
+        [&](const LiteralVec& literalVec) -> ExpressionResult {
       VectorWithMemoryLimit<IdOrLiteralOrIri> resultAsVec(ctx->_allocator);
       resultAsVec.reserve(literalVec.size());
-      ql::ranges::copy(
-          literalVec |
-              ql::views::transform([](const std::optional<Literal>& literal) {
-                return IdOrLiteralOrIri(LiteralOrIri(
-                    literal.value_or(Literal::literalWithNormalizedContent(
-                        asNormalizedStringViewUnsafe("")))));
-              }),
-          std::back_inserter(resultAsVec));
+      ql::ranges::copy(literalVec | ql::views::transform(convertLiteral),
+                       std::back_inserter(resultAsVec));
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: DuDaAG
body: Why doesn't then `return std::visit(ad_utility::OverloadCallOperator{visitLiteralResult, visitLiteralVecResult}, result);` work anymore with `move(result)`?
created_at: 2025-04-14T10:09:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -501,37 +500,38 @@ class ConcatExpression : public detail::VariadicExpression {
         size_t i = 0;
         for (auto& el : gen) {
           auto literal = valueGetter(std::move(el), ctx);
-          concatOrSetLiteral(resultAsVec[i], literal, isInitializedFlag);
+          concatOrSetLiteral(resultAsVec[i], literal, isFirstLiteral);
           ctx->cancellationHandle_->throwIfCancelled();
           ++i;
         }
       }
       ctx->cancellationHandle_->throwIfCancelled();
     };
-    ql::ranges::for_each(
-        childrenVec(), [&ctx, &visitSingleExpressionResult](const auto& child) {
-          std::visit(visitSingleExpressionResult, child->evaluate(ctx));
-        });
+    ql::ranges::for_each(childrenVec(), [&ctx, &visitSingleExpressionResult,
+                                         &isFirstLiteral](const auto& child) {
+      std::visit(visitSingleExpressionResult, child->evaluate(ctx));
+      isFirstLiteral = false;
+    });
 
     // Lift the result from `string` to `IdOrLiteralOrIri` which is needed for
     // the expression module.
 
     auto visitLiteralResult =
-        [&](const std::optional<Literal>& literalSoFar) -> ExpressionResult {
-      return convertLiteral(literalSoFar);
+        [&](std::optional<Literal>& literalSoFar) -> ExpressionResult {
+      return moveLiteralToResult(literalSoFar);
     };
 
     auto visitLiteralVecResult =
-        [&](const LiteralVec& literalVec) -> ExpressionResult {
+        [&](LiteralVec& literalVec) -> ExpressionResult {
       VectorWithMemoryLimit<IdOrLiteralOrIri> resultAsVec(ctx->_allocator);
       resultAsVec.reserve(literalVec.size());
-      ql::ranges::copy(literalVec | ql::views::transform(convertLiteral),
+      ql::ranges::copy(literalVec | ql::views::transform(moveLiteralToResult),
                        std::back_inserter(resultAsVec));
       return resultAsVec;
     };
     return std::visit(ad_utility::OverloadCallOperator{visitLiteralResult,
                                                        visitLiteralVecResult},
-                      std::move(result));
+                      result);
path: src/engine/sparqlExpressions/StringExpressions.cpp
user: login: joka921
body: That is fine, if you take a (non-const) `&` , then you cannot move into it, 
But your lambdas move anyway, so that is okay here as is.
created_at: 2025-04-14T10:29:40Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-04-01T23:24:43Z
body: ## [Codecov](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?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.53086%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 90.80%. Comparing base [(`5bdcee1`)](https://app.codecov.io/gh/ad-freiburg/qlever/commit/5bdcee14f7f75d75afe0d4f781cb6ab316d95d7a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) to head [(`9d9a8d8`)](https://app.codecov.io/gh/ad-freiburg/qlever/commit/9d9a8d832b52ad3e710626c7bf64dc34b9f1a354?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/1924?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/engine/sparqlExpressions/StringExpressions.cpp](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?src=pr&el=tree&filepath=src%2Fengine%2FsparqlExpressions%2FStringExpressions.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg#diff-c3JjL2VuZ2luZS9zcGFycWxFeHByZXNzaW9ucy9TdHJpbmdFeHByZXNzaW9ucy5jcHA=) | 98.57% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg) |
| [src/parser/Literal.cpp](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?src=pr&el=tree&filepath=src%2Fparser%2FLiteral.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ad-freiburg#diff-c3JjL3BhcnNlci9MaXRlcmFsLmNwcA==) | 90.90% | [0 Missing and 1 partial :warning: ](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?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    #1924      +/-   ##
==========================================
+ Coverage   90.79%   90.80%   +0.01%     
==========================================
  Files         417      417              
  Lines       41077    41125      +48     
  Branches     4631     4635       +4     
==========================================
+ Hits        37294    37343      +49     
+ Misses       2415     2414       -1     
  Partials     1368     1368              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ad-freiburg/qlever/pull/1924?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: joka921
created_at: 2025-04-15T09:01:53Z
body: This looks great now.
The failing conformance workflow is not your fault, but somehow it is. 
The conformance tool itself relies on a wrong behavior of `CONCAT`, so this PR breaks it. 
I have opened [a pull request](https://github.com/ad-freiburg/sparql-conformance/pull/13) for the conformance tool to fix this. 
As soon as that is merged, I will let everything run here again (for some reason the coverage report is also missing, but that is probably unrleated).
author_association: MEMBER
user: login: sparql-conformance[bot]
created_at: 2025-04-15T15:03:25Z
body: 
### Conformance check passed ✅


#### Test Status Changes 📊

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

Details: https://qlever.cs.uni-freiburg.de/sparql-conformance-ui?cur=9d9a8d832b52ad3e710626c7bf64dc34b9f1a354&prev=5bdcee14f7f75d75afe0d4f781cb6ab316d95d7a
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-15T16:19:09Z
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=1924) **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=1924&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=1924&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=1924&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=1924&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=1924&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=ad-freiburg_qlever&pullRequest=1924)


author_association: NONE