Skip to content
Open
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 @@ -8,6 +8,7 @@
package de.ii.xtraplatform.features.sql.infra.db;

import de.ii.xtraplatform.base.domain.LogContext.MARKER;
import de.ii.xtraplatform.features.domain.FeatureMutationConstraintException;
import de.ii.xtraplatform.features.domain.FeatureMutationHookException;
import de.ii.xtraplatform.features.sql.domain.SqlSession;
import java.sql.Connection;
Expand Down Expand Up @@ -83,8 +84,7 @@ public String run(
}
batchStmt.addBatch(sql);
} catch (SQLException e) {
throw new IllegalStateException(
"Mutation statement failed: " + e.getMessage() + " — statement: " + sql, e);
throw mutationFailed("Mutation statement failed: ", sql, e);
}
batchedSql.add(sql);
batchedConsumers.add(consumer);
Expand Down Expand Up @@ -120,8 +120,7 @@ public String run(
firstGeneratedId = returnedId;
}
} catch (SQLException e) {
throw new IllegalStateException(
"Mutation statement failed: " + e.getMessage() + " — statement: " + sql, e);
throw mutationFailed("Mutation statement failed: ", sql, e);
}
}

Expand Down Expand Up @@ -162,8 +161,7 @@ public List<String> runReturning(String sql) {
}
return ids;
} catch (SQLException e) {
throw new IllegalStateException(
"Mutation statement failed: " + e.getMessage() + " — statement: " + sql, e);
throw mutationFailed("Mutation statement failed: ", sql, e);
}
}

Expand Down Expand Up @@ -229,6 +227,38 @@ public List<String> drainWarnings() {
return drained;
}

/**
* Wrap a failed mutation statement. A rejection in SQLSTATE class 23 (integrity constraint
* violation — CHECK, foreign key, unique index, or a trigger raising one) is caused by the data
* the client sent, so it gets its own exception type that callers can report and log quietly;
* anything else stays an IllegalStateException and keeps its stack trace.
*/
private static RuntimeException mutationFailed(String prefix, String sql, SQLException e) {
String message = prefix + e.getMessage() + " — statement: " + sql;
String sqlState = constraintSqlState(e);
return sqlState != null
? new FeatureMutationConstraintException(message, e, sqlState)
: new IllegalStateException(message, e);
}

/**
* The SQLSTATE of the first integrity-constraint violation in the chain, or null. Iterating the
* SQLException itself walks both the causal chain and the next-exception chain, which matters for
* the batch path: executeBatch() reports a BatchUpdateException whose actual error is a
* next-exception, not a cause.
*/
private static String constraintSqlState(SQLException e) {
for (Throwable t : e) {
if (t instanceof SQLException) {
String state = ((SQLException) t).getSQLState();
if (state != null && state.startsWith("23")) {
return state;
}
}
}
return null;
}

private void flushBatch(
Statement batchStmt, List<String> batchedSql, List<Consumer<String>> batchedConsumers) {
if (batchedSql.isEmpty()) {
Expand All @@ -243,9 +273,7 @@ private void flushBatch(
batchStmt.clearBatch();
harvestWarnings(batchStmt);
} catch (SQLException e) {
throw new IllegalStateException(
"Batched mutation failed: " + e.getMessage() + " — first statement: " + batchedSql.get(0),
e);
throw mutationFailed("Batched mutation failed: ", batchedSql.get(0), e);
}
// Preserve the per-statement consumer contract: each batched statement returns no id, so
// call consumers with null in order (in practice these are no-ops for child/junction/FK
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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;

/**
* Thrown when a mutation statement is rejected by the database because it violates an integrity
* constraint — a CHECK or foreign-key constraint, a unique index, or a trigger raising an error in
* SQLSTATE class 23. This is caused by the data the client sent, not by a bug or an infrastructure
* problem: it is reported to the client and rolls the transaction back, so callers should log it
* quietly, without a stack trace.
*/
public class FeatureMutationConstraintException extends RuntimeException {

private final String sqlState;

public FeatureMutationConstraintException(String message, Throwable cause, String sqlState) {
super(message, cause);
this.sqlState = sqlState;
}

/** The SQLSTATE reported by the database, always in class 23. */
public String getSqlState() {
return sqlState;
}
}
Loading