From 94cd89899ab1bef480588155129c66be613d02a3 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Mon, 10 Aug 2026 09:11:16 -0400 Subject: [PATCH] Report a machine-readable summary; stop failing on no-result statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled changes: the summary is what a caller should read instead of parsing prose, and the statement fix is what makes a non-zero exit code usable at all. Summary: emit one "@summary {json}" line carrying status, row count and elapsed ms. Deliberately on stderr — migration-mode stdout carries the progress output pipekit streams to its live log, and query-mode stdout must stay pure CSV, so structured data on stdout would corrupt one or the other. Previously the row count reached the caller only because a \r progress tick and a trailing " rows written" happened to render on the same line, which pipekit then matched with three different regexes. Statements: query mode used executeQuery(), which raises when a statement returns no result set — Postgres "No results were returned by the query", SQL Server "The statement did not return a result set". DDL, INSERT, DELETE and TRUNCATE all hit this despite succeeding, so pipekit carries a _BENIGN_EXCEPTION_SUBSTRINGS allowlist to tell a working TRUNCATE from a real failure. That made --strict unusable: it would have failed every merge, staging DDL and hook. Now execute() + getUpdateCount() treats them as the successes they are and reports rows affected. Co-Authored-By: Claude Opus 5 --- jrunner/src/main/java/jrunner/jrunner.java | 73 ++++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/jrunner/src/main/java/jrunner/jrunner.java b/jrunner/src/main/java/jrunner/jrunner.java index b2e09d3..259c32f 100644 --- a/jrunner/src/main/java/jrunner/jrunner.java +++ b/jrunner/src/main/java/jrunner/jrunner.java @@ -276,18 +276,34 @@ public class jrunner { stmt = scon.createStatement(); stmt.setFetchSize(10000); tsStart = Timestamp.from(Instant.now()); + summaryStart = tsStart; if (!queryMode) { System.out.println(tsStart); } - rs = stmt.executeQuery(sq); - //while (rs.next()) { - // System.out.println(rs.getString("x")); - //} + // execute() rather than executeQuery(): a statement that returns no + // result set (DDL, INSERT, DELETE, TRUNCATE, MERGE) is a normal, + // successful outcome, but executeQuery() raises for it — Postgres + // "No results were returned by the query", SQL Server "The + // statement did not return a result set". Callers had to allowlist + // those messages to tell a real failure from a working TRUNCATE, + // which made a non-zero exit code impossible to adopt. + boolean hasResultSet = stmt.execute(sq); + if (!hasResultSet) { + int updated = stmt.getUpdateCount(); + if (!queryMode) { + System.out.println("------------no result set---------------------------------"); + System.out.println("rows affected: " + updated); + } + emitSummary("ok", updated >= 0 ? updated : -1, summaryElapsedMs()); + closeQuietly(scon, dcon, queryMode); + return; + } + rs = stmt.getResultSet(); } catch (SQLException e) { System.out.println("issue retrieving rows from source:"); e.printStackTrace(); die(); - } + } //---------------------------------------build meta--------------------------------------------------------------- try { @@ -355,6 +371,7 @@ public class jrunner { // onto the last progress tick; the trailing " rows written" makes // it parseable. System.out.print("\r" + src.rowsWritten()); + summaryRows = src.rowsWritten(); } catch (Exception e) { e.printStackTrace(); die(); @@ -397,6 +414,7 @@ public class jrunner { } cin.endCopy(); System.out.print("\r" + rows); + summaryRows = rows; } catch (Exception e) { e.printStackTrace(); die(); @@ -545,6 +563,7 @@ public class jrunner { stmtd = dcon.createStatement(); stmtd.executeUpdate(sql); System.out.print("\r" + t); + summaryRows = t; } catch (SQLException e) { e.printStackTrace(); System.out.println(sql); @@ -573,6 +592,7 @@ public class jrunner { tsEnd = Timestamp.from(Instant.now()); System.out.println(tsStart); System.out.println(tsEnd); + emitSummary("ok", summaryRows, summaryElapsedMs()); } //long time = Duration.between(tsStart, tsEnd).toMillis(); //System.out.println("time elapsed: " + time); @@ -785,9 +805,52 @@ public class jrunner { private static boolean strictExit = false; private static void die() { + emitSummary("error", summaryRows, summaryElapsedMs()); System.exit(strictExit ? 1 : 0); } + // Row count for the summary. Migration has three write paths (SQLServer + // bulk copy, Postgres COPY, batched INSERT) each with its own counter, so + // whichever runs records the total here. + private static long summaryRows = -1; + private static Timestamp summaryStart = null; + + private static long summaryElapsedMs() { + if (summaryStart == null) return -1; + return Duration.between(summaryStart.toInstant(), Instant.now()).toMillis(); + } + + // A single machine-readable line for the calling process. + // + // Deliberately on stderr: migration-mode stdout carries human-readable + // progress that pipekit streams to its live log, and query-mode stdout must + // stay pure CSV, so anything structured on stdout would corrupt one or the + // other. stderr is already captured in full by the caller. + // + // Replaces having to regex a row count out of prose — the count previously + // reached the caller only because a \r progress tick and a trailing + // " rows written" happened to render on the same line. + private static void emitSummary(String status, long rows, long ms) { + StringBuilder sb = new StringBuilder("@summary {\"status\":\""); + sb.append(status).append("\""); + if (rows >= 0) sb.append(",\"rows\":").append(rows); + if (ms >= 0) sb.append(",\"ms\":").append(ms); + sb.append("}"); + System.err.println(sb); + } + + // Best-effort connection close for the early return taken when a statement + // produced no result set. A failure here cannot invalidate work that the + // server already committed, so it is reported but not fatal. + private static void closeQuietly(Connection scon, Connection dcon, boolean queryMode) { + try { + if (scon != null) scon.close(); + if (!queryMode && dcon != null) dcon.close(); + } catch (SQLException e) { + System.err.println("issue closing connections: " + e.getMessage()); + } + } + private static String escapeTSV(String value) { return value.replaceAll("\t", " ").replaceAll("\n", " ").replaceAll("\r", " "); }