Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,11 @@ private static String sqlString(String value) {
return "'" + value.replace("'", "''") + "'";
}

@Override
public List<String> execute(List<String> statements) {
return sqlSession.execute(statements);
}

@Override
public void commit() {
sqlSession.commit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ String run(
*/
List<String> runReturning(String sql);

/**
* Executes raw statements in order on this session's connection, ignoring any result sets, and
* returns the non-fatal SQL warnings they produced (e.g. PostgreSQL {@code RAISE WARNING} /
* {@code RAISE NOTICE}). A statement that fails throws and leaves the transaction open for
* rollback. Intended for transaction-lifecycle hooks (session setup, pre-commit checks).
*
* @param statements the statements to execute, in order
* @return the collected warning messages across all statements, in execution order
*/
List<String> execute(List<String> statements);

/** Commits all mutations performed against this session. */
void commit();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
package de.ii.xtraplatform.features.sql.infra.db;

import de.ii.xtraplatform.base.domain.LogContext.MARKER;
import de.ii.xtraplatform.features.domain.FeatureMutationHookException;
import de.ii.xtraplatform.features.sql.domain.SqlSession;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -158,6 +160,31 @@ public List<String> runReturning(String sql) {
}
}

@Override
public List<String> execute(List<String> statements) {
if (finalised) {
throw new IllegalStateException("SQL session is closed");
}
List<String> warnings = new ArrayList<>();
for (String sql : statements) {
if (LOGGER.isDebugEnabled(MARKER.SQL)) {
LOGGER.debug(MARKER.SQL, "Executing hook statement: {}", sql);
}
try (Statement statement = connection.createStatement()) {
statement.execute(sql);
for (SQLWarning w = statement.getWarnings(); w != null; w = w.getNextWarning()) {
warnings.add(w.getMessage());
}
} catch (SQLException e) {
// Expected, configuration-driven failure (e.g. a check function RAISE EXCEPTION) — carry
// the warnings collected so far so they survive the failure path.
throw new FeatureMutationHookException(
"Hook statement failed: " + e.getMessage() + " — statement: " + sql, e, warnings);
}
}
return warnings;
}

// Generators emit "RETURNING null" for child / junction / FK-update statements — these have
// no caller-meaningful return value and their consumers are no-ops. Main inserts use
// "RETURNING <pk>" and must run individually so their generated id can drive child SQL.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2026 interactive instruments GmbH
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package de.ii.xtraplatform.features.domain;

import java.util.List;

/**
* Thrown by {@link FeatureTransactions.Session#execute(List)} when one of the raw statements (a
* transaction-lifecycle hook) fails. This is an expected, configuration-driven outcome — it
* triggers a rollback and is reported to the client — not an illegal state, so callers should log
* it quietly. Any non-fatal warnings collected from statements that ran before the failing one are
* carried in {@link #getWarnings()} so they are not lost on the failure path.
*/
public class FeatureMutationHookException extends RuntimeException {

private final List<String> warnings;

public FeatureMutationHookException(String message, Throwable cause, List<String> warnings) {
super(message, cause);
this.warnings = List.copyOf(warnings);
}

/** Warnings emitted by hook statements that ran successfully before the failing statement. */
public List<String> getWarnings() {
return warnings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,26 @@ default MutationResult cloneAndPatchFeature(
"Clone-and-patch is not supported by this feature provider session");
}

/**
* Executes raw native statements in order against this session's open transaction and returns
* the non-fatal warnings they produced (e.g. PostgreSQL {@code RAISE WARNING} / {@code RAISE
* NOTICE}). A statement that fails throws a {@link FeatureMutationHookException} (carrying any
* warnings collected before the failing statement), leaving the transaction open for rollback.
* Used by transaction-lifecycle hooks (session setup, pre-commit checks). The default
* implementation accepts an empty list as a no-op and otherwise throws {@link
* UnsupportedOperationException} for providers that cannot run native statements.
*
* @param statements the statements to execute, in order
* @return the collected warning messages across all statements, in execution order
*/
default List<String> execute(List<String> statements) {
if (!statements.isEmpty()) {
throw new UnsupportedOperationException(
"Raw statement execution is not supported by this feature provider session");
}
return List.of();
}

/** Commits all mutations performed against this session. Throws if already finalised. */
void commit();

Expand Down
Loading