This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new f290065ed21 Add -outputFormat and -outputFile options to PostQuery CLI
command (#19610)
f290065ed21 is described below
commit f290065ed212f147292f6efa01720863faaef2f0
Author: Kazuki Kanda <[email protected]>
AuthorDate: Sat Oct 24 06:36:32 2026 +0900
Add -outputFormat and -outputFile options to PostQuery CLI command (#19610)
---
docker/images/pinot/Dockerfile | 2 +-
docker/images/pinot/Dockerfile.build | 2 +-
docker/images/pinot/Dockerfile.package | 2 +-
pinot-tools/pom.xml | 4 +
.../tools/admin/command/PostQueryCommand.java | 130 +++++++-
.../tools/admin/command/PostQueryCommandTest.java | 330 +++++++++++++++++++++
6 files changed, 466 insertions(+), 4 deletions(-)
diff --git a/docker/images/pinot/Dockerfile b/docker/images/pinot/Dockerfile
index 3d5d857c4a7..b121007ad9a 100644
--- a/docker/images/pinot/Dockerfile
+++ b/docker/images/pinot/Dockerfile
@@ -49,7 +49,7 @@ FROM apachepinot/pinot-base-runtime:${PINOT_BASE_IMAGE_TAG}
LABEL [email protected]
ENV PINOT_HOME=/opt/pinot
-ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=false"
+ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=true"
VOLUME ["${PINOT_HOME}/configs", "${PINOT_HOME}/data"]
diff --git a/docker/images/pinot/Dockerfile.build
b/docker/images/pinot/Dockerfile.build
index 0fe132a4b11..f82b415db69 100644
--- a/docker/images/pinot/Dockerfile.build
+++ b/docker/images/pinot/Dockerfile.build
@@ -44,7 +44,7 @@ RUN git clone https://github.com/apache/pinot.git
${PINOT_BUILD_DIR} && \
cp -r build/* ${PINOT_HOME}/. && \
chmod +x ${PINOT_HOME}/bin/*.sh
-ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=false"
+ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=true"
VOLUME ["${PINOT_HOME}/configs", "${PINOT_HOME}/data"]
diff --git a/docker/images/pinot/Dockerfile.package
b/docker/images/pinot/Dockerfile.package
index 2390adce39b..76b6d3053ba 100644
--- a/docker/images/pinot/Dockerfile.package
+++ b/docker/images/pinot/Dockerfile.package
@@ -23,7 +23,7 @@ FROM apachepinot/pinot-base-runtime:${PINOT_RUNTIME_IMAGE_TAG}
LABEL [email protected]
ENV PINOT_HOME=/opt/pinot
-ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=false"
+ENV JAVA_OPTS="-Xms4G -Xmx4G -Dpinot.admin.system.exit=true"
VOLUME ["${PINOT_HOME}/configs", "${PINOT_HOME}/data"]
diff --git a/pinot-tools/pom.xml b/pinot-tools/pom.xml
index fff7a755564..57827e8d0a2 100644
--- a/pinot-tools/pom.xml
+++ b/pinot-tools/pom.xml
@@ -69,6 +69,10 @@
<groupId>org.apache.pinot</groupId>
<artifactId>pinot-csv</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-csv</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.pinot</groupId>
<artifactId>pinot-protobuf</artifactId>
diff --git
a/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/PostQueryCommand.java
b/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/PostQueryCommand.java
index d0e281132c1..0c5d715ad45 100644
---
a/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/PostQueryCommand.java
+++
b/pinot-tools/src/main/java/org/apache/pinot/tools/admin/command/PostQueryCommand.java
@@ -18,8 +18,17 @@
*/
package org.apache.pinot.tools.admin.command;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.Map;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
import org.apache.pinot.common.auth.AuthProviderUtils;
import org.apache.pinot.spi.auth.AuthProvider;
import org.apache.pinot.spi.utils.CommonConstants;
@@ -36,6 +45,12 @@ import picocli.CommandLine;
public class PostQueryCommand extends AbstractBaseAdminCommand implements
Command {
private static final Logger LOGGER =
LoggerFactory.getLogger(PostQueryCommand.class.getName());
+ /// Supported output formats for the query response: `JSON` (default, raw
broker response) or
+ /// `CSV` (rendered from `resultTable`; see `formatResponse` for fallback
behavior).
+ public enum OutputFormat {
+ JSON, CSV
+ }
+
@CommandLine.Option(names = {"-brokerHost"}, required = false, description =
"host name for broker.")
private String _brokerHost;
@@ -63,8 +78,22 @@ public class PostQueryCommand extends
AbstractBaseAdminCommand implements Comman
@CommandLine.Option(names = {"-o", "-option"}, required = false, description
= "Additional options '-o key=value'")
private Map<String, String> _additionalOptions = new HashMap<>();
+ @CommandLine.Option(names = {"-outputFormat"}, required = false,
+ description = "Output format for the query response: JSON (default) or
CSV (upper-case).")
+ private OutputFormat _outputFormat = OutputFormat.JSON;
+
+ @CommandLine.Option(names = {"-outputFile"}, required = false,
+ description = "File path to write the query response to. If omitted, the
response is only "
+ + "logged (pre-existing behavior).")
+ private String _outputFile;
+
private AuthProvider _authProvider;
+ /// Whether the most recent `formatResponse()` result is safe to write to
`_outputFile` in the
+ /// requested format. This is `false` when CSV formatting falls back to the
raw response or when
+ /// the CSV would omit response-level failure metadata such as exceptions or
`partialResult`.
+ private boolean _lastResponseSafeForOutputFile = true;
+
@Override
public String getName() {
return "PostQuery";
@@ -125,6 +154,16 @@ public class PostQueryCommand extends
AbstractBaseAdminCommand implements Comman
return this;
}
+ public PostQueryCommand setOutputFormat(OutputFormat outputFormat) {
+ _outputFormat = outputFormat;
+ return this;
+ }
+
+ public PostQueryCommand setOutputFile(String outputFile) {
+ _outputFile = outputFile;
+ return this;
+ }
+
public String run()
throws Exception {
if (_brokerHost == null) {
@@ -138,8 +177,86 @@ public class PostQueryCommand extends
AbstractBaseAdminCommand implements Comman
payload.putAll(_additionalOptions);
}
String request = JsonUtils.objectToString(payload);
- return sendRequest("POST", url, request, AuthProviderUtils.makeAuthHeaders(
+ String response = sendRequest("POST", url, request,
AuthProviderUtils.makeAuthHeaders(
AuthProviderUtils.makeAuthProvider(_authProvider, _authTokenUrl,
_authToken, _user, _password)));
+ return formatResponse(response);
+ }
+
+ /// Renders the raw broker response according to `_outputFormat`. Never
throws for a
+ /// CSV-incompatible response: falls back to the raw response whenever the
response isn't valid
+ /// JSON, or has no `resultTable` (e.g. the query errored out), so no error
detail is ever lost
+ /// from the logged/returned string. Marks the response unsafe for file
output whenever this
+ /// fallback happens, so `execute()` can refuse to write a non-CSV body to
`-outputFile`
+ /// when CSV was requested. A partial response is still rendered for
logging, but is marked
+ /// unsafe for file output because CSV cannot preserve its response-level
diagnostics.
+ String formatResponse(String rawResponse) {
+ _lastResponseSafeForOutputFile = true;
+ if (_outputFormat != OutputFormat.CSV) {
+ return rawResponse;
+ }
+ JsonNode root;
+ try {
+ root = JsonUtils.stringToJsonNode(rawResponse);
+ } catch (IOException e) {
+ LOGGER.warn("Response is not valid JSON (e.g. a broker/proxy error
page); "
+ + "falling back to the raw response instead of CSV.", e);
+ _lastResponseSafeForOutputFile = false;
+ return rawResponse;
+ }
+ JsonNode resultTable = root.get("resultTable");
+ if (resultTable == null || resultTable.isNull()) {
+ LOGGER.warn("Response has no 'resultTable' (e.g. the query may have
errored out); "
+ + "falling back to JSON output instead of CSV.");
+ _lastResponseSafeForOutputFile = false;
+ return rawResponse;
+ }
+ JsonNode exceptions = root.path("exceptions");
+ if (exceptions.size() > 0 || root.path("partialResult").asBoolean(false)) {
+ LOGGER.warn("Response has a 'resultTable' but also reports exceptions
and/or a partial "
+ + "result; CSV output only renders resultTable rows, so this detail
is not reflected "
+ + "in the CSV. Full broker response: {}", rawResponse);
+ _lastResponseSafeForOutputFile = false;
+ }
+ JsonNode columnNames = resultTable.path("dataSchema").path("columnNames");
+ JsonNode rows = resultTable.path("rows");
+ StringWriter stringWriter = new StringWriter();
+ try (CSVPrinter csvPrinter = new CSVPrinter(stringWriter,
CSVFormat.DEFAULT)) {
+ for (JsonNode columnName : columnNames) {
+ csvPrinter.print(columnName.asText());
+ }
+ csvPrinter.println();
+ for (JsonNode row : rows) {
+ Iterator<JsonNode> cells = row.elements();
+ while (cells.hasNext()) {
+ JsonNode cell = cells.next();
+ // SQL NULL (JSON null) must render as an empty CSV field, matching
Pinot CLI's
+ // existing CSV output convention. Without this, a null cell and a
STRING cell holding
+ // the literal text "null" would be indistinguishable in the export.
Note:
+ // csvPrinter.print(null) (not print("")) is used here for
correctness in the general
+ // case: for a *leading* column, commons-csv's MINIMAL quote mode
quotes an explicit
+ // empty string as `""` (to keep it distinguishable from a
null/omitted leading field)
+ // but leaves print(null) unquoted, so only print(null) is
guaranteed to produce an
+ // unquoted empty field regardless of column position. (For
non-leading columns,
+ // print("") and print(null) already produce the same unquoted
output; a STRING cell
+ // holding an actual empty string in a non-leading column remains
indistinguishable
+ // from NULL either way -- a pre-existing, unrelated ambiguity this
change does not
+ // introduce or attempt to fix.)
+ if (cell.isNull()) {
+ csvPrinter.print(null);
+ } else {
+ csvPrinter.print(cell.isTextual() ? cell.asText() :
cell.toString());
+ }
+ }
+ csvPrinter.println();
+ }
+ } catch (IOException e) {
+ // CSVPrinter only throws IOException for the underlying Appendable; a
StringWriter never
+ // throws, so this is unreachable in practice. Fall back to JSON rather
than propagate.
+ LOGGER.warn("Unexpected error rendering CSV; falling back to JSON
output.", e);
+ _lastResponseSafeForOutputFile = false;
+ return rawResponse;
+ }
+ return stringWriter.toString();
}
@Override
@@ -147,6 +264,17 @@ public class PostQueryCommand extends
AbstractBaseAdminCommand implements Comman
throws Exception {
String result = run();
LOGGER.info("Result: {}", result);
+ if (_outputFile != null) {
+ if (_outputFormat == OutputFormat.CSV &&
!_lastResponseSafeForOutputFile) {
+ // The response either could not be rendered as CSV or the CSV would
omit failure metadata.
+ // Refuse the write so a file consumer cannot mistake fallback JSON or
incomplete rows for
+ // a successful CSV export. The failure details remain visible in the
logs.
+ LOGGER.warn("Not writing '{}': response is not safe for CSV file
output. Use "
+ + "-outputFormat JSON to inspect the full response.", _outputFile);
+ return false;
+ }
+ Files.write(new File(_outputFile).toPath(),
result.getBytes(StandardCharsets.UTF_8));
+ }
return true;
}
}
diff --git
a/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/PostQueryCommandTest.java
b/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/PostQueryCommandTest.java
new file mode 100644
index 00000000000..349f83eb929
--- /dev/null
+++
b/pinot-tools/src/test/java/org/apache/pinot/tools/admin/command/PostQueryCommandTest.java
@@ -0,0 +1,330 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.tools.admin.command;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import picocli.CommandLine;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+
+public class PostQueryCommandTest {
+ private static final String JSON_RESPONSE_WITH_RESULT_TABLE =
+ "{\"resultTable\":{\"dataSchema\":{\"columnNames\":[\"col1\",\"col2\"],"
+ +
"\"columnDataTypes\":[\"STRING\",\"LONG\"]},\"rows\":[[\"a\",1],[\"b,c\",2]]},"
+ + "\"exceptions\":[],\"numRowsResultSet\":2}";
+
+ private static final String JSON_RESPONSE_WITH_NULL_AND_LITERAL_NULL_STRING =
+ "{\"resultTable\":{\"dataSchema\":{\"columnNames\":[\"col1\",\"col2\"],"
+ +
"\"columnDataTypes\":[\"STRING\",\"STRING\"]},\"rows\":[[null,\"null\"]]},"
+ + "\"exceptions\":[],\"numRowsResultSet\":1}";
+
+ private static final String JSON_RESPONSE_WITHOUT_RESULT_TABLE =
+
"{\"exceptions\":[{\"errorCode\":150,\"message\":\"QueryExecutionError\"}]}";
+
+ private static final String JSON_RESPONSE_WITH_RESULT_TABLE_AND_EXCEPTIONS =
+
"{\"resultTable\":{\"dataSchema\":{\"columnNames\":[\"col1\"],\"columnDataTypes\":[\"STRING\"]},"
+ +
"\"rows\":[[\"a\"]]},\"exceptions\":[{\"errorCode\":200,\"message\":\"partial
failure\"}]}";
+
+ private static final String JSON_RESPONSE_WITH_PARTIAL_RESULT =
+
"{\"resultTable\":{\"dataSchema\":{\"columnNames\":[\"col1\"],\"columnDataTypes\":[\"STRING\"]},"
+ + "\"rows\":[[\"a\"]]},\"exceptions\":[],\"partialResult\":true}";
+
+ private static final String NOT_JSON_RESPONSE = "<html><body>502 Bad
Gateway</body></html>";
+
+ // A dedicated per-test temp directory, not the shared system temp dir.
Pinot CI runs unit-test
+ // classes in three parallel forks, and other pinot-tools tests also create
files under the
+ // shared system temp dir; snapshotting that shared directory would make
file-count assertions
+ // flaky. This directory is created fresh in setUp() and is only ever
touched by this test.
+ private File _tempDir;
+ private File _tempOutputFile;
+
+ @BeforeMethod
+ public void setUp()
+ throws Exception {
+ _tempDir =
Files.createTempDirectory("post-query-command-test-dir-").toFile();
+ _tempOutputFile = File.createTempFile("post-query-command-test-", ".out",
_tempDir);
+ }
+
+ @AfterMethod
+ public void tearDown() {
+ if (_tempDir != null) {
+ File[] files = _tempDir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ file.delete();
+ }
+ }
+ _tempDir.delete();
+ }
+ }
+
+ @Test
+ public void testFormatResponseDefaultIsJson() {
+ PostQueryCommand command = new PostQueryCommand();
+ String formatted = command.formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE);
+ assertEquals(formatted, JSON_RESPONSE_WITH_RESULT_TABLE);
+ }
+
+ @Test
+ public void testFormatResponseCsvRendersResultTable() {
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String csv = command.formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE);
+ String[] lines = csv.split("\r\n");
+ assertEquals(lines.length, 3);
+ assertEquals(lines[0], "col1,col2");
+ assertEquals(lines[1], "a,1");
+ // A comma-containing cell value must be quoted by CSV rendering, not
silently break columns.
+ assertEquals(lines[2], "\"b,c\",2");
+ }
+
+ @Test
+ public void
testFormatResponseCsvRendersSqlNullAsEmptyFieldDistinctFromLiteralNullString() {
+ // A JSON null cell (SQL NULL) must render as an empty CSV field, matching
Pinot CLI's
+ // existing convention -- distinct from a STRING cell that literally
contains "null".
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String csv =
command.formatResponse(JSON_RESPONSE_WITH_NULL_AND_LITERAL_NULL_STRING);
+ String[] lines = csv.split("\r\n");
+ assertEquals(lines.length, 2);
+ assertEquals(lines[0], "col1,col2");
+ assertEquals(lines[1], ",null");
+ }
+
+ @Test
+ public void testFormatResponseCsvFallsBackToJsonWithoutResultTable() {
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String formatted =
command.formatResponse(JSON_RESPONSE_WITHOUT_RESULT_TABLE);
+ assertEquals(formatted, JSON_RESPONSE_WITHOUT_RESULT_TABLE);
+ }
+
+ @Test
+ public void testFormatResponseCsvFallsBackToRawResponseWhenNotJson() {
+ // e.g. a proxy/broker error page that isn't valid JSON at all -- must not
throw.
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String formatted = command.formatResponse(NOT_JSON_RESPONSE);
+ assertEquals(formatted, NOT_JSON_RESPONSE);
+ }
+
+ @Test
+ public void
testFormatResponseCsvWithResultTableAndExceptionsStillRendersCsv() {
+ // A resultTable can coexist with a non-empty 'exceptions' list (partial
failure). CSV mode
+ // should still render the table (rather than silently discarding it)
while separately
+ // warning that the CSV doesn't carry the exception detail.
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String csv =
command.formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE_AND_EXCEPTIONS);
+ assertEquals(csv, "col1\r\na\r\n");
+ }
+
+ @Test
+ public void testFormatResponseCsvWithPartialResultStillRendersCsv() {
+ PostQueryCommand command = new PostQueryCommand();
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ String csv = command.formatResponse(JSON_RESPONSE_WITH_PARTIAL_RESULT);
+ assertEquals(csv, "col1\r\na\r\n");
+ }
+
+ @Test
+ public void testCliParsingDefaultOutputFormatIsJson() {
+ PostQueryCommand command = new PostQueryCommand();
+ new CommandLine(command).parseArgs("-query", "select 1");
+ assertEquals(command.formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE),
JSON_RESPONSE_WITH_RESULT_TABLE);
+ }
+
+ @Test
+ public void testCliParsingOutputFormatCsv() {
+ PostQueryCommand command = new PostQueryCommand();
+ new CommandLine(command).parseArgs("-query", "select 1", "-outputFormat",
"CSV");
+ String csv = command.formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE);
+ assertTrue(csv.startsWith("col1,col2"));
+ }
+
+ @Test
+ public void testExecuteWritesFormattedResponseToOutputFile()
+ throws Exception {
+ PostQueryCommand command = new PostQueryCommand() {
+ @Override
+ public String run()
+ throws Exception {
+ // Skip the real broker HTTP call; exercise execute()'s output-file
writing with a
+ // canned CSV-formatted response instead.
+ return "col1,col2\r\na,1\r\n";
+ }
+ };
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+ command.execute();
+ String written = new String(Files.readAllBytes(_tempOutputFile.toPath()),
StandardCharsets.UTF_8);
+ assertEquals(written, "col1,col2\r\na,1\r\n");
+ }
+
+ @Test
+ public void
testExecuteDoesNotWriteOutputFileWhenCsvRequestedButResponseNotRenderableAsCsv()
+ throws Exception {
+ // execute() must not let a file consumer mistake a raw JSON/error
fallback body for a
+ // successful CSV export: when -outputFormat CSV was requested but
formatResponse() fell back
+ // (e.g. broker error response with no resultTable), execute() must report
failure and must
+ // not write -outputFile at all.
+ assertTrue(_tempOutputFile.delete());
+ PostQueryCommand command = new PostQueryCommand() {
+ @Override
+ public String run() {
+ // formatResponse() is invoked for real here (not overridden), so the
CSV-fallback path
+ // and _lastResponseSafeForOutputFile bookkeeping are exercised
end-to-end.
+ return formatResponse(JSON_RESPONSE_WITHOUT_RESULT_TABLE);
+ }
+ };
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+ boolean result = command.execute();
+ assertFalse(result);
+ assertFalse(_tempOutputFile.exists());
+ }
+
+ @Test
+ public void testExecuteDoesNotWriteCsvFileForRowsWithExceptions()
+ throws Exception {
+ assertTrue(_tempOutputFile.delete());
+ PostQueryCommand command =
commandReturning(JSON_RESPONSE_WITH_RESULT_TABLE_AND_EXCEPTIONS);
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+
+ assertFalse(command.execute());
+ assertFalse(_tempOutputFile.exists());
+ }
+
+ @Test
+ public void testExecuteDoesNotWriteCsvFileForPartialResult()
+ throws Exception {
+ assertTrue(_tempOutputFile.delete());
+ PostQueryCommand command =
commandReturning(JSON_RESPONSE_WITH_PARTIAL_RESULT);
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+
+ assertFalse(command.execute());
+ assertFalse(_tempOutputFile.exists());
+ }
+
+ @Test
+ public void testExecuteRejectedCsvLeavesExistingOutputFileUntouched()
+ throws Exception {
+ String sentinel = "previous successful export";
+ Files.write(_tempOutputFile.toPath(),
sentinel.getBytes(StandardCharsets.UTF_8));
+ PostQueryCommand command =
commandReturning(JSON_RESPONSE_WITH_PARTIAL_RESULT);
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+
+ assertFalse(command.execute());
+ String contents = new String(Files.readAllBytes(_tempOutputFile.toPath()),
StandardCharsets.UTF_8);
+ assertEquals(contents, sentinel);
+ }
+
+ @Test
+ public void
testExecuteWritesCsvFormattedResponseToOutputFileOnSuccessfulCsvRendering()
+ throws Exception {
+ // End-to-end coverage for the success path that the CSV-fallback-failure
test above does not
+ // exercise: -outputFormat CSV + -outputFile, with a response that CAN be
rendered as CSV,
+ // must both report success and write the actual CSV-rendered body (not
the raw JSON) to disk.
+ assertTrue(_tempOutputFile.delete());
+ PostQueryCommand command = new PostQueryCommand() {
+ @Override
+ public String run() {
+ // formatResponse() is invoked for real here, so the CSV success path
and
+ // _lastResponseSafeForOutputFile bookkeeping are exercised
end-to-end, matching the
+ // fallback-failure test's style above.
+ return formatResponse(JSON_RESPONSE_WITH_RESULT_TABLE);
+ }
+ };
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+ boolean result = command.execute();
+ assertTrue(result);
+ String written = new String(Files.readAllBytes(_tempOutputFile.toPath()),
StandardCharsets.UTF_8);
+ assertEquals(written, "col1,col2\r\na,1\r\n\"b,c\",2\r\n");
+ }
+
+ @Test
+ public void testExecuteWithoutOutputFileDoesNotWriteAnyFile()
+ throws Exception {
+ // Regression guard: execute() must not write to any file when -outputFile
was never set.
+ // Uses a dedicated per-test temp directory (see setUp()) rather than the
shared system temp
+ // dir, so a file created/removed by an unrelated parallel test can't make
this flaky.
+ String[] filesBefore = _tempDir.list();
+ PostQueryCommand command = new PostQueryCommand() {
+ @Override
+ public String run()
+ throws Exception {
+ return "col1,col2\r\na,1\r\n";
+ }
+ };
+ command.execute();
+ String[] filesAfter = _tempDir.list();
+ assertEquals(filesAfter, filesBefore);
+ }
+
+ @Test
+ public void testExecuteRecomputesCsvFileSafetyForEachResponseOnSameInstance()
+ throws Exception {
+ assertTrue(_tempOutputFile.delete());
+ String[] responses = {
+ JSON_RESPONSE_WITH_PARTIAL_RESULT,
+ JSON_RESPONSE_WITH_RESULT_TABLE,
+ JSON_RESPONSE_WITH_RESULT_TABLE_AND_EXCEPTIONS
+ };
+ AtomicInteger responseIndex = new AtomicInteger();
+ PostQueryCommand command = new PostQueryCommand() {
+ @Override
+ public String run() {
+ return formatResponse(responses[responseIndex.getAndIncrement()]);
+ }
+ };
+ command.setOutputFormat(PostQueryCommand.OutputFormat.CSV);
+ command.setOutputFile(_tempOutputFile.getAbsolutePath());
+
+ assertFalse(command.execute());
+ assertFalse(_tempOutputFile.exists());
+
+ assertTrue(command.execute());
+ String successfulCsv = "col1,col2\r\na,1\r\n\"b,c\",2\r\n";
+ assertEquals(Files.readString(_tempOutputFile.toPath(),
StandardCharsets.UTF_8), successfulCsv);
+
+ assertFalse(command.execute());
+ assertEquals(Files.readString(_tempOutputFile.toPath(),
StandardCharsets.UTF_8), successfulCsv);
+ }
+
+ private static PostQueryCommand commandReturning(String rawResponse) {
+ return new PostQueryCommand() {
+ @Override
+ public String run() {
+ return formatResponse(rawResponse);
+ }
+ };
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]