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 + "-c clear target table";
|
||||
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 + "--strict exit 1 on failure (default exits 0, for legacy callers)";
|
||||
msg = msg + nl + "--help info";
|
||||
@ -736,14 +736,85 @@ public class jrunner {
|
||||
case "tsv":
|
||||
outputTSV(rs, cols);
|
||||
break;
|
||||
case "table":
|
||||
case "json":
|
||||
outputJSON(rs, cols);
|
||||
break;
|
||||
default:
|
||||
outputCSV(rs, cols);
|
||||
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 {
|
||||
// Print header row
|
||||
for (int i = 1; i <= cols; i++) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user