outBindings = new ArrayList<>();
+ for (int i = 0; i < unescapedColumns.size(); i++) {
+ final String col = unescapedColumns.get(i);
+ final DataType dt = i < resultColumnTypes.size() ? resultColumnTypes.get(i) : DataType.STRING;
+ outBindings.add(new QueryOutParameterBinding() {
+ @Override
+ public String getName() {
+ return col;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return dt;
+ }
+ });
}
+ return QueryResult.of(builder, List.of(), parameterBindings, outBindings, Map.of());
}
return QueryResult.of(builder,
Collections.emptyList(),
@@ -1994,4 +2043,102 @@ private boolean canUseWildcardForSelect(AnnotationMetadata annotationMetadata, P
}
+ /**
+ * Default implementation of {@link ReturningSelectionVisitor} used by {@link SqlQueryBuilder}
+ * to render the projection for SQL RETURNING clauses (INSERT/UPDATE/DELETE).
+ *
+ * In addition to emitting the selection into the SQL buffer, this visitor collects:
+ *
+ * - Unescaped column names in declaration order via {@link #getUnescapedColumns()}
+ * - Result column data types via {@link #getResultColumnTypes()}
+ *
+ * The collected metadata is used by dialects such as Oracle that require {@code RETURNING ... INTO}
+ * OUT parameters instead of a result set.
+ *
+ * This type is not thread-safe and is intended for per-query use only.
+ * It is an internal implementation detail and not part of the public API.
+ *
+ * @see SqlQueryBuilder#createReturningSelectionVisitor(AnnotationMetadata, QueryState, boolean)
+ * @see ReturningSelectionVisitor
+ */
+ @Internal
+ protected final class DefaultReturningSelectionVisitor extends SqlSelectionVisitor implements ReturningSelectionVisitor {
+ private final List unescapedColumns = new ArrayList<>();
+ private final List resultColumnTypes = new ArrayList<>();
+
+ DefaultReturningSelectionVisitor(QueryState queryState, AnnotationMetadata annotationMetadata, boolean distinct) {
+ super(queryState, annotationMetadata, distinct);
+ }
+
+ @Override
+ public List getUnescapedColumns() {
+ return unescapedColumns;
+ }
+
+ @Override
+ public List getResultColumnTypes() {
+ return resultColumnTypes;
+ }
+
+ @Override
+ public void selectAllColumns(AnnotationMetadata annotationMetadata, PersistentEntity entity, @Nullable String alias) {
+ // Mirror base behavior, but also collect unescaped column names and types for OUT parameter metadata
+ boolean escape = shouldEscape(entity);
+ NamingStrategy namingStrategy = getNamingStrategy(entity);
+ int length = query.length();
+ PersistentEntityUtils.traversePersistentProperties(entity, (associations, property) -> {
+ appendProperty(query, associations, property, namingStrategy, alias, escape);
+ unescapedColumns.add(getMappedName(namingStrategy, associations, property));
+ resultColumnTypes.add(property.getDataType());
+ });
+ int newLength = query.length();
+ if (newLength == length) {
+ // Fallback to wildcard if no properties were appended (shouldn't normally happen for non-JSON entities)
+ if (alias != null) {
+ query.append(alias).append(DOT);
+ }
+ query.append("*");
+ } else {
+ query.setLength(newLength - 1);
+ }
+ }
+
+ @Override
+ protected void appendPropertyProjection(QueryPropertyPath propertyPath) {
+ boolean jsonEntity = isJsonEntity(annotationMetadata, entity);
+ if (!computePropertyPaths() || jsonEntity) {
+ // Delegate to default rendering; collect best-effort name/type
+ super.appendPropertyProjection(propertyPath);
+ PersistentProperty prop = propertyPath.getPropertyPath().getProperty();
+ unescapedColumns.add(prop.getPersistedName());
+ resultColumnTypes.add(prop.getDataType());
+ return;
+ }
+ String tableAlias = propertyPath.getTableAlias();
+ boolean escape = propertyPath.shouldEscape();
+ NamingStrategy namingStrategy = propertyPath.getNamingStrategy();
+ boolean[] needsTrimming = {false};
+ int[] propertiesCount = new int[1];
+
+ PersistentEntityUtils.traversePersistentProperties(propertyPath.getAssociations(), propertyPath.getProperty(), traverseEmbedded(), (associations, property) -> {
+ appendProperty(query, associations, property, namingStrategy, tableAlias, escape);
+ unescapedColumns.add(getMappedName(namingStrategy, associations, property));
+ resultColumnTypes.add(property.getDataType());
+ needsTrimming[0] = true;
+ propertiesCount[0]++;
+ });
+ if (needsTrimming[0]) {
+ query.setLength(query.length() - 1);
+ }
+ if (StringUtils.isNotEmpty(columnAlias)) {
+ if (propertiesCount[0] > 1) {
+ throw new IllegalStateException("Cannot apply a column alias: " + columnAlias + " with expanded property: " + propertyPath);
+ }
+ if (propertiesCount[0] == 1) {
+ query.append(AS_CLAUSE).append(columnAlias);
+ }
+ }
+ }
+ }
+
}
diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/QueryOutParameterBinding.java b/data-model/src/main/java/io/micronaut/data/model/runtime/QueryOutParameterBinding.java
new file mode 100644
index 00000000000..051871317a7
--- /dev/null
+++ b/data-model/src/main/java/io/micronaut/data/model/runtime/QueryOutParameterBinding.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017-2025 original authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.micronaut.data.model.runtime;
+
+import io.micronaut.core.annotation.Experimental;
+import io.micronaut.core.annotation.Internal;
+import io.micronaut.data.model.DataType;
+import org.jspecify.annotations.NonNull;
+
+/**
+ * Runtime OUT parameter binding metadata for a stored query (e.g. Oracle RETURNING ... INTO ...).
+ * Mirrors the builder-time metadata and is used by repository operations to register
+ * CallableStatement OUT parameters in correct order and JDBC type.
+ *
+ * @since 5.0
+ */
+@Experimental
+@Internal
+public interface QueryOutParameterBinding {
+
+ /**
+ * @return The name of the OUT column/parameter if available.
+ */
+ @NonNull
+ String name();
+
+ /**
+ * @return The data type for the OUT parameter when known.
+ */
+ @NonNull
+ DataType dataType();
+}
diff --git a/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java b/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java
index 71274ff779c..41608a3a7d2 100644
--- a/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java
+++ b/data-model/src/main/java/io/micronaut/data/model/runtime/StoredQuery.java
@@ -44,7 +44,6 @@ public interface StoredQuery extends Named, StoredDataOperation {
*
* @return The root entity type
*/
-
Class getRootEntity();
/**
@@ -58,7 +57,6 @@ public interface StoredQuery extends Named, StoredDataOperation {
*
* @return The query to execute
*/
-
String getQuery();
/**
@@ -66,7 +64,6 @@ public interface StoredQuery extends Named, StoredDataOperation {
*
* @return The query to execute
*/
-
String[] getExpandableQueryParts();
/**
@@ -81,7 +78,6 @@ public interface StoredQuery extends Named, StoredDataOperation {
*
* @return The query result type
*/
-
Class getResultType();
/**
@@ -90,13 +86,11 @@ public interface StoredQuery extends Named, StoredDataOperation {
* @return The query result type
*/
@Override
-
Argument getResultArgument();
/**
* @return The result data type.
*/
-
DataType getResultDataType();
/**
@@ -145,7 +139,6 @@ default boolean isDtoProjection() {
*
* @return The parameter binding.
*/
-
default Map getQueryHints() {
return Collections.emptyMap();
}
@@ -154,7 +147,6 @@ default Map getQueryHints() {
* @return The all join paths
* @since 4.8.1
*/
-
default Set getJoinPaths() {
return Collections.emptySet();
}
@@ -202,7 +194,6 @@ default Map> getParameterExpressions() {
* @return The query limit
* @since 4.13
*/
-
default Limit getQueryLimit() {
return Limit.UNLIMITED;
}
@@ -211,11 +202,21 @@ default Limit getQueryLimit() {
* @return The runtime sort
* @since 4.13
*/
-
default Sort getSort() {
return Sort.UNSORTED;
}
+ /**
+ * OUT parameters metadata for this stored query (e.g. Oracle RETURNING ... INTO ...).
+ * Order corresponds to the order in which OUT parameters must be registered.
+ *
+ * @return list of OUT parameter bindings or empty if none
+ * @since 5.0
+ */
+ default List getOutParameterBindings() {
+ return List.of();
+ }
+
/**
* Describes the operation type.
*/
diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java
index 7ff5b5329e9..e655ba4ecd3 100644
--- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java
+++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/RepositoryTypeElementVisitor.java
@@ -63,6 +63,8 @@
import io.micronaut.data.model.query.builder.QueryBuilder;
import io.micronaut.data.model.query.builder.QueryParameterBinding;
import io.micronaut.data.model.query.builder.QueryResult;
+import io.micronaut.data.model.query.builder.QueryOutParameterBinding;
+import io.micronaut.data.intercept.annotation.DataMethodQueryOutParameter;
import io.micronaut.data.model.query.builder.jpa.JpaQueryBuilder;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import io.micronaut.data.processor.model.SourcePersistentEntity;
@@ -688,6 +690,18 @@ private void addQueryDefinition(MethodMatchContext methodMatchContext,
if (parameterBinding.stream().anyMatch(QueryParameterBinding::isExpandable)) {
annotationBuilder.member(DataMethodQuery.META_MEMBER_EXPANDABLE_QUERY, queryResult.getQueryParts().toArray(new String[0]));
}
+ // OUT parameter bindings (e.g. Oracle RETURNING ... INTO ...)
+ List outBindings = queryResult.getOutParameterBindings();
+ if (CollectionUtils.isNotEmpty(outBindings)) {
+ List> outAnnotations = new ArrayList<>(outBindings.size());
+ for (QueryOutParameterBinding b : outBindings) {
+ AnnotationValueBuilder> outBuilder = AnnotationValue.builder(DataMethodQueryOutParameter.class);
+ outBuilder.member(DataMethodQueryOutParameter.META_MEMBER_NAME, b.getName());
+ outBuilder.member(DataMethodQueryOutParameter.META_MEMBER_DATA_TYPE, b.getDataType());
+ outAnnotations.add(outBuilder.build());
+ }
+ annotationBuilder.member(DataMethodQuery.META_MEMBER_OUT_PARAMETERS, outAnnotations.toArray(new AnnotationValue[0]));
+ }
int max = queryResult.getMax();
if (max > -1) {
diff --git a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java
index 9f3831b4bcb..8bcfb112526 100644
--- a/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java
+++ b/data-processor/src/main/java/io/micronaut/data/processor/visitors/finders/RawQueryMethodMatcher.java
@@ -49,9 +49,18 @@
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.Collections;
import java.util.Optional;
+import java.util.stream.Collectors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import io.micronaut.data.annotation.Repository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.model.DataType;
+import io.micronaut.data.model.Association;
+import io.micronaut.data.model.query.builder.QueryOutParameterBinding;
+import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
/**
* Finder with custom defied query used to return a single result.
@@ -303,22 +312,242 @@ private QueryResult getQueryResult(MethodMatchContext matchContext,
queryParts.add(newQueryString.substring(lastOffset).replace(COLON_TEMP_REPLACEMENT, COLON));
}
String finalQueryString = newQueryString.replace(COLON_TEMP_REPLACEMENT, COLON);
- return new QueryResult() {
- @Override
- public String getQuery() {
- return finalQueryString;
- }
- @Override
- public List getQueryParts() {
- return queryParts;
+ String cleanLower = SQL_COMMENT_PATTERN.matcher(finalQueryString).replaceAll("").trim().toLowerCase(Locale.ENGLISH);
+ boolean hasReturning = RETURNING_PATTERN.matcher(cleanLower).find();
+ if (hasReturning) {
+ Dialect dialect = matchContext.getRepositoryClass().enumValue(Repository.class, "dialect", Dialect.class)
+ .orElse(Dialect.ANSI);
+ if (dialect == Dialect.ORACLE) {
+ SourcePersistentEntity entity = persistentEntity != null ? persistentEntity : matchContext.getRootEntity();
+ String finalQueryLower = finalQueryString.toLowerCase(Locale.ENGLISH);
+ int returningIdx = finalQueryLower.lastIndexOf("returning");
+ String afterReturning = finalQueryString.substring(returningIdx + "returning".length()).trim();
+ int intoPos = indexOfIntoIgnoreCase(afterReturning);
+ String selection = intoPos > -1 ? afterReturning.substring(0, intoPos).trim() : afterReturning;
+
+ List outBindings = new ArrayList<>();
+ List outColumns = new ArrayList<>();
+ if (selection.equals("*")) {
+ if (entity == null) {
+ throw new MatchFailedException("RETURNING * requires a repository root entity to resolve columns");
+ }
+ entity.getPersistentProperties().forEach(pp -> {
+ if (pp instanceof Association assoc) {
+ switch (assoc.getKind()) {
+ case ONE_TO_MANY, MANY_TO_MANY -> {
+ }
+ default -> {
+ String colName = pp.getPersistedName();
+ DataType dt = DataType.STRING;
+ try {
+ var ae = assoc.getAssociatedEntity();
+ if (ae != null && ae.hasIdentity()) {
+ dt = ae.getIdentity().getDataType();
+ }
+ } catch (Exception ignored) {
+ }
+ outColumns.add(colName);
+ DataType finalDt = dt;
+ outBindings.add(new QueryOutParameterBinding() {
+ @Override
+ public String getName() {
+ return colName;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return finalDt;
+ }
+ });
+ }
+ }
+ } else {
+ String colName = pp.getPersistedName();
+ DataType dt = pp.getDataType();
+ outColumns.add(colName);
+ DataType finalDt = dt;
+ outBindings.add(new QueryOutParameterBinding() {
+ @Override
+ public String getName() {
+ return colName;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return finalDt;
+ }
+ });
+ }
+ });
+ // Ensure id column is included last if not already present
+ if (entity.hasIdentity() && outColumns.stream().noneMatch(c -> c.equals(entity.getIdentity().getPersistedName()))) {
+ var id = entity.getIdentity();
+ outColumns.add(id.getPersistedName());
+ DataType dt = id.getDataType();
+ outBindings.add(new QueryOutParameterBinding() {
+ @Override
+ public String getName() {
+ return id.getPersistedName();
+ }
+
+ @Override
+ public DataType getDataType() {
+ return dt;
+ }
+ });
+ }
+ } else {
+ List parts = splitByComma(selection).stream().map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList());
+ if (parts.size() > 1) {
+ throw new MatchFailedException("Non-custom returning methods support only entity (*) or a single property; multiple columns in RETURNING are not supported in custom @Query for Oracle");
+ }
+ for (String part : parts) {
+ String col = part;
+ if ((col.startsWith("\"") && col.endsWith("\"")) || (col.startsWith("`") && col.endsWith("`"))) {
+ col = col.substring(1, col.length() - 1);
+ }
+ outColumns.add(col);
+ DataType dt = DataType.STRING;
+ if (entity != null) {
+ var prop = entity.getPropertyByNameIgnoreCase(col);
+ if (prop == null) {
+ for (var p : entity.getPersistentProperties()) {
+ if (p.getPersistedName().equalsIgnoreCase(col)) {
+ prop = p;
+ break;
+ }
+ }
+ }
+ if (prop != null) {
+ if (prop instanceof Association assocProp) {
+ try {
+ var ae = assocProp.getAssociatedEntity();
+ if (ae != null && ae.hasIdentity()) {
+ dt = ae.getIdentity().getDataType();
+ }
+ } catch (Exception ignored) {
+ dt = DataType.STRING;
+ }
+ } else if (prop.getDataType() != null) {
+ dt = prop.getDataType();
+ }
+ }
+ }
+ DataType finalDt = dt;
+ String finalCol = col;
+ outBindings.add(new QueryOutParameterBinding() {
+ @Override
+ public String getName() {
+ return finalCol;
+ }
+
+ @Override
+ public DataType getDataType() {
+ return finalDt;
+ }
+ });
+ }
+ }
+ if (outBindings.isEmpty()) {
+ throw new MatchFailedException("RETURNING clause must contain at least one column for Oracle");
+ }
+ int outCount = outBindings.size();
+ String intoPlaceholders = String.join(",", Collections.nCopies(outCount, "?"));
+
+ List quotedColumns = outColumns.stream().map(RawQueryMethodMatcher::quoteOracleIdentifier).toList();
+ // Adjust query parts to produce final positional SQL via RepositoryTypeElementVisitor
+ if (!parameterBindings.isEmpty()) {
+ // Prefix BEGIN to the first literal part
+ if (!queryParts.isEmpty()) {
+ queryParts.set(0, "BEGIN " + queryParts.get(0));
+ } else {
+ queryParts.add("BEGIN ");
+ }
+ // Replace the tail (after last param) to enforce RETURNING ... INTO ...; END;
+ int last = queryParts.size() - 1;
+ String tail = last >= 0 ? queryParts.get(last) : "";
+ int idx = tail.toLowerCase(Locale.ENGLISH).lastIndexOf("returning");
+ String newTail;
+ if (idx >= 0) {
+ newTail = tail.substring(0, idx) + "RETURNING " + String.join(",", quotedColumns) + " INTO " + intoPlaceholders + "; END;";
+ } else {
+ newTail = tail + " RETURNING " + String.join(",", quotedColumns) + " INTO " + intoPlaceholders + "; END;";
+ }
+ if (last >= 0) {
+ queryParts.set(last, newTail);
+ } else {
+ queryParts.add(newTail);
+ }
+ } else {
+ // No parameters: construct single-part final SQL with positional markers only for OUT
+ String withoutSemi = stripTrailingSemicolon(finalQueryString);
+ int retIdx = withoutSemi.toLowerCase(Locale.ENGLISH).lastIndexOf("returning");
+ String beforeReturning = withoutSemi.substring(0, retIdx);
+ String finalSql = "BEGIN " + beforeReturning + " RETURNING " + String.join(",", quotedColumns) + " INTO " + intoPlaceholders + "; END;";
+ queryParts = new ArrayList<>();
+ queryParts.add(finalSql);
+ }
+ // Build the assembled SQL from queryParts: parts represent SQL fragments between
+ // positional IN-parameter placeholders (SqlQueryBuilder.DEFAULT_POSITIONAL_PARAMETER_MARKER).
+ String assembledSql;
+ if (queryParts.size() == 1) {
+ assembledSql = queryParts.get(0);
+ } else {
+ var sqlBuilder = new StringBuilder(queryParts.get(0));
+ for (int i = 1; i < queryParts.size(); i++) {
+ sqlBuilder.append(SqlQueryBuilder.DEFAULT_POSITIONAL_PARAMETER_MARKER).append(queryParts.get(i));
+ }
+ assembledSql = sqlBuilder.toString();
+ }
+ return QueryResult.of(assembledSql, queryParts, parameterBindings, outBindings, Map.of());
}
+ }
+
+ // Default: no transformation
+ return QueryResult.of(finalQueryString, queryParts, parameterBindings);
+ }
- @Override
- public List getParameterBindings() {
- return parameterBindings;
+ private static int indexOfIntoIgnoreCase(String s) {
+ String lower = s.toLowerCase(Locale.ENGLISH);
+ return lower.indexOf(" into ");
+ }
+
+ private static String stripTrailingSemicolon(String s) {
+ String t = s.trim();
+ if (t.endsWith(";")) {
+ return t.substring(0, t.length() - 1);
+ }
+ return s;
+ }
+
+ private static List splitByComma(String s) {
+ List parts = new ArrayList<>();
+ int depth = 0;
+ StringBuilder current = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '(') {
+ depth++;
+ }
+ if (c == ')') {
+ depth--;
}
- };
+ if (c == ',' && depth == 0) {
+ parts.add(current.toString());
+ current.setLength(0);
+ } else {
+ current.append(c);
+ }
+ }
+ if (current.length() > 0) {
+ parts.add(current.toString());
+ }
+ return parts;
+ }
+
+ private static String quoteOracleIdentifier(String identifier) {
+ return '"' + identifier.toUpperCase(Locale.ENGLISH) + '"';
}
public static QueryParameterBinding addBinding(MethodMatchContext matchContext,
diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy
index 16fcb62b4b9..7d52b68be2e 100644
--- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy
+++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildDeleteSpec.groovy
@@ -700,4 +700,36 @@ interface PersonRepository extends CrudRepository {
deleteQuery.replace('\n', ' ') == "WITH ids AS (SELECT id FROM person) DELETE FROM person WHERE id = :id "
method.classValue(DataMethod, "interceptor").get() == DeleteAllInterceptor
}
+
+ void "ORACLE test build delete returning "() {
+ given:
+ def repository = buildRepository('test.BookRepository', """
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.Author;
+
+@JdbcRepository(dialect = Dialect.ORACLE)
+@io.micronaut.context.annotation.Executable
+interface BookRepository extends GenericRepository {
+ Book deleteReturning(Book book);
+ String deleteReturningTitle(Book book);
+}
+""")
+ when:
+ def deleteReturningMethod = repository.findPossibleMethods("deleteReturning").findFirst().get()
+ def deleteReturningTitleMethod = repository.findPossibleMethods("deleteReturningTitle").findFirst().get()
+ then:
+ getQuery(deleteReturningMethod) == 'BEGIN DELETE FROM "BOOK" WHERE ("ID" = ?) RETURNING "ID","AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED" INTO ?,?,?,?,?,?,?; END;'
+ getDataResultType(deleteReturningMethod) == "io.micronaut.data.tck.entities.Book"
+ getParameterPropertyPaths(deleteReturningMethod) == ["id"] as String[]
+ getDataInterceptor(deleteReturningMethod) == "io.micronaut.data.intercept.DeleteOneInterceptor"
+ getResultDataType(deleteReturningMethod) == DataType.ENTITY
+ getQuery(deleteReturningTitleMethod) == 'BEGIN DELETE FROM "BOOK" WHERE ("ID" = ?) RETURNING "TITLE" INTO ?; END;'
+ getDataResultType(deleteReturningTitleMethod) == "java.lang.String"
+ getParameterPropertyPaths(deleteReturningTitleMethod) == ["id"] as String[]
+ getDataInterceptor(deleteReturningTitleMethod) == "io.micronaut.data.intercept.DeleteReturningOneInterceptor"
+ getResultDataType(deleteReturningTitleMethod) == DataType.STRING
+ }
}
diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy
index d31bb54e335..efb2238db49 100644
--- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy
+++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildInsertSpec.groovy
@@ -33,6 +33,7 @@ import static io.micronaut.data.processor.visitors.TestUtils.getDataInterceptor
import static io.micronaut.data.processor.visitors.TestUtils.getDataResultType
import static io.micronaut.data.processor.visitors.TestUtils.getDataTypes
import static io.micronaut.data.processor.visitors.TestUtils.getOperationType
+import static io.micronaut.data.processor.visitors.TestUtils.getOutBindingParameters
import static io.micronaut.data.processor.visitors.TestUtils.getParameterPropertyPaths
import static io.micronaut.data.processor.visitors.TestUtils.getQuery
import static io.micronaut.data.processor.visitors.TestUtils.getRawQuery
@@ -614,4 +615,105 @@ interface PersonRepository extends CrudRepository {
insertQuery.replace('\n', ' ') == "WITH ids AS (SELECT id FROM person) INSERT INTO person(name, age, enabled) VALUES (:name, :age, TRUE) "
method.classValue(DataMethod, "interceptor").get() == SaveEntityInterceptor
}
+
+ void "ORACLE test build save returning"() {
+ given:
+ def repository = buildRepository('test.BookRepository', """
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.data.tck.entities.Book;
+
+@JdbcRepository(dialect= Dialect.ORACLE)
+@io.micronaut.context.annotation.Executable
+interface BookRepository extends GenericRepository {
+ Book saveReturning(Book book);
+}
+""")
+ when:
+ def saveReturningMethod = repository.findPossibleMethods("saveReturning").findFirst().get()
+ def outBindingParameters = getOutBindingParameters(saveReturningMethod)
+ then:
+ getQuery(saveReturningMethod) == 'BEGIN INSERT INTO "BOOK" ("AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED") VALUES (?,?,?,?,?,?) RETURNING "AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED","ID" INTO ?,?,?,?,?,?,?; END;'
+ getDataResultType(saveReturningMethod) == "io.micronaut.data.tck.entities.Book"
+ getParameterPropertyPaths(saveReturningMethod) == ["author.id", "genre.id", "title", "totalPages", "publisher.id", "lastUpdated"] as String[]
+ getDataInterceptor(saveReturningMethod) == "io.micronaut.data.intercept.SaveEntityInterceptor"
+ getResultDataType(saveReturningMethod) == DataType.ENTITY
+ getOperationType(saveReturningMethod) == DataMethod.OperationType.INSERT_RETURNING
+ outBindingParameters.length == 7
+ outBindingParameters[0].name == "author_id"
+ outBindingParameters[0].dataType == DataType.LONG
+ outBindingParameters[1].name == "genre_id"
+ outBindingParameters[1].dataType == DataType.LONG
+ outBindingParameters[2].name == "title"
+ outBindingParameters[2].dataType == DataType.STRING
+ outBindingParameters[3].name == "total_pages"
+ outBindingParameters[3].dataType == DataType.INTEGER
+ outBindingParameters[4].name == "publisher_id"
+ outBindingParameters[4].dataType == DataType.LONG
+ outBindingParameters[5].name == "last_updated"
+ outBindingParameters[5].dataType == DataType.TIMESTAMP
+ outBindingParameters[6].name == "id"
+ outBindingParameters[6].dataType == DataType.LONG
+ }
+
+ void "ORACLE custom @Query insert returning multiple columns should fail"() {
+ when:
+ buildRepository('test.BookRepository', """
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.annotation.Query;
+
+@JdbcRepository(dialect= Dialect.ORACLE)
+@io.micronaut.context.annotation.Executable
+interface BookRepository extends GenericRepository {
+ @Query("INSERT INTO book (author_id,genre_id,title,total_pages,publisher_id,last_updated) VALUES (:authorId,:genreId,:title,:totalPages,:publisherId,:lastUpdated) RETURNING id, title")
+ String brokenInsertReturning(Long authorId, Long genreId, String title, int totalPages, Long publisherId, java.time.LocalDateTime lastUpdated);
+}
+""")
+ then:
+ def ex = thrown(RuntimeException)
+ ex.message.contains("multiple columns in RETURNING are not supported")
+ }
+
+ void "ORACLE custom @Query insert returning produces valid SQL"() {
+ given:
+ def repository = buildRepository('test.BookRepository', """
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.annotation.Query;
+import java.time.LocalDateTime;
+
+@JdbcRepository(dialect= Dialect.ORACLE)
+@io.micronaut.context.annotation.Executable
+interface BookRepository extends GenericRepository {
+ @Query("INSERT INTO \\"BOOK\\" (\\"AUTHOR_ID\\",\\"GENRE_ID\\",\\"TITLE\\",\\"TOTAL_PAGES\\",\\"PUBLISHER_ID\\",\\"LAST_UPDATED\\") VALUES (:authorId,:genreId,:title,:totalPages,:publisherId,:lastUpdated) RETURNING *")
+ Book customInsertReturning(Long authorId, Long genreId, String title, int totalPages, Long publisherId, LocalDateTime lastUpdated);
+
+ @Query("INSERT INTO \\"BOOK\\" (\\"AUTHOR_ID\\",\\"GENRE_ID\\",\\"TITLE\\",\\"TOTAL_PAGES\\",\\"PUBLISHER_ID\\",\\"LAST_UPDATED\\") VALUES (:authorId,:genreId,:title,:totalPages,:publisherId,:lastUpdated) RETURNING \\"TITLE\\"")
+ String customInsertReturningTitle(Long authorId, Long genreId, String title, int totalPages, Long publisherId, LocalDateTime lastUpdated);
+}
+""")
+ when:
+ def customInsertReturningMethod = repository.findPossibleMethods("customInsertReturning").findFirst().get()
+ def outBindingEntityParameters = getOutBindingParameters(customInsertReturningMethod)
+ def customInsertReturningTitleMethod = repository.findPossibleMethods("customInsertReturningTitle").findFirst().get()
+ def outBindingTitleParameters = getOutBindingParameters(customInsertReturningTitleMethod)
+ then:
+ getRawQuery(customInsertReturningMethod) == 'BEGIN INSERT INTO "BOOK" ("AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED") VALUES (?,?,?,?,?,?) RETURNING "AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED","ID" INTO ?,?,?,?,?,?,?; END;'
+ getDataResultType(customInsertReturningMethod) == "io.micronaut.data.tck.entities.Book"
+ getDataInterceptor(customInsertReturningMethod) == "io.micronaut.data.intercept.InsertReturningOneInterceptor"
+ getOperationType(customInsertReturningMethod) == DataMethod.OperationType.INSERT_RETURNING
+ outBindingEntityParameters.length == 7
+
+ getRawQuery(customInsertReturningTitleMethod) == 'BEGIN INSERT INTO "BOOK" ("AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED") VALUES (?,?,?,?,?,?) RETURNING "TITLE" INTO ?; END;'
+ getDataResultType(customInsertReturningTitleMethod) == "java.lang.String"
+ getDataInterceptor(customInsertReturningTitleMethod) == "io.micronaut.data.intercept.InsertReturningOneInterceptor"
+ getOperationType(customInsertReturningTitleMethod) == DataMethod.OperationType.INSERT_RETURNING
+ outBindingTitleParameters.length == 1
+ }
}
diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy
index 6b89f5bd3a0..9ddf815ae93 100644
--- a/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy
+++ b/data-processor/src/test/groovy/io/micronaut/data/processor/sql/BuildUpdateSpec.groovy
@@ -814,30 +814,68 @@ interface AccountRepository extends CrudRepository {
getResultDataType(update) == null
}
-// void "ORACLE test build update returning "() {
-// given:
-// def repository = buildRepository('test.BookRepository', """
-//import io.micronaut.data.jdbc.annotation.JdbcRepository;
-//import io.micronaut.data.model.query.builder.sql.Dialect;
-//import io.micronaut.data.repository.GenericRepository;
-//import io.micronaut.data.tck.entities.Book;
-//import io.micronaut.data.tck.entities.Author;
-//
-//@JdbcRepository(dialect= Dialect.ORACLE)
-//@io.micronaut.context.annotation.Executable
-//interface BookRepository extends GenericRepository {
-//
-// Book updateReturning(Book book);
-//
-//}
-//""")
-// when:
-// def updateReturningCustomMethod = repository.findPossibleMethods("updateReturning").findFirst().get()
-// then:
-// getQuery(updateReturningCustomMethod) == 'UPDATE "BOOK" SET "AUTHOR_ID"=?,"GENRE_ID"=?,"TITLE"=?,"TOTAL_PAGES"=?,"PUBLISHER_ID"=?,"LAST_UPDATED"=? WHERE ("ID" = ?) RETURNING "ID","AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED" INTO "ID","AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED"'
-// getDataResultType(updateReturningCustomMethod) == "io.micronaut.data.tck.entities.Book"
-// getParameterPropertyPaths(updateReturningCustomMethod) == ["author.id", "genre.id", "title", "totalPages", "publisher.id", "lastUpdated", "id"] as String[]
-// getDataInterceptor(updateReturningCustomMethod) == "io.micronaut.data.intercept.UpdateReturningInterceptor"
-// }
+ void "ORACLE test build update returning "() {
+ given:
+ def repository = buildRepository('test.BookRepository', """
+import io.micronaut.data.jdbc.annotation.JdbcRepository;
+import io.micronaut.data.model.query.builder.sql.Dialect;
+import io.micronaut.data.repository.GenericRepository;
+import io.micronaut.data.tck.entities.Book;
+import io.micronaut.data.tck.entities.Author;
+import java.time.LocalDateTime;
+
+@JdbcRepository(dialect= Dialect.ORACLE)
+@io.micronaut.context.annotation.Executable
+interface BookRepository extends GenericRepository {
+ Book updateReturning(Book book);
+ String updateReturningTitle(Book book);
+ @Query("UPDATE \\"BOOK\\" SET \\"TITLE\\"=:title,\\"TOTAL_PAGES\\"=:totalPages,\\"LAST_UPDATED\\"=:lastUpdated WHERE \\"ID\\"=:bookId RETURNING *")
+ Book customUpdateReturning(Long bookId, String title, int totalPages, LocalDateTime lastUpdated);
+
+}
+""")
+ when:
+ def updateReturningMethod = repository.findPossibleMethods("updateReturning").findFirst().get()
+ def updateOutBindingParameters = getOutBindingParameters(updateReturningMethod)
+ def updateReturningTitleMethod = repository.findPossibleMethods("updateReturningTitle").findFirst().get()
+ def updateTitleOutBindingParameters = getOutBindingParameters(updateReturningTitleMethod)
+ def customUpdateReturningMethod = repository.findPossibleMethods("customUpdateReturning").findFirst().get()
+ def customUpdateOutBindingParameters = getOutBindingParameters(customUpdateReturningMethod)
+ then:
+ getQuery(updateReturningMethod) == 'BEGIN UPDATE "BOOK" SET "AUTHOR_ID"=?,"GENRE_ID"=?,"TITLE"=?,"TOTAL_PAGES"=?,"PUBLISHER_ID"=?,"LAST_UPDATED"=? WHERE ("ID" = ?) RETURNING "ID","AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED" INTO ?,?,?,?,?,?,?; END;'
+ getDataResultType(updateReturningMethod) == "io.micronaut.data.tck.entities.Book"
+ getParameterPropertyPaths(updateReturningMethod) == ["author.id", "genre.id", "title", "totalPages", "publisher.id", "lastUpdated", "id"] as String[]
+ getDataInterceptor(updateReturningMethod) == "io.micronaut.data.intercept.UpdateEntityInterceptor"
+ getQuery(updateReturningTitleMethod) == 'BEGIN UPDATE "BOOK" SET "AUTHOR_ID"=?,"GENRE_ID"=?,"TITLE"=?,"TOTAL_PAGES"=?,"PUBLISHER_ID"=?,"LAST_UPDATED"=? WHERE ("ID" = ?) RETURNING "TITLE" INTO ?; END;'
+ getDataResultType(updateReturningTitleMethod) == "java.lang.String"
+ getParameterPropertyPaths(updateReturningTitleMethod) == ["author.id", "genre.id", "title", "totalPages", "publisher.id", "lastUpdated", "id"] as String[]
+ getDataInterceptor(updateReturningTitleMethod) == "io.micronaut.data.intercept.UpdateReturningOneInterceptor"
+
+ getRawQuery(customUpdateReturningMethod) == 'BEGIN UPDATE "BOOK" SET "TITLE"=?,"TOTAL_PAGES"=?,"LAST_UPDATED"=? WHERE "ID"=? RETURNING "AUTHOR_ID","GENRE_ID","TITLE","TOTAL_PAGES","PUBLISHER_ID","LAST_UPDATED","ID" INTO ?,?,?,?,?,?,?; END;'
+ getDataResultType(customUpdateReturningMethod) == "io.micronaut.data.tck.entities.Book"
+ getDataInterceptor(customUpdateReturningMethod) == "io.micronaut.data.intercept.UpdateReturningOneInterceptor"
+
+ updateOutBindingParameters.length == 7
+ updateOutBindingParameters[0].name == "id"
+ updateOutBindingParameters[0].dataType == DataType.LONG
+ updateOutBindingParameters[1].name == "author_id"
+ updateOutBindingParameters[1].dataType == DataType.LONG
+ updateOutBindingParameters[2].name == "genre_id"
+ updateOutBindingParameters[2].dataType == DataType.LONG
+ updateOutBindingParameters[3].name == "title"
+ updateOutBindingParameters[3].dataType == DataType.STRING
+ updateOutBindingParameters[4].name == "total_pages"
+ updateOutBindingParameters[4].dataType == DataType.INTEGER
+ updateOutBindingParameters[5].name == "publisher_id"
+ updateOutBindingParameters[5].dataType == DataType.LONG
+ updateOutBindingParameters[6].name == "last_updated"
+ updateOutBindingParameters[6].dataType == DataType.TIMESTAMP
+
+ updateTitleOutBindingParameters.length == 1
+ updateTitleOutBindingParameters[0].name == "title"
+ updateTitleOutBindingParameters[0].dataType == DataType.STRING
+
+ customUpdateOutBindingParameters.length == 7
+ }
}
diff --git a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy
index 81d8c1a67c1..d8d3705e156 100644
--- a/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy
+++ b/data-processor/src/test/groovy/io/micronaut/data/processor/visitors/TestUtils.groovy
@@ -22,6 +22,8 @@ import io.micronaut.core.annotation.AnnotationValue
import io.micronaut.data.annotation.Join
import io.micronaut.data.annotation.Query
import io.micronaut.data.intercept.annotation.DataMethod
+import io.micronaut.data.intercept.annotation.DataMethodQuery
+import io.micronaut.data.intercept.annotation.DataMethodQueryOutParameter
import io.micronaut.data.intercept.annotation.DataMethodQueryParameter
import io.micronaut.data.model.DataType;
import io.micronaut.data.processor.model.SourcePersistentEntity;
@@ -185,6 +187,18 @@ class TestUtils {
.toArray(String[]::new)
}
+ static OutBindingParameter[] getOutBindingParameters(AnnotationMetadata annotationMetadata) {
+ return getOutBindingParameters(annotationMetadata.getAnnotation(DataMethod))
+ }
+
+ static OutBindingParameter[] getOutBindingParameters(AnnotationValue annotationValue) {
+ return annotationValue.getAnnotations(DataMethodQuery.META_MEMBER_OUT_PARAMETERS, DataMethodQueryOutParameter)
+ .stream()
+ .map(p -> new OutBindingParameter(name: p.getRequiredValue("name", String.class),
+ dataType: p.getRequiredValue("dataType", DataType)))
+ .toArray(OutBindingParameter[]::new)
+ }
+
static Boolean[] getParameterExpressions(AnnotationValue annotationValue) {
return annotationValue.getAnnotations(DataMethod.META_MEMBER_PARAMETERS, DataMethodQueryParameter)
.stream()
@@ -236,4 +250,9 @@ class TestUtils {
.collect(Collectors.toMap((AnnotationValue av) -> av.stringValue().get(),
(AnnotationValue av) -> av.enumValue("type", Join.Type).get()))
}
+
+ static class OutBindingParameter {
+ String name
+ DataType dataType
+ }
}
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java
index 1cc2da89259..ffbf3cc5d80 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/operations/internal/sql/DefaultSqlStoredQuery.java
@@ -24,6 +24,7 @@
import io.micronaut.data.model.JsonDataType;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
+import io.micronaut.data.model.runtime.QueryOutParameterBinding;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.QueryResultInfo;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
@@ -96,6 +97,11 @@ public QueryResultInfo getQueryResultInfo() {
return queryResultInfo;
}
+ @Override
+ public List getOutParameterBindings() {
+ return getStoredQueryDelegate().getOutParameterBindings();
+ }
+
@Override
public boolean isExpandableQuery() {
return expandableQuery;
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java
index 72cef48e3f1..7b0b9ca726d 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DefaultStoredQuery.java
@@ -29,6 +29,7 @@
import io.micronaut.data.intercept.annotation.DataMethod;
import io.micronaut.data.intercept.annotation.DataMethodQuery;
import io.micronaut.data.intercept.annotation.DataMethodQueryParameter;
+import io.micronaut.data.intercept.annotation.DataMethodQueryOutParameter;
import io.micronaut.data.model.AssociationUtils;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.JsonDataType;
@@ -38,6 +39,7 @@
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import io.micronaut.data.model.runtime.DefaultStoredDataOperation;
import io.micronaut.data.model.runtime.QueryParameterBinding;
+import io.micronaut.data.model.runtime.QueryOutParameterBinding;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.operations.HintsCapableRepository;
import io.micronaut.inject.ExecutableMethod;
@@ -89,6 +91,7 @@ public final class DefaultStoredQuery extends DefaultStoredDataOperation<
@Nullable
private Set joinPaths = null;
private final List queryParameters;
+ private final List outParameterBindings;
private final boolean rawQuery;
private final boolean jsonEntity;
private final OperationType operationType;
@@ -242,6 +245,9 @@ public DefaultStoredQuery(
dataMethodQuery.getAnnotations(DataMethodQuery.META_MEMBER_PARAMETERS, DataMethodQueryParameter.class),
isNumericPlaceHolder
);
+ this.outParameterBindings = getOutParameters(
+ dataMethodQuery.getAnnotations(DataMethodQuery.META_MEMBER_OUT_PARAMETERS, DataMethodQueryOutParameter.class)
+ );
this.jsonEntity = DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(annotationMetadata);
this.parameterExpressions = annotationMetadata.getAnnotationValuesByType(ParameterExpression.class).stream()
.collect(Collectors.toMap(av -> av.stringValue("name").orElseThrow(), av -> av));
@@ -317,6 +323,19 @@ private static List getQueryParameters(List getOutParameters(List> params) {
+ if (params == null || params.isEmpty()) {
+ return List.of();
+ }
+ List outParams = new ArrayList<>(params.size());
+ for (AnnotationValue av : params) {
+ DataType dataType = av.enumValue(DataMethodQueryOutParameter.META_MEMBER_DATA_TYPE, DataType.class).orElseThrow();
+ String name = av.stringValue(DataMethodQueryOutParameter.META_MEMBER_NAME).orElseThrow();
+ outParams.add(new StoredOutParameter(name, dataType));
+ }
+ return outParams;
+ }
+
@Override
public Limit getQueryLimit() {
return limit;
@@ -332,6 +351,11 @@ public List getQueryBindings() {
return queryParameters;
}
+ @Override
+ public List getOutParameterBindings() {
+ return outParameterBindings == null ? List.of() : outParameterBindings;
+ }
+
@Override
public Set getJoinPaths() {
if (joinPaths == null) {
@@ -508,4 +532,7 @@ private static AnnotationValue getRequiredDataMethod(ExecutableMetho
}
return av;
}
+
+ private record StoredOutParameter(String name, DataType dataType) implements QueryOutParameterBinding {
+ }
}
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java
index afdd8743010..53c647bd431 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/DelegateStoredQuery.java
@@ -22,6 +22,7 @@
import io.micronaut.data.model.Limit;
import io.micronaut.data.model.Sort;
import io.micronaut.data.model.query.JoinPath;
+import io.micronaut.data.model.runtime.QueryOutParameterBinding;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.StoredQuery;
@@ -74,6 +75,11 @@ default List getQueryBindings() {
return getStoredQueryDelegate().getQueryBindings();
}
+ @Override
+ default List getOutParameterBindings() {
+ return getStoredQueryDelegate().getOutParameterBindings();
+ }
+
@Override
default Class getResultType() {
return getStoredQueryDelegate().getResultType();
diff --git a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/QueryResultStoredQuery.java b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/QueryResultStoredQuery.java
index c5f60fceb33..28c5e6f4b2a 100644
--- a/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/QueryResultStoredQuery.java
+++ b/data-runtime/src/main/java/io/micronaut/data/runtime/query/internal/QueryResultStoredQuery.java
@@ -23,6 +23,7 @@
import io.micronaut.data.model.query.JoinPath;
import io.micronaut.data.model.query.builder.QueryResult;
import io.micronaut.data.model.runtime.QueryParameterBinding;
+import io.micronaut.data.model.runtime.QueryOutParameterBinding;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
@@ -45,6 +46,7 @@ public final class QueryResultStoredQuery extends BasicStoredQuery {
private final QueryResult queryResult;
private final Set joinPaths;
+ private final List outParameterBindings;
public QueryResultStoredQuery(String name,
AnnotationMetadata annotationMetadata,
@@ -68,6 +70,7 @@ public QueryResultStoredQuery(String name,
operationType);
this.queryResult = queryResult;
this.joinPaths = joinPaths == null ? Collections.emptySet() : Set.copyOf(joinPaths);
+ this.outParameterBindings = mapOut(queryResult.getOutParameterBindings());
}
public QueryResultStoredQuery(String name,
@@ -94,6 +97,7 @@ public QueryResultStoredQuery(String name,
operationType);
this.queryResult = queryResult;
this.joinPaths = joinPaths == null ? Collections.emptySet() : Set.copyOf(joinPaths);
+ this.outParameterBindings = mapOut(queryResult.getOutParameterBindings());
}
public static QueryResultStoredQuery single(OperationType operationType,
@@ -175,11 +179,42 @@ public QueryResult getQueryResult() {
return queryResult;
}
+ @Override
+ public List getOutParameterBindings() {
+ return outParameterBindings;
+ }
+
@Override
public Set getJoinPaths() {
return joinPaths;
}
+ private static List mapOut(List bindings) {
+ if (bindings == null || bindings.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List out = new ArrayList<>(bindings.size());
+ for (io.micronaut.data.model.query.builder.QueryOutParameterBinding b : bindings) {
+ out.add(new QueryResultOutParameterBinding(b));
+ }
+ return out;
+ }
+
+ private record QueryResultOutParameterBinding(
+ io.micronaut.data.model.query.builder.QueryOutParameterBinding delegate) implements QueryOutParameterBinding {
+
+ @Override
+ public String name() {
+ return delegate.getName();
+ }
+
+ @Override
+ public DataType dataType() {
+ return delegate.getDataType();
+ }
+
+ }
+
private static final class QueryResultParameterBinding implements QueryParameterBinding {
private final io.micronaut.data.model.query.builder.QueryParameterBinding p;
private final List all;