diff --git a/src/engine/sparqlExpressions/GroupConcatExpression.cpp b/src/engine/sparqlExpressions/GroupConcatExpression.cpp index 6ef036239f..29d98ddc0b 100644 --- a/src/engine/sparqlExpressions/GroupConcatExpression.cpp +++ b/src/engine/sparqlExpressions/GroupConcatExpression.cpp @@ -33,8 +33,20 @@ sparqlExpression::GroupConcatExpression::evaluate( result.reserve(20000); bool firstIteration = true; for (auto& inp : generator) { - auto literal = detail::LiteralValueGetterWithoutStrFunction{}( - std::move(inp), context); + // For GROUP_CONCAT, we need to: + // 1. Convert IRIs to string literals (implicit STR() behavior) + // 2. Preserve language tags on literals (for mergeLanguageTags) + // + // LiteralValueGetterWithoutStrFunction preserves language tags but + // returns nullopt for IRIs. LiteralValueGetterWithStrFunction handles + // IRIs but strips language tags. We try WithoutStr first, and only + // fall back to WithStr for IRIs (when WithoutStr returns nullopt). + auto literal = + detail::LiteralValueGetterWithoutStrFunction{}(inp, context); + if (!literal.has_value()) { + // This is an IRI (or other non-literal). Use WithStr to convert it. + literal = detail::LiteralValueGetterWithStrFunction{}(inp, context); + } if (firstIteration) { firstIteration = false; detail::pushLanguageTag(langTag, literal); diff --git a/test/engine/GroupConcatExpressionTest.cpp b/test/engine/GroupConcatExpressionTest.cpp index 5d3efc0398..f8929fc8b7 100644 --- a/test/engine/GroupConcatExpressionTest.cpp +++ b/test/engine/GroupConcatExpressionTest.cpp @@ -130,6 +130,50 @@ TEST(GroupConcatExpression, concatenationWithLanguageTags) { lit("\"a;a\"")); } +// _____________________________________________________________________________ +// Test that IRIs are converted to strings (like implicit STR() application). +// This tests vocabulary IRIs, local vocab IRIs, and literals mixed together. +TEST(GroupConcatExpression, concatenationWithIris) { + auto* qec = ad_utility::testing::getQec(); + auto getId = ad_utility::testing::makeGetId(qec->getIndex()); + + auto iri = [](std::string_view s) { + return tc::LiteralOrIri{tc::Iri::fromIriref(s)}; + }; + + LocalVocab localVocab; + IdTable input{1, ad_utility::makeUnlimitedAllocator()}; + + // 1. Add a vocabulary IRI. + Id xId = getId(""); + input.push_back({xId}); + expectIdsAreConcatenatedTo(false, input, + IdOrLiteralOrIri{LocalVocabEntry{lit("\"x\"")}}); + + // 2. Add a local vocab IRI. + auto localIriIdx = localVocab.getIndexAndAddIfNotContained( + LocalVocabEntry{iri("")}); + input.push_back({Id::makeFromLocalVocabIndex(localIriIdx)}); + expectIdsAreConcatenatedTo( + false, input, + IdOrLiteralOrIri{LocalVocabEntry{lit("\"x;http://example.org/foo\"")}}); + + // 3. Add a local vocab literal. + auto literalIdx = + localVocab.getIndexAndAddIfNotContained(LocalVocabEntry{lit("\"bar\"")}); + input.push_back({Id::makeFromLocalVocabIndex(literalIdx)}); + expectIdsAreConcatenatedTo(false, input, + IdOrLiteralOrIri{LocalVocabEntry{ + lit("\"x;http://example.org/foo;bar\"")}}); + + // 4. Add another vocabulary IRI. + Id labelId = getId("