From 6fe2bea08912a7c0837849108c385ef008d4355d Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 18 Jun 2026 23:04:59 -0400 Subject: [PATCH] feat: -b bulk copy into Postgres dest via COPY FROM STDIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends -b to Postgres destinations: stream the source ResultSet into PG with COPY
FROM STDIN (FORMAT csv) via the JDBC CopyManager, instead of batched INSERTs. COPY is text-based so the server parses each field into the column type — no per-type quoting needed. Every non-null value is CSV-quoted (so empty string stays distinct from NULL, which is an empty unquoted field); rows are flushed in 1000-row buffers with a 10k-row progress counter. Validated DB2->PG: numeric precision (123.4567), jsonb, unicode, embedded quotes, NULL vs empty-string all correct. Co-Authored-By: Claude Opus 4.8 --- jrunner/src/main/java/jrunner/jrunner.java | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/jrunner/src/main/java/jrunner/jrunner.java b/jrunner/src/main/java/jrunner/jrunner.java index 7abef0d..e4533a3 100644 --- a/jrunner/src/main/java/jrunner/jrunner.java +++ b/jrunner/src/main/java/jrunner/jrunner.java @@ -10,6 +10,9 @@ import com.microsoft.sqlserver.jdbc.SQLServerBulkCopy; import com.microsoft.sqlserver.jdbc.SQLServerBulkCopyOptions; import com.microsoft.sqlserver.jdbc.ISQLServerBulkData; import com.microsoft.sqlserver.jdbc.SQLServerException; +import org.postgresql.PGConnection; +import org.postgresql.copy.CopyManager; +import org.postgresql.copy.CopyIn; public class jrunner { //static final String QUERY = "SELECT * from rlarp.osm LIMIT 100"; @@ -333,6 +336,48 @@ public class jrunner { e.printStackTrace(); System.exit(0); } + } else if (bulk && dcu.toLowerCase().startsWith("jdbc:postgresql:")) { + //-------------------------------bulk copy (COPY, Postgres dest)-------------------------------------------- + // Stream the source ResultSet into Postgres via COPY ... FROM STDIN. + // COPY is text-based: each field is sent as CSV text and the server + // parses it into the column type, so there's no per-type quoting. + // Non-null values are always CSV-quoted; NULL is an empty unquoted + // field; column order must match the dest (positional, as always). + System.out.println("------------bulk copy (COPY)------------------------------"); + try { + CopyManager cm = ((PGConnection) dcon).getCopyAPI(); + CopyIn cin = cm.copyIn("COPY " + dt + " FROM STDIN WITH (FORMAT csv)"); + StringBuilder buf = new StringBuilder(); + long rows = 0; + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + if (i > 1) { buf.append(','); } + String val = rs.getString(i); + if (!rs.wasNull() && val != null) { + if (trim) { val = val.trim(); } + buf.append('"').append(val.replace("\"", "\"\"")).append('"'); + } + // else: empty field -> NULL + } + buf.append('\n'); + rows++; + if (rows % 1000 == 0) { + byte[] b = buf.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + cin.writeToCopy(b, 0, b.length); + buf.setLength(0); + if (rows % 10000 == 0) { System.out.print("\r" + rows); System.out.flush(); } + } + } + if (buf.length() > 0) { + byte[] b = buf.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); + cin.writeToCopy(b, 0, b.length); + } + cin.endCopy(); + System.out.print("\r" + rows); + } catch (Exception e) { + e.printStackTrace(); + System.exit(0); + } } else { System.out.println("------------row count-------------------------------------"); //-------------------------------build & execute sql-------------------------------------------------------------