Implement -f json
json and table were advertised in --help but both fell through to
outputCSV, so -f json silently produced CSV. table is now dropped from
the help text rather than advertised and unimplemented; json is real.
Two things CSV cannot express, both of which callers need:
* NULL vs empty string. outputCSV writes an empty field for both, so
the merge, reconcile and the wizard all see them as identical. JSON
emits null and "" distinctly.
* Column types. The wizard's SQL-entry mode defaults every dest column
to text purely because CSV carries no type metadata; the header now
reports name, type, precision and scale (verified against DB2 for i:
CHAR precision=5, DECIMAL precision=7 scale=2).
Values are JSON strings, never JSON numbers — a DECIMAL rendered as a
number would pass through a float and lose exactness. Output is written
incrementally so a large result set streams as CSV does while remaining
one valid document. Zero-row results still report their columns, which is
what query-column introspection relies on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
94cd89899a
commit
b63b966621
@ -68,7 +68,7 @@ public class jrunner {
|
|||||||
msg = msg + nl + "-t trim text";
|
msg = msg + nl + "-t trim text";
|
||||||
msg = msg + nl + "-c clear target table";
|
msg = msg + nl + "-c clear target table";
|
||||||
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
msg = msg + nl + "-b bulk copy into destination (SQL Server dest only)";
|
||||||
msg = msg + nl + "-f output format (csv, tsv, table, json) - default: csv";
|
msg = msg + nl + "-f output format (csv, tsv, json) - default: csv";
|
||||||
msg = msg + nl + "--passfile path to the connection alias file - default: ~/.jrunnerpass";
|
msg = msg + nl + "--passfile path to the connection alias file - default: ~/.jrunnerpass";
|
||||||
msg = msg + nl + "--strict exit 1 on failure (default exits 0, for legacy callers)";
|
msg = msg + nl + "--strict exit 1 on failure (default exits 0, for legacy callers)";
|
||||||
msg = msg + nl + "--help info";
|
msg = msg + nl + "--help info";
|
||||||
@ -736,14 +736,85 @@ public class jrunner {
|
|||||||
case "tsv":
|
case "tsv":
|
||||||
outputTSV(rs, cols);
|
outputTSV(rs, cols);
|
||||||
break;
|
break;
|
||||||
case "table":
|
|
||||||
case "json":
|
case "json":
|
||||||
|
outputJSON(rs, cols);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
outputCSV(rs, cols);
|
outputCSV(rs, cols);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON output. Two things CSV cannot express, both of which callers need:
|
||||||
|
//
|
||||||
|
// * NULL vs empty string. outputCSV writes an empty field for both, so
|
||||||
|
// everything downstream — the merge, reconcile, the wizard — sees them
|
||||||
|
// as identical. Here NULL is JSON null and '' is "".
|
||||||
|
// * Column types. Without them the wizard's SQL-entry mode has to default
|
||||||
|
// every destination column to text, because a CSV stream carries no
|
||||||
|
// type metadata for the caller to map.
|
||||||
|
//
|
||||||
|
// Values are emitted as JSON strings rather than JSON numbers: a DECIMAL
|
||||||
|
// rendered as a JSON number would go through a float and lose exactness,
|
||||||
|
// and callers already treat query output as text. null is the one non-string.
|
||||||
|
//
|
||||||
|
// Written incrementally rather than assembled in memory, so a large result
|
||||||
|
// set streams the same way CSV does while still being one valid document.
|
||||||
|
private static void outputJSON(ResultSet rs, int cols) throws SQLException {
|
||||||
|
ResultSetMetaData md = rs.getMetaData();
|
||||||
|
StringBuilder head = new StringBuilder("{\"columns\":[");
|
||||||
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
if (i > 1) head.append(",");
|
||||||
|
head.append("{\"name\":\"").append(escapeJSON(md.getColumnName(i))).append("\"");
|
||||||
|
head.append(",\"type\":\"").append(escapeJSON(md.getColumnTypeName(i))).append("\"");
|
||||||
|
head.append(",\"precision\":").append(md.getPrecision(i));
|
||||||
|
head.append(",\"scale\":").append(md.getScale(i));
|
||||||
|
head.append("}");
|
||||||
|
}
|
||||||
|
head.append("],\"rows\":[");
|
||||||
|
System.out.print(head);
|
||||||
|
|
||||||
|
boolean firstRow = true;
|
||||||
|
while (rs.next()) {
|
||||||
|
if (!firstRow) System.out.print(",");
|
||||||
|
firstRow = false;
|
||||||
|
System.out.print("[");
|
||||||
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
if (i > 1) System.out.print(",");
|
||||||
|
String value = rs.getString(i);
|
||||||
|
if (rs.wasNull() || value == null) {
|
||||||
|
System.out.print("null");
|
||||||
|
} else {
|
||||||
|
System.out.print("\"" + escapeJSON(value) + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.print("]");
|
||||||
|
}
|
||||||
|
System.out.println("]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeJSON(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder(value.length() + 16);
|
||||||
|
for (int i = 0; i < value.length(); i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '"': sb.append("\\\""); break;
|
||||||
|
case '\\': sb.append("\\\\"); break;
|
||||||
|
case '\n': sb.append("\\n"); break;
|
||||||
|
case '\r': sb.append("\\r"); break;
|
||||||
|
case '\t': sb.append("\\t"); break;
|
||||||
|
case '\b': sb.append("\\b"); break;
|
||||||
|
case '\f': sb.append("\\f"); break;
|
||||||
|
default:
|
||||||
|
// Other control characters are illegal raw in JSON strings.
|
||||||
|
if (c < 0x20) sb.append(String.format("\\u%04x", (int) c));
|
||||||
|
else sb.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private static void outputCSV(ResultSet rs, int cols) throws SQLException {
|
private static void outputCSV(ResultSet rs, int cols) throws SQLException {
|
||||||
// Print header row
|
// Print header row
|
||||||
for (int i = 1; i <= cols; i++) {
|
for (int i = 1; i <= cols; i++) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user