Skip to content

Commit b81d975

Browse files
author
Philip Z
committed
Mark JDBC transaction rollback-only after caught SQL failures
1 parent 7f99d1e commit b81d975

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

‎teaql-provider-jdbc/src/main/java/io/teaql/provider/jdbc/JdbcSqlExecutor.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public List<Map<String, Object>> queryForList(String sql, Object[] params) {
184184
return result;
185185
}
186186
} catch (SQLException e) {
187+
markTransactionRollbackOnly();
187188
throw new RuntimeException("JDBC query failed for parameterized SQL: " + sql, e);
188189
} finally {
189190
if (owned && connection != null) try { connection.close(); } catch (SQLException ignored) { }
@@ -210,6 +211,7 @@ public <T extends io.teaql.core.Entity> List<T> query(
210211
}
211212
return result;
212213
} catch (SQLException exception) {
214+
markTransactionRollbackOnly();
213215
throw new RuntimeException("JDBC typed query failed for parameterized SQL: " + sql, exception);
214216
} finally {
215217
if (owned && connection != null) try { connection.close(); } catch (SQLException ignored) { }
@@ -339,6 +341,7 @@ public void execute(String sql) {
339341
ps.execute();
340342
}
341343
} catch (SQLException e) {
344+
markTransactionRollbackOnly();
342345
throw new RuntimeException(e);
343346
} finally {
344347
if (owned && connection != null) try { connection.close(); } catch (SQLException ignored) { }
@@ -364,6 +367,7 @@ public int update(String sql, Object[] params) {
364367
return ps.executeUpdate();
365368
}
366369
} catch (SQLException e) {
370+
markTransactionRollbackOnly();
367371
throw new RuntimeException(e);
368372
} finally {
369373
if (owned && connection != null) try { connection.close(); } catch (SQLException ignored) { }
@@ -387,6 +391,7 @@ public int[] batchUpdate(String sql, List<Object[]> paramsList) {
387391
return ps.executeBatch();
388392
}
389393
} catch (SQLException e) {
394+
markTransactionRollbackOnly();
390395
throw new RuntimeException(e);
391396
} finally {
392397
if (owned && connection != null) try { connection.close(); } catch (SQLException ignored) { }
@@ -432,6 +437,13 @@ public void executeInTransaction(Runnable action) {
432437
}
433438
}
434439

440+
private void markTransactionRollbackOnly() {
441+
// A failed statement may have left earlier writes in the shared
442+
// transaction. Without a savepoint, catching that failure cannot
443+
// make the transaction safe to commit.
444+
if (transactionConnection.get() != null) transactionRollbackOnly.set(true);
445+
}
446+
435447
@Override public boolean hasActiveTransaction() { return transactionConnection.get() != null; }
436448

437449
static void bind(PreparedStatement statement, int index, Object value) throws SQLException {

‎teaql-provider-jdbc/src/test/java/io/teaql/provider/jdbc/JdbcSqlExecutorTest.java‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,50 @@ public void nestedFailureCaughtByCallerStillRollsBackOuterTransaction() {
9191
.get(0).get("total")).intValue());
9292
}
9393

94+
@Test
95+
public void caughtJdbcWriteFailureStillRollsBackOuterTransaction() {
96+
assertThrows(IllegalStateException.class, () -> sqlExecutor.executeInTransaction(() -> {
97+
sqlExecutor.update("INSERT INTO test_user (id, name, age) VALUES (?, ?, ?)",
98+
new Object[] {94, "first write", 20});
99+
try {
100+
sqlExecutor.update("INSERT INTO test_user (id, name, age) VALUES (?, ?, ?)",
101+
new Object[] {94, "duplicate key", 21});
102+
} catch (RuntimeException expected) {
103+
// A caller may handle a low-level JDBC failure before returning.
104+
}
105+
}));
106+
107+
assertEquals(0, ((Number) sqlExecutor.queryForList(
108+
"SELECT count(*) AS total FROM test_user", new Object[0])
109+
.get(0).get("total")).intValue());
110+
}
111+
112+
@Test
113+
public void caughtStatementOrReadFailureCannotCommitEarlierWrites() {
114+
List<Runnable> failures = List.of(
115+
() -> sqlExecutor.execute("INSERT INTO test_user (id, name, age) VALUES (94, 'duplicate', 21)"),
116+
() -> sqlExecutor.batchUpdate(
117+
"INSERT INTO test_user (id, name, age) VALUES (?, ?, ?)",
118+
java.util.Collections.singletonList(new Object[] {94, "duplicate", 21})),
119+
() -> sqlExecutor.queryForList("SELECT * FROM missing_test_table", new Object[0]),
120+
() -> sqlExecutor.query("SELECT * FROM missing_test_table", new Object[0],
121+
row -> new TypedRow()));
122+
for (Runnable failedStatement : failures) {
123+
assertThrows(IllegalStateException.class, () -> sqlExecutor.executeInTransaction(() -> {
124+
sqlExecutor.update("INSERT INTO test_user (id, name, age) VALUES (?, ?, ?)",
125+
new Object[] {94, "must roll back", 20});
126+
try {
127+
failedStatement.run();
128+
} catch (RuntimeException expected) {
129+
// The failed statement must poison the shared transaction.
130+
}
131+
}));
132+
assertEquals(0, ((Number) sqlExecutor.queryForList(
133+
"SELECT count(*) AS total FROM test_user", new Object[0])
134+
.get(0).get("total")).intValue());
135+
}
136+
}
137+
94138
@Test
95139
public void testQueryForListNormalizesAliasesForPortableMapping() {
96140
List<Map<String, Object>> rows = sqlExecutor.queryForList(

0 commit comments

Comments
 (0)