You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by cr...@apache.org on 2026/10/22 07:29:46 UTC
(iotdb) 01/01: Add measurement files, time ranges, progress and logs to export-data
This is an automated email from the ASF dual-hosted git repository.
CritasWang pushed a commit to branch wx/plm-doc-861
in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit edf4484904e034ce2d70a10fcfbbd318897c0adf
Author: CritasWang <cr...@outlook.com>
AuthorDate: Thu Oct 22 15:29:17 2026 +0800
Add measurement files, time ranges, progress and logs to export-data
- -mf/--measurement_file exports the measurements matched by the IoTDB path
patterns in a file, one output per measurement named by its full path.
The whole file is validated before any query, and each unmatched pattern
is reported with its line.
- -start_time/-end_time form an inclusive interval for measurement-file and
automatic Table exports. Queries run as written and report the ignored
bounds as a WARNING.
- Progress is reported every two seconds: one redrawn line in a terminal,
one line per update otherwise, with an ETA in measurement-file mode.
- Every run appends a JSON Lines log, <target>/log_export_data_<time>.log
by default or the path given with -l/--export_data_log, ending with one
FINISH event. The console ends with the summary, after the log path.
- CSV and SQL files are split by data rows; the INSERT statements of one
row stay in one file.
- Ctrl+C and SIGTERM stop at the next row, close the query and the session
and exit with 130 or 143; failures exit with 1.
- A Table query exported as SQL or TsFile still writes to the table it
reads, as in #18828; -table is ignored with a WARNING, and every query is
checked before the first one runs.
---
.../iotdb/tools/it/ExportDataMeasurementIT.java | 505 ++++++++++++++
.../en/org/apache/iotdb/cli/i18n/CliMessages.java | 141 ++++
.../zh/org/apache/iotdb/cli/i18n/CliMessages.java | 141 ++++
.../org/apache/iotdb/tool/common/Constants.java | 5 +
.../org/apache/iotdb/tool/common/OptionsUtil.java | 98 ++-
.../apache/iotdb/tool/data/AbstractExportData.java | 187 ++++--
.../org/apache/iotdb/tool/data/ExportConfig.java | 363 +++++++++++
.../org/apache/iotdb/tool/data/ExportConsole.java | 137 ++++
.../org/apache/iotdb/tool/data/ExportData.java | 526 +++++++--------
.../apache/iotdb/tool/data/ExportDataTable.java | 635 +++++++++---------
.../org/apache/iotdb/tool/data/ExportDataTree.java | 709 ++++++++++----------
.../apache/iotdb/tool/data/ExportFileNames.java | 114 ++++
.../iotdb/tool/data/ExportProgressReporter.java | 638 ++++++++++++++++++
.../org/apache/iotdb/tool/data/ExportTask.java | 31 +
.../apache/iotdb/tool/data/ExportTextWriter.java | 126 ++++
.../apache/iotdb/tool/data/ExportTimeRange.java | 84 +++
.../apache/iotdb/tool/data/MeasurementFile.java | 145 ++++
.../iotdb/tool/data/DataToolRegressionTest.java | 293 ++++++---
.../iotdb/tool/data/ExportDataFeatureTest.java | 726 +++++++++++++++++++++
.../iotdb/tool/data/ExportDataRegressionTest.java | 300 +++++++++
scripts/tools/README.md | 130 ++++
scripts/tools/export-data.sh | 18 +-
scripts/tools/windows/export-data.bat | 6 +-
23 files changed, 4901 insertions(+), 1157 deletions(-)
diff --git a/integration-test/src/test/java/org/apache/iotdb/tools/it/ExportDataMeasurementIT.java b/integration-test/src/test/java/org/apache/iotdb/tools/it/ExportDataMeasurementIT.java
new file mode 100644
index 00000000000..37b869901a5
--- /dev/null
+++ b/integration-test/src/test/java/org/apache/iotdb/tools/it/ExportDataMeasurementIT.java
@@ -0,0 +1,505 @@
+/*
+ * 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.iotdb.tools.it;
+
+import org.apache.iotdb.isession.ISession;
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.AfterClass;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/** Runs the actual export command and verifies files, exit status and round trips. */
+@RunWith(IoTDBTestRunner.class)
+@Category({LocalStandaloneIT.class, ClusterIT.class})
+public class ExportDataMeasurementIT {
+ @Rule public TemporaryFolder folder = new TemporaryFolder();
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ EnvFactory.getEnv().initClusterEnvironment();
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ EnvFactory.getEnv().cleanClusterEnvironment();
+ }
+
+ @Test
+ public void csvPatternsSplitsPrefixTimeAndEmptyMeasurements() throws Exception {
+ try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+ session.executeNonQueryStatement(
+ "CREATE TIMESERIES root.export_csv.d.pre1 WITH DATATYPE=INT32");
+ session.executeNonQueryStatement(
+ "CREATE TIMESERIES root.export_csv.d.pre2 WITH DATATYPE=TEXT");
+ session.executeNonQueryStatement(
+ "CREATE TIMESERIES root.export_csv.d.empty WITH DATATYPE=INT32");
+ for (int t = 100; t <= 300; t += 100) {
+ session.executeNonQueryStatement(
+ "INSERT INTO root.export_csv.d(time,pre1,pre2) VALUES(" + t + "," + t + ",'温度,计')");
+ }
+ }
+ Path input =
+ file(
+ "\uFEFF# patterns\nroot.export_csv.d.pre*\nroot.export_csv.*.*\nroot.export_csv.**\nroot.missing.**\nroot.export_csv.d.pre*\n");
+ Path target = folder.newFolder("csv output").toPath();
+ Result result =
+ export(
+ target,
+ "-ft",
+ "csv",
+ "-mf",
+ input.toString(),
+ "-pfn",
+ "backup",
+ "-lpf",
+ "1",
+ "-tf",
+ "timestamp",
+ "-start_time",
+ "100",
+ "-end_time",
+ "200");
+ assertSuccess(result);
+ assertEquals(
+ "Time,root.export_csv.d.pre1\r\n100,100\r\n",
+ Files.readString(target.resolve("backup_root.export_csv.d.pre1_0.csv")));
+ assertEquals(
+ "Time,root.export_csv.d.pre1\r\n200,200\r\n",
+ Files.readString(target.resolve("backup_root.export_csv.d.pre1_1.csv")));
+ assertEquals(4, outputs(target, ".csv").size());
+ String log = log(target);
+ assertTrue(log, log.contains(member("total_measurement_num", 3)));
+ assertTrue(log, log.contains(member("exported_measurement_num", 3)));
+ assertTrue(log, log.contains(member("zero_row_tasks", 1)));
+ assertTrue(log, log.contains(member("exported_percent", "100.00%")));
+ assertTrue(log, log.contains(member("event", "WARNING")));
+ assertTrue(log, log.contains(member("line", 5)));
+ assertTrue(log, log.contains(member("exported_data_rows", 4)));
+ // The console is for people: the warning and the summary, never the JSON events.
+ assertTrue(
+ result.text,
+ result
+ .text
+ .lines()
+ .anyMatch(
+ "WARNING: No measurements matched root.missing.** (measurement file line 5)."
+ ::equals));
+ assertTrue(
+ result.text,
+ result.text.contains("Export completely! Measurements: 3/3, rows: 4, files: 4, elapsed: "));
+ assertFalse(result.text, result.text.contains("{\""));
+ }
+
+ @Test
+ public void sqlAndTsFileRoundTripAlignedTypesAndQuotedPaths() throws Exception {
+ for (String format : Arrays.asList("sql", "tsfile")) {
+ String database = "root.export_" + format;
+ String d = database + ".d";
+ try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+ session.executeNonQueryStatement(
+ "CREATE ALIGNED TIMESERIES "
+ + d
+ + "(s INT32,txt TEXT,stamp TIMESTAMP,day DATE,blob BLOB,`a.b` STRING)");
+ session.executeNonQueryStatement(
+ "CREATE TIMESERIES " + database + ".plain.n WITH DATATYPE=DOUBLE");
+ session.executeNonQueryStatement(
+ "INSERT INTO "
+ + d
+ + "(time,s,txt,stamp,day,blob,`a.b`) ALIGNED VALUES(100,10,'温度',123,'2026-10-14',X'CAFE','quoted'),(200,20,'温度2',456,'2026-10-15',X'BEEF','quoted2'),(300,30,'外部',789,'2026-10-16',X'ABCD','outside')");
+ session.executeNonQueryStatement(
+ "INSERT INTO " + database + ".plain(time,n) VALUES(100,1.5),(200,2.5),(300,3.5)");
+ }
+ Path target = folder.newFolder(format).toPath();
+ Path input = file(database + ".**\n");
+ assertSuccess(
+ export(
+ target,
+ "-ft",
+ format,
+ "-mf",
+ input.toString(),
+ "-start_time",
+ "100",
+ "-end_time",
+ "200"));
+ List<Path> files = outputs(target, "." + format);
+ assertEquals(7, files.size());
+ try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+ session.executeNonQueryStatement(
+ format.equals("sql")
+ ? "DELETE FROM " + database + ".** WHERE time >= 0"
+ : "DELETE DATABASE " + database);
+ for (Path path : files) {
+ if (format.equals("sql")) {
+ for (String sql : Files.readAllLines(path)) {
+ session.executeNonQueryStatement(sql);
+ }
+ } else {
+ session.executeNonQueryStatement(
+ "LOAD '" + path.toAbsolutePath() + "' WITH ('verify'='true')");
+ }
+ }
+ try (SessionDataSet data =
+ session.executeQueryStatement("SELECT s,txt,stamp,day,blob,`a.b` FROM " + d)) {
+ SessionDataSet.DataIterator it = data.iterator();
+ assertTrue(it.next());
+ assertEquals(100, it.getLong(1));
+ assertEquals(10, it.getInt(2));
+ assertEquals("温度", it.getString(3));
+ assertEquals(123, it.getLong(4));
+ assertEquals("2026-10-14", it.getString(5));
+ assertEquals("0xcafe", it.getString(6).toLowerCase());
+ assertEquals("quoted", it.getString(7));
+ assertTrue(it.next());
+ assertEquals(200, it.getLong(1));
+ assertEquals(20, it.getInt(2));
+ assertFalse(it.next());
+ }
+ try (SessionDataSet data =
+ session.executeQueryStatement("SELECT n FROM " + database + ".plain")) {
+ assertTrue(data.hasNext());
+ assertEquals(1.5, data.next().getField(0).getDoubleV(), 0.0);
+ assertTrue(data.hasNext());
+ assertEquals(2.5, data.next().getField(0).getDoubleV(), 0.0);
+ assertFalse(data.hasNext());
+ }
+ }
+ }
+ }
+
+ @Test
+ public void originalQueryAndTableModesAndFailures() throws Exception {
+ try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+ session.executeNonQueryStatement(
+ "INSERT INTO root.export_query.d(time,s) VALUES(100,10),(200,20)");
+ }
+ Path query = folder.newFolder("query").toPath();
+ assertSuccess(
+ export(
+ query,
+ "-ft",
+ "csv",
+ "-q",
+ "SELECT s FROM root.export_query.d",
+ "-tf",
+ "timestamp",
+ "-start_time",
+ "200"));
+ assertTrue(Files.readString(query.resolve("dump0_0.csv")).contains("100,10"));
+ String queryLog = log(query);
+ assertFalse(queryLog.contains("exported_measurement_num"));
+ // The query runs as written, so the ignored time range is a warning rather than a START field.
+ assertTrue(queryLog, queryLog.contains(member("event", "WARNING")));
+ assertTrue(
+ queryLog,
+ queryLog
+ .lines()
+ .filter(line -> line.contains(member("event", "START")))
+ .noneMatch(line -> line.contains("\"start_time\":")));
+ Path failure = folder.newFolder("failed").toPath();
+ Result badQuery = export(failure, "-ft", "csv", "-q", "invalid sql");
+ assertNotEquals(0, badQuery.code);
+ assertTrue(badQuery.text, badQuery.text.contains("Export failed after "));
+ assertFalse(badQuery.text, badQuery.text.contains("Export completely!"));
+ assertTrue(log(failure).contains(member("status", "FAILED")));
+ assertFalse(log(failure).contains(member("status", "SUCCESS")));
+ Path empty = folder.newFolder("unmatched").toPath();
+ assertNotEquals(
+ 0, export(empty, "-ft", "csv", "-mf", file("root.no_measurements.**\n").toString()).code);
+ assertNotEquals(
+ 0, export(folder.newFolder("alias").toPath(), "-ft", "csv", "-f", "backup").code);
+ assertNotEquals(
+ 0,
+ export(
+ folder.newFolder("badfile").toPath(),
+ "-ft",
+ "csv",
+ "-mf",
+ file("root.export_query.d.s\ninvalid..path\n").toString())
+ .code);
+ try (ITableSession session = EnvFactory.getEnv().getTableSessionConnection()) {
+ session.executeNonQueryStatement("CREATE DATABASE export_table");
+ session.executeNonQueryStatement("USE export_table");
+ session.executeNonQueryStatement(
+ "CREATE TABLE metrics(id STRING TAG, v INT32 FIELD, stamp TIMESTAMP FIELD, day DATE FIELD, blob BLOB FIELD)");
+ session.executeNonQueryStatement(
+ "INSERT INTO metrics(time,id,v,stamp,day,blob) VALUES(100,'d1',10,123,'2026-10-14',X'CAFE'),(200,'d1',20,456,'2026-10-15',X'BEEF'),(300,'d1',30,789,'2026-10-16',X'ABCD')");
+ }
+ for (String format : Arrays.asList("csv", "sql", "tsfile")) {
+ Path target = folder.newFolder("table_" + format).toPath();
+ assertSuccess(
+ export(
+ target,
+ "-sql_dialect",
+ "table",
+ "-db",
+ "export_table",
+ "-ft",
+ format,
+ "-start_time",
+ "100",
+ "-end_time",
+ "200"));
+ assertTrue(log(target).contains(member("exported_data_rows", 2)));
+ assertFalse(log(target).contains("exported_measurement_num"));
+ assertEquals(1, outputs(target, "." + format).size());
+ // SQL and TsFile output take the table from the query.
+ Path custom = folder.newFolder("table_query_" + format).toPath();
+ assertSuccess(
+ export(
+ custom,
+ "-sql_dialect",
+ "table",
+ "-db",
+ "export_table",
+ "-ft",
+ format,
+ "-q",
+ "SELECT * FROM metrics WHERE time=100"));
+ assertTrue(log(custom).contains(member("exported_data_rows", 1)));
+ }
+ assertNotEquals(
+ 0,
+ export(
+ folder.newFolder("table_mf").toPath(),
+ "-sql_dialect",
+ "table",
+ "-db",
+ "export_table",
+ "-ft",
+ "csv",
+ "-mf",
+ file("root.**\n").toString())
+ .code);
+ }
+
+ @Test
+ public void cancellationHasOneTerminalEvent() throws Exception {
+ org.junit.Assume.assumeFalse(System.getProperty("os.name").toLowerCase().startsWith("windows"));
+ StringBuilder patterns = new StringBuilder();
+ for (int i = 0; i < 10000; i++) {
+ patterns.append("root.cancel.d").append(i).append(".s\n");
+ }
+ Path input = file(patterns.toString());
+ Path target = folder.newFolder("cancelled").toPath();
+ Path console = folder.newFile().toPath();
+ List<String> command = command(target, "-ft", "csv", "-mf", input.toString());
+ Process process =
+ new ProcessBuilder(command)
+ .redirectErrorStream(true)
+ .redirectOutput(console.toFile())
+ .start();
+ try {
+ long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
+ while (!Files.readString(console).contains("Export started")
+ && System.nanoTime() < deadline
+ && process.isAlive()) {
+ Thread.sleep(25);
+ }
+ assertTrue(Files.readString(console), process.isAlive());
+ process.destroy();
+ assertTrue(process.waitFor(30, TimeUnit.SECONDS));
+ String output = Files.readString(console);
+ assertEquals(output, 1, output.split("Export cancelled after ", -1).length - 1);
+ assertFalse(output, output.contains("Export completely!"));
+ String log = log(target);
+ assertEquals(log, 1, log.split(member("event", "FINISH"), -1).length - 1);
+ assertTrue(log, log.contains(member("status", "CANCELLED")));
+ // destroy() sends SIGTERM; the process exits with its status, which FINISH reports.
+ assertEquals(output, 143, process.exitValue());
+ assertTrue(log, log.contains(member("exit_code", 143)));
+ // The worker closes its own session, so no transport is closed under a request.
+ assertFalse(output, output.contains("SocketException"));
+ } finally {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ }
+ }
+
+ @Test
+ public void benchmarkTwentyThousandMeasurements() throws Exception {
+ Assume.assumeTrue(Boolean.getBoolean("export.measurements.benchmark"));
+ List<IMeasurementSchema> schemas = new ArrayList<>();
+ for (int i = 0; i < 100; i++) {
+ schemas.add(new MeasurementSchema("s" + i, TSDataType.INT32));
+ }
+ StringBuilder patterns = new StringBuilder();
+ try (ISession session = EnvFactory.getEnv().getSessionConnection()) {
+ for (int d = 0; d < 200; d++) {
+ String device = "root.export_bench.d" + d;
+ Tablet tablet = new Tablet(device, schemas, 1440);
+ for (int row = 0; row < 1440; row++) {
+ tablet.addTimestamp(row, row * 60000L);
+ for (int s = 0; s < 100; s++) {
+ tablet.addValue("s" + s, row, row);
+ }
+ }
+ session.insertTablet(tablet);
+ for (int s = 0; s < 100; s++) {
+ patterns.append(device).append(".s").append(s).append('\n');
+ }
+ }
+ }
+ Path input = file(patterns.toString());
+ Path target = folder.newFolder("benchmark").toPath();
+ long started = System.nanoTime();
+ assertSuccess(export(target, "-ft", "csv", "-mf", input.toString(), "-tf", "timestamp"));
+ long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
+ List<Path> outputs = outputs(target, ".csv");
+ assertEquals(20000, outputs.size());
+ long rows = 0;
+ for (Path output : outputs) {
+ try (Stream<String> lines = Files.lines(output)) {
+ rows += lines.count() - 1;
+ }
+ }
+ assertEquals(28800000, rows);
+ String log = log(target);
+ assertTrue(log.contains(member("exported_measurement_num", 20000)));
+ assertTrue(log.contains(member("exported_data_rows", 28800000)));
+ System.out.println(
+ "export_measurement_benchmark devices=200 measurements=20000 points="
+ + rows
+ + " elapsed_ms="
+ + elapsedMillis);
+ Files.writeString(
+ Path.of("target/export-measurement-benchmark.txt"),
+ "devices=200 measurements=20000 points=" + rows + " elapsed_ms=" + elapsedMillis + "\n");
+ }
+
+ private Result export(Path target, String... args) throws Exception {
+ List<String> command = command(target, args);
+ Path console = folder.newFile().toPath();
+ Process process =
+ new ProcessBuilder(command)
+ .redirectErrorStream(true)
+ .redirectOutput(console.toFile())
+ .start();
+ try {
+ if (!process.waitFor(60, TimeUnit.SECONDS)) {
+ // A benchmark may legitimately take longer; regular tests retain the bounded wait.
+ if (Boolean.getBoolean("export.measurements.benchmark")) {
+ while (!process.waitFor(60, TimeUnit.SECONDS)) {
+ System.out.println("Export benchmark is running.");
+ }
+ } else {
+ fail("Export command timed out: " + Files.readString(console));
+ }
+ }
+ return new Result(process.exitValue(), Files.readString(console));
+ } finally {
+ if (process.isAlive()) {
+ process.destroyForcibly();
+ }
+ }
+ }
+
+ private List<String> command(Path target, String... args) {
+ List<String> command = new ArrayList<>();
+ command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
+ command.add("-cp");
+ command.add(EnvFactory.getEnv().getLibPath());
+ command.add("org.apache.iotdb.tool.data.ExportData");
+ command.addAll(
+ Arrays.asList(
+ "-h",
+ EnvFactory.getEnv().getIP(),
+ "-p",
+ EnvFactory.getEnv().getPort(),
+ "-u",
+ SessionConfig.DEFAULT_USER,
+ "-pw",
+ SessionConfig.DEFAULT_PASSWORD,
+ "-t",
+ target.toString()));
+ command.addAll(Arrays.asList(args));
+ return command;
+ }
+
+ private void assertSuccess(Result result) {
+ assertEquals(result.text, 0, result.code);
+ assertEquals(result.text, 1, result.text.split("Export completely!", -1).length - 1);
+ assertFalse(result.text, result.text.contains("Export failed"));
+ }
+
+ /** A member as the JSON Lines log writes it, such as "status":"FAILED" or "exit_code":130. */
+ private static String member(String key, Object value) {
+ return '"' + key + "\":" + (value instanceof Number ? value : "\"" + value + '"');
+ }
+
+ private Path file(String content) throws Exception {
+ return Files.writeString(folder.newFile().toPath(), content, StandardCharsets.UTF_8);
+ }
+
+ private List<Path> outputs(Path directory, String suffix) throws Exception {
+ try (Stream<Path> files = Files.list(directory)) {
+ return files
+ .filter(file -> file.toString().endsWith(suffix))
+ .sorted()
+ .collect(Collectors.toList());
+ }
+ }
+
+ private String log(Path directory) throws Exception {
+ return Files.readString(outputs(directory, ".log").get(0));
+ }
+
+ private static class Result {
+ final int code;
+ final String text;
+
+ Result(int code, String text) {
+ this.code = code;
+ this.text = text;
+ }
+ }
+}
diff --git a/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
index ec00a7dc559..7b0dde1ea72 100644
--- a/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
+++ b/iotdb-client/cli/src/main/i18n/en/org/apache/iotdb/cli/i18n/CliMessages.java
@@ -116,4 +116,145 @@ public final class CliMessages {
public static final String LOG_INPUT_TIME_FORMAT_ARG_NOT_SUPPORTED_00172A7B = "Input time format {} is not supported, ";
public static final String LOG_PLEASE_INPUT_LIKE_YYYY_MM_DD_HH_MM_SS_SSS_9318BFC7 = "please input like yyyy-MM-dd\\ HH:mm:ss.SSS or yyyy-MM-dd'T'HH:mm:ss.SSS%n";
+ // Export data
+ public static final String MESSAGE_EXPORT_LOG_FILE_DEFAULT_TARGET_LOG_EXPORT_DATA_YYYYMMDDHHMMSS_LOG_B29FC35F =
+ "Export log file (default: <target>/log_export_data_yyyyMMddHHmmss.log).";
+ public static final String MESSAGE_INCLUSIVE_START_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_69FE75CF =
+ "Inclusive start time for measurement-file or automatic Table export.";
+ public static final String MESSAGE_INCLUSIVE_END_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_D9C24D95 =
+ "Inclusive end time for measurement-file or automatic Table export.";
+ public static final String MESSAGE_UTF_8_IOTDB_PATH_PATTERN_FILE_FOR_TREE_EXPORT_MUTUALLY_EXCLUSIVE_WITH_Q_DD1F2CDA =
+ "UTF-8 IoTDB path-pattern file for Tree export; mutually exclusive with -q.";
+ public static final String MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5 =
+ "Table to export (optional). Ignored when -q is given.";
+ public static final String
+ MESSAGE_DATABASE_TO_EXPORT_REQUIRED_EXPORTS_ALL_OF_ITS_TABLES_UNLESS_TABLE_OR_Q_IS_GIVEN_F125EEA1 =
+ "Database to export (required). Exports all of its tables unless -table or -q is given.";
+ public static final String MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9 =
+ "Maximum data rows per CSV or SQL file (default 10000).";
+ public static final String EXCEPTION_FILE_PREFIX_MUST_NOT_CONTAIN_PATH_SEPARATORS_ARG_C89823EE =
+ "File prefix must not contain path separators: %s";
+ public static final String EXCEPTION_INVALID_OUTPUT_TIME_FORMAT_ARG_9C690675 =
+ "Invalid output time format: %s";
+ public static final String EXCEPTION_EXPORTING_INFORMATION_SCHEMA_IS_NOT_SUPPORTED_6FD0FB85 =
+ "Exporting information_schema is not supported.";
+ public static final String EXCEPTION_THE_EXPORT_LOG_MUST_DIFFER_FROM_THE_MEASUREMENT_INPUT_FILE_088A542C =
+ "The export log must differ from the measurement input file.";
+ public static final String LOG_PLEASE_INPUT_YOUR_PASSWORD_AEE49DC5 =
+ "Please input your password:";
+ public static final String LOG_PLEASE_INPUT_YOUR_TRUST_STORE_4C50D824 =
+ "Please input your trust_store:";
+ public static final String LOG_PLEASE_INPUT_YOUR_TRUST_STORE_PASSWORD_86ED5CB6 =
+ "Please input your trust_store_password:";
+ public static final String LOG_PLEASE_INPUT_YOUR_KEY_STORE_AC5DB91F =
+ "Please input your key_store:";
+ public static final String LOG_PLEASE_INPUT_YOUR_KEY_STORE_PASSWORD_19ED16D4 =
+ "Please input your key_store_password:";
+ public static final String EXCEPTION_UNSUPPORTED_SQL_DIALECT_ARG_C4D0D97C =
+ "Unsupported sql_dialect: %s";
+ public static final String EXCEPTION_SPECIFY_AN_EXPORT_FORMAT_CSV_SQL_OR_TSFILE_RECEIVED_ARG_5B5211B5 =
+ "Specify an export format: csv, sql or tsfile (received: %s).";
+ public static final String EXCEPTION_MEASUREMENT_FILE_IS_SUPPORTED_ONLY_IN_THE_TREE_MODEL_1ED604B5 =
+ "-measurement_file is supported only in the Tree model.";
+ public static final String EXCEPTION_MEASUREMENT_FILE_AND_Q_ARE_MUTUALLY_EXCLUSIVE_7A7457D6 =
+ "-measurement_file and -q are mutually exclusive.";
+ public static final String EXCEPTION_UNEXPECTED_POSITIONAL_ARGUMENTS_ARG_027F44CB =
+ "Unexpected positional arguments: %s";
+ public static final String EXCEPTION_INVALID_POSITIVE_INTEGER_FOR_OPTION_ARG_ARG_30FB488D =
+ "Invalid positive integer for option %s: %s";
+ public static final String EXCEPTION_OPTION_ARG_REQUIRES_TRUE_OR_FALSE_ARG_0DC9BE3D =
+ "Option %s requires true or false: %s";
+ public static final String EXCEPTION_START_TIME_MUST_BE_LESS_THAN_OR_EQUAL_TO_END_TIME_3551199C =
+ "start_time must be less than or equal to end_time.";
+ public static final String EXCEPTION_CANNOT_PARSE_TIME_ARG_ARG_40257DF7 =
+ "Cannot parse time %s: %s";
+ public static final String EXCEPTION_MEASUREMENT_INPUT_MUST_BE_A_READABLE_REGULAR_FILE_ARG_43D4A272 =
+ "Measurement input must be a readable regular file: %s";
+ public static final String EXCEPTION_A_COMPLETE_IOTDB_PATH_PATTERN_STARTING_WITH_ROOT_IS_REQUIRED_7D000A79 =
+ "A complete IoTDB path pattern starting with root is required.";
+ public static final String EXCEPTION_INVALID_MEASUREMENT_INPUT_ARG_AT_LINE_ARG_CONTENT_ARG_ARG_27A0F948 =
+ "Invalid measurement input %s at line %d, content=%s: %s";
+ public static final String EXCEPTION_CANNOT_READ_UTF_8_MEASUREMENT_FILE_ARG_AT_LINE_ARG_ARG_A5739D3F =
+ "Cannot read UTF-8 measurement file %s at line %d: %s";
+ public static final String EXCEPTION_MEASUREMENT_INPUT_CONTAINS_NO_PATH_PATTERNS_ARG_96760070 =
+ "Measurement input contains no path patterns: %s";
+ public static final String EXCEPTION_MEASUREMENT_OUTPUT_FILENAME_EXCEEDS_THE_SUPPORTED_LENGTH_ARG_FF01275D =
+ "Measurement output filename exceeds the supported length: %s";
+ public static final String EXCEPTION_MEASUREMENTS_ARG_AND_ARG_MAP_TO_THE_SAME_OUTPUT_FILENAME_ARG_58E1FD4D =
+ "Measurements %s and %s map to the same output filename: %s";
+ public static final String EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_LOG_FILE_ARG_F01BE79D =
+ "Export output conflicts with the log file: %s";
+ public static final String LOG_EXPORT_STARTED_45465087 =
+ "Export started.";
+ public static final String LOG_EXPORT_COMPLETELY_34200D19 =
+ "Export completely!";
+ public static final String EXCEPTION_EXPORT_CANCELLED_85D44F54 =
+ "Export cancelled.";
+ public static final String LOG_EXPORT_FAILED_77EE72CD =
+ "Export failed.";
+ public static final String LOG_CANNOT_WRITE_OR_CLOSE_THE_EXPORT_LOG_32579AC6 =
+ "Cannot write or close the export log.";
+ public static final String LOG_EXPORT_STARTED_ARG_ARG_TARGET_ARG_LOG_ARG_BD58AECB =
+ "Export started (%s, %s): target %s, log %s";
+ public static final String LOG_MEASUREMENTS_TO_EXPORT_ARG_PATH_PATTERNS_ARG_A0AC5F31 =
+ "Measurements to export: %d (path patterns: %d).";
+ public static final String LOG_EXPORTING_ARG_ARG_ARG_ARG_ROWS_ARG_ETA_ARG_C41CA790 =
+ "Exporting %s %d/%d %s rows %s ETA %s";
+ public static final String LOG_ARG_ARG_ARG_MEASUREMENTS_ARG_ROWS_ELAPSED_ARG_ETA_ARG_CURRENT_ARG_B6DEA0BD =
+ "[%s] %d/%d measurements, %s rows, elapsed %s, ETA %s, current %s";
+ public static final String LOG_EXPORTING_ARG_ROWS_ARG_ARG_ROWS_S_56BD441C =
+ "Exporting %s rows %s %s rows/s";
+ public static final String LOG_ARG_ROWS_ELAPSED_ARG_ARG_ROWS_S_AE3CF660 =
+ "%s rows, elapsed %s, %s rows/s";
+ public static final String LOG_WARNING_ARG_05BE32E9 =
+ "WARNING: %s";
+ public static final String LOG_EXPORT_COMPLETELY_MEASUREMENTS_ARG_ARG_ROWS_ARG_FILES_ARG_ELAPSED_ARG_36D9F8E5 =
+ "Export completely! Measurements: %d/%d, rows: %s, files: %s, elapsed: %s.";
+ public static final String LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B =
+ "Export completely! Rows: %s, files: %s, elapsed: %s.";
+ public static final String LOG_EXPORT_FAILED_AFTER_ARG_ARG_C123F0EF =
+ "Export failed after %s: %s";
+ public static final String LOG_EXPORT_FAILED_AFTER_ARG_DA6D56F9 =
+ "Export failed after %s.";
+ public static final String LOG_EXPORT_CANCELLED_AFTER_ARG_ROWS_EXPORTED_ARG_0999F99F =
+ "Export cancelled after %s. Rows exported: %s.";
+ public static final String LOG_LOG_ARG_FBF61F63 =
+ "Log: %s";
+ public static final String LOG_ARG_82AC62F7 =
+ "%.2f%%";
+ public static final String EXCEPTION_AGGREGATE_QUERIES_CANNOT_BE_EXPORTED_AS_SQL_OR_TSFILE_48BC1932 =
+ "Aggregate queries cannot be exported as SQL or TsFile.";
+ public static final String LOG_NO_MEASUREMENTS_MATCHED_ARG_MEASUREMENT_FILE_LINE_ARG_94C4EF10 =
+ "No measurements matched %s (measurement file line %d).";
+ public static final String LOG_START_TIME_AND_END_TIME_DO_NOT_APPLY_TO_QUERIES_AND_ARE_IGNORED_02DC37CC =
+ "-start_time and -end_time do not apply to queries and are ignored.";
+ public static final String LOG_TABLE_DOES_NOT_APPLY_TO_QUERIES_AND_IS_IGNORED_DBA31209 =
+ "-table does not apply to queries and is ignored.";
+ public static final String EXCEPTION_NO_MEASUREMENTS_REMAIN_AFTER_PATTERN_EXPANSION_AND_DEDUPLICATION_40D930B7 =
+ "No measurements remain after pattern expansion and deduplication.";
+ public static final String EXCEPTION_THE_TARGET_DATABASE_ARG_DOES_NOT_EXIST_80E096EB =
+ "The target database %s does not exist.";
+ public static final String EXCEPTION_THERE_ARE_NO_TABLES_OR_THE_TARGET_TABLE_ARG_DOES_NOT_EXIST_E5F88A98 =
+ "There are no tables or the target table %s does not exist.";
+ public static final String LOG_PLEASE_INPUT_QUERY_EEAD5433 =
+ "Please input query: ";
+ public static final String EXCEPTION_NO_EXPORT_QUERY_WAS_PROVIDED_09E24A92 =
+ "No export query was provided.";
+ public static final String LOG_EXPORT_FAILED_ARG_57F9420F =
+ "Export failed: %s";
+ public static final String EXCEPTION_CANNOT_OBTAIN_SCHEMA_FOR_MEASUREMENT_ARG_D3F1DAF3 =
+ "Cannot obtain schema for measurement %s.";
+ public static final String EXCEPTION_SQL_TSFILE_OUTPUT_REQUIRES_THE_TIME_COLUMN_AND_UNALIASED_COLUMNS_FROM_TABLE_ARG_C5E77CD3 =
+ "SQL/TsFile output requires the time column and unaliased columns from table %s.";
+ public static final String EXCEPTION_SQL_OUTPUT_REQUIRES_ORIGINAL_MEASUREMENT_PATHS_OR_AN_ALIGN_BY_DEVICE_QUERY_ADA72EA8 =
+ "SQL output requires original measurement paths or an ALIGN BY DEVICE query.";
+
+
+
+
+ public static final String EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_MEASUREMENT_INPUT_FILE_ARG_1CBEBB6C =
+ "Export output conflicts with the measurement input file: %s";
+ public static final String EXCEPTION_TREE_TSFILE_OUTPUT_REQUIRES_A_TIME_COLUMN_AND_ORIGINAL_MEASUREMENT_PATHS_1DCEE8EA =
+ "Tree TsFile output requires a time column and original measurement paths.";
+
}
diff --git a/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
index 8321a28d08e..c4a2d1b78ab 100644
--- a/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
+++ b/iotdb-client/cli/src/main/i18n/zh/org/apache/iotdb/cli/i18n/CliMessages.java
@@ -112,4 +112,145 @@ public final class CliMessages {
public static final String LOG_INPUT_TIME_FORMAT_ARG_NOT_SUPPORTED_00172A7B = "不支持输入时间格式 {},";
public static final String LOG_PLEASE_INPUT_LIKE_YYYY_MM_DD_HH_MM_SS_SSS_9318BFC7 = "请输入类似 yyyy-MM-dd\\ HH:mm:ss.SSS 或 yyyy-MM-dd'T'HH:mm:ss.SSS 的格式%n";
+ // Export data
+ public static final String MESSAGE_EXPORT_LOG_FILE_DEFAULT_TARGET_LOG_EXPORT_DATA_YYYYMMDDHHMMSS_LOG_B29FC35F =
+ "导出日志文件(默认:<target>/log_export_data_yyyyMMddHHmmss.log)。";
+ public static final String MESSAGE_INCLUSIVE_START_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_69FE75CF =
+ "测点文件或 Table 自动导出的开始时间(含边界)。";
+ public static final String MESSAGE_INCLUSIVE_END_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_D9C24D95 =
+ "测点文件或 Table 自动导出的结束时间(含边界)。";
+ public static final String MESSAGE_UTF_8_IOTDB_PATH_PATTERN_FILE_FOR_TREE_EXPORT_MUTUALLY_EXCLUSIVE_WITH_Q_DD1F2CDA =
+ "Tree 导出的 UTF-8 IoTDB 路径 pattern 文件,与 -q 互斥。";
+ public static final String MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5 =
+ "要导出的表(可选)。指定 -q 时忽略。";
+ public static final String
+ MESSAGE_DATABASE_TO_EXPORT_REQUIRED_EXPORTS_ALL_OF_ITS_TABLES_UNLESS_TABLE_OR_Q_IS_GIVEN_F125EEA1 =
+ "要导出的数据库(必填)。不带 -table 或 -q 时导出其中所有表。";
+ public static final String MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9 =
+ "每个 CSV 或 SQL 文件的最大数据行数(默认 10000)。";
+ public static final String EXCEPTION_FILE_PREFIX_MUST_NOT_CONTAIN_PATH_SEPARATORS_ARG_C89823EE =
+ "文件名前缀不能包含路径分隔符:%s";
+ public static final String EXCEPTION_INVALID_OUTPUT_TIME_FORMAT_ARG_9C690675 =
+ "输出时间格式无效:%s";
+ public static final String EXCEPTION_EXPORTING_INFORMATION_SCHEMA_IS_NOT_SUPPORTED_6FD0FB85 =
+ "不支持导出 information_schema。";
+ public static final String EXCEPTION_THE_EXPORT_LOG_MUST_DIFFER_FROM_THE_MEASUREMENT_INPUT_FILE_088A542C =
+ "导出日志不能与测点输入文件相同。";
+ public static final String LOG_PLEASE_INPUT_YOUR_PASSWORD_AEE49DC5 =
+ "请输入密码:";
+ public static final String LOG_PLEASE_INPUT_YOUR_TRUST_STORE_4C50D824 =
+ "请输入 trust_store:";
+ public static final String LOG_PLEASE_INPUT_YOUR_TRUST_STORE_PASSWORD_86ED5CB6 =
+ "请输入 trust_store_password:";
+ public static final String LOG_PLEASE_INPUT_YOUR_KEY_STORE_AC5DB91F =
+ "请输入 key_store:";
+ public static final String LOG_PLEASE_INPUT_YOUR_KEY_STORE_PASSWORD_19ED16D4 =
+ "请输入 key_store_password:";
+ public static final String EXCEPTION_UNSUPPORTED_SQL_DIALECT_ARG_C4D0D97C =
+ "不支持的 sql_dialect:%s";
+ public static final String EXCEPTION_SPECIFY_AN_EXPORT_FORMAT_CSV_SQL_OR_TSFILE_RECEIVED_ARG_5B5211B5 =
+ "请指定导出格式 csv、sql 或 tsfile(收到:%s)。";
+ public static final String EXCEPTION_MEASUREMENT_FILE_IS_SUPPORTED_ONLY_IN_THE_TREE_MODEL_1ED604B5 =
+ "-measurement_file 仅支持 Tree 模型。";
+ public static final String EXCEPTION_MEASUREMENT_FILE_AND_Q_ARE_MUTUALLY_EXCLUSIVE_7A7457D6 =
+ "-measurement_file 和 -q 互斥。";
+ public static final String EXCEPTION_UNEXPECTED_POSITIONAL_ARGUMENTS_ARG_027F44CB =
+ "不支持的位置参数:%s";
+ public static final String EXCEPTION_INVALID_POSITIVE_INTEGER_FOR_OPTION_ARG_ARG_30FB488D =
+ "选项 %s 的正整数值无效:%s";
+ public static final String EXCEPTION_OPTION_ARG_REQUIRES_TRUE_OR_FALSE_ARG_0DC9BE3D =
+ "选项 %s 必须为 true 或 false:%s";
+ public static final String EXCEPTION_START_TIME_MUST_BE_LESS_THAN_OR_EQUAL_TO_END_TIME_3551199C =
+ "start_time 必须小于或等于 end_time。";
+ public static final String EXCEPTION_CANNOT_PARSE_TIME_ARG_ARG_40257DF7 =
+ "无法解析时间 %s:%s";
+ public static final String EXCEPTION_MEASUREMENT_INPUT_MUST_BE_A_READABLE_REGULAR_FILE_ARG_43D4A272 =
+ "测点输入必须是可读的普通文件:%s";
+ public static final String EXCEPTION_A_COMPLETE_IOTDB_PATH_PATTERN_STARTING_WITH_ROOT_IS_REQUIRED_7D000A79 =
+ "必须提供以 root 开头的完整 IoTDB 路径 pattern。";
+ public static final String EXCEPTION_INVALID_MEASUREMENT_INPUT_ARG_AT_LINE_ARG_CONTENT_ARG_ARG_27A0F948 =
+ "测点输入 %s 的第 %d 行无效,原文=%s:%s";
+ public static final String EXCEPTION_CANNOT_READ_UTF_8_MEASUREMENT_FILE_ARG_AT_LINE_ARG_ARG_A5739D3F =
+ "无法读取 UTF-8 测点文件 %s 的第 %d 行:%s";
+ public static final String EXCEPTION_MEASUREMENT_INPUT_CONTAINS_NO_PATH_PATTERNS_ARG_96760070 =
+ "测点输入没有有效路径 pattern:%s";
+ public static final String EXCEPTION_MEASUREMENT_OUTPUT_FILENAME_EXCEEDS_THE_SUPPORTED_LENGTH_ARG_FF01275D =
+ "测点输出文件名超过支持的长度:%s";
+ public static final String EXCEPTION_MEASUREMENTS_ARG_AND_ARG_MAP_TO_THE_SAME_OUTPUT_FILENAME_ARG_58E1FD4D =
+ "测点 %s 和 %s 对应同一个输出文件名:%s";
+ public static final String EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_LOG_FILE_ARG_F01BE79D =
+ "导出输出与日志文件冲突:%s";
+ public static final String LOG_EXPORT_STARTED_45465087 =
+ "导出已开始。";
+ public static final String LOG_EXPORT_COMPLETELY_34200D19 =
+ "导出完成!";
+ public static final String EXCEPTION_EXPORT_CANCELLED_85D44F54 =
+ "导出已取消。";
+ public static final String LOG_EXPORT_FAILED_77EE72CD =
+ "导出失败。";
+ public static final String LOG_CANNOT_WRITE_OR_CLOSE_THE_EXPORT_LOG_32579AC6 =
+ "无法写入或关闭导出日志。";
+ public static final String LOG_EXPORT_STARTED_ARG_ARG_TARGET_ARG_LOG_ARG_BD58AECB =
+ "导出已开始(%s,%s):输出目录 %s,日志 %s";
+ public static final String LOG_MEASUREMENTS_TO_EXPORT_ARG_PATH_PATTERNS_ARG_A0AC5F31 =
+ "待导出测点:%d 个(路径 pattern:%d 个)。";
+ public static final String LOG_EXPORTING_ARG_ARG_ARG_ARG_ROWS_ARG_ETA_ARG_C41CA790 =
+ "导出中 %s %d/%d %s 行 %s 剩余 %s";
+ public static final String LOG_ARG_ARG_ARG_MEASUREMENTS_ARG_ROWS_ELAPSED_ARG_ETA_ARG_CURRENT_ARG_B6DEA0BD =
+ "[%s] %d/%d 个测点,%s 行,已用 %s,剩余 %s,当前 %s";
+ public static final String LOG_EXPORTING_ARG_ROWS_ARG_ARG_ROWS_S_56BD441C =
+ "导出中 %s 行 %s %s 行/秒";
+ public static final String LOG_ARG_ROWS_ELAPSED_ARG_ARG_ROWS_S_AE3CF660 =
+ "%s 行,已用 %s,%s 行/秒";
+ public static final String LOG_WARNING_ARG_05BE32E9 =
+ "警告:%s";
+ public static final String LOG_EXPORT_COMPLETELY_MEASUREMENTS_ARG_ARG_ROWS_ARG_FILES_ARG_ELAPSED_ARG_36D9F8E5 =
+ "导出完成!测点:%d/%d,行数:%s,文件:%s 个,耗时:%s。";
+ public static final String LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B =
+ "导出完成!行数:%s,文件:%s 个,耗时:%s。";
+ public static final String LOG_EXPORT_FAILED_AFTER_ARG_ARG_C123F0EF =
+ "导出失败(已用 %s):%s";
+ public static final String LOG_EXPORT_FAILED_AFTER_ARG_DA6D56F9 =
+ "导出失败(已用 %s)。";
+ public static final String LOG_EXPORT_CANCELLED_AFTER_ARG_ROWS_EXPORTED_ARG_0999F99F =
+ "导出已取消(已用 %s),已导出 %s 行。";
+ public static final String LOG_LOG_ARG_FBF61F63 =
+ "日志:%s";
+ public static final String LOG_ARG_82AC62F7 =
+ "%.2f%%";
+ public static final String EXCEPTION_AGGREGATE_QUERIES_CANNOT_BE_EXPORTED_AS_SQL_OR_TSFILE_48BC1932 =
+ "聚合查询不能导出为 SQL 或 TsFile。";
+ public static final String LOG_NO_MEASUREMENTS_MATCHED_ARG_MEASUREMENT_FILE_LINE_ARG_94C4EF10 =
+ "%s 未匹配到任何测点(测点文件第 %d 行)。";
+ public static final String LOG_START_TIME_AND_END_TIME_DO_NOT_APPLY_TO_QUERIES_AND_ARE_IGNORED_02DC37CC =
+ "-start_time 和 -end_time 对查询不生效,已忽略。";
+ public static final String LOG_TABLE_DOES_NOT_APPLY_TO_QUERIES_AND_IS_IGNORED_DBA31209 =
+ "-table 对查询不生效,已忽略。";
+ public static final String EXCEPTION_NO_MEASUREMENTS_REMAIN_AFTER_PATTERN_EXPANSION_AND_DEDUPLICATION_40D930B7 =
+ "pattern 展开并去重后没有实际测点。";
+ public static final String EXCEPTION_THE_TARGET_DATABASE_ARG_DOES_NOT_EXIST_80E096EB =
+ "目标数据库 %s 不存在。";
+ public static final String EXCEPTION_THERE_ARE_NO_TABLES_OR_THE_TARGET_TABLE_ARG_DOES_NOT_EXIST_E5F88A98 =
+ "没有可导出的表,或目标表 %s 不存在。";
+ public static final String LOG_PLEASE_INPUT_QUERY_EEAD5433 =
+ "请输入查询:";
+ public static final String EXCEPTION_NO_EXPORT_QUERY_WAS_PROVIDED_09E24A92 =
+ "没有提供导出查询。";
+ public static final String LOG_EXPORT_FAILED_ARG_57F9420F =
+ "导出失败:%s";
+ public static final String EXCEPTION_CANNOT_OBTAIN_SCHEMA_FOR_MEASUREMENT_ARG_D3F1DAF3 =
+ "无法获取测点 %s 的 schema。";
+ public static final String EXCEPTION_SQL_TSFILE_OUTPUT_REQUIRES_THE_TIME_COLUMN_AND_UNALIASED_COLUMNS_FROM_TABLE_ARG_C5E77CD3 =
+ "SQL/TsFile 输出需要 time 列及表 %s 的原始列(不能使用别名)。";
+ public static final String EXCEPTION_SQL_OUTPUT_REQUIRES_ORIGINAL_MEASUREMENT_PATHS_OR_AN_ALIGN_BY_DEVICE_QUERY_ADA72EA8 =
+ "SQL 输出需要原始测点路径或 ALIGN BY DEVICE 查询。";
+
+
+
+
+ public static final String EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_MEASUREMENT_INPUT_FILE_ARG_1CBEBB6C =
+ "导出输出与测点输入文件冲突:%s";
+ public static final String EXCEPTION_TREE_TSFILE_OUTPUT_REQUIRES_A_TIME_COLUMN_AND_ORIGINAL_MEASUREMENT_PATHS_1DCEE8EA =
+ "Tree TsFile 输出需要 time 列及原始测点路径。";
+
}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
index b6999dea427..86c8419ab1b 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/Constants.java
@@ -231,6 +231,11 @@ public class Constants {
public static final String TARGET_FILE_NAME = "prefix_file_name";
public static final String TARGET_FILE_DESC = "Export file name .(optional)";
+ public static final String MEASUREMENT_FILE_ARGS = "mf";
+ public static final String MEASUREMENT_FILE_NAME = "measurement_file";
+ public static final String EXPORT_DATA_LOG_ARGS = "l";
+ public static final String EXPORT_DATA_LOG_NAME = "export_data_log";
+
public static final String RPC_MAX_FRAME_SIZE_ARGS = "mfs";
public static final String RPC_MAX_FRAME_SIZE_NAME = "rpc_max_frame_size";
public static final String RPC_MAX_FRAME_SIZE_DESC =
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
index e7ca2317be6..59d07a7bfdc 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/common/OptionsUtil.java
@@ -19,17 +19,13 @@
package org.apache.iotdb.tool.common;
+import org.apache.iotdb.cli.i18n.CliMessages;
+
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
public class OptionsUtil extends Constants {
- private static boolean isImport = true;
-
- public static void setIsImport(boolean isImport) {
- OptionsUtil.isImport = isImport;
- }
-
public static Options createHelpOptions() {
final Options options = new Options();
Option opHelp = Option.builder(HELP_ARGS).longOpt(HELP_ARGS).hasArg().desc(HELP_DESC).build();
@@ -167,6 +163,10 @@ public class OptionsUtil extends Constants {
}
public static Options createImportCommonOptions() {
+ return createDataCommonOptions(true);
+ }
+
+ private static Options createDataCommonOptions(boolean importing) {
Options options = new Options();
Option opFileType =
@@ -175,7 +175,7 @@ public class OptionsUtil extends Constants {
.argName(FILE_TYPE_ARGS_NAME)
.required()
.hasArg()
- .desc(isImport ? FILE_TYPE_DESC_IMPORT : FILE_TYPE_DESC_EXPORT)
+ .desc(importing ? FILE_TYPE_DESC_IMPORT : FILE_TYPE_DESC_EXPORT)
.build();
options.addOption(opFileType);
@@ -191,7 +191,7 @@ public class OptionsUtil extends Constants {
}
public static Options createExportCommonOptions() {
- Options options = createImportCommonOptions();
+ Options options = createDataCommonOptions(false);
Option opFile =
Option.builder(TARGET_DIR_ARGS)
@@ -239,11 +239,53 @@ public class OptionsUtil extends Constants {
.build();
options.addOption(opRpcMaxFrameSize);
+ options.addOption(
+ Option.builder(EXPORT_DATA_LOG_ARGS)
+ .longOpt(EXPORT_DATA_LOG_NAME)
+ .hasArg()
+ .argName(EXPORT_DATA_LOG_NAME)
+ .desc(
+ CliMessages
+ .MESSAGE_EXPORT_LOG_FILE_DEFAULT_TARGET_LOG_EXPORT_DATA_YYYYMMDDHHMMSS_LOG_B29FC35F)
+ .build());
+ options.addOption(
+ Option.builder(START_TIME_ARGS)
+ .longOpt(START_TIME_ARGS)
+ .hasArg()
+ .desc(
+ CliMessages
+ .MESSAGE_INCLUSIVE_START_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_69FE75CF)
+ .build());
+ options.addOption(
+ Option.builder(END_TIME_ARGS)
+ .longOpt(END_TIME_ARGS)
+ .hasArg()
+ .desc(
+ CliMessages
+ .MESSAGE_INCLUSIVE_END_TIME_FOR_MEASUREMENT_FILE_OR_AUTOMATIC_TABLE_EXPORT_D9C24D95)
+ .build());
+ options.addOption(
+ Option.builder(TIME_ZONE_ARGS)
+ .longOpt(TIME_ZONE_NAME)
+ .hasArg()
+ .desc(TIME_ZONE_DESC)
+ .build());
+
return options;
}
public static Options createTreeExportCommonOptions() {
- return createExportCommonOptions();
+ Options options = createExportCommonOptions();
+ options.addOption(
+ Option.builder(MEASUREMENT_FILE_ARGS)
+ .longOpt(MEASUREMENT_FILE_NAME)
+ .hasArg()
+ .argName(MEASUREMENT_FILE_NAME)
+ .desc(
+ CliMessages
+ .MESSAGE_UTF_8_IOTDB_PATH_PATTERN_FILE_FOR_TREE_EXPORT_MUTUALLY_EXCLUSIVE_WITH_Q_DD1F2CDA)
+ .build());
+ return options;
}
public static Options createTableExportCommonOptions() {
@@ -255,7 +297,9 @@ public class OptionsUtil extends Constants {
.argName(DB_ARGS)
.hasArg()
.required()
- .desc(DB_DESC)
+ .desc(
+ CliMessages
+ .MESSAGE_DATABASE_TO_EXPORT_REQUIRED_EXPORTS_ALL_OF_ITS_TABLES_UNLESS_TABLE_OR_Q_IS_GIVEN_F125EEA1)
.build();
options.addOption(opDatabase);
@@ -264,28 +308,10 @@ public class OptionsUtil extends Constants {
.longOpt(TABLE_ARGS)
.argName(TABLE_ARGS)
.hasArg()
- .desc(TABLE_DESC_EXPORT)
+ .desc(CliMessages.MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5)
.build();
options.addOption(opTable);
- Option opStartTime =
- Option.builder(START_TIME_ARGS)
- .longOpt(START_TIME_ARGS)
- .argName(START_TIME_ARGS)
- .hasArg()
- .desc(START_TIME_DESC)
- .build();
- options.addOption(opStartTime);
-
- Option opEndTime =
- Option.builder(END_TIME_ARGS)
- .longOpt(END_TIME_ARGS)
- .argName(END_TIME_ARGS)
- .hasArg()
- .desc(END_TIME_DESC)
- .build();
- options.addOption(opEndTime);
-
return options;
}
@@ -297,7 +323,7 @@ public class OptionsUtil extends Constants {
.longOpt(TABLE_ARGS)
.argName(TABLE_ARGS)
.hasArg()
- .desc(TABLE_DESC_EXPORT)
+ .desc(CliMessages.MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5)
.build();
options.addOption(opTable);
@@ -315,7 +341,7 @@ public class OptionsUtil extends Constants {
.longOpt(LINES_PER_FILE_NAME)
.argName(LINES_PER_FILE_NAME)
.hasArg()
- .desc(LINES_PER_FILE_DESC)
+ .desc(CliMessages.MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9)
.build();
options.addOption(opLinesPerFile);
@@ -348,7 +374,7 @@ public class OptionsUtil extends Constants {
.longOpt(TABLE_ARGS)
.argName(TABLE_ARGS)
.hasArg()
- .desc(TABLE_DESC_EXPORT)
+ .desc(CliMessages.MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5)
.build();
options.addOption(opTable);
@@ -390,7 +416,7 @@ public class OptionsUtil extends Constants {
.longOpt(TABLE_ARGS)
.argName(TABLE_ARGS)
.hasArg()
- .desc(TABLE_DESC_EXPORT)
+ .desc(CliMessages.MESSAGE_TABLE_TO_EXPORT_OPTIONAL_IGNORED_WHEN_Q_IS_GIVEN_4F37C0B5)
.build();
options.addOption(opTable);
@@ -408,7 +434,7 @@ public class OptionsUtil extends Constants {
.longOpt(LINES_PER_FILE_NAME)
.argName(LINES_PER_FILE_NAME)
.hasArg()
- .desc(LINES_PER_FILE_DESC)
+ .desc(CliMessages.MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9)
.build();
options.addOption(opLinesPerFile);
@@ -450,7 +476,7 @@ public class OptionsUtil extends Constants {
.longOpt(LINES_PER_FILE_NAME)
.argName(LINES_PER_FILE_NAME)
.hasArg()
- .desc(LINES_PER_FILE_DESC)
+ .desc(CliMessages.MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9)
.build();
options.addOption(opLinesPerFile);
@@ -492,7 +518,7 @@ public class OptionsUtil extends Constants {
.longOpt(LINES_PER_FILE_NAME)
.argName(LINES_PER_FILE_NAME)
.hasArg()
- .desc(LINES_PER_FILE_DESC)
+ .desc(CliMessages.MESSAGE_MAXIMUM_DATA_ROWS_PER_CSV_OR_SQL_FILE_DEFAULT_10000_000CEFB9)
.build();
options.addOption(opLinesPerFile);
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
index e3c33950d42..ad1815d42f8 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/AbstractExportData.java
@@ -19,79 +19,174 @@
package org.apache.iotdb.tool.data;
-import org.apache.iotdb.cli.utils.IoTPrinter;
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.exception.ArgsErrorException;
import org.apache.iotdb.isession.SessionDataSet;
-import org.apache.iotdb.rpc.IoTDBConnectionException;
import org.apache.iotdb.rpc.RpcUtils;
-import org.apache.iotdb.rpc.StatementExecutionException;
-import org.apache.iotdb.tool.common.Constants;
+import org.apache.iotdb.session.Session;
-import org.apache.thrift.TException;
-
-import java.time.Instant;
-import java.time.ZonedDateTime;
+import java.io.IOException;
+import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
-public abstract class AbstractExportData extends AbstractDataTool {
+/** Model-specific exporters share configuration and explicit resource ownership. */
+public abstract class AbstractExportData implements AutoCloseable {
+ private static final Pattern AGGREGATE =
+ Pattern.compile(
+ "\\b(count|sum|avg|extreme|max_value|min_value|first_value|last_value|max_time|min_time|stddev|stddev_pop|stddev_samp|variance|var_pop|var_samp|max_by|min_by)\\b\\s*\\(",
+ Pattern.CASE_INSENSITIVE);
+ protected final ExportConfig config;
+ protected final ExportProgressReporter progress;
+ protected ZoneId zoneId;
+ protected String timestampPrecision;
+ private final List<Resource<?>> resources = new ArrayList<>();
+ private boolean closed;
- private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
+ AbstractExportData(ExportConfig config, ExportProgressReporter progress) {
+ this.config = config;
+ this.progress = progress;
+ }
- public abstract void init()
- throws IoTDBConnectionException, StatementExecutionException, TException;
+ public abstract void init() throws Exception;
- public abstract boolean exportBySql(String sql, int index);
+ abstract void export(ExportTask task) throws Exception;
- protected static String formatSqlValue(String value, String type) {
- if (value == null) {
- return "NULL";
+ protected void readTimeSettings(Session session) throws Exception {
+ session.open(false);
+ timestampPrecision = session.getTimestampPrecision();
+ if (config.timeZone != null) {
+ session.setTimeZone(config.timeZone);
}
- if ("TEXT".equalsIgnoreCase(type) || "STRING".equalsIgnoreCase(type)) {
- return "'" + value.replace("'", "''") + "'";
+ zoneId = ZoneId.of(session.getTimeZone());
+ }
+
+ ExportTimeRange timeRange() throws ArgsErrorException {
+ return ExportTimeRange.parse(config.startTime, config.endTime, zoneId, timestampPrecision);
+ }
+
+ protected static void legalCheck(String sql) throws ArgsErrorException {
+ if (AGGREGATE.matcher(sql).find()) {
+ throw new ArgsErrorException(
+ CliMessages.EXCEPTION_AGGREGATE_QUERIES_CANNOT_BE_EXPORTED_AS_SQL_OR_TSFILE_48BC1932);
}
- return value;
}
- protected static List<String> getCsvHeaders(SessionDataSet dataSet, boolean treeModel) {
- List<String> headers = new ArrayList<>(dataSet.getColumnNames());
- if (Boolean.TRUE.equals(needDataTypePrinted)) {
- List<String> types = dataSet.getColumnTypes();
+ /** With -dt, value columns are written as name(TYPE); time and the Tree Device column are not. */
+ protected List<String> csvHeaders(List<String> names, List<String> types, boolean tree) {
+ List<String> headers = new ArrayList<>(names);
+ if (config.printDataType) {
for (int i = 0; i < headers.size(); i++) {
String name = headers.get(i);
- if (!"Time".equalsIgnoreCase(name) && !(treeModel && "Device".equals(name))) {
- headers.set(i, String.format("%s(%s)", name, types.get(i)));
+ if (!"Time".equalsIgnoreCase(name) && !(tree && "Device".equals(name))) {
+ headers.set(i, name + "(" + types.get(i) + ")");
}
}
}
return headers;
}
- protected static void legalCheck(String sql) {
- String aggregatePattern =
- "\\b(count|sum|avg|extreme|max_value|min_value|first_value|last_value|max_time|min_time|stddev|stddev_pop|stddev_samp|variance|var_pop|var_samp|max_by|min_by)\\b\\s*\\(";
- Pattern pattern = Pattern.compile(aggregatePattern, Pattern.CASE_INSENSITIVE);
- Matcher matcher = pattern.matcher(sql);
- if (matcher.find()) {
- ioTPrinter.println("The sql you entered is invalid, please don't use aggregate query.");
- System.exit(Constants.CODE_ERROR);
+ /** One value of an INSERT statement, in a form both SQL dialects read back. */
+ protected static String sqlValue(SessionDataSet.DataIterator iterator, String type, int column)
+ throws Exception {
+ if (iterator.isNull(column)) {
+ return "NULL";
+ }
+ switch (type) {
+ case "TEXT":
+ case "STRING":
+ case "DATE":
+ return "'" + iterator.getString(column).replace("'", "''") + "'";
+ case "TIMESTAMP":
+ return Long.toString(iterator.getLong(column));
+ case "BLOB":
+ return "X'" + iterator.getString(column).substring(2) + "'";
+ default:
+ return iterator.getString(column);
}
}
- protected static String timeTrans(Long time) {
- switch (timeFormat) {
- case "default":
- return RpcUtils.parseLongToDateWithPrecision(
- DateTimeFormatter.ISO_OFFSET_DATE_TIME, time, zoneId, timestampPrecision);
- case "timestamp":
- case "long":
- case "number":
- return String.valueOf(time);
- default:
- return ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), zoneId)
- .format(DateTimeFormatter.ofPattern(timeFormat));
+ protected String timeTrans(long time) {
+ if ("timestamp".equals(config.timeFormat)
+ || "number".equals(config.timeFormat)
+ || "long".equals(config.timeFormat)) {
+ return Long.toString(time);
+ }
+ DateTimeFormatter formatter =
+ "default".equals(config.timeFormat)
+ ? DateTimeFormatter.ISO_OFFSET_DATE_TIME
+ : DateTimeFormatter.ofPattern(config.timeFormat);
+ return RpcUtils.parseLongToDateWithPrecision(formatter, time, zoneId, timestampPrecision);
+ }
+
+ protected <T extends AutoCloseable> Resource<T> own(T resource) throws Exception {
+ synchronized (resources) {
+ if (closed) {
+ resource.close();
+ throw new IOException(CliMessages.EXCEPTION_EXPORT_CANCELLED_85D44F54);
+ }
+ Resource<T> owned = new Resource<>(resource);
+ resources.add(owned);
+ return owned;
+ }
+ }
+
+ /** Closing from the shutdown hook and normal execution releases each handle only once. */
+ protected final class Resource<T extends AutoCloseable> implements AutoCloseable {
+ private final T value;
+ private boolean released;
+
+ private Resource(T value) {
+ this.value = value;
+ }
+
+ T get() {
+ return value;
+ }
+
+ @Override
+ public synchronized void close() throws Exception {
+ if (!released) {
+ released = true;
+ try {
+ value.close();
+ } finally {
+ synchronized (resources) {
+ resources.remove(this);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public synchronized void close() throws Exception {
+ List<Resource<?>> remaining;
+ synchronized (resources) {
+ if (closed) {
+ return;
+ }
+ closed = true;
+ remaining = new ArrayList<>(resources);
+ }
+ Collections.reverse(remaining);
+ Exception failure = null;
+ for (Resource<?> resource : remaining) {
+ try {
+ resource.close();
+ } catch (Exception e) {
+ if (failure == null) {
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
+ }
+ }
+ }
+ if (failure != null) {
+ throw failure;
}
}
}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConfig.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConfig.java
new file mode 100644
index 00000000000..2e2d60b568d
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConfig.java
@@ -0,0 +1,363 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.cli.type.ExitType;
+import org.apache.iotdb.cli.utils.CliContext;
+import org.apache.iotdb.cli.utils.JlineUtils;
+import org.apache.iotdb.exception.ArgsErrorException;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.session.TableSessionBuilder;
+import org.apache.iotdb.tool.common.Constants;
+import org.apache.iotdb.tool.common.OptionsUtil;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.jline.reader.LineReader;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+/** Immutable options for one invocation, independent of the import tool's static configuration. */
+final class ExportConfig {
+ final boolean tree;
+ final String format;
+ final String host;
+ final int port;
+ final String username;
+ final String password;
+ final boolean useSsl;
+ final String trustStore;
+ final String trustStorePassword;
+ final String keyStore;
+ final String keyStorePassword;
+ final String sslProtocol;
+ final Path targetDirectory;
+ final Path measurementFile;
+ final Path logFile;
+ final String prefix;
+ final String query;
+ final String database;
+ final String table;
+ final String startTime;
+ final String endTime;
+ final String timeZone;
+ final String timeFormat;
+ final long timeout;
+ final int rpcMaxFrameSize;
+ final int linesPerFile;
+ final boolean printDataType;
+ final boolean aligned;
+
+ private ExportConfig(CommandLine options, ZonedDateTime startedAt)
+ throws ArgsErrorException, IOException {
+ tree =
+ Constants.SQL_DIALECT_VALUE_TREE.equalsIgnoreCase(
+ options.getOptionValue(Constants.SQL_DIALECT_ARGS, Constants.SQL_DIALECT_VALUE_TREE));
+ format = options.getOptionValue(Constants.FILE_TYPE_ARGS).toLowerCase(Locale.ROOT);
+ host = options.getOptionValue(Constants.HOST_ARGS, Constants.HOST_DEFAULT_VALUE);
+ port = (int) positiveNumber(options, Constants.PORT_ARGS, 6667, 65535);
+ username = options.getOptionValue(Constants.USERNAME_ARGS, Constants.USERNAME_DEFAULT_VALUE);
+ timeout = positiveNumber(options, Constants.TIMEOUT_ARGS, Long.MAX_VALUE, Long.MAX_VALUE);
+ rpcMaxFrameSize =
+ (int)
+ positiveNumber(
+ options, Constants.RPC_MAX_FRAME_SIZE_ARGS, 536870912, Integer.MAX_VALUE);
+ linesPerFile =
+ (int) positiveNumber(options, Constants.LINES_PER_FILE_ARGS, 10000, Integer.MAX_VALUE);
+ printDataType = booleanOption(options, Constants.DATA_TYPE_ARGS, false);
+ aligned = booleanOption(options, Constants.ALIGNED_ARGS, false);
+ useSsl = booleanOption(options, Constants.USE_SSL_ARGS, false);
+ targetDirectory =
+ Path.of(options.getOptionValue(Constants.TARGET_DIR_ARGS)).toAbsolutePath().normalize();
+ String input = options.getOptionValue(Constants.MEASUREMENT_FILE_ARGS);
+ measurementFile = input == null ? null : Path.of(input).toAbsolutePath().normalize();
+ query = options.getOptionValue(Constants.QUERY_COMMAND_ARGS);
+ prefix =
+ options.getOptionValue(
+ Constants.TARGET_FILE_ARGS,
+ measurementFile == null ? Constants.DUMP_FILE_NAME_DEFAULT : "");
+ if (prefix.contains("/") || prefix.contains("\\") || prefix.indexOf('\0') >= 0) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_FILE_PREFIX_MUST_NOT_CONTAIN_PATH_SEPARATORS_ARG_C89823EE,
+ prefix));
+ }
+ String log = options.getOptionValue(Constants.EXPORT_DATA_LOG_ARGS);
+ logFile =
+ log == null
+ ? targetDirectory.resolve(
+ "log_export_data_"
+ + startedAt.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ + ".log")
+ : Path.of(log).toAbsolutePath().normalize();
+ database = lower(options.getOptionValue(Constants.DB_ARGS));
+ table = lower(options.getOptionValue(Constants.TABLE_ARGS));
+ startTime = options.getOptionValue(Constants.START_TIME_ARGS);
+ endTime = options.getOptionValue(Constants.END_TIME_ARGS);
+ timeZone = options.getOptionValue(Constants.TIME_ZONE_ARGS);
+ timeFormat = options.getOptionValue(Constants.TIME_FORMAT_ARGS, "default");
+ if (!"default".equals(timeFormat)
+ && !"timestamp".equals(timeFormat)
+ && !"number".equals(timeFormat)
+ && !"long".equals(timeFormat)) {
+ try {
+ DateTimeFormatter.ofPattern(timeFormat);
+ } catch (IllegalArgumentException e) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_INVALID_OUTPUT_TIME_FORMAT_ARG_9C690675, timeFormat));
+ }
+ }
+ if (!tree && "information_schema".equals(database)) {
+ throw new ArgsErrorException(
+ CliMessages.EXCEPTION_EXPORTING_INFORMATION_SCHEMA_IS_NOT_SUPPORTED_6FD0FB85);
+ }
+ if (measurementFile != null && logFile.equals(measurementFile)) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_THE_EXPORT_LOG_MUST_DIFFER_FROM_THE_MEASUREMENT_INPUT_FILE_088A542C);
+ }
+ // Only construct a terminal reader if an option actually needs a prompt.
+ LineReader reader = null;
+ if ((options.hasOption(Constants.PW_ARGS) && options.getOptionValue(Constants.PW_ARGS) == null)
+ || (useSsl
+ && (options.getOptionValue(Constants.TRUST_STORE_ARGS) == null
+ || options.getOptionValue(Constants.TRUST_STORE_PWD_ARGS) == null
+ || ((options.hasOption(Constants.KEY_STORE_ARGS)
+ || options.hasOption(Constants.KEY_STORE_PWD_ARGS))
+ && (options.getOptionValue(Constants.KEY_STORE_ARGS) == null
+ || options.getOptionValue(Constants.KEY_STORE_PWD_ARGS) == null))))) {
+ reader =
+ JlineUtils.getLineReader(
+ new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION),
+ username,
+ host,
+ Integer.toString(port));
+ }
+ password =
+ options.hasOption(Constants.PW_ARGS)
+ ? valueOrPrompt(
+ options,
+ Constants.PW_ARGS,
+ reader,
+ CliMessages.LOG_PLEASE_INPUT_YOUR_PASSWORD_AEE49DC5)
+ : Constants.PW_DEFAULT_VALUE;
+ trustStore =
+ useSsl
+ ? valueOrPrompt(
+ options,
+ Constants.TRUST_STORE_ARGS,
+ reader,
+ CliMessages.LOG_PLEASE_INPUT_YOUR_TRUST_STORE_4C50D824)
+ : null;
+ trustStorePassword =
+ useSsl
+ ? valueOrPrompt(
+ options,
+ Constants.TRUST_STORE_PWD_ARGS,
+ reader,
+ CliMessages.LOG_PLEASE_INPUT_YOUR_TRUST_STORE_PASSWORD_86ED5CB6)
+ : null;
+ keyStore =
+ useSsl
+ && (options.hasOption(Constants.KEY_STORE_ARGS)
+ || options.hasOption(Constants.KEY_STORE_PWD_ARGS))
+ ? valueOrPrompt(
+ options,
+ Constants.KEY_STORE_ARGS,
+ reader,
+ CliMessages.LOG_PLEASE_INPUT_YOUR_KEY_STORE_AC5DB91F)
+ : null;
+ keyStorePassword =
+ keyStore == null
+ ? null
+ : valueOrPrompt(
+ options,
+ Constants.KEY_STORE_PWD_ARGS,
+ reader,
+ CliMessages.LOG_PLEASE_INPUT_YOUR_KEY_STORE_PASSWORD_19ED16D4);
+ sslProtocol = options.getOptionValue(Constants.SSL_PROTOCOL_ARGS);
+ }
+
+ static CommandLine inspect(String[] args) throws ParseException {
+ // Parse a union without required flags to select the dialect/format, without silently
+ // accepting unknown arguments. The selected option set is subsequently parsed strictly.
+ Options all = new Options();
+ Options[] sets = {
+ OptionsUtil.createExportCsvOptions(),
+ OptionsUtil.createExportSqlOptions(),
+ OptionsUtil.createExportTsFileOptions(),
+ OptionsUtil.createTableExportCsvOptions(),
+ OptionsUtil.createTableExportSqlOptions(),
+ OptionsUtil.createTableExportTsFileOptions()
+ };
+ for (Options set : sets) {
+ for (Option option : set.getOptions()) {
+ Option copy = (Option) option.clone();
+ copy.setRequired(false);
+ all.addOption(copy);
+ }
+ }
+ all.addOption(Option.builder(Constants.HELP_ARGS).hasArg().optionalArg(true).build());
+ return new DefaultParser(false).parse(all, args);
+ }
+
+ static ExportConfig parse(String[] args, ZonedDateTime startedAt)
+ throws ParseException, ArgsErrorException, IOException {
+ CommandLine inspected = inspect(args);
+ String dialect = inspected.getOptionValue(Constants.SQL_DIALECT_ARGS, "tree");
+ if (!"tree".equalsIgnoreCase(dialect) && !"table".equalsIgnoreCase(dialect)) {
+ throw new ArgsErrorException(
+ String.format(CliMessages.EXCEPTION_UNSUPPORTED_SQL_DIALECT_ARG_C4D0D97C, dialect));
+ }
+ String format = inspected.getOptionValue(Constants.FILE_TYPE_ARGS);
+ if (format == null
+ || (!"csv".equalsIgnoreCase(format)
+ && !"sql".equalsIgnoreCase(format)
+ && !"tsfile".equalsIgnoreCase(format))) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_SPECIFY_AN_EXPORT_FORMAT_CSV_SQL_OR_TSFILE_RECEIVED_ARG_5B5211B5,
+ format));
+ }
+ if (inspected.hasOption(Constants.MEASUREMENT_FILE_ARGS)) {
+ if (!"tree".equalsIgnoreCase(dialect)) {
+ throw new ArgsErrorException(
+ CliMessages.EXCEPTION_MEASUREMENT_FILE_IS_SUPPORTED_ONLY_IN_THE_TREE_MODEL_1ED604B5);
+ }
+ if (inspected.hasOption(Constants.QUERY_COMMAND_ARGS)) {
+ throw new ArgsErrorException(
+ CliMessages.EXCEPTION_MEASUREMENT_FILE_AND_Q_ARE_MUTUALLY_EXCLUSIVE_7A7457D6);
+ }
+ }
+ CommandLine selected =
+ new DefaultParser(false).parse(options("tree".equalsIgnoreCase(dialect), format), args);
+ if (!selected.getArgList().isEmpty()) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_UNEXPECTED_POSITIONAL_ARGUMENTS_ARG_027F44CB,
+ selected.getArgList()));
+ }
+ return new ExportConfig(selected, startedAt);
+ }
+
+ static Options options(boolean tree, String format) {
+ if ("csv".equalsIgnoreCase(format)) {
+ return tree
+ ? OptionsUtil.createExportCsvOptions()
+ : OptionsUtil.createTableExportCsvOptions();
+ }
+ if ("sql".equalsIgnoreCase(format)) {
+ return tree
+ ? OptionsUtil.createExportSqlOptions()
+ : OptionsUtil.createTableExportSqlOptions();
+ }
+ return tree
+ ? OptionsUtil.createExportTsFileOptions()
+ : OptionsUtil.createTableExportTsFileOptions();
+ }
+
+ private static long positiveNumber(CommandLine options, String key, long fallback, long maximum)
+ throws ArgsErrorException {
+ String value = options.getOptionValue(key);
+ if (value == null) {
+ return fallback;
+ }
+ try {
+ long parsed = Long.parseLong(value);
+ if (parsed > 0 && parsed <= maximum) {
+ return parsed;
+ }
+ } catch (NumberFormatException ignored) {
+ // Report the option as well as the original value.
+ }
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_INVALID_POSITIVE_INTEGER_FOR_OPTION_ARG_ARG_30FB488D,
+ key,
+ value));
+ }
+
+ private static boolean booleanOption(CommandLine options, String key, boolean fallback)
+ throws ArgsErrorException {
+ String value = options.getOptionValue(key);
+ if (value == null) {
+ return fallback;
+ }
+ if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value)) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_OPTION_ARG_REQUIRES_TRUE_OR_FALSE_ARG_0DC9BE3D, key, value));
+ }
+ return Boolean.parseBoolean(value);
+ }
+
+ private static String lower(String value) {
+ return value == null ? null : value.toLowerCase(Locale.ROOT);
+ }
+
+ private static String valueOrPrompt(
+ CommandLine options, String key, LineReader reader, String prompt) {
+ String value = options.getOptionValue(key);
+ return value == null ? reader.readLine(prompt, '\0') : value;
+ }
+
+ Session newTreeSession() {
+ Session.Builder builder =
+ new Session.Builder()
+ .host(host)
+ .port(port)
+ .username(username)
+ .password(password)
+ .zoneId(null)
+ .thriftMaxFrameSize(rpcMaxFrameSize);
+ if (useSsl) {
+ builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePassword);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePassword);
+ }
+ if (sslProtocol != null) {
+ builder.sslProtocol(sslProtocol);
+ }
+ }
+ return builder.build();
+ }
+
+ void configureSsl(TableSessionBuilder builder) {
+ if (useSsl) {
+ builder.useSSL(true).trustStore(trustStore).trustStorePwd(trustStorePassword);
+ if (keyStore != null) {
+ builder.keyStore(keyStore).keyStorePwd(keyStorePassword);
+ }
+ if (sslProtocol != null) {
+ builder.sslProtocol(sslProtocol);
+ }
+ }
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConsole.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConsole.java
new file mode 100644
index 00000000000..8601165bd7d
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportConsole.java
@@ -0,0 +1,137 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.jline.utils.WCWidth;
+
+import java.io.Console;
+import java.io.PrintStream;
+
+/** What a person reads while exporting; the export log keeps the machine-readable events. */
+final class ExportConsole {
+ // Querying the terminal for its size would replace the signal handlers that report the exit
+ // status, so the width comes from COLUMNS, which the shell launcher sets, or is taken as 80.
+ private static final int DEFAULT_WIDTH = 80;
+ private static final String ELLIPSIS = "...";
+
+ private final PrintStream out;
+ private final boolean terminal;
+ private final int width;
+ private int drawn;
+
+ ExportConsole(PrintStream out, boolean terminal) {
+ this(out, terminal, columns());
+ }
+
+ ExportConsole(PrintStream out, boolean terminal, int width) {
+ this.out = out;
+ this.terminal = terminal;
+ this.width = width;
+ }
+
+ static boolean isTerminal() {
+ Console console = System.console();
+ if (console == null) {
+ return false;
+ }
+ try {
+ // From JDK 22 redirected streams also get a console; isTerminal tells them apart.
+ return (Boolean) Console.class.getMethod("isTerminal").invoke(console);
+ } catch (ReflectiveOperationException e) {
+ return true;
+ }
+ }
+
+ private static int columns() {
+ try {
+ int columns = Integer.parseInt(System.getenv("COLUMNS"));
+ return columns >= 20 ? columns : DEFAULT_WIDTH;
+ } catch (NumberFormatException e) {
+ return DEFAULT_WIDTH;
+ }
+ }
+
+ /** Prints a lasting line, first erasing a progress line that would otherwise share its row. */
+ void line(String text) {
+ erase();
+ out.println(text);
+ out.flush();
+ }
+
+ /**
+ * A terminal redraws one line in place, shortening {@code tail} from the left to fit. Redirected
+ * output gets {@code plain} as a line of its own, so files and container logs hold no redraws; so
+ * does a terminal too narrow for {@code text}, since a carriage return reaches only the last row
+ * of a wrapped line and would leave the rest of every redraw behind.
+ */
+ void progress(String text, String tail, String plain) {
+ if (!terminal || width(text) > width - 1) {
+ erase();
+ out.println(plain);
+ out.flush();
+ return;
+ }
+ // The last column stays free: filling it makes some terminals wrap before the next redraw.
+ int room = width - 1 - width(text) - 2;
+ String shown =
+ tail.isEmpty() || room <= ELLIPSIS.length() ? text : text + " " + left(tail, room);
+ int shownWidth = width(shown);
+ out.print('\r' + shown + " ".repeat(Math.max(0, drawn - shownWidth)));
+ out.flush();
+ drawn = shownWidth;
+ }
+
+ private void erase() {
+ if (drawn > 0) {
+ out.print('\r' + " ".repeat(drawn) + '\r');
+ drawn = 0;
+ }
+ }
+
+ /** The end of {@code text} that fits in {@code columns}, marked as shortened when cut. */
+ static String left(String text, int columns) {
+ if (width(text) <= columns) {
+ return text;
+ }
+ int budget = columns - ELLIPSIS.length();
+ int start = text.length();
+ int used = 0;
+ while (start > 0) {
+ int codePoint = text.codePointBefore(start);
+ int codePointWidth = Math.max(0, WCWidth.wcwidth(codePoint));
+ if (used + codePointWidth > budget) {
+ break;
+ }
+ used += codePointWidth;
+ start -= Character.charCount(codePoint);
+ }
+ // Cut between path nodes, so the shown part is whole nodes such as line19.s0.
+ int node = text.indexOf('.', start);
+ if (start > 0 && text.charAt(start - 1) != '.' && node >= 0 && node + 1 < text.length()) {
+ start = node + 1;
+ }
+ return ELLIPSIS + text.substring(start);
+ }
+
+ /** Display columns: a CJK character takes two. */
+ static int width(String text) {
+ return text.codePoints().map(c -> Math.max(0, WCWidth.wcwidth(c))).sum();
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
index 63a2fdf45c7..a199d8ba182 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportData.java
@@ -19,313 +19,291 @@
package org.apache.iotdb.tool.data;
+import org.apache.iotdb.cli.i18n.CliMessages;
import org.apache.iotdb.cli.type.ExitType;
import org.apache.iotdb.cli.utils.CliContext;
-import org.apache.iotdb.cli.utils.IoTPrinter;
import org.apache.iotdb.cli.utils.JlineUtils;
import org.apache.iotdb.exception.ArgsErrorException;
-import org.apache.iotdb.rpc.IoTDBConnectionException;
-import org.apache.iotdb.rpc.StatementExecutionException;
import org.apache.iotdb.tool.common.Constants;
-import org.apache.iotdb.tool.common.OptionsUtil;
import org.apache.commons.cli.CommandLine;
-import org.apache.commons.cli.CommandLineParser;
-import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
-import org.apache.commons.cli.Options;
-import org.apache.commons.cli.ParseException;
-import org.apache.thrift.TException;
-import org.apache.tsfile.external.commons.lang3.ObjectUtils;
-import org.apache.tsfile.external.commons.lang3.StringUtils;
-import org.jline.reader.LineReader;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.UserInterruptException;
+import org.jline.utils.Signals;
-import java.io.File;
import java.io.IOException;
-import java.util.Arrays;
+import java.nio.file.Files;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
-/**
- * Export CSV file.
- *
- * @version 1.0.0 20170719
- */
-public class ExportData extends AbstractDataTool {
+/** Entry point and lifecycle for one export invocation. */
+public final class ExportData {
+ /** How long cancellation waits for the worker to close its own query and session. */
+ private static final long CANCEL_GRACE_SECONDS = 3;
+
+ /** POSIX numbers of the signals that shut the JVM down; it then exits with 128 + number. */
+ private static final Map<String, Integer> TERMINATING_SIGNALS =
+ Map.of("HUP", 1, "INT", 2, "TERM", 15);
+
+ /** Exit status chosen by a terminating signal, or 0 before one arrives. */
+ private static final AtomicInteger SIGNAL_EXIT_CODE = new AtomicInteger();
+
+ /** Reads the Tree query when neither a query nor a measurement file is supplied. */
+ @FunctionalInterface
+ interface QueryPrompt {
+ String read(ExportConfig config) throws IOException;
+ }
- private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
+ private ExportData() {}
- @SuppressWarnings({
- "squid:S3776",
- "squid:S2093"
- }) // Suppress high Cognitive Complexity warning, ignore try-with-resources
- /* main function of export csv tool. */
public static void main(String[] args) {
- OptionsUtil.setIsImport(false);
- Options helpOptions = OptionsUtil.createHelpOptions();
- Options tsFileOptions = OptionsUtil.createExportTsFileOptions();
- Options csvOptions = OptionsUtil.createExportCsvOptions();
- Options sqlOptions = OptionsUtil.createExportSqlOptions();
- HelpFormatter hf = new HelpFormatter();
- CommandLine commandLine = null;
- CommandLineParser parser = new DefaultParser();
- hf.setOptionComparator(null); // avoid reordering
- hf.setWidth(Constants.MAX_HELP_CONSOLE_WIDTH);
+ System.exit(run(args));
+ }
- if (args == null || args.length == 0) {
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- System.exit(Constants.CODE_ERROR);
- }
+ static int run(String[] args) {
+ return run(args, ExportData::promptQuery);
+ }
+
+ // Tests replace the prompt: JLine reads the process terminal rather than a replaced System.in.
+ static int run(String[] args, QueryPrompt queryPrompt) {
+ ZonedDateTime startedAt = ZonedDateTime.now();
+ ExportProgressReporter progress = null;
+ AbstractExportData exporter = null;
+ Thread hook = null;
+ AtomicBoolean cancelled = new AtomicBoolean();
+ CountDownLatch finished = new CountDownLatch(1);
+ Throwable failure = null;
try {
- commandLine = parser.parse(helpOptions, args, true);
- } catch (ParseException e) {
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- System.exit(Constants.CODE_ERROR);
- }
- final List<String> argList = Arrays.asList(args);
- int helpIndex = argList.indexOf(Constants.MINUS + Constants.HELP_ARGS);
- int sql_dialect = argList.indexOf(Constants.MINUS + Constants.SQL_DIALECT_ARGS); // -sql_dialect
- if (sql_dialect >= 0
- && !Constants.SQL_DIALECT_VALUE_TREE.equalsIgnoreCase(argList.get(sql_dialect + 1))) {
- final String sqlDialectValue = argList.get(sql_dialect + 1);
- if (Constants.SQL_DIALECT_VALUE_TABLE.equalsIgnoreCase(sqlDialectValue)) {
- sqlDialectTree = false;
- csvOptions = OptionsUtil.createTableExportCsvOptions();
- tsFileOptions = OptionsUtil.createTableExportTsFileOptions();
- sqlOptions = OptionsUtil.createTableExportSqlOptions();
- } else {
- ioTPrinter.println(String.format("sql_dialect %s is not support", sqlDialectValue));
- printHelpOptions(
- Constants.IMPORT_CLI_HEAD,
- Constants.IMPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- System.exit(Constants.CODE_ERROR);
+ if (args == null || args.length == 0) {
+ help(true, null);
+ return Constants.CODE_ERROR;
}
- }
- int ftIndex = argList.indexOf(Constants.MINUS + Constants.FILE_TYPE_ARGS);
- if (ftIndex < 0) {
- ftIndex = argList.indexOf(Constants.MINUS + Constants.FILE_TYPE_NAME);
- }
- if (helpIndex >= 0) {
- fileType = argList.get(helpIndex + 1);
- if (StringUtils.isNotBlank(fileType)) {
- if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(fileType)) {
- printHelpOptions(null, Constants.EXPORT_CLI_PREFIX, hf, tsFileOptions, null, null, false);
- } else if (Constants.CSV_SUFFIXS.equalsIgnoreCase(fileType)) {
- printHelpOptions(null, Constants.EXPORT_CLI_PREFIX, hf, null, csvOptions, null, false);
- } else if (Constants.SQL_SUFFIXS.equalsIgnoreCase(fileType)) {
- printHelpOptions(null, Constants.EXPORT_CLI_PREFIX, hf, null, null, sqlOptions, false);
- } else {
- ioTPrinter.println(String.format("File type %s is not support", fileType));
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- }
- } else {
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
+ CommandLine inspected = ExportConfig.inspect(args);
+ if (inspected.hasOption(Constants.HELP_ARGS)) {
+ help(
+ !"table".equalsIgnoreCase(inspected.getOptionValue(Constants.SQL_DIALECT_ARGS)),
+ inspected.getOptionValue(Constants.HELP_ARGS));
+ return Constants.CODE_OK;
}
- System.exit(Constants.CODE_ERROR);
- } else if (ftIndex >= 0) {
- fileType = argList.get(ftIndex + 1);
- if (StringUtils.isNotBlank(fileType)) {
- if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(fileType)) {
- try {
- commandLine = parser.parse(tsFileOptions, args, true);
- } catch (ParseException e) {
- ioTPrinter.println("Parse error: " + e.getMessage());
- printHelpOptions(
- null, Constants.EXPORT_CLI_PREFIX, hf, tsFileOptions, null, null, false);
- System.exit(Constants.CODE_ERROR);
- }
- } else if (Constants.CSV_SUFFIXS.equalsIgnoreCase(fileType)) {
- try {
- commandLine = parser.parse(csvOptions, args, true);
- } catch (ParseException e) {
- ioTPrinter.println("Parse error: " + e.getMessage());
- printHelpOptions(null, Constants.EXPORT_CLI_PREFIX, hf, null, csvOptions, null, false);
- System.exit(Constants.CODE_ERROR);
- }
- } else if (Constants.SQL_SUFFIXS.equalsIgnoreCase(fileType)) {
- try {
- commandLine = parser.parse(sqlOptions, args, true);
- } catch (ParseException e) {
- ioTPrinter.println("Parse error: " + e.getMessage());
- printHelpOptions(null, Constants.EXPORT_CLI_PREFIX, hf, null, null, sqlOptions, false);
- System.exit(Constants.CODE_ERROR);
- }
- } else {
- ioTPrinter.println(String.format("File type %s is not support", fileType));
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- System.exit(Constants.CODE_ERROR);
- }
- } else {
- printHelpOptions(
- Constants.EXPORT_CLI_HEAD,
- Constants.EXPORT_CLI_PREFIX,
- hf,
- tsFileOptions,
- csvOptions,
- sqlOptions,
- true);
- System.exit(Constants.CODE_ERROR);
+ ExportConfig config = ExportConfig.parse(args, startedAt);
+ if (config.measurementFile != null
+ && Files.exists(config.measurementFile)
+ && Files.exists(config.logFile)
+ && Files.isSameFile(config.measurementFile, config.logFile)) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_THE_EXPORT_LOG_MUST_DIFFER_FROM_THE_MEASUREMENT_INPUT_FILE_088A542C);
}
- } else {
- ioTPrinter.println(
- String.format(Constants.REQUIRED_ARGS_ERROR_MSG, Constants.FILE_TYPE_NAME));
- System.exit(Constants.CODE_ERROR);
- }
- int exitCode = Constants.CODE_OK;
- try {
- parseBasicParams(commandLine);
- parseSpecialParams(commandLine);
- if (!checkTimeFormat()) {
- System.exit(Constants.CODE_ERROR);
+ String query = config.query;
+ if (query == null && config.tree && config.measurementFile == null) {
+ // Prompt before the progress timer starts, so periodic events cannot interleave with input.
+ query = queryPrompt.read(config);
}
- AbstractExportData exportData;
- exportData = new ExportDataTree();
- exportData.init();
- if (!sqlDialectTree) {
- exportData = new ExportDataTable();
- exportData.init();
+ Files.createDirectories(config.targetDirectory);
+ progress = new ExportProgressReporter(config, System.nanoTime(), System.out);
+ exporter =
+ config.tree
+ ? new ExportDataTree(config, progress)
+ : new ExportDataTable(config, progress);
+ ExportProgressReporter reporter = progress;
+ AbstractExportData ownedExporter = exporter;
+ Thread worker = Thread.currentThread();
+ hook =
+ new Thread(
+ () -> {
+ int signalExitCode = SIGNAL_EXIT_CODE.get();
+ cancelled.set(true);
+ reporter.cancel(signalExitCode == 0 ? Constants.CODE_ERROR : signalExitCode);
+ worker.interrupt();
+ try {
+ // The worker closes its query and session once an in-flight call returns.
+ // Closing them underneath that call makes Thrift log a socket error.
+ if (finished.await(CANCEL_GRACE_SECONDS, TimeUnit.SECONDS)) {
+ return;
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ // The worker is blocked in a call that ignores interrupts; closing ends it.
+ Throwable closeFailure = null;
+ try {
+ ownedExporter.close();
+ } catch (Exception e) {
+ closeFailure = e;
+ }
+ try {
+ reporter.finish(ExportProgressReporter.Status.CANCELLED, closeFailure);
+ } catch (Exception ignored) {
+ // finish already reports a log failure to the console.
+ }
+ },
+ "export-data-shutdown");
+ recordTerminatingSignals();
+ Runtime.getRuntime().addShutdownHook(hook);
+ progress.start(
+ config,
+ query != null,
+ () -> {
+ worker.interrupt();
+ // Unblock IO cleanup without making the progress timer access the session.
+ Thread cleanup =
+ new Thread(
+ () -> {
+ try {
+ ownedExporter.close();
+ } catch (Exception ignored) {
+ // The reporter retains the original log failure.
+ }
+ },
+ "export-data-log-failure");
+ cleanup.setDaemon(true);
+ cleanup.start();
+ });
+ MeasurementFile input =
+ config.measurementFile == null ? null : MeasurementFile.read(config.measurementFile);
+ List<ExportTask> tasks = query == null ? null : queryTasks(config, query);
+ progress.phase(ExportProgressReporter.Phase.CONNECTING);
+ exporter.init();
+ if (input != null) {
+ tasks = ((ExportDataTree) exporter).planMeasurements(input, exporter.timeRange());
+ } else if (tasks == null) {
+ tasks = ((ExportDataTable) exporter).planTables(exporter.timeRange());
+ } else if (!config.tree) {
+ tasks = ((ExportDataTable) exporter).planQueries(tasks);
}
- if (queryCommand == null) {
- if (sqlDialectTree) {
- LineReader lineReader =
- JlineUtils.getLineReader(
- new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION),
- username,
- host,
- port);
- String sql = lineReader.readLine(Constants.EXPORT_CLI_PREFIX + "> please input query: ");
- ioTPrinter.println(sql);
- String[] values = sql.trim().split(";");
- for (int i = 0; i < values.length; i++) {
- if (!exportData.exportBySql(values[i], i)) {
- exitCode = Constants.CODE_ERROR;
- }
- }
- } else {
- if (!exportData.exportBySql(null, 0)) {
- exitCode = Constants.CODE_ERROR;
+ for (ExportTask task : tasks) {
+ progress.checkActive();
+ exporter.export(task);
+ }
+ progress.checkActive();
+ progress.phase(ExportProgressReporter.Phase.CLOSING);
+ } catch (Exception e) {
+ failure = e;
+ if (e instanceof UserInterruptException || e instanceof EndOfFileException) {
+ cancelled.set(true);
+ }
+ } finally {
+ Exception closeFailure = null;
+ if (exporter != null) {
+ try {
+ exporter.close();
+ } catch (Exception e) {
+ closeFailure = e;
+ if (failure == null) {
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
}
}
- } else {
- String[] values = queryCommand.trim().split(";");
- for (int i = 0; i < values.length; i++) {
- if (!exportData.exportBySql(values[i], i)) {
- exitCode = Constants.CODE_ERROR;
+ }
+ if (progress != null) {
+ try {
+ // A cancelled export reports cleanup failures, not the interruption it caused.
+ progress.finish(
+ cancelled.get()
+ ? ExportProgressReporter.Status.CANCELLED
+ : failure == null
+ ? ExportProgressReporter.Status.SUCCESS
+ : ExportProgressReporter.Status.FAILED,
+ cancelled.get() ? closeFailure : failure);
+ } catch (Exception e) {
+ if (failure == null) {
+ failure = e;
+ } else {
+ failure.addSuppressed(e);
}
}
+ } else if (cancelled.get()) {
+ // Input was abandoned at a prompt before the export started.
+ System.out.println(CliMessages.EXCEPTION_EXPORT_CANCELLED_85D44F54);
+ } else if (failure != null) {
+ System.out.println(
+ String.format(CliMessages.LOG_EXPORT_FAILED_ARG_57F9420F, failure.getMessage()));
}
- } catch (IOException e) {
- ioTPrinter.println("Failed to operate on file, because " + e.getMessage());
- exitCode = Constants.CODE_ERROR;
- } catch (ArgsErrorException e) {
- ioTPrinter.println("Invalid args: " + e.getMessage());
- exitCode = Constants.CODE_ERROR;
- } catch (IoTDBConnectionException | StatementExecutionException e) {
- ioTPrinter.println("Connect failed because " + e.getMessage());
- exitCode = Constants.CODE_ERROR;
- } catch (TException e) {
- ioTPrinter.println(
- "Can not get the timestamp precision from server because " + e.getMessage());
- exitCode = Constants.CODE_ERROR;
+ if (hook != null) {
+ try {
+ Runtime.getRuntime().removeShutdownHook(hook);
+ } catch (IllegalStateException ignored) {
+ // JVM shutdown owns the cancellation hook once it has started.
+ }
+ }
+ // A running cancellation hook waits for this before the JVM exits.
+ finished.countDown();
}
- System.exit(exitCode);
+ return failure == null
+ && !cancelled.get()
+ && (progress == null || progress.status() == ExportProgressReporter.Status.SUCCESS)
+ ? Constants.CODE_OK
+ : Constants.CODE_ERROR;
}
- private static void parseSpecialParams(CommandLine commandLine) throws ArgsErrorException {
- targetDirectory =
- checkRequiredArg(Constants.TARGET_DIR_ARGS, Constants.TARGET_DIR_NAME, commandLine, null);
- targetFile = commandLine.getOptionValue(Constants.TARGET_FILE_ARGS);
- needDataTypePrinted = Boolean.valueOf(commandLine.getOptionValue(Constants.DATA_TYPE_ARGS));
- queryCommand = commandLine.getOptionValue(Constants.QUERY_COMMAND_ARGS);
- exportType = commandLine.getOptionValue(Constants.FILE_TYPE_ARGS);
- String timeoutString = commandLine.getOptionValue(Constants.TIMEOUT_ARGS);
- if (timeoutString != null) {
- timeout = Long.parseLong(timeoutString);
- }
- String rpcMaxFrameSizeString = commandLine.getOptionValue(Constants.RPC_MAX_FRAME_SIZE_ARGS);
- if (rpcMaxFrameSizeString != null) {
- rpcMaxFrameSize = Integer.parseInt(rpcMaxFrameSizeString);
- }
- if (needDataTypePrinted == null) {
- needDataTypePrinted = true;
- }
- if (targetFile == null) {
- targetFile = Constants.DUMP_FILE_NAME_DEFAULT;
- }
- timeFormat = commandLine.getOptionValue(Constants.TIME_FORMAT_ARGS);
- if (timeFormat == null) {
- timeFormat = "default";
- }
- timeZoneID = commandLine.getOptionValue(Constants.TIME_ZONE_ARGS);
- if (!targetDirectory.endsWith("/") && !targetDirectory.endsWith("\\")) {
- targetDirectory += File.separator;
- }
- final File file = new File(targetDirectory);
- if (!file.isDirectory() && !file.mkdirs()) {
- ioTPrinter.println(String.format("Failed to create directories %s", targetDirectory));
- System.exit(Constants.CODE_ERROR);
- }
- if (commandLine.getOptionValue(Constants.LINES_PER_FILE_ARGS) != null) {
- linesPerFile = Integer.parseInt(commandLine.getOptionValue(Constants.LINES_PER_FILE_ARGS));
- }
- if (commandLine.getOptionValue(Constants.ALIGNED_ARGS) != null) {
- aligned = Boolean.valueOf(commandLine.getOptionValue(Constants.ALIGNED_ARGS));
+ // Shutdown hooks cannot see the signal, so record it to report the status the JVM exits with.
+ private static void recordTerminatingSignals() {
+ TERMINATING_SIGNALS.forEach(
+ (name, number) ->
+ // Without the signal on this platform, the JVM default handler stays in place.
+ Signals.register(
+ name,
+ () -> {
+ SIGNAL_EXIT_CODE.set(128 + number);
+ System.exit(128 + number);
+ }));
+ }
+
+ private static String promptQuery(ExportConfig config) throws IOException {
+ return JlineUtils.getLineReader(
+ new CliContext(System.in, System.out, System.err, ExitType.EXCEPTION),
+ config.username,
+ config.host,
+ Integer.toString(config.port))
+ .readLine(CliMessages.LOG_PLEASE_INPUT_QUERY_EEAD5433);
+ }
+
+ static List<ExportTask> queryTasks(ExportConfig config, String query) throws ArgsErrorException {
+ if (query == null || query.trim().isEmpty()) {
+ throw new ArgsErrorException(CliMessages.EXCEPTION_NO_EXPORT_QUERY_WAS_PROVIDED_09E24A92);
}
- if (commandLine.getOptionValue(Constants.DB_ARGS) != null) {
- database = commandLine.getOptionValue(Constants.DB_ARGS).toLowerCase();
- if (ObjectUtils.isNotEmpty(database) && "information_schema".equalsIgnoreCase(database)) {
- ioTPrinter.println(
- String.format("Does not support exporting system databases %s", database));
- System.exit(Constants.CODE_ERROR);
+ List<ExportTask> tasks = new ArrayList<>();
+ for (String sql : query.trim().split(";")) {
+ if (sql.trim().isEmpty()) {
+ continue;
}
+ tasks.add(
+ new ExportTask(
+ sql.trim(),
+ config.targetDirectory.resolve(config.prefix + tasks.size()),
+ null,
+ null));
}
- if (commandLine.getOptionValue(Constants.TABLE_ARGS) != null) {
- table = commandLine.getOptionValue(Constants.TABLE_ARGS).toLowerCase();
- }
- if (commandLine.getOptionValue(Constants.START_TIME_ARGS) != null) {
- startTime = commandLine.getOptionValue(Constants.START_TIME_ARGS);
- }
- if (commandLine.getOptionValue(Constants.END_TIME_ARGS) != null) {
- endTime = commandLine.getOptionValue(Constants.END_TIME_ARGS);
+ if (tasks.isEmpty()) {
+ throw new ArgsErrorException(CliMessages.EXCEPTION_NO_EXPORT_QUERY_WAS_PROVIDED_09E24A92);
}
+ return tasks;
+ }
+
+ private static void help(boolean tree, String format) {
+ HelpFormatter formatter = new HelpFormatter();
+ formatter.setOptionComparator(null);
+ formatter.setWidth(Constants.MAX_HELP_CONSOLE_WIDTH);
+ boolean all =
+ format == null
+ || (!"csv".equalsIgnoreCase(format)
+ && !"sql".equalsIgnoreCase(format)
+ && !"tsfile".equalsIgnoreCase(format));
+ AbstractDataTool.printHelpOptions(
+ all ? Constants.EXPORT_CLI_HEAD : null,
+ Constants.EXPORT_CLI_PREFIX,
+ formatter,
+ all || "tsfile".equalsIgnoreCase(format) ? ExportConfig.options(tree, "tsfile") : null,
+ all || "csv".equalsIgnoreCase(format) ? ExportConfig.options(tree, "csv") : null,
+ all || "sql".equalsIgnoreCase(format) ? ExportConfig.options(tree, "sql") : null,
+ all);
}
}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
index d60b5d559b6..55e5742b1ab 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTable.java
@@ -20,16 +20,15 @@
package org.apache.iotdb.tool.data;
import org.apache.iotdb.cli.i18n.CliMessages;
-import org.apache.iotdb.cli.utils.IoTPrinter;
import org.apache.iotdb.commons.queryengine.plan.relational.sql.parser.CaseInsensitiveStream;
import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlBaseVisitor;
import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlLexer;
import org.apache.iotdb.db.relational.grammar.sql.RelationalSqlParser;
+import org.apache.iotdb.exception.ArgsErrorException;
import org.apache.iotdb.isession.ITableSession;
import org.apache.iotdb.isession.SessionDataSet;
-import org.apache.iotdb.rpc.IoTDBConnectionException;
-import org.apache.iotdb.rpc.StatementExecutionException;
import org.apache.iotdb.rpc.UrlUtils;
+import org.apache.iotdb.session.Session;
import org.apache.iotdb.session.TableSessionBuilder;
import org.apache.iotdb.tool.common.Constants;
@@ -40,162 +39,114 @@ import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.misc.ParseCancellationException;
+import org.apache.tsfile.enums.ColumnCategory;
import org.apache.tsfile.enums.TSDataType;
-import org.apache.tsfile.exception.write.WriteProcessException;
-import org.apache.tsfile.external.commons.collections4.CollectionUtils;
-import org.apache.tsfile.external.commons.lang3.ObjectUtils;
-import org.apache.tsfile.external.commons.lang3.StringUtils;
import org.apache.tsfile.file.metadata.ColumnSchema;
import org.apache.tsfile.file.metadata.ColumnSchemaBuilder;
-import org.apache.tsfile.fileSystem.FSFactoryProducer;
-import org.apache.tsfile.read.common.Field;
import org.apache.tsfile.read.common.RowRecord;
import org.apache.tsfile.write.record.Tablet;
-import org.apache.tsfile.write.schema.IMeasurementSchema;
import org.apache.tsfile.write.v4.ITsFileWriter;
import org.apache.tsfile.write.v4.TsFileWriterBuilder;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.stream.Collectors;
public class ExportDataTable extends AbstractExportData {
+ private ITableSession session;
- private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
- private static ITableSession tableSession;
- private static List<String> tables = new ArrayList<>();
- private static List<String> allTables = new ArrayList<>();
- private static long processedRows;
- private static long lastPrintTime;
+ ExportDataTable(ExportConfig config, ExportProgressReporter progress) {
+ super(config, progress);
+ }
+
+ ExportDataTable(ExportConfig config, ExportProgressReporter progress, ITableSession session)
+ throws Exception {
+ this(config, progress);
+ this.session = own(session).get();
+ zoneId = ZoneId.of("UTC");
+ timestampPrecision = "ms";
+ }
@Override
- public void init() throws IoTDBConnectionException, StatementExecutionException {
- TableSessionBuilder tableSessionBuilder =
- new TableSessionBuilder()
- .nodeUrls(Collections.singletonList(UrlUtils.formatTEndPointIpv4AndIpv6Url(host, port)))
- .username(username)
- .password(password)
- .database(database)
- .thriftMaxFrameSize(rpcMaxFrameSize);
- if (useSsl) {
- tableSessionBuilder = configureSsl(tableSessionBuilder);
+ public void init() throws Exception {
+ // TableSession does not expose precision or timezone; retain the existing metadata handshake.
+ try (Resource<Session> metadata = own(config.newTreeSession())) {
+ readTimeSettings(metadata.get());
}
- tableSession = tableSessionBuilder.build();
- SessionDataSet sessionDataSet = tableSession.executeQueryStatement("show databases", timeout);
- List<String> databases = new ArrayList<>();
- while (sessionDataSet.hasNext()) {
- databases.add(sessionDataSet.next().getField(0).getStringValue());
- }
- if (CollectionUtils.isEmpty(databases) || !databases.contains(database)) {
- ioTPrinter.println(String.format(Constants.TARGET_DATABASE_NOT_EXIST_MSG, database));
- System.exit(1);
- }
- sessionDataSet = tableSession.executeQueryStatement("show tables", timeout);
- allTables.clear();
- while (sessionDataSet.hasNext()) {
- allTables.add(sessionDataSet.next().getField(0).getStringValue());
+ TableSessionBuilder builder =
+ new TableSessionBuilder()
+ .nodeUrls(
+ Collections.singletonList(
+ UrlUtils.formatTEndPointIpv4AndIpv6Url(config.host, config.port)))
+ .username(config.username)
+ .password(config.password)
+ .database(config.database)
+ .zoneId(zoneId)
+ .thriftMaxFrameSize(config.rpcMaxFrameSize);
+ config.configureSsl(builder);
+ session = own(builder.build()).get();
+ if (!names("show databases").contains(config.database)) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_THE_TARGET_DATABASE_ARG_DOES_NOT_EXIST_80E096EB,
+ config.database));
}
- boolean filterByTable = StringUtils.isBlank(queryCommand) && ObjectUtils.isNotEmpty(table);
- if (CollectionUtils.isEmpty(allTables) || (filterByTable && !allTables.contains(table))) {
- ioTPrinter.println(String.format(Constants.TARGET_TABLE_NOT_EXIST_MSG, table));
- System.exit(1);
+ }
+
+ List<ExportTask> planTables(ExportTimeRange range) throws Exception {
+ List<String> tables = names("show tables");
+ if (tables.isEmpty() || (config.table != null && !tables.contains(config.table))) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_THERE_ARE_NO_TABLES_OR_THE_TARGET_TABLE_ARG_DOES_NOT_EXIST_E5F88A98,
+ config.table));
}
- tables.clear();
- if (filterByTable) {
- tables.add(table);
- } else {
- tables.addAll(allTables);
+ if (config.table != null) {
+ tables = Collections.singletonList(config.table);
}
- if (ObjectUtils.isNotEmpty(sessionDataSet)) {
- sessionDataSet.close();
+ List<ExportTask> tasks = new ArrayList<>();
+ for (String table : tables) {
+ tasks.add(
+ new ExportTask(
+ "SELECT * FROM " + identifier(table) + range.whereClause(),
+ config.targetDirectory.resolve(config.prefix + tasks.size()),
+ null,
+ table));
}
+ return tasks;
}
- @Override
- public boolean exportBySql(String sql, int index) {
- List<String> exportSql = new ArrayList<>();
- boolean customQuery = StringUtils.isNotBlank(sql);
- boolean success = true;
- if (customQuery) {
- if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
- || Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
- legalCheck(sql);
- }
- exportSql.add(sql);
- } else {
- StringBuilder sqlBuilder;
- for (String table : tables) {
- sqlBuilder = new StringBuilder("select * from ").append(quoteIdentifier(table));
- if (StringUtils.isNotBlank(startTime) || StringUtils.isNotBlank(endTime)) {
- sqlBuilder.append(" where ");
- if (StringUtils.isNotBlank(startTime)) {
- sqlBuilder.append("time >= ").append(startTime);
- }
- if (StringUtils.isNotBlank(startTime) && StringUtils.isNotBlank(endTime)) {
- sqlBuilder.append(" and ");
- }
- if (StringUtils.isNotBlank(endTime)) {
- sqlBuilder.append("time <= ").append(endTime);
- }
- }
- exportSql.add(sqlBuilder.toString());
- }
+ /** SQL and TsFile output are written to the table each query reads; -table does not apply. */
+ List<ExportTask> planQueries(List<ExportTask> tasks) throws Exception {
+ if ("csv".equals(config.format)) {
+ return tasks;
}
- for (int i = 0; i < exportSql.size(); i++) {
- String path = targetDirectory + targetFile + (customQuery ? index : i);
- try {
- String queryTable =
- customQuery
- ? (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
- || Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)
- ? getQueryTable(sql)
- : null)
- : tables.get(i);
- try (SessionDataSet sessionDataSet =
- tableSession.executeQueryStatement(exportSql.get(i), timeout)) {
- if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)) {
- exportToSqlFile(sessionDataSet, queryTable, path);
- } else if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
- long start = System.currentTimeMillis();
- boolean isComplete = exportToTsFile(sessionDataSet, path + ".tsfile", queryTable);
- if (isComplete) {
- long end = System.currentTimeMillis();
- ioTPrinter.println("Export completely!cost: " + (end - start) + " ms.");
- }
- } else {
- exportToCsvFile(sessionDataSet, path);
- }
- sessionDataSet.closeOperationHandle();
- ioTPrinter.println(Constants.EXPORT_COMPLETELY);
- }
- } catch (StatementExecutionException
- | IoTDBConnectionException
- | IOException
- | WriteProcessException e) {
- ioTPrinter.println("Cannot dump result because: " + e.getMessage());
- success = false;
- }
+ // Every query is checked before the first one runs, so a rejected query writes no files.
+ List<String> tables = names("show tables");
+ List<ExportTask> planned = new ArrayList<>(tasks.size());
+ for (ExportTask task : tasks) {
+ planned.add(
+ new ExportTask(task.sql(), task.outputBase(), null, queryTable(task.sql(), tables)));
}
- return success;
+ return planned;
}
- private static String getQueryTable(String sql) throws StatementExecutionException {
- String message =
- String.format(
- CliMessages
- .EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768,
- database);
+ private String queryTable(String sql, List<String> tables) throws ArgsErrorException {
+ ArgsErrorException invalid =
+ new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768,
+ config.database));
RelationalSqlLexer lexer =
new RelationalSqlLexer(new CaseInsensitiveStream(CharStreams.fromString(sql)));
- RelationalSqlParser parser = new RelationalSqlParser(new CommonTokenStream(lexer));
lexer.removeErrorListeners();
lexer.addErrorListener(
new BaseErrorListener() {
@@ -205,11 +156,12 @@ public class ExportDataTable extends AbstractExportData {
Object offendingSymbol,
int line,
int charPositionInLine,
- String error,
+ String message,
RecognitionException cause) {
throw new ParseCancellationException(message);
}
});
+ RelationalSqlParser parser = new RelationalSqlParser(new CommonTokenStream(lexer));
parser.removeErrorListeners();
parser.setErrorHandler(new BailErrorStrategy());
List<RelationalSqlParser.QualifiedNameContext> sources = new ArrayList<>();
@@ -222,22 +174,22 @@ public class ExportDataTable extends AbstractExportData {
}
}.visit(parser.singleStatement());
} catch (ParseCancellationException e) {
- throw new StatementExecutionException(message);
+ throw invalid;
}
- // Multiple source relations do not have an unambiguous destination table.
+ // A query reading several relations has no single destination table.
if (sources.size() != 1) {
- throw new StatementExecutionException(message);
+ throw invalid;
}
- List<RelationalSqlParser.IdentifierContext> names = sources.get(0).identifier();
- if (names.size() > 2
- || (names.size() == 2 && !identifierValue(names.get(0)).equalsIgnoreCase(database))) {
- throw new StatementExecutionException(message);
+ List<RelationalSqlParser.IdentifierContext> parts = sources.get(0).identifier();
+ if (parts.size() > 2
+ || (parts.size() == 2 && !identifierValue(parts.get(0)).equals(config.database))) {
+ throw invalid;
}
- String name = identifierValue(names.get(names.size() - 1));
- if (!allTables.contains(name)) {
- throw new StatementExecutionException(message);
+ String table = identifierValue(parts.get(parts.size() - 1));
+ if (!tables.contains(table)) {
+ throw invalid;
}
- return name;
+ return table;
}
private static String identifierValue(RelationalSqlParser.IdentifierContext context) {
@@ -250,239 +202,236 @@ public class ExportDataTable extends AbstractExportData {
return value.toLowerCase(Locale.ROOT);
}
- private static String quoteIdentifier(String name) {
- return "\"" + name.replace("\"", "\"\"") + "\"";
- }
-
- private void exportToSqlFile(SessionDataSet sessionDataSet, String table, String filePath)
- throws IOException, IoTDBConnectionException, StatementExecutionException {
- processedRows = 0;
- lastPrintTime = 0;
- StringBuilder sqlBuilder;
- List<String> headers = sessionDataSet.getColumnNames();
- String prevSql =
- "insert into "
- + quoteIdentifier(table)
- + "("
- + headers.stream()
- .map(ExportDataTable::quoteIdentifier)
- .collect(Collectors.joining(","))
- + ") values(";
- SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
- List<String> columnTypeList = iterator.getColumnTypeList();
- int totalColumns = columnTypeList.size();
- int fileIndex = 0;
- boolean fromOuterLoop = false;
- while (iterator.next()) {
- final String finalFilePath = filePath + "_" + fileIndex + ".sql";
- int countLine = 0;
- fromOuterLoop = true;
- try (FileWriter writer = new FileWriter(finalFilePath)) {
- while (countLine++ < linesPerFile && (fromOuterLoop || iterator.next())) {
- fromOuterLoop = false;
- sqlBuilder = new StringBuilder();
- sqlBuilder.append(prevSql);
- for (int curColumnIndex = 0; curColumnIndex < totalColumns; curColumnIndex++) {
- if (curColumnIndex > 0) {
- sqlBuilder.append(",");
- }
- String columnType = columnTypeList.get(curColumnIndex);
- String columnValue = iterator.getString(curColumnIndex + 1);
- sqlBuilder.append(formatSqlValue(columnValue, columnType));
- }
- sqlBuilder.append(");\n");
- writer.write(sqlBuilder.toString());
- processedRows += 1;
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
- }
- }
- writer.flush();
- fileIndex++;
+ private List<String> names(String sql) throws Exception {
+ List<String> names = new ArrayList<>();
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement(sql, config.timeout))) {
+ while (result.get().hasNext()) {
+ progress.checkActive();
+ names.add(result.get().next().getField(0).getStringValue());
}
}
- ioTPrinter.print("\n");
+ return names;
}
- private Boolean exportToTsFile(SessionDataSet sessionDataSet, String filePath, String table)
- throws IOException,
- IoTDBConnectionException,
- StatementExecutionException,
- WriteProcessException {
- processedRows = 0;
- lastPrintTime = 0;
- List<String> columnNamesRaw = sessionDataSet.getColumnNames();
- List<TSDataType> columnTypesRaw =
- sessionDataSet.getColumnTypes().stream().map(t -> getType(t)).collect(Collectors.toList());
- File f = FSFactoryProducer.getFSFactory().getFile(filePath);
- if (f.exists()) {
- Files.delete(f.toPath());
+ @Override
+ void export(ExportTask task) throws Exception {
+ if (!"csv".equals(config.format)) {
+ legalCheck(task.sql());
}
- boolean isEmpty = false;
- Map<String, Integer> deviceColumnIndices = new HashMap<>();
- List<ColumnSchema> columnSchemas = collectSchemas(columnNamesRaw, deviceColumnIndices, table);
- try (ITsFileWriter tsFileWriter =
- new TsFileWriterBuilder()
- .file(f)
- .tableSchema(new org.apache.tsfile.file.metadata.TableSchema(table, columnSchemas))
- .memoryThreshold(Constants.memoryThreshold)
- .build()) {
- List<String> columnNames = new ArrayList<>(columnNamesRaw);
- List<TSDataType> columnTypes = new ArrayList<>(columnTypesRaw);
- int timeIndex = columnNamesRaw.indexOf(timeColumn.toLowerCase());
- if (timeIndex >= 0) {
- columnNames.remove(timeIndex);
- columnTypes.remove(timeIndex);
+ progress.taskStarted(task);
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement(task.sql(), config.timeout))) {
+ switch (config.format) {
+ case "sql":
+ exportSql(result.get(), task);
+ break;
+ case "tsfile":
+ exportTsFile(result.get(), task);
+ break;
+ default:
+ exportCsv(result.get(), task);
}
- Tablet tablet = new Tablet(columnNames, columnTypes);
- if (ObjectUtils.isNotEmpty(tablet)) {
- writeWithTablets(sessionDataSet, tablet, deviceColumnIndices, tsFileWriter);
- } else {
- isEmpty = true;
- }
- }
- if (isEmpty) {
- ioTPrinter.println("!!!Warning:Tablet is empty,no data can be exported.");
- return false;
}
- return true;
+ progress.taskCompleted();
}
- private void exportToCsvFile(SessionDataSet sessionDataSet, String filePath)
- throws IOException, IoTDBConnectionException, StatementExecutionException {
- processedRows = 0;
- lastPrintTime = 0;
- List<String> headers = getCsvHeaders(sessionDataSet, false);
- int fileIndex = 0;
- SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
- List<String> columnTypeList = iterator.getColumnTypeList();
- int totalColumns = columnTypeList.size();
- boolean fromOuterloop = false;
- while (iterator.next()) {
- final String finalFilePath = filePath + "_" + fileIndex + ".csv";
- final CSVPrinterWrapper csvPrinterWrapper = new CSVPrinterWrapper(finalFilePath);
- try {
- csvPrinterWrapper.printRecord(headers);
- fromOuterloop = true;
- int countLine = 0;
- while (countLine++ < linesPerFile && (fromOuterloop || iterator.next())) {
- fromOuterloop = false;
- for (int curColumnIndex = 0; curColumnIndex < totalColumns; curColumnIndex++) {
- String curType = columnTypeList.get(curColumnIndex);
- if (curType.equalsIgnoreCase("TIMESTAMP")) {
- csvPrinterWrapper.print(timeTrans(iterator.getLong(curColumnIndex + 1)));
- } else {
- if (iterator.isNull(curColumnIndex + 1)) {
- csvPrinterWrapper.print("");
- } else {
- String columnValue = iterator.getString(curColumnIndex + 1);
- if (curType.equalsIgnoreCase("TEXT") || curType.equalsIgnoreCase("STRING")) {
- csvPrinterWrapper.print("\"" + columnValue + "\"");
- } else {
- csvPrinterWrapper.print(columnValue);
- }
- }
+ private void exportCsv(SessionDataSet data, ExportTask task) throws Exception {
+ List<String> names = data.getColumnNames();
+ SessionDataSet.DataIterator iterator = data.iterator();
+ List<String> types = iterator.getColumnTypeList();
+ try (Resource<ExportTextWriter> output =
+ own(new ExportTextWriter(config, task, progress, csvHeaders(names, types, false)))) {
+ while (iterator.next()) {
+ progress.checkActive();
+ List<Object> values = new ArrayList<>(names.size());
+ for (int i = 0; i < names.size(); i++) {
+ if (iterator.isNull(i + 1)) {
+ values.add("");
+ } else if ("TIMESTAMP".equals(types.get(i))) {
+ values.add(timeTrans(iterator.getLong(i + 1)));
+ } else {
+ String value = iterator.getString(i + 1);
+ if ("TEXT".equals(types.get(i)) || "STRING".equals(types.get(i))) {
+ value = '"' + value + '"';
}
- }
- csvPrinterWrapper.println();
- processedRows += 1;
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
+ values.add(value);
}
}
- fileIndex++;
- csvPrinterWrapper.flush();
- } finally {
- csvPrinterWrapper.close();
+ output.get().csvRow(values);
}
+ output.get().complete();
}
- ioTPrinter.print("\n");
}
- private static void writeWithTablets(
- SessionDataSet sessionDataSet,
- Tablet tablet,
- Map<String, Integer> deviceColumnIndices,
- ITsFileWriter tsFileWriter)
- throws IoTDBConnectionException,
- StatementExecutionException,
- IOException,
- WriteProcessException {
- while (sessionDataSet.hasNext()) {
- RowRecord rowRecord = sessionDataSet.next();
- List<Field> fields = rowRecord.getFields();
- int rowIndex = tablet.getRowSize();
- tablet.addTimestamp(
- rowIndex, fields.get(deviceColumnIndices.get(timeColumn.toLowerCase())).getLongV());
- List<IMeasurementSchema> schemas = tablet.getSchemas();
- for (int i = 0; i < schemas.size(); i++) {
- IMeasurementSchema measurementSchema = schemas.get(i);
- // -1 for time not in fields
- final String measurementName = measurementSchema.getMeasurementName();
- if (timeColumn.equalsIgnoreCase(measurementName)) {
- continue;
+ private void exportSql(SessionDataSet data, ExportTask task) throws Exception {
+ validateColumns(data, task.table());
+ List<String> headers = data.getColumnNames();
+ SessionDataSet.DataIterator iterator = data.iterator();
+ List<String> types = iterator.getColumnTypeList();
+ List<String> quotedHeaders = new ArrayList<>();
+ for (String header : headers) {
+ quotedHeaders.add(identifier(header));
+ }
+ String prefix =
+ "insert into "
+ + identifier(task.table())
+ + "("
+ + String.join(",", quotedHeaders)
+ + ") values(";
+ try (Resource<ExportTextWriter> output =
+ own(new ExportTextWriter(config, task, progress, null))) {
+ while (iterator.next()) {
+ progress.checkActive();
+ StringBuilder sql = new StringBuilder(prefix);
+ for (int i = 0; i < headers.size(); i++) {
+ if (i > 0) {
+ sql.append(',');
+ }
+ sql.append(sqlValue(iterator, types.get(i), i + 1));
}
- Object value =
- fields
- .get(deviceColumnIndices.get(measurementName))
- .getObjectValue(measurementSchema.getType());
- tablet.addValue(measurementName, rowIndex, value);
+ output.get().sqlRow(sql.append(");\n").toString());
}
+ output.get().complete();
+ }
+ }
- if (tablet.getRowSize() == tablet.getMaxRowNumber()) {
- writeToTsFile(tsFileWriter, tablet);
- processedRows += tablet.getRowSize();
- tablet.initBitMaps();
- tablet.reset();
- }
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
+ private Map<String, ColumnSchema> validateColumns(SessionDataSet data, String table)
+ throws Exception {
+ Map<String, ColumnSchema> schemas = new LinkedHashMap<>();
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement("DESCRIBE " + identifier(table), config.timeout))) {
+ while (result.get().hasNext()) {
+ progress.checkActive();
+ RowRecord row = result.get().next();
+ String name = row.getField(0).getStringValue();
+ if (!"time".equals(name)) {
+ schemas.put(
+ name,
+ new ColumnSchemaBuilder()
+ .name(name)
+ .dataType(TSDataType.valueOf(row.getField(1).getStringValue()))
+ .category(ColumnCategory.valueOf(row.getField(2).getStringValue()))
+ .build());
+ }
}
}
- if (tablet.getRowSize() != 0) {
- writeToTsFile(tsFileWriter, tablet);
- processedRows += tablet.getRowSize();
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
+ List<String> columns = data.getColumnNames();
+ List<String> types = data.getColumnTypes();
+ if (!columns.contains("time")) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_SQL_TSFILE_OUTPUT_REQUIRES_THE_TIME_COLUMN_AND_UNALIASED_COLUMNS_FROM_TABLE_ARG_C5E77CD3,
+ table));
+ }
+ for (int i = 0; i < columns.size(); i++) {
+ String column = columns.get(i);
+ if ("time".equals(column)) {
+ continue;
+ }
+ ColumnSchema schema = schemas.get(column);
+ if (schema == null || schema.getDataType() != TSDataType.valueOf(types.get(i))) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_SQL_TSFILE_OUTPUT_REQUIRES_THE_TIME_COLUMN_AND_UNALIASED_COLUMNS_FROM_TABLE_ARG_C5E77CD3,
+ table));
}
}
- ioTPrinter.print("\n");
- }
-
- private static void writeToTsFile(ITsFileWriter tsFileWriter, Tablet tablet)
- throws IOException, WriteProcessException {
- tsFileWriter.write(tablet);
+ return schemas;
}
- private List<ColumnSchema> collectSchemas(
- List<String> columnNames, Map<String, Integer> deviceColumnIndices, String table)
- throws IoTDBConnectionException, StatementExecutionException {
- List<ColumnSchema> columnSchemas = new ArrayList<>();
- SessionDataSet sessionDataSet =
- tableSession.executeQueryStatement("describe " + quoteIdentifier(table));
- while (sessionDataSet.hasNext()) {
- RowRecord rowRecord = sessionDataSet.next();
- final String columnName = rowRecord.getField(0).getStringValue();
- if (timeColumn.equalsIgnoreCase(columnName)) {
+ private void exportTsFile(SessionDataSet data, ExportTask task) throws Exception {
+ Map<String, ColumnSchema> available = validateColumns(data, task.table());
+ List<String> columns = data.getColumnNames();
+ List<ColumnSchema> schemas = new ArrayList<>();
+ List<String> names = new ArrayList<>();
+ List<TSDataType> types = new ArrayList<>();
+ List<ColumnCategory> categories = new ArrayList<>();
+ List<Integer> indices = new ArrayList<>();
+ for (int i = 0; i < columns.size(); i++) {
+ String column = columns.get(i);
+ if ("time".equals(column)) {
continue;
}
- columnSchemas.add(
- new ColumnSchemaBuilder()
- .name(columnName)
- .dataType(getType(rowRecord.getField(1).getStringValue()))
- .category(getColumnCategory(rowRecord.getField(2).getStringValue()))
- .build());
+ ColumnSchema schema = available.get(column);
+ schemas.add(schema);
+ names.add(column);
+ types.add(schema.getDataType());
+ categories.add(schema.getColumnCategory());
+ indices.add(i + 1);
}
- for (int i = 0; i < columnNames.size(); i++) {
- deviceColumnIndices.put(columnNames.get(i), i);
+ // All tags are needed to preserve device identity in a TsFile.
+ for (ColumnSchema schema : available.values()) {
+ if (schema.getColumnCategory() == ColumnCategory.TAG
+ && !names.contains(schema.getColumnName())) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_SQL_TSFILE_OUTPUT_REQUIRES_THE_TIME_COLUMN_AND_UNALIASED_COLUMNS_FROM_TABLE_ARG_C5E77CD3,
+ task.table()));
+ }
}
- if (ObjectUtils.isNotEmpty(sessionDataSet)) {
- sessionDataSet.close();
+ Path file = Path.of(task.outputBase() + ".tsfile");
+ ExportFileNames.checkOutputCollision(file, config);
+ progress.fileStarted(file);
+ Files.deleteIfExists(file);
+ long startRows = progress.rows();
+ try (Resource<ITsFileWriter> output =
+ own(
+ new TsFileWriterBuilder()
+ .file(file.toFile())
+ .tableSchema(new org.apache.tsfile.file.metadata.TableSchema(task.table(), schemas))
+ .memoryThreshold(Constants.memoryThreshold)
+ .build())) {
+ Tablet tablet = new Tablet(task.table(), names, types, categories);
+ tablet.initBitMaps();
+ SessionDataSet.DataIterator iterator = data.iterator();
+ int time = columns.indexOf("time") + 1;
+ while (iterator.next()) {
+ progress.checkActive();
+ int row = tablet.getRowSize();
+ tablet.addTimestamp(row, iterator.getLong(time));
+ for (int i = 0; i < names.size(); i++) {
+ int column = indices.get(i);
+ Object value = null;
+ if (!iterator.isNull(column)) {
+ switch (types.get(i)) {
+ case TIMESTAMP:
+ value = iterator.getLong(column);
+ break;
+ case DATE:
+ value = iterator.getDate(column);
+ break;
+ case BLOB:
+ value = iterator.getBlob(column);
+ break;
+ default:
+ value = iterator.getObject(column);
+ }
+ }
+ tablet.addValue(names.get(i), row, value);
+ }
+ if (tablet.getRowSize() == tablet.getMaxRowNumber()) {
+ writeTablet(output.get(), tablet);
+ }
+ }
+ if (tablet.getRowSize() > 0) {
+ writeTablet(output.get(), tablet);
+ }
}
- return columnSchemas;
+ progress.fileCompleted(file, progress.rows() - startRows);
+ }
+
+ private void writeTablet(ITsFileWriter writer, Tablet tablet) throws Exception {
+ writer.write(tablet);
+ progress.rowsWritten(tablet.getRowSize());
+ tablet.reset();
+ tablet.initBitMaps();
+ }
+
+ private static String identifier(String name) {
+ return "\"" + name.replace("\"", "\"\"") + "\"";
}
}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
index bab4fa88b92..65c5d13dfdd 100644
--- a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportDataTree.java
@@ -19,21 +19,15 @@
package org.apache.iotdb.tool.data;
-import org.apache.iotdb.cli.utils.IoTPrinter;
-import org.apache.iotdb.isession.SessionConfig;
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.exception.ArgsErrorException;
import org.apache.iotdb.isession.SessionDataSet;
-import org.apache.iotdb.rpc.IoTDBConnectionException;
-import org.apache.iotdb.rpc.StatementExecutionException;
import org.apache.iotdb.session.Session;
-import org.apache.iotdb.tool.common.Constants;
-import org.apache.thrift.TException;
import org.apache.tsfile.enums.TSDataType;
-import org.apache.tsfile.exception.write.WriteProcessException;
-import org.apache.tsfile.external.commons.collections4.CollectionUtils;
import org.apache.tsfile.file.metadata.enums.CompressionType;
import org.apache.tsfile.file.metadata.enums.TSEncoding;
-import org.apache.tsfile.fileSystem.FSFactoryProducer;
import org.apache.tsfile.read.common.Field;
import org.apache.tsfile.read.common.Path;
import org.apache.tsfile.read.common.RowRecord;
@@ -42,16 +36,13 @@ import org.apache.tsfile.write.record.Tablet;
import org.apache.tsfile.write.schema.IMeasurementSchema;
import org.apache.tsfile.write.schema.MeasurementSchema;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
import java.nio.file.Files;
-import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -59,420 +50,398 @@ import static org.apache.iotdb.commons.schema.SchemaConstant.AUDIT_DATABASE;
import static org.apache.iotdb.commons.schema.SchemaConstant.SYSTEM_DATABASE;
public class ExportDataTree extends AbstractExportData {
+ private Session session;
+ private final Map<String, Boolean> alignment = new HashMap<>();
+ private final Map<String, ExportTask.Measurement> measurements = new LinkedHashMap<>();
- private static final IoTPrinter ioTPrinter = new IoTPrinter(System.out);
- private static Session session;
- private static long processedRows;
- private static long lastPrintTime;
+ ExportDataTree(ExportConfig config, ExportProgressReporter progress) {
+ super(config, progress);
+ }
- @Override
- public void init() throws IoTDBConnectionException, StatementExecutionException, TException {
- Session.Builder sessionBuilder =
- new Session.Builder()
- .host(host)
- .port(Integer.parseInt(port))
- .username(username)
- .password(password)
- .fetchSize(SessionConfig.DEFAULT_FETCH_SIZE)
- .zoneId(null)
- .thriftDefaultBufferSize(SessionConfig.DEFAULT_INITIAL_BUFFER_CAPACITY)
- .thriftMaxFrameSize(rpcMaxFrameSize)
- .enableRedirection(SessionConfig.DEFAULT_REDIRECTION_MODE)
- .version(SessionConfig.DEFAULT_VERSION);
- if (useSsl) {
- sessionBuilder = configureSsl(sessionBuilder);
- }
- session = sessionBuilder.build();
- session.open(false);
- timestampPrecision = session.getTimestampPrecision();
- if (timeZoneID != null) {
- session.setTimeZone(timeZoneID);
- }
- zoneId = ZoneId.of(session.getTimeZone());
+ // A connected Session can be supplied for output regression tests.
+ ExportDataTree(ExportConfig config, ExportProgressReporter progress, Session session)
+ throws Exception {
+ this(config, progress);
+ this.session = own(session).get();
+ zoneId = java.time.ZoneId.of("UTC");
+ timestampPrecision = "ms";
}
@Override
- public boolean exportBySql(String sql, int index) {
- if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)
- || Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
- legalCheck(sql);
- }
- final String path = targetDirectory + targetFile + index;
- try (SessionDataSet sessionDataSet = session.executeQueryStatement(sql, timeout)) {
- if (Constants.SQL_SUFFIXS.equalsIgnoreCase(exportType)) {
- exportToSqlFile(sessionDataSet, path);
- } else if (Constants.TSFILE_SUFFIXS.equalsIgnoreCase(exportType)) {
- long start = System.currentTimeMillis();
- boolean isComplete = exportToTsFile(sessionDataSet, path + ".tsfile");
- if (isComplete) {
- long end = System.currentTimeMillis();
- ioTPrinter.println("Export completely!cost: " + (end - start) + " ms.");
+ public void init() throws Exception {
+ session = own(config.newTreeSession()).get();
+ readTimeSettings(session);
+ }
+
+ List<ExportTask> planMeasurements(MeasurementFile input, ExportTimeRange range) throws Exception {
+ progress.phase(ExportProgressReporter.Phase.DISCOVERING);
+ long matches = 0;
+ int unmatched = 0;
+ for (MeasurementFile.PatternLine pattern : input.patterns()) {
+ progress.checkActive();
+ long patternMatches = 0;
+ try (Resource<SessionDataSet> result =
+ own(
+ session.executeQueryStatement(
+ "show timeseries " + pattern.path().getFullPath(), config.timeout))) {
+ SessionDataSet data = result.get();
+ List<String> columns = data.getColumnNames();
+ while (data.hasNext()) {
+ progress.checkActive();
+ ExportTask.Measurement measurement = measurement(data.next(), columns);
+ measurements.putIfAbsent(measurement.path().getFullPath(), measurement);
+ patternMatches++;
}
- } else {
- exportToCsvFile(sessionDataSet, path);
}
- sessionDataSet.closeOperationHandle();
- ioTPrinter.println(Constants.EXPORT_COMPLETELY);
- return true;
- } catch (StatementExecutionException
- | IoTDBConnectionException
- | IOException
- | WriteProcessException e) {
- ioTPrinter.println("Cannot dump result because: " + e.getMessage());
- return false;
+ matches += patternMatches;
+ if (patternMatches == 0) {
+ unmatched++;
+ // The message names the pattern; the log also records which file it came from.
+ progress.warning(
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_NO_MEASUREMENTS_MATCHED_ARG_MEASUREMENT_FILE_LINE_ARG_94C4EF10,
+ pattern.original(),
+ pattern.line()),
+ "source",
+ input.source(),
+ "line",
+ pattern.line(),
+ "pattern",
+ pattern.original());
+ }
+ }
+ progress.discovered(input, matches, measurements.size(), unmatched);
+ if (measurements.isEmpty()) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_NO_MEASUREMENTS_REMAIN_AFTER_PATTERN_EXPANSION_AND_DEDUPLICATION_40D930B7);
+ }
+ ExportFileNames names = new ExportFileNames();
+ List<ExportTask> tasks = new ArrayList<>(measurements.size());
+ for (ExportTask.Measurement measurement : measurements.values()) {
+ PartialPath path = measurement.path();
+ java.nio.file.Path output = names.measurement(config, path.getFullPath());
+ // Validate collisions before the first data query. Split paths are also checked when opened.
+ ExportFileNames.checkOutputCollision(
+ java.nio.file.Path.of(
+ output + ("tsfile".equals(config.format) ? ".tsfile" : "_0." + config.format)),
+ config);
+ String sql =
+ "SELECT "
+ + path.getMeasurement()
+ + " FROM "
+ + devicePath(path).getFullPath()
+ + range.whereClause();
+ tasks.add(new ExportTask(sql, output, measurement, null));
}
+ return tasks;
}
- private void exportToSqlFileWithAlignDevice(SessionDataSet sessionDataSet, String filePath)
- throws IOException, IoTDBConnectionException, StatementExecutionException {
- processedRows = 0;
- lastPrintTime = 0;
-
- List<String> measurementNames = new ArrayList<>(sessionDataSet.getColumnNames());
- if (CollectionUtils.isEmpty(measurementNames) || measurementNames.size() <= 1) {
- return;
- } else {
- measurementNames.remove("Time");
- measurementNames.remove("Device");
- }
- String sqlPrefix =
- String.format(
- "INSERT INTO %s(TIMESTAMP,%s) ALIGNED VALUES (%s);\n",
- "%s", String.join(",", measurementNames), "%d,%s");
+ private static PartialPath devicePath(PartialPath measurement) {
+ String[] nodes = measurement.getNodes();
+ return new PartialPath(java.util.Arrays.copyOf(nodes, nodes.length - 1));
+ }
- SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
- List<String> columnTypeList = iterator.getColumnTypeList();
- int totalColumns = columnTypeList.size();
- String deviceName = null;
- int fileIndex = 0;
- int currentLines = 0;
- String filePathTemplate = filePath + "_%d" + ".sql";
- FileWriter writer = null;
- while (iterator.next()) {
- if (writer == null) {
- writer = new FileWriter(String.format(filePathTemplate, fileIndex));
- }
- deviceName = iterator.getString(2);
- if (deviceName.startsWith(SYSTEM_DATABASE + ".")
- || deviceName.startsWith(AUDIT_DATABASE + ".")) {
- continue;
- }
- List<String> values = new ArrayList<>();
- for (int index = 2; index < totalColumns; index++) {
- values.add(formatSqlValue(iterator.getString(index + 1), columnTypeList.get(index)));
- }
- long timestamp = iterator.getLong(1);
- writer.write(String.format(sqlPrefix, deviceName, timestamp, String.join(",", values)));
- processedRows += 1;
- currentLines += 1;
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
- }
- if (currentLines >= linesPerFile) {
- writer.flush();
- writer.close();
- fileIndex += 1;
- writer = null;
- currentLines = 0;
- }
+ private ExportTask.Measurement measurement(RowRecord record, List<String> columns)
+ throws Exception {
+ List<Field> fields = record.getFields();
+ PartialPath path = new PartialPath(fields.get(columns.indexOf("Timeseries")).getStringValue());
+ TSDataType type =
+ AbstractDataTool.getType(fields.get(columns.indexOf("DataType")).getStringValue());
+ MeasurementSchema schema = new MeasurementSchema(path.getMeasurement(), type);
+ String encoding = fields.get(columns.indexOf("Encoding")).getStringValue();
+ String compression = fields.get(columns.indexOf("Compression")).getStringValue();
+ // Logical views may omit physical encoding and compression metadata.
+ if (encoding != null && !"null".equals(encoding)) {
+ schema.setEncoding(TSEncoding.valueOf(encoding));
}
- if (writer != null) {
- writer.flush();
- writer.close();
+ if (compression != null && !"null".equals(compression)) {
+ schema.setCompressionType(CompressionType.valueOf(compression));
}
- ioTPrinter.print("\n");
+ return new ExportTask.Measurement(path, schema);
}
- private void exportToSqlFileWithoutAlign(SessionDataSet sessionDataSet, String filePath)
- throws IoTDBConnectionException, StatementExecutionException, IOException {
- processedRows = 0;
- lastPrintTime = 0;
- List<String> headers = sessionDataSet.getColumnNames();
- Map<String, List<Integer>> deviceColumnIndices = new LinkedHashMap<>();
- Map<Integer, String> measurements = new HashMap<>();
- for (int i = 1; i < headers.size(); i++) {
- Path path = new Path(headers.get(i), true);
- String device = path.getDeviceString();
- if (device.startsWith(SYSTEM_DATABASE + ".") || device.startsWith(AUDIT_DATABASE + ".")) {
- continue;
+ @Override
+ void export(ExportTask task) throws Exception {
+ if (!"csv".equals(config.format) && task.measurement() == null) {
+ legalCheck(task.sql());
+ }
+ progress.taskStarted(task);
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement(task.sql(), config.timeout))) {
+ SessionDataSet data = result.get();
+ switch (config.format) {
+ case "sql":
+ exportSql(data, task);
+ break;
+ case "tsfile":
+ exportTsFile(data, task);
+ break;
+ default:
+ exportCsv(data, task);
}
- deviceColumnIndices.computeIfAbsent(device, key -> new ArrayList<>()).add(i);
- measurements.put(i, path.getMeasurement());
}
+ progress.taskCompleted();
+ }
- SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
- List<String> columnTypes = iterator.getColumnTypeList();
- int fileIndex = 0;
- int currentLines = 0;
- FileWriter writer = null;
- try {
+ private void exportCsv(SessionDataSet data, ExportTask task) throws Exception {
+ List<String> names = data.getColumnNames();
+ SessionDataSet.DataIterator iterator = data.iterator();
+ List<String> types = iterator.getColumnTypeList();
+ try (Resource<ExportTextWriter> output =
+ own(new ExportTextWriter(config, task, progress, csvHeaders(names, types, true)))) {
while (iterator.next()) {
- for (Map.Entry<String, List<Integer>> entry : deviceColumnIndices.entrySet()) {
- List<String> names = new ArrayList<>();
- List<String> values = new ArrayList<>();
- for (int index : entry.getValue()) {
- if (!iterator.isNull(index + 1)) {
- names.add(measurements.get(index));
- values.add(formatSqlValue(iterator.getString(index + 1), columnTypes.get(index)));
- }
- }
- // A timestamp present in another device must not create an empty record here.
- if (values.isEmpty()) {
+ progress.checkActive();
+ List<Object> values = new ArrayList<>(types.size());
+ for (int i = 0; i < types.size(); i++) {
+ if (iterator.isNull(i + 1)) {
+ values.add("");
continue;
}
- if (writer == null) {
- writer = new FileWriter(filePath + "_" + fileIndex + ".sql");
+ if (i == 0 && "Time".equals(names.get(i))) {
+ values.add(timeTrans(iterator.getLong(i + 1)));
+ continue;
}
- writer.write(
- String.format(
- "INSERT INTO %s(TIMESTAMP,%s) VALUES (%d,%s);\n",
- entry.getKey(),
- String.join(",", names),
- iterator.getLong(1),
- String.join(",", values)));
- if (++currentLines >= linesPerFile) {
- writer.close();
- writer = null;
- fileIndex++;
- currentLines = 0;
+ String value = iterator.getString(i + 1);
+ if (("TEXT".equalsIgnoreCase(types.get(i)) || "STRING".equalsIgnoreCase(types.get(i)))
+ && !value.startsWith("root.")) {
+ value = '"' + value + '"';
}
+ values.add(value);
}
- processedRows++;
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
- }
- }
- } finally {
- if (writer != null) {
- writer.close();
+ output.get().csvRow(values);
}
+ output.get().complete();
}
- ioTPrinter.print("\n");
}
- private void exportToSqlFile(SessionDataSet sessionDataSet, String filePath)
- throws IOException, IoTDBConnectionException, StatementExecutionException {
- List<String> headers = sessionDataSet.getColumnNames();
- boolean alignByDeviceQuery = headers.contains("Device");
- if (alignByDeviceQuery) {
- exportToSqlFileWithAlignDevice(sessionDataSet, filePath);
+ private void exportSql(SessionDataSet data, ExportTask task) throws Exception {
+ List<String> headers = data.getColumnNames();
+ SessionDataSet.DataIterator iterator = data.iterator();
+ List<String> types = iterator.getColumnTypeList();
+ boolean byDevice = headers.contains("Device");
+ // Column lists are built once: parsing the column paths for every row slowed SQL export.
+ SqlColumns deviceRow = new SqlColumns();
+ Map<String, SqlColumns> deviceColumns = new LinkedHashMap<>();
+ if (byDevice) {
+ for (int i = 2; i < headers.size(); i++) {
+ deviceRow.add(i, headers.get(i));
+ }
} else {
- exportToSqlFileWithoutAlign(sessionDataSet, filePath);
- }
- }
-
- private static Boolean exportToTsFile(SessionDataSet sessionDataSet, String filePath)
- throws IOException,
- IoTDBConnectionException,
- StatementExecutionException,
- WriteProcessException {
- List<String> columnNames = sessionDataSet.getColumnNames();
- List<String> columnTypes = sessionDataSet.getColumnTypes();
- File f = FSFactoryProducer.getFSFactory().getFile(filePath);
- if (f.exists()) {
- Files.delete(f.toPath());
- }
- boolean isEmpty = false;
- try (TsFileWriter tsFileWriter = new TsFileWriter(f)) {
- // device -> column indices in columnNames
- Map<String, List<Integer>> deviceColumnIndices = new HashMap<>();
- Set<String> alignedDevices = new HashSet<>();
- Map<String, List<IMeasurementSchema>> deviceSchemaMap = new LinkedHashMap<>();
-
- collectSchemas(
- columnNames, columnTypes, deviceSchemaMap, alignedDevices, deviceColumnIndices);
-
- List<Tablet> tabletList = constructTablets(deviceSchemaMap, alignedDevices, tsFileWriter);
-
- if (!tabletList.isEmpty()) {
- writeWithTablets(
- sessionDataSet, tabletList, alignedDevices, tsFileWriter, deviceColumnIndices);
- tsFileWriter.flush();
- } else {
- isEmpty = true;
+ for (int i = 1; i < headers.size(); i++) {
+ PartialPath path = new PartialPath(headers.get(i));
+ if (!"root".equals(path.getFirstNode()) || path.hasWildcard()) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_SQL_OUTPUT_REQUIRES_ORIGINAL_MEASUREMENT_PATHS_OR_AN_ALIGN_BY_DEVICE_QUERY_ADA72EA8);
+ }
+ deviceColumns
+ .computeIfAbsent(devicePath(path).getFullPath(), key -> new SqlColumns())
+ .add(i, path.getMeasurement());
}
}
- if (isEmpty) {
- ioTPrinter.println("!!!Warning:Tablet is empty,no data can be exported.");
- return false;
- }
- return true;
- }
-
- private void exportToCsvFile(SessionDataSet sessionDataSet, String filePath)
- throws IOException, IoTDBConnectionException, StatementExecutionException {
- processedRows = 0;
- lastPrintTime = 0;
- List<String> headers = getCsvHeaders(sessionDataSet, true);
- int fileIndex = 0;
- SessionDataSet.DataIterator iterator = sessionDataSet.iterator();
- List<String> columnTypeList = iterator.getColumnTypeList();
- int totalColumns = columnTypeList.size();
- boolean fromOuterloop = false;
- while (iterator.next()) {
- final String finalFilePath = filePath + "_" + fileIndex + ".csv";
- CSVPrinterWrapper csvPrinterWrapper = new CSVPrinterWrapper(finalFilePath);
- try {
- csvPrinterWrapper.printRecord(headers);
- fromOuterloop = true;
- int i = 0;
- while (i++ < linesPerFile && (fromOuterloop || iterator.next())) {
- fromOuterloop = false;
- csvPrinterWrapper.print(timeTrans(iterator.getLong(1)));
- for (int curColumnIndex = 1; curColumnIndex < totalColumns; curColumnIndex++) {
- if (iterator.isNull(curColumnIndex + 1)) {
- csvPrinterWrapper.print("");
- } else {
- String columnValue = iterator.getString(curColumnIndex + 1);
- String curType = columnTypeList.get(curColumnIndex);
- if ((curType.equalsIgnoreCase("TEXT") || curType.equalsIgnoreCase("STRING"))
- && !columnValue.startsWith("root.")) {
- csvPrinterWrapper.print("\"" + columnValue + "\"");
- } else {
- csvPrinterWrapper.print(columnValue);
- }
- }
- }
- csvPrinterWrapper.println();
- processedRows += 1;
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
+ try (Resource<ExportTextWriter> output =
+ own(new ExportTextWriter(config, task, progress, null))) {
+ while (iterator.next()) {
+ progress.checkActive();
+ // One result row is one data row: its statements for every device stay in one file.
+ StringBuilder row = new StringBuilder();
+ if (byDevice) {
+ // Every row of a device has the same columns, so a missing value is written as NULL.
+ appendInsert(row, iterator, types, iterator.getString(2), deviceRow, true, false);
+ } else {
+ for (Map.Entry<String, SqlColumns> device : deviceColumns.entrySet()) {
+ boolean aligned =
+ config.aligned || (task.measurement() != null && isAligned(device.getKey()));
+ // A row also carries the times of other devices: null columns are left out.
+ appendInsert(row, iterator, types, device.getKey(), device.getValue(), aligned, true);
}
}
- fileIndex++;
- csvPrinterWrapper.flush();
- } finally {
- csvPrinterWrapper.close();
+ if (row.length() > 0) {
+ output.get().sqlRow(row.toString());
+ }
}
+ output.get().complete();
}
- ioTPrinter.print("\n");
}
- private static void writeWithTablets(
- SessionDataSet sessionDataSet,
- List<Tablet> tabletList,
- Set<String> alignedDevices,
- TsFileWriter tsFileWriter,
- Map<String, List<Integer>> deviceColumnIndices)
- throws IoTDBConnectionException,
- StatementExecutionException,
- IOException,
- WriteProcessException {
- processedRows = 0;
- lastPrintTime = 0;
- while (sessionDataSet.hasNext()) {
- RowRecord rowRecord = sessionDataSet.next();
- List<Field> fields = rowRecord.getFields();
-
- for (Tablet tablet : tabletList) {
- String deviceId = tablet.getDeviceId();
- List<Integer> columnIndices = deviceColumnIndices.get(deviceId);
- int rowIndex = tablet.getRowSize();
- tablet.addTimestamp(rowIndex, rowRecord.getTimestamp());
- List<IMeasurementSchema> schemas = tablet.getSchemas();
+ /** The result columns of one device and the measurement list its INSERT statements name. */
+ private static final class SqlColumns {
+ private final List<Integer> columns = new ArrayList<>();
+ private final List<String> measurements = new ArrayList<>();
+ private final StringBuilder names = new StringBuilder();
- for (int i = 0, columnIndicesSize = columnIndices.size(); i < columnIndicesSize; i++) {
- Integer columnIndex = columnIndices.get(i);
- IMeasurementSchema measurementSchema = schemas.get(i);
- Object value = fields.get(columnIndex - 1).getObjectValue(measurementSchema.getType());
- tablet.addValue(measurementSchema.getMeasurementName(), rowIndex, value);
- }
+ private void add(int column, String measurement) {
+ columns.add(column);
+ measurements.add(measurement);
+ names.append(',').append(measurement);
+ }
+ }
- if (tablet.getRowSize() == tablet.getMaxRowNumber()) {
- writeToTsFile(alignedDevices, tsFileWriter, tablet);
- processedRows += tablet.getRowSize();
- tablet.reset();
+ private static void appendInsert(
+ StringBuilder row,
+ SessionDataSet.DataIterator iterator,
+ List<String> types,
+ String device,
+ SqlColumns columns,
+ boolean aligned,
+ boolean skipNulls)
+ throws Exception {
+ if (device.startsWith(SYSTEM_DATABASE + ".") || device.startsWith(AUDIT_DATABASE + ".")) {
+ return;
+ }
+ StringBuilder values = new StringBuilder();
+ // Stays null while every column has a value, so the prepared list is used.
+ StringBuilder names = null;
+ for (int i = 0; i < columns.columns.size(); i++) {
+ int column = columns.columns.get(i) + 1;
+ if (skipNulls && iterator.isNull(column)) {
+ if (names == null) {
+ names = new StringBuilder();
+ for (String measurement : columns.measurements.subList(0, i)) {
+ names.append(',').append(measurement);
+ }
}
+ continue;
}
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
+ if (names != null) {
+ names.append(',').append(columns.measurements.get(i));
}
+ values.append(',').append(sqlValue(iterator, types.get(column - 1), column));
}
-
- for (Tablet tablet : tabletList) {
- if (tablet.getRowSize() != 0) {
- writeToTsFile(alignedDevices, tsFileWriter, tablet);
- processedRows += tablet.getRowSize();
- }
- if (System.currentTimeMillis() - lastPrintTime > updateTimeInterval) {
- ioTPrinter.printf(Constants.PROCESSED_PROGRESS, processedRows);
- lastPrintTime = System.currentTimeMillis();
- }
+ if (values.length() == 0) {
+ return;
}
- ioTPrinter.print("\n");
- }
-
- private static void writeToTsFile(
- Set<String> deviceFilterSet, TsFileWriter tsFileWriter, Tablet tablet)
- throws IOException, WriteProcessException {
- if (deviceFilterSet.contains(tablet.getDeviceId())) {
- tsFileWriter.writeAligned(tablet);
- } else {
- tsFileWriter.writeTree(tablet);
+ row.append("INSERT INTO ")
+ .append(device)
+ .append("(TIMESTAMP")
+ .append(names == null ? columns.names : names)
+ .append(')');
+ if (aligned) {
+ row.append(" ALIGNED");
}
+ row.append(" VALUES (").append(iterator.getLong(1)).append(values).append(");\n");
}
- private static List<Tablet> constructTablets(
- Map<String, List<IMeasurementSchema>> deviceSchemaMap,
- Set<String> alignedDevices,
- TsFileWriter tsFileWriter)
- throws WriteProcessException {
- List<Tablet> tabletList = new ArrayList<>(deviceSchemaMap.size());
- for (Map.Entry<String, List<IMeasurementSchema>> stringListEntry : deviceSchemaMap.entrySet()) {
- String deviceId = stringListEntry.getKey();
- List<IMeasurementSchema> schemaList = stringListEntry.getValue();
- Tablet tablet = new Tablet(deviceId, schemaList);
- tablet.initBitMaps();
- Path path = new Path(tablet.getDeviceId());
- if (alignedDevices.contains(tablet.getDeviceId())) {
- tsFileWriter.registerAlignedTimeseries(path, schemaList);
- } else {
- tsFileWriter.registerTimeseries(path, schemaList);
+ private boolean isAligned(String device) throws Exception {
+ Boolean known = alignment.get(device);
+ if (known != null) {
+ return known;
+ }
+ boolean aligned = false;
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement("show devices " + device, config.timeout))) {
+ SessionDataSet data = result.get();
+ if (data.hasNext()) {
+ List<Field> fields = data.next().getFields();
+ aligned = fields.size() > 1 && "true".equalsIgnoreCase(fields.get(1).getStringValue());
}
- tabletList.add(tablet);
}
- return tabletList;
+ alignment.put(device, aligned);
+ return aligned;
}
- private static void collectSchemas(
- List<String> columnNames,
- List<String> columnTypes,
- Map<String, List<IMeasurementSchema>> deviceSchemaMap,
- Set<String> alignedDevices,
- Map<String, List<Integer>> deviceColumnIndices)
- throws IoTDBConnectionException, StatementExecutionException {
+ private void exportTsFile(SessionDataSet data, ExportTask task) throws Exception {
+ java.nio.file.Path file = java.nio.file.Path.of(task.outputBase() + ".tsfile");
+ ExportFileNames.checkOutputCollision(file, config);
+ List<String> columnNames = data.getColumnNames();
+ Map<String, List<IMeasurementSchema>> schemas = new LinkedHashMap<>();
+ Map<String, List<Integer>> indices = new LinkedHashMap<>();
+ Set<String> alignedDevices = new HashSet<>();
for (int i = 0; i < columnNames.size(); i++) {
- String column = columnNames.get(i);
- if (!column.startsWith("root.")) {
+ String name = columnNames.get(i);
+ if (i == 0 && "Time".equals(name)) {
continue;
}
- TSDataType tsDataType = getType(columnTypes.get(i));
- Path path = new Path(column, true);
- String deviceId = path.getDeviceString();
- // query whether the device is aligned or not
- try (SessionDataSet deviceDataSet =
- session.executeQueryStatement("show devices " + deviceId, timeout)) {
- List<Field> deviceList = deviceDataSet.next().getFields();
- if (deviceList.size() > 1 && "true".equals(deviceList.get(1).getStringValue())) {
- alignedDevices.add(deviceId);
+ if (i == 0 || !name.startsWith("root.")) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_TREE_TSFILE_OUTPUT_REQUIRES_A_TIME_COLUMN_AND_ORIGINAL_MEASUREMENT_PATHS_1DCEE8EA);
+ }
+ ExportTask.Measurement measurement = measurements.get(name);
+ if (measurement == null
+ && task.measurement() != null
+ && task.measurement().path().getFullPath().equals(name)) {
+ measurement = task.measurement();
+ }
+ if (measurement == null) {
+ try (Resource<SessionDataSet> result =
+ own(session.executeQueryStatement("show timeseries " + name, config.timeout))) {
+ if (!result.get().hasNext()) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_CANNOT_OBTAIN_SCHEMA_FOR_MEASUREMENT_ARG_D3F1DAF3, name));
+ }
+ measurement = measurement(result.get().next(), result.get().getColumnNames());
}
+ measurements.put(name, measurement);
}
+ String device = devicePath(measurement.path()).getFullPath();
+ if (isAligned(device)) {
+ alignedDevices.add(device);
+ }
+ schemas.computeIfAbsent(device, key -> new ArrayList<>()).add(measurement.schema());
+ indices.computeIfAbsent(device, key -> new ArrayList<>()).add(i);
+ }
+ progress.fileStarted(file);
+ Files.deleteIfExists(file);
+ long startRows = progress.rows();
+ try (Resource<TsFileWriter> output = own(new TsFileWriter(file.toFile()))) {
+ TsFileWriter writer = output.get();
+ List<Tablet> tablets = new ArrayList<>();
+ for (Map.Entry<String, List<IMeasurementSchema>> schema : schemas.entrySet()) {
+ Tablet tablet = new Tablet(schema.getKey(), schema.getValue());
+ tablet.initBitMaps();
+ if (alignedDevices.contains(schema.getKey())) {
+ writer.registerAlignedTimeseries(new Path(schema.getKey()), schema.getValue());
+ } else {
+ writer.registerTimeseries(new Path(schema.getKey()), schema.getValue());
+ }
+ tablets.add(tablet);
+ }
+ long pendingRows = 0;
+ while (!tablets.isEmpty() && data.hasNext()) {
+ progress.checkActive();
+ RowRecord row = data.next();
+ for (Tablet tablet : tablets) {
+ int rowIndex = tablet.getRowSize();
+ tablet.addTimestamp(rowIndex, row.getTimestamp());
+ List<Integer> columns = indices.get(tablet.getDeviceId());
+ for (int i = 0; i < columns.size(); i++) {
+ IMeasurementSchema schema = tablet.getSchemas().get(i);
+ tablet.addValue(
+ schema.getMeasurementName(),
+ rowIndex,
+ row.getFields().get(columns.get(i) - 1).getObjectValue(schema.getType()));
+ }
+ }
+ pendingRows++;
+ if (tablets.get(0).getRowSize() == tablets.get(0).getMaxRowNumber()) {
+ writeTablets(tablets, alignedDevices, writer);
+ progress.rowsWritten(pendingRows);
+ pendingRows = 0;
+ }
+ }
+ if (pendingRows > 0) {
+ writeTablets(tablets, alignedDevices, writer);
+ progress.rowsWritten(pendingRows);
+ }
+ writer.flush();
+ }
+ progress.fileCompleted(file, progress.rows() - startRows);
+ }
- // query timeseries metadata
- MeasurementSchema measurementSchema =
- new MeasurementSchema(path.getMeasurement(), tsDataType);
- List<Field> seriesList =
- session.executeQueryStatement("show timeseries " + column, timeout).next().getFields();
- measurementSchema.setEncoding(TSEncoding.valueOf(seriesList.get(4).getStringValue()));
- measurementSchema.setCompressionType(
- CompressionType.valueOf(seriesList.get(5).getStringValue()));
-
- deviceSchemaMap.computeIfAbsent(deviceId, key -> new ArrayList<>()).add(measurementSchema);
- deviceColumnIndices.computeIfAbsent(deviceId, key -> new ArrayList<>()).add(i);
+ private static void writeTablets(List<Tablet> tablets, Set<String> aligned, TsFileWriter writer)
+ throws Exception {
+ for (Tablet tablet : tablets) {
+ if (aligned.contains(tablet.getDeviceId())) {
+ writer.writeAligned(tablet);
+ } else {
+ writer.writeTree(tablet);
+ }
+ tablet.reset();
+ tablet.initBitMaps();
}
}
}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportFileNames.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportFileNames.java
new file mode 100644
index 00000000000..871d8d13ef5
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportFileNames.java
@@ -0,0 +1,114 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.exception.ArgsErrorException;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.text.Normalizer;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/** Retains readable full paths, escaping characters that cannot be used on Windows or Unix. */
+final class ExportFileNames {
+ private final Map<String, String> names = new HashMap<>();
+
+ Path measurement(ExportConfig config, String measurement) throws ArgsErrorException {
+ String name = encode((config.prefix.isEmpty() ? "" : config.prefix + "_") + measurement);
+ // Leave room for the split index and extension on filesystems with 255-byte components.
+ if (name.getBytes(StandardCharsets.UTF_8).length > 220) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_MEASUREMENT_OUTPUT_FILENAME_EXCEEDS_THE_SUPPORTED_LENGTH_ARG_FF01275D,
+ measurement));
+ }
+ String key = Normalizer.normalize(name, Normalizer.Form.NFC).toLowerCase(Locale.ROOT);
+ String previous = names.putIfAbsent(key, measurement);
+ if (previous != null && !previous.equals(measurement)) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_MEASUREMENTS_ARG_AND_ARG_MAP_TO_THE_SAME_OUTPUT_FILENAME_ARG_58E1FD4D,
+ previous,
+ measurement,
+ name));
+ }
+ return config.targetDirectory.resolve(name);
+ }
+
+ private static String encode(String value) {
+ StringBuilder result = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char character = value.charAt(i);
+ if (character < 32
+ || "\\/:*?\"<>|%".indexOf(character) >= 0
+ || (i == value.length() - 1 && (character == '.' || character == ' '))) {
+ for (byte b : String.valueOf(character).getBytes(StandardCharsets.UTF_8)) {
+ result.append('%');
+ result.append(Character.toUpperCase(Character.forDigit((b & 0xff) >>> 4, 16)));
+ result.append(Character.toUpperCase(Character.forDigit(b & 15, 16)));
+ }
+ } else {
+ result.append(character);
+ }
+ }
+ return result.toString();
+ }
+
+ static void checkOutputCollision(Path output, ExportConfig config) throws ArgsErrorException {
+ checkLogCollision(output, config.logFile);
+ Path input = config.measurementFile;
+ if (input != null
+ && (output.equals(input)
+ || (java.nio.file.Files.exists(output)
+ && java.nio.file.Files.exists(input)
+ && sameFile(output, input)))) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_MEASUREMENT_INPUT_FILE_ARG_1CBEBB6C,
+ output));
+ }
+ }
+
+ static void checkLogCollision(Path output, Path log) throws ArgsErrorException {
+ if (output.equals(log)
+ || (java.nio.file.Files.exists(output)
+ && java.nio.file.Files.exists(log)
+ && sameFile(output, log))) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_EXPORT_OUTPUT_CONFLICTS_WITH_THE_LOG_FILE_ARG_F01BE79D,
+ output));
+ }
+ }
+
+ private static boolean sameFile(Path left, Path right) throws ArgsErrorException {
+ try {
+ return java.nio.file.Files.isSameFile(left, right);
+ } catch (java.io.IOException e) {
+ throw new ArgsErrorException(e.getMessage());
+ }
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportProgressReporter.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportProgressReporter.java
new file mode 100644
index 00000000000..fc3a75fdace
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportProgressReporter.java
@@ -0,0 +1,638 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil;
+import org.apache.iotdb.tool.common.Constants;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.io.PrintStream;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.text.NumberFormat;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * One task's counters, its JSON Lines log and its console view. The timer never touches a Session
+ * or result set.
+ */
+final class ExportProgressReporter implements AutoCloseable {
+ enum Status {
+ RUNNING,
+ SUCCESS,
+ FAILED,
+ CANCELLED
+ }
+
+ enum Phase {
+ VALIDATING,
+ CONNECTING,
+ DISCOVERING,
+ EXPORTING,
+ CLOSING
+ }
+
+ private final Writer log;
+ private final ExportConsole console;
+ private final String model;
+ private final long startNanos;
+ private final AtomicLong rows = new AtomicLong();
+ private final ScheduledExecutorService timer;
+ private volatile IOException logFailure;
+ private volatile boolean stopping;
+ private volatile int cancelExitCode = Constants.CODE_ERROR;
+ private volatile Status status = Status.RUNNING;
+ private volatile Phase phase = Phase.VALIDATING;
+ private long totalMeasurements = -1;
+ private long completedMeasurements;
+ private long completedTasks;
+ private long zeroRowTasks;
+ private long files;
+ private long taskStartRows;
+ private long exportStartNanos;
+ private String currentMeasurement = "";
+ private String output = "";
+ private Path logFile;
+ private boolean closed;
+ private Runnable failureHandler = () -> {};
+
+ ExportProgressReporter(ExportConfig config, long startNanos, PrintStream console)
+ throws IOException {
+ this(
+ Files.newBufferedWriter(
+ config.logFile,
+ StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE,
+ StandardOpenOption.APPEND),
+ config.tree ? "tree" : "table",
+ startNanos,
+ new ExportConsole(console, ExportConsole.isTerminal()));
+ }
+
+ // Writer injection exercises real write/flush failures without relying on filesystem permissions.
+ ExportProgressReporter(Writer log, String model, long startNanos, PrintStream console) {
+ this(log, model, startNanos, new ExportConsole(console, false));
+ }
+
+ ExportProgressReporter(Writer log, String model, long startNanos, ExportConsole console) {
+ this.log = log;
+ this.console = console;
+ this.model = model;
+ this.startNanos = startNanos;
+ timer =
+ Executors.newSingleThreadScheduledExecutor(
+ runnable -> {
+ Thread thread = new Thread(runnable, "export-data-progress");
+ thread.setDaemon(true);
+ return thread;
+ });
+ }
+
+ synchronized void start(ExportConfig config, boolean queryMode, Runnable onFailure)
+ throws IOException {
+ failureHandler = onFailure;
+ logFile = config.logFile;
+ if (emit(
+ "INFO",
+ "START",
+ Status.RUNNING,
+ fields(
+ "format",
+ config.format,
+ "input",
+ config.measurementFile,
+ "mode",
+ config.measurementFile != null ? "measurements" : queryMode ? "query" : "automatic",
+ // A query runs as written, so a time range here would misstate what it exports.
+ "start_time",
+ queryMode ? null : config.startTime,
+ "end_time",
+ queryMode ? null : config.endTime,
+ "target",
+ config.targetDirectory,
+ "log",
+ config.logFile,
+ "lines_per_file",
+ config.linesPerFile,
+ "message",
+ CliMessages.LOG_EXPORT_STARTED_45465087))) {
+ console.line(
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_EXPORT_STARTED_ARG_ARG_TARGET_ARG_LOG_ARG_BD58AECB,
+ model,
+ config.format,
+ config.targetDirectory,
+ config.logFile));
+ }
+ if (queryMode && (config.startTime != null || config.endTime != null)) {
+ warning(
+ CliMessages.LOG_START_TIME_AND_END_TIME_DO_NOT_APPLY_TO_QUERIES_AND_ARE_IGNORED_02DC37CC,
+ "start_time",
+ config.startTime,
+ "end_time",
+ config.endTime);
+ }
+ if (queryMode && config.table != null) {
+ warning(
+ CliMessages.LOG_TABLE_DOES_NOT_APPLY_TO_QUERIES_AND_IS_IGNORED_DBA31209,
+ "table",
+ config.table);
+ }
+ ScheduledExecutorUtil.safelyScheduleAtFixedRate(timer, this::tick, 2, 2, TimeUnit.SECONDS);
+ }
+
+ private void tick() {
+ try {
+ progress();
+ } catch (Exception e) {
+ logFailure =
+ e instanceof IOException
+ ? (IOException) e
+ : new IOException(CliMessages.LOG_CANNOT_WRITE_OR_CLOSE_THE_EXPORT_LOG_32579AC6, e);
+ stopping = true;
+ failureHandler.run();
+ }
+ }
+
+ synchronized void progress() throws IOException {
+ if (!stopping
+ && status == Status.RUNNING
+ && !closed
+ && emit("INFO", "PROGRESS", Status.RUNNING, fields())) {
+ showProgress();
+ }
+ }
+
+ private void showProgress() {
+ String exported = count(rows.get());
+ String elapsed = duration(elapsedSeconds());
+ if (totalMeasurements < 0) {
+ // Without a measurement file there is no total, so no percentage or remaining time either.
+ String rate = count(rowsPerSecond());
+ console.progress(
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_EXPORTING_ARG_ROWS_ARG_ARG_ROWS_S_56BD441C,
+ exported,
+ elapsed,
+ rate),
+ "",
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_ARG_ROWS_ELAPSED_ARG_ARG_ROWS_S_AE3CF660,
+ exported,
+ elapsed,
+ rate));
+ return;
+ }
+ // Whole points, so the console never shows 100% before the last measurement completes.
+ String percent =
+ (totalMeasurements == 0 ? 0 : completedMeasurements * 100 / totalMeasurements) + "%";
+ long remaining = remainingSeconds();
+ String eta = remaining < 0 ? "--" : duration(remaining);
+ console.progress(
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_EXPORTING_ARG_ARG_ARG_ARG_ROWS_ARG_ETA_ARG_C41CA790,
+ percent,
+ completedMeasurements,
+ totalMeasurements,
+ exported,
+ elapsed,
+ eta),
+ currentMeasurement,
+ String.format(
+ Locale.ROOT,
+ CliMessages
+ .LOG_ARG_ARG_ARG_MEASUREMENTS_ARG_ROWS_ELAPSED_ARG_ETA_ARG_CURRENT_ARG_B6DEA0BD,
+ percent,
+ completedMeasurements,
+ totalMeasurements,
+ exported,
+ elapsed,
+ eta,
+ currentMeasurement.isEmpty() ? "-" : currentMeasurement));
+ }
+
+ void phase(Phase phase) throws IOException {
+ checkActive();
+ this.phase = phase;
+ }
+
+ synchronized void discovered(MeasurementFile input, long matches, int measurements, int unmatched)
+ throws IOException {
+ checkActive();
+ totalMeasurements = measurements;
+ exportStartNanos = System.nanoTime();
+ if (emit(
+ "INFO",
+ "DISCOVERY",
+ Status.RUNNING,
+ fields(
+ "source",
+ input.source(),
+ "input_lines",
+ input.lines(),
+ "patterns",
+ input.patterns().size(),
+ "duplicate_patterns",
+ input.duplicates(),
+ "matches",
+ matches,
+ "duplicate_matches",
+ matches - measurements,
+ "unmatched_patterns",
+ unmatched))) {
+ console.line(
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_MEASUREMENTS_TO_EXPORT_ARG_PATH_PATTERNS_ARG_A0AC5F31,
+ measurements,
+ input.patterns().size()));
+ }
+ }
+
+ /** {@code message} says on its own what the warning is about; its details go to the log only. */
+ synchronized void warning(String message, Object... details) throws IOException {
+ Map<String, Object> values = fields(details);
+ values.put("message", message);
+ if (emit("WARN", "WARNING", Status.RUNNING, values)) {
+ console.line(String.format(Locale.ROOT, CliMessages.LOG_WARNING_ARG_05BE32E9, message));
+ }
+ }
+
+ synchronized void taskStarted(ExportTask task) throws IOException {
+ checkActive();
+ phase = Phase.EXPORTING;
+ currentMeasurement = task.measurement() == null ? "" : task.measurement().path().getFullPath();
+ // FILE_START names the output once the task opens a file; a task without rows opens none.
+ output = "";
+ taskStartRows = rows.get();
+ emit("INFO", "TASK_START", Status.RUNNING, fields("sql", task.sql(), "table", task.table()));
+ }
+
+ synchronized void taskCompleted() throws IOException {
+ checkActive();
+ long taskRows = rows.get() - taskStartRows;
+ completedTasks++;
+ if (totalMeasurements >= 0) {
+ completedMeasurements++;
+ }
+ if (taskRows == 0) {
+ zeroRowTasks++;
+ }
+ emit("INFO", "TASK_COMPLETE", Status.RUNNING, fields("task_rows", taskRows));
+ }
+
+ synchronized void fileStarted(Path file) throws IOException {
+ checkActive();
+ output = file.toString();
+ emit("INFO", "FILE_START", Status.RUNNING, fields());
+ }
+
+ synchronized void fileCompleted(Path file, long fileRows) throws IOException {
+ checkActive();
+ files++;
+ emit("INFO", "FILE_COMPLETE", Status.RUNNING, fields("output", file, "file_rows", fileRows));
+ }
+
+ void rowsWritten(long count) throws IOException {
+ checkActive();
+ rows.addAndGet(count);
+ }
+
+ long rows() {
+ return rows.get();
+ }
+
+ void checkActive() throws IOException {
+ if (logFailure != null) {
+ throw logFailure;
+ }
+ if (stopping || Thread.currentThread().isInterrupted()) {
+ throw new InterruptedIOException(CliMessages.EXCEPTION_EXPORT_CANCELLED_85D44F54);
+ }
+ }
+
+ /** Stops at the next check; a cancelled finish reports the status the process exits with. */
+ void cancel(int exitCode) {
+ cancelExitCode = exitCode;
+ stopping = true;
+ }
+
+ Status status() {
+ return status;
+ }
+
+ synchronized void finish(Status terminal, Throwable error) throws IOException {
+ if (status != Status.RUNNING || closed) {
+ return;
+ }
+ stopping = true;
+ timer.shutdownNow();
+ if (terminal == Status.SUCCESS && logFailure != null) {
+ terminal = Status.FAILED;
+ error = logFailure;
+ }
+ String reason = reason(error);
+ String line =
+ event(
+ terminal == Status.SUCCESS ? "INFO" : "ERROR",
+ "FINISH",
+ terminal,
+ fields(
+ "exit_code",
+ exitCode(terminal),
+ "completed_tasks",
+ completedTasks,
+ "zero_row_tasks",
+ zeroRowTasks,
+ "message",
+ terminal == Status.SUCCESS
+ ? CliMessages.LOG_EXPORT_COMPLETELY_34200D19
+ : terminal == Status.CANCELLED
+ ? CliMessages.EXCEPTION_EXPORT_CANCELLED_85D44F54
+ : CliMessages.LOG_EXPORT_FAILED_77EE72CD,
+ "error",
+ reason));
+ IOException writeError = null;
+ try {
+ log.write(line + System.lineSeparator());
+ log.flush();
+ } catch (IOException e) {
+ writeError = e;
+ }
+ try {
+ log.close();
+ } catch (IOException e) {
+ if (writeError == null) {
+ writeError = e;
+ } else {
+ writeError.addSuppressed(e);
+ }
+ }
+ closed = true;
+ if (writeError != null) {
+ logFailure = writeError;
+ // The log cannot record its own failure, so only the console reports it.
+ reason =
+ CliMessages.LOG_CANNOT_WRITE_OR_CLOSE_THE_EXPORT_LOG_32579AC6 + ' ' + reason(writeError);
+ if (terminal == Status.CANCELLED) {
+ console.line(String.format(Locale.ROOT, CliMessages.LOG_WARNING_ARG_05BE32E9, reason));
+ } else {
+ terminal = Status.FAILED;
+ }
+ }
+ status = terminal;
+ // Scripts read the outcome from the last console line, so the log path is printed first.
+ if (logFile != null) {
+ console.line(String.format(Locale.ROOT, CliMessages.LOG_LOG_ARG_FBF61F63, logFile));
+ }
+ console.line(summary(terminal, reason));
+ if (writeError != null) {
+ throw writeError;
+ }
+ }
+
+ private String summary(Status terminal, String reason) {
+ String exported = count(rows.get());
+ String elapsed = duration(elapsedSeconds());
+ if (terminal == Status.SUCCESS) {
+ return totalMeasurements >= 0
+ ? String.format(
+ Locale.ROOT,
+ CliMessages
+ .LOG_EXPORT_COMPLETELY_MEASUREMENTS_ARG_ARG_ROWS_ARG_FILES_ARG_ELAPSED_ARG_36D9F8E5,
+ completedMeasurements,
+ totalMeasurements,
+ exported,
+ count(files),
+ elapsed)
+ : String.format(
+ Locale.ROOT,
+ CliMessages.LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B,
+ exported,
+ count(files),
+ elapsed);
+ }
+ if (terminal == Status.CANCELLED) {
+ return String.format(
+ Locale.ROOT,
+ CliMessages.LOG_EXPORT_CANCELLED_AFTER_ARG_ROWS_EXPORTED_ARG_0999F99F,
+ elapsed,
+ exported);
+ }
+ return reason == null
+ ? String.format(Locale.ROOT, CliMessages.LOG_EXPORT_FAILED_AFTER_ARG_DA6D56F9, elapsed)
+ : String.format(
+ Locale.ROOT, CliMessages.LOG_EXPORT_FAILED_AFTER_ARG_ARG_C123F0EF, elapsed, reason);
+ }
+
+ private static String reason(Throwable error) {
+ if (error == null) {
+ return null;
+ }
+ return error.getMessage() == null ? error.toString() : error.getMessage();
+ }
+
+ private int exitCode(Status terminal) {
+ return terminal == Status.SUCCESS
+ ? Constants.CODE_OK
+ : terminal == Status.CANCELLED ? cancelExitCode : Constants.CODE_ERROR;
+ }
+
+ /** Appends one event to the log; false once the log has closed and records nothing more. */
+ private boolean emit(String level, String event, Status eventStatus, Map<String, Object> details)
+ throws IOException {
+ if (closed || status != Status.RUNNING) {
+ return false;
+ }
+ String line = event(level, event, eventStatus, details);
+ try {
+ log.write(line + System.lineSeparator());
+ log.flush();
+ } catch (IOException e) {
+ logFailure = e;
+ throw e;
+ }
+ return true;
+ }
+
+ private String event(
+ String level, String event, Status eventStatus, Map<String, Object> details) {
+ // The export requirement's progress fields come first, in its order; event details follow.
+ // These keys are the stable, locale-independent log schema, not translated prose.
+ Map<String, Object> values =
+ fields(
+ "progress_id",
+ ProcessHandle.current().pid(),
+ "timestamp",
+ ZonedDateTime.now()
+ .truncatedTo(ChronoUnit.SECONDS)
+ .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
+ "model",
+ model,
+ "status",
+ eventStatus,
+ "exported_data_rows",
+ rows.get());
+ if (totalMeasurements >= 0) {
+ values.put("exported_measurement_num", completedMeasurements);
+ values.put("total_measurement_num", totalMeasurements);
+ values.put(
+ "exported_percent",
+ String.format(
+ Locale.ROOT,
+ CliMessages.LOG_ARG_82AC62F7,
+ totalMeasurements == 0 ? 0.0 : completedMeasurements * 100.0 / totalMeasurements));
+ long remaining = remainingSeconds();
+ values.put("estimated_remaining", remaining < 0 ? "UNKNOWN" : duration(remaining));
+ }
+ values.put("elapsed_time", duration(elapsedSeconds()));
+ values.put("output", output.isEmpty() ? null : output);
+ values.put("event", event);
+ values.put("level", level);
+ values.put("phase", phase);
+ values.put("rows_per_second", rowsPerSecond());
+ values.put("files", files);
+ values.put("current_measurement", currentMeasurement.isEmpty() ? null : currentMeasurement);
+ values.putAll(details);
+ return json(values);
+ }
+
+ /** Seconds left at the average measurement duration, or -1 while too few have completed. */
+ private long remainingSeconds() {
+ // Measurements differ widely in size, so the first few say little about the rest.
+ long sample = Math.min(totalMeasurements, Math.max(3, (totalMeasurements + 19) / 20));
+ if (completedMeasurements == 0 || completedMeasurements < sample) {
+ return -1;
+ }
+ // Rounded up, so work still left never shows as 00:00:00.
+ return (long)
+ Math.ceil(
+ (System.nanoTime() - exportStartNanos)
+ / 1_000_000_000.0
+ * (totalMeasurements - completedMeasurements)
+ / completedMeasurements);
+ }
+
+ private long elapsedSeconds() {
+ return Math.max(0, System.nanoTime() - startNanos) / 1_000_000_000;
+ }
+
+ private long rowsPerSecond() {
+ return Math.round(
+ rows.get() / Math.max(1.0, Math.max(0, System.nanoTime() - startNanos) / 1_000_000_000.0));
+ }
+
+ static Map<String, Object> fields(Object... values) {
+ Map<String, Object> fields = new LinkedHashMap<>();
+ for (int i = 0; i < values.length; i += 2) {
+ fields.put((String) values[i], values[i + 1]);
+ }
+ return fields;
+ }
+
+ /** One flat JSON object without null members; integers stay numbers, the rest are strings. */
+ static String json(Map<String, Object> values) {
+ StringBuilder json = new StringBuilder("{");
+ values.forEach(
+ (key, value) -> {
+ if (value == null) {
+ return;
+ }
+ if (json.length() > 1) {
+ json.append(',');
+ }
+ string(json, key).append(':');
+ if (value instanceof Integer || value instanceof Long) {
+ json.append(value);
+ } else {
+ string(json, String.valueOf(value));
+ }
+ });
+ return json.append('}').toString();
+ }
+
+ private static StringBuilder string(StringBuilder json, String value) {
+ json.append('"');
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '"' || c == '\\') {
+ json.append('\\').append(c);
+ } else if (c == '\n') {
+ json.append("\\n");
+ } else if (c == '\r') {
+ json.append("\\r");
+ } else if (c == '\t') {
+ json.append("\\t");
+ } else if (c < 0x20) {
+ json.append("\\u00")
+ .append(Character.forDigit(c >> 4, 16))
+ .append(Character.forDigit(c & 0xF, 16));
+ } else {
+ json.append(c);
+ }
+ }
+ return json.append('"');
+ }
+
+ private static String count(long value) {
+ return NumberFormat.getIntegerInstance(Locale.ROOT).format(value);
+ }
+
+ private static String duration(long seconds) {
+ long hours = seconds / 3600;
+ long minutes = seconds / 60 % 60;
+ long remainder = seconds % 60;
+ return (hours < 10 ? "0" : "")
+ + hours
+ + ":"
+ + (minutes < 10 ? "0" : "")
+ + minutes
+ + ":"
+ + (remainder < 10 ? "0" : "")
+ + remainder;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ timer.shutdownNow();
+ if (!closed) {
+ closed = true;
+ log.close();
+ }
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTask.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTask.java
new file mode 100644
index 00000000000..5d647eb67e5
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTask.java
@@ -0,0 +1,31 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.commons.path.PartialPath;
+
+import org.apache.tsfile.write.schema.MeasurementSchema;
+
+import java.nio.file.Path;
+
+/** Query and output metadata travel together, rather than in separately indexed lists. */
+record ExportTask(String sql, Path outputBase, Measurement measurement, String table) {
+ record Measurement(PartialPath path, MeasurementSchema schema) {}
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTextWriter.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTextWriter.java
new file mode 100644
index 00000000000..be0295a5ad8
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTextWriter.java
@@ -0,0 +1,126 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.csv.QuoteMode;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Shared CSV/SQL splitting and reliable IO. Partial files are closed without a completion event.
+ */
+final class ExportTextWriter implements AutoCloseable {
+ private final ExportConfig config;
+ private final ExportTask task;
+ private final ExportProgressReporter progress;
+ private final List<String> headers;
+ private BufferedWriter writer;
+ private CSVPrinter csv;
+ private Path file;
+ private int index;
+ private long fileRows;
+
+ ExportTextWriter(
+ ExportConfig config, ExportTask task, ExportProgressReporter progress, List<String> headers) {
+ this.config = config;
+ this.task = task;
+ this.progress = progress;
+ this.headers = headers;
+ }
+
+ private void open() throws Exception {
+ if (writer != null) {
+ return;
+ }
+ progress.checkActive();
+ file = Path.of(task.outputBase() + "_" + index + (headers == null ? ".sql" : ".csv"));
+ ExportFileNames.checkOutputCollision(file, config);
+ progress.fileStarted(file);
+ writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8);
+ fileRows = 0;
+ if (headers != null) {
+ csv =
+ CSVFormat.Builder.create(CSVFormat.DEFAULT)
+ .setHeader()
+ .setSkipHeaderRecord(true)
+ .setEscape('\\')
+ .setQuoteMode(QuoteMode.NONE)
+ .build()
+ .print(writer);
+ csv.printRecord(headers);
+ }
+ }
+
+ synchronized void csvRow(List<Object> values) throws Exception {
+ open();
+ csv.printRecord(values);
+ rowWritten();
+ }
+
+ synchronized void sqlRow(String sql) throws Exception {
+ open();
+ writer.write(sql);
+ rowWritten();
+ }
+
+ private void rowWritten() throws IOException {
+ fileRows++;
+ progress.rowsWritten(1);
+ if (fileRows >= config.linesPerFile) {
+ completeFile();
+ }
+ }
+
+ synchronized void complete() throws IOException {
+ completeFile();
+ }
+
+ private void completeFile() throws IOException {
+ if (writer == null) {
+ return;
+ }
+ close();
+ progress.fileCompleted(file, fileRows);
+ index++;
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (writer != null) {
+ try {
+ if (csv != null) {
+ csv.close();
+ } else {
+ writer.close();
+ }
+ } finally {
+ writer = null;
+ csv = null;
+ }
+ }
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTimeRange.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTimeRange.java
new file mode 100644
index 00000000000..9205c9cd67e
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/ExportTimeRange.java
@@ -0,0 +1,84 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.commons.queryengine.utils.DateTimeUtils;
+import org.apache.iotdb.exception.ArgsErrorException;
+
+import java.time.ZoneId;
+
+/** A closed interval, resolved using the connected server's precision and session time zone. */
+final class ExportTimeRange {
+ private final Long start;
+ private final Long end;
+
+ private ExportTimeRange(Long start, Long end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ static ExportTimeRange parse(String start, String end, ZoneId zone, String precision)
+ throws ArgsErrorException {
+ Long startValue = parseBound(start, zone, precision);
+ Long endValue = parseBound(end, zone, precision);
+ if (startValue != null && endValue != null && startValue > endValue) {
+ throw new ArgsErrorException(
+ CliMessages.EXCEPTION_START_TIME_MUST_BE_LESS_THAN_OR_EQUAL_TO_END_TIME_3551199C);
+ }
+ return new ExportTimeRange(startValue, endValue);
+ }
+
+ private static Long parseBound(String value, ZoneId zone, String precision)
+ throws ArgsErrorException {
+ if (value == null || value.trim().isEmpty()) {
+ return null;
+ }
+ String trimmed = value.trim();
+ try {
+ return Long.parseLong(trimmed);
+ } catch (NumberFormatException ignored) {
+ try {
+ return DateTimeUtils.convertDatetimeStrToLong(trimmed, zone, precision);
+ } catch (RuntimeException e) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_CANNOT_PARSE_TIME_ARG_ARG_40257DF7, value, e.getMessage()));
+ }
+ }
+ }
+
+ String whereClause() {
+ if (start == null && end == null) {
+ return "";
+ }
+ StringBuilder sql = new StringBuilder(" WHERE ");
+ if (start != null) {
+ sql.append("time >= ").append(start);
+ }
+ if (start != null && end != null) {
+ sql.append(" AND ");
+ }
+ if (end != null) {
+ sql.append("time <= ").append(end);
+ }
+ return sql.toString();
+ }
+}
diff --git a/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/MeasurementFile.java b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/MeasurementFile.java
new file mode 100644
index 00000000000..d5f56c5f895
--- /dev/null
+++ b/iotdb-client/cli/src/main/java/org/apache/iotdb/tool/data/MeasurementFile.java
@@ -0,0 +1,145 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.exception.ArgsErrorException;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Validates the entire UTF-8 input before any metadata or data query is issued. */
+record MeasurementFile(Path source, List<PatternLine> patterns, int lines, int duplicates) {
+ record PatternLine(int line, String original, PartialPath path) {}
+
+ static MeasurementFile read(Path source) throws IOException, ArgsErrorException {
+ if (!Files.isRegularFile(source) || !Files.isReadable(source)) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_MEASUREMENT_INPUT_MUST_BE_A_READABLE_REGULAR_FILE_ARG_43D4A272,
+ source));
+ }
+ Map<String, PatternLine> distinct = new LinkedHashMap<>();
+ int lineNumber = 0;
+ int duplicates = 0;
+ String original = "";
+ try (BufferedInputStream reader = new BufferedInputStream(Files.newInputStream(source))) {
+ byte[] bytes;
+ while ((bytes = readLine(reader)) != null) {
+ try {
+ original =
+ StandardCharsets.UTF_8
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
+ .decode(ByteBuffer.wrap(bytes))
+ .toString();
+ } catch (IOException e) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_INVALID_MEASUREMENT_INPUT_ARG_AT_LINE_ARG_CONTENT_ARG_ARG_27A0F948,
+ source,
+ lineNumber + 1,
+ new String(bytes, StandardCharsets.UTF_8),
+ e.getMessage()));
+ }
+ lineNumber++;
+ String pattern = original;
+ if (lineNumber == 1 && pattern.startsWith("\uFEFF")) {
+ pattern = pattern.substring(1);
+ }
+ pattern = pattern.trim();
+ if (pattern.isEmpty() || pattern.startsWith("#")) {
+ continue;
+ }
+ try {
+ PartialPath path = new PartialPath(pattern);
+ if (!"root".equals(path.getFirstNode()) || path.getNodeLength() < 2) {
+ throw new ArgsErrorException(
+ CliMessages
+ .EXCEPTION_A_COMPLETE_IOTDB_PATH_PATTERN_STARTING_WITH_ROOT_IS_REQUIRED_7D000A79);
+ }
+ PatternLine previous =
+ distinct.putIfAbsent(path.getFullPath(), new PatternLine(lineNumber, original, path));
+ if (previous != null) {
+ duplicates++;
+ }
+ } catch (IllegalPathException | ArgsErrorException e) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages
+ .EXCEPTION_INVALID_MEASUREMENT_INPUT_ARG_AT_LINE_ARG_CONTENT_ARG_ARG_27A0F948,
+ source,
+ lineNumber,
+ original,
+ e.getMessage()));
+ }
+ }
+ } catch (IOException e) {
+ throw new IOException(
+ String.format(
+ CliMessages.EXCEPTION_CANNOT_READ_UTF_8_MEASUREMENT_FILE_ARG_AT_LINE_ARG_ARG_A5739D3F,
+ source,
+ lineNumber + 1,
+ e.getMessage()),
+ e);
+ }
+ if (distinct.isEmpty()) {
+ throw new ArgsErrorException(
+ String.format(
+ CliMessages.EXCEPTION_MEASUREMENT_INPUT_CONTAINS_NO_PATH_PATTERNS_ARG_96760070,
+ source));
+ }
+ return new MeasurementFile(
+ source, List.copyOf(new ArrayList<>(distinct.values())), lineNumber, duplicates);
+ }
+
+ private static byte[] readLine(BufferedInputStream input) throws IOException {
+ ByteArrayOutputStream line = new ByteArrayOutputStream();
+ int next;
+ while ((next = input.read()) != -1) {
+ if (next == '\n') {
+ return line.toByteArray();
+ }
+ if (next == '\r') {
+ input.mark(1);
+ if (input.read() != '\n') {
+ input.reset();
+ }
+ return line.toByteArray();
+ }
+ line.write(next);
+ }
+ return line.size() == 0 ? null : line.toByteArray();
+ }
+}
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
index ff4a59ada91..43f90cca6a5 100644
--- a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/DataToolRegressionTest.java
@@ -19,6 +19,8 @@
package org.apache.iotdb.tool.data;
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.exception.ArgsErrorException;
import org.apache.iotdb.isession.ITableSession;
import org.apache.iotdb.isession.SessionDataSet;
import org.apache.iotdb.isession.pool.ITableSessionPool;
@@ -33,6 +35,8 @@ import org.apache.iotdb.tool.tsfile.ImportTsFileScanTool;
import org.apache.commons.csv.CSVParser;
import org.apache.tsfile.enums.ColumnCategory;
import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.RowRecord;
+import org.apache.tsfile.utils.Binary;
import org.apache.tsfile.write.record.Tablet;
import org.junit.After;
import org.junit.Before;
@@ -40,15 +44,19 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
-import java.io.File;
import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -61,12 +69,15 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -87,8 +98,6 @@ public class DataToolRegressionTest extends AbstractDataTool {
for (Class<?> owner :
Arrays.asList(
AbstractDataTool.class,
- ExportDataTree.class,
- ExportDataTable.class,
ImportDataTree.class,
ImportDataTable.class,
ImportTsFileBase.class,
@@ -101,20 +110,14 @@ public class DataToolRegressionTest extends AbstractDataTool {
}
}
directory = temporaryFolder.getRoot().toPath();
- targetDirectory = directory + File.separator;
- targetFile = "dump";
- exportType = "sql";
fileType = "csv";
- timeFormat = "timestamp";
timeColumn = "Time";
timestampPrecision = "ms";
database = "db";
table = "t";
aligned = false;
- linesPerFile = 10000;
batchPointSize = 100;
threadNum = 1;
- needDataTypePrinted = false;
}
@After
@@ -126,31 +129,37 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testMultiDeviceSqlPreservesColumnMappingAndSplitsFiles() throws Exception {
- linesPerFile = 1;
- mockTree(
- dataSet(
- List.of("Time", "root.a.d1.s1", "root.a.d2.s2", "root.a.d1.s3"),
- List.of("INT64", "INT32", "INT32", "INT32"),
- new Object[] {1L, 11, 22, 33},
- new Object[] {2L, null, 44, null}));
- assertTrue(new ExportDataTree().exportBySql("select * from root.a.*", 0));
- assertEquals("INSERT INTO root.a.d1(TIMESTAMP,s1,s3) VALUES (1,11,33);\n", read("dump0_0.sql"));
- assertEquals("INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (1,22);\n", read("dump0_1.sql"));
- assertEquals("INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (2,44);\n", read("dump0_2.sql"));
- assertFalse(Files.exists(directory.resolve("dump0_3.sql")));
+ Session session =
+ mockTree(
+ dataSet(
+ List.of("Time", "root.a.d1.s1", "root.a.d2.s2", "root.a.d1.s3"),
+ List.of("INT64", "INT32", "INT32", "INT32"),
+ new Object[] {1L, 11, 22, 33},
+ new Object[] {2L, null, 44, null}));
+ exportTree(session, "select * from root.a.*", 0, "-ft", "sql", "-lpf", "1");
+ // -lpf counts data rows, so the statements of one row share a file.
+ assertEquals(
+ "INSERT INTO root.a.d1(TIMESTAMP,s1,s3) VALUES (1,11,33);\n"
+ + "INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (1,22);\n",
+ read("dump0_0.sql"));
+ assertEquals("INSERT INTO root.a.d2(TIMESTAMP,s2) VALUES (2,44);\n", read("dump0_1.sql"));
+ assertFalse(Files.exists(directory.resolve("dump0_2.sql")));
}
@Test
public void testCustomTableQueriesUseDistinctFilesAndActualTables() throws Exception {
ITableSession session = mockTable("table_a", "table_b");
+ describe(session, "table_a", "v", "INT32", "FIELD");
+ describe(session, "table_b", "v", "INT32", "FIELD");
+ String qualified = "SELECT * FROM \"db\".\"table_b\" WHERE tag = 'from table_a'";
SessionDataSet first =
dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new Object[] {1L, 11});
+ when(session.executeQueryStatement(eq("select * from table_a"), anyLong())).thenReturn(first);
SessionDataSet second =
dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new Object[] {2L, 22});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(first, second);
- ExportDataTable tool = new ExportDataTable();
- assertTrue(tool.exportBySql("select * from table_a", 0));
- assertTrue(tool.exportBySql("SELECT * FROM \"db\".\"table_b\" WHERE tag = 'from table_a'", 1));
+ when(session.executeQueryStatement(eq(qualified), anyLong())).thenReturn(second);
+ exportTable(session, "select * from table_a", 0, "-ft", "sql");
+ exportTable(session, qualified, 1, "-ft", "sql");
assertEquals("insert into \"table_a\"(\"time\",\"v\") values(1,11);\n", read("dump0_0.sql"));
assertEquals("insert into \"table_b\"(\"time\",\"v\") values(2,22);\n", read("dump1_0.sql"));
}
@@ -158,27 +167,26 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testCustomTableQueryOverridesTableFilter() throws Exception {
ITableSession session = mockTable("table_a", "table_b");
- table = "table_a";
- set(ExportDataTable.class, "tables", new ArrayList<>(List.of("table_a")));
+ describe(session, "table_b", "v", "INT32", "FIELD");
SessionDataSet result =
dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new Object[] {1L, 11});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
+ when(session.executeQueryStatement(eq("select * from table_b"), anyLong())).thenReturn(result);
- assertTrue(new ExportDataTable().exportBySql("select * from table_b", 0));
+ exportTable(session, "select * from table_b", 0, "-ft", "sql", "-table", "table_a");
assertEquals("insert into \"table_b\"(\"time\",\"v\") values(1,11);\n", read("dump0_0.sql"));
}
@Test
public void testDefaultTableExportStillUsesTableFilter() throws Exception {
ITableSession session = mockTable("table_a", "table_b");
- table = "table_a";
- set(ExportDataTable.class, "tables", new ArrayList<>(List.of("table_a")));
+ describe(session, "table_a", "v", "INT32", "FIELD");
SessionDataSet result =
dataSet(List.of("time", "v"), List.of("TIMESTAMP", "INT32"), new Object[] {1L, 11});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
+ when(session.executeQueryStatement(eq("SELECT * FROM \"table_a\""), anyLong()))
+ .thenReturn(result);
- assertTrue(new ExportDataTable().exportBySql(null, 0));
- verify(session).executeQueryStatement("select * from \"table_a\"", timeout);
+ exportTable(session, null, 0, "-ft", "sql", "-table", "table_a");
+ verify(session).executeQueryStatement(eq("SELECT * FROM \"table_a\""), anyLong());
assertEquals("insert into \"table_a\"(\"time\",\"v\") values(1,11);\n", read("dump0_0.sql"));
assertFalse(Files.exists(directory.resolve("dump1_0.sql")));
}
@@ -186,10 +194,12 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testQuotedTableAndColumnNames() throws Exception {
ITableSession session = mockTable("odd\"name");
+ describe(session, "odd\"name", "select", "INT32", "FIELD");
SessionDataSet result =
dataSet(List.of("time", "select"), List.of("TIMESTAMP", "INT32"), new Object[] {1L, 11});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
- assertTrue(new ExportDataTable().exportBySql("select * from \"ODD\"\"NAME\"", 0));
+ when(session.executeQueryStatement(eq("select * from \"ODD\"\"NAME\""), anyLong()))
+ .thenReturn(result);
+ exportTable(session, "select * from \"ODD\"\"NAME\"", 0, "-ft", "sql");
assertEquals(
"insert into \"odd\"\"name\"(\"time\",\"select\") values(1,11);\n", read("dump0_0.sql"));
}
@@ -197,26 +207,41 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testAmbiguousAndCrossDatabaseTableQueriesFailBeforeWriting() throws Exception {
ITableSession session = mockTable("table_a", "table_b");
- ExportDataTable tool = new ExportDataTable();
- assertFalse(
- tool.exportBySql("select * from table_a join table_b on table_a.time = table_b.time", 0));
- assertFalse(tool.exportBySql("select * from other_db.table_b", 1));
- assertFalse(tool.exportBySql("select * from", 2));
- assertFalse(tool.exportBySql("select * from table_b #invalid_character", 3));
- verify(session, never()).executeQueryStatement(anyString(), anyLong());
+ String message =
+ String.format(
+ CliMessages
+ .EXCEPTION_SQL_TSFILE_EXPORT_REQUIRES_A_QUERY_FROM_A_SINGLE_TABLE_IN_DATABASE_ARG_C9EB3768,
+ "db");
+ for (String query :
+ List.of(
+ "select * from table_a join table_b on table_a.time = table_b.time",
+ "select * from other_db.table_b",
+ "select * from",
+ "select * from table_b #invalid_character",
+ // All queries are checked before the first one runs.
+ "select * from table_a; select * from table_a, table_b")) {
+ ArgsErrorException e =
+ assertThrows(
+ query,
+ ArgsErrorException.class,
+ () -> exportTable(session, null, 0, "-ft", "sql", "-q", query));
+ assertEquals(query, message, e.getMessage());
+ }
+ verify(session, never()).executeQueryStatement(not(eq("show tables")), anyLong());
assertFalse(Files.exists(directory.resolve("dump0_0.sql")));
}
@Test
public void testTableSqlEscapesStringsAndPreservesNull() throws Exception {
ITableSession session = mockTable("t");
+ describe(session, "t", "tag", "STRING", "TAG", "value", "TEXT", "FIELD");
SessionDataSet result =
dataSet(
List.of("time", "tag", "value"),
List.of("TIMESTAMP", "STRING", "TEXT"),
new Object[] {1L, "O'Brien\\path", null});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
- assertTrue(new ExportDataTable().exportBySql("select * from t", 0));
+ when(session.executeQueryStatement(eq("select * from t"), anyLong())).thenReturn(result);
+ exportTable(session, "select * from t", 0, "-ft", "sql");
assertEquals(
"insert into \"t\"(\"time\",\"tag\",\"value\") values(1,'O''Brien\\path',NULL);\n",
read("dump0_0.sql"));
@@ -224,12 +249,13 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testAlignedTreeSqlEscapesStringsAndPreservesNull() throws Exception {
- mockTree(
- dataSet(
- List.of("Time", "Device", "s1", "s2"),
- List.of("INT64", "TEXT", "TEXT", "STRING"),
- new Object[] {1L, "root.a.d1", "a'b\"\\c", null}));
- assertTrue(new ExportDataTree().exportBySql("select * from root.a.* align by device", 0));
+ Session session =
+ mockTree(
+ dataSet(
+ List.of("Time", "Device", "s1", "s2"),
+ List.of("INT64", "TEXT", "TEXT", "STRING"),
+ new Object[] {1L, "root.a.d1", "a'b\"\\c", null}));
+ exportTree(session, "select * from root.a.* align by device", 0, "-ft", "sql");
assertEquals(
"INSERT INTO root.a.d1(TIMESTAMP,s1,s2) ALIGNED VALUES (1,'a''b\"\\c',NULL);\n",
read("dump0_0.sql"));
@@ -237,43 +263,49 @@ public class DataToolRegressionTest extends AbstractDataTool {
@Test
public void testTreeCsvTypeHeadersInEverySplitFile() throws Exception {
- exportType = "csv";
- linesPerFile = 1;
- needDataTypePrinted = true;
- mockTree(
- dataSet(
- List.of("Time", "Device", "s1"),
- List.of("INT64", "TEXT", "DOUBLE"),
- new Object[] {1L, "root.a.d1", 1.5},
- new Object[] {2L, "root.a.d1", 2.5}));
- assertTrue(new ExportDataTree().exportBySql("select * from root.a.* align by device", 0));
+ Session typed =
+ mockTree(
+ dataSet(
+ List.of("Time", "Device", "s1"),
+ List.of("INT64", "TEXT", "DOUBLE"),
+ new Object[] {1L, "root.a.d1", 1.5},
+ new Object[] {2L, "root.a.d1", 2.5}));
+ exportTree(
+ typed,
+ "select * from root.a.* align by device",
+ 0,
+ "-ft",
+ "csv",
+ "-lpf",
+ "1",
+ "-dt",
+ "true");
assertEquals("Time,Device,s1(DOUBLE)", firstLine("dump0_0.csv"));
assertEquals("Time,Device,s1(DOUBLE)", firstLine("dump0_1.csv"));
- needDataTypePrinted = false;
- mockTree(
- dataSet(
- List.of("Time", "root.a.d1.s1"), List.of("INT64", "DOUBLE"), new Object[] {1L, 1.5}));
- assertTrue(new ExportDataTree().exportBySql("select * from root.a.d1", 1));
+ Session plain =
+ mockTree(
+ dataSet(
+ List.of("Time", "root.a.d1.s1"),
+ List.of("INT64", "DOUBLE"),
+ new Object[] {1L, 1.5}));
+ exportTree(plain, "select * from root.a.d1", 1, "-ft", "csv");
assertEquals("Time,root.a.d1.s1", firstLine("dump1_0.csv"));
}
@Test
public void testTableCsvTypeHeadersAndMultipleQueryFiles() throws Exception {
- exportType = "csv";
- needDataTypePrinted = true;
ITableSession session = mockTable("t");
SessionDataSet first =
dataSet(
List.of("time", "device", "v"),
List.of("TIMESTAMP", "STRING", "DOUBLE"),
new Object[] {1L, "sensor", 1.5});
+ when(session.executeQueryStatement(eq("select * from t"), anyLong())).thenReturn(first);
SessionDataSet second =
dataSet(List.of("time", "v"), List.of("TIMESTAMP", "DOUBLE"), new Object[] {2L, 2.5});
- when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(first, second);
- ExportDataTable tool = new ExportDataTable();
- assertTrue(tool.exportBySql("select * from t", 0));
- needDataTypePrinted = false;
- assertTrue(tool.exportBySql("select time,v from t", 1));
+ when(session.executeQueryStatement(eq("select time,v from t"), anyLong())).thenReturn(second);
+ exportTable(session, "select * from t", 0, "-ft", "csv", "-dt", "true");
+ exportTable(session, "select time,v from t", 1, "-ft", "csv");
assertEquals("time,device(STRING),v(DOUBLE)", firstLine("dump0_0.csv"));
assertEquals("time,v", firstLine("dump1_0.csv"));
}
@@ -283,8 +315,12 @@ public class DataToolRegressionTest extends AbstractDataTool {
Session session = mock(Session.class);
when(session.executeQueryStatement(anyString(), anyLong()))
.thenThrow(new IoTDBConnectionException("disconnect"));
- set(ExportDataTree.class, "session", session);
- assertFalse(new ExportDataTree().exportBySql("select * from root.a.d1", 0));
+ // ExportData.run reports the failure: FINISH is FAILED and the exit code is 1.
+ IoTDBConnectionException e =
+ assertThrows(
+ IoTDBConnectionException.class,
+ () -> exportTree(session, "select * from root.a.d1", 0, "-ft", "sql"));
+ assertEquals("disconnect", e.getMessage());
}
@Test
@@ -709,8 +745,6 @@ public class DataToolRegressionTest extends AbstractDataTool {
})
.when(session)
.insert(any(Tablet.class));
- exportType = "csv";
- needDataTypePrinted = typed;
for (int i = 0; i < 2; i++) {
List<String> columns =
i == 0
@@ -719,7 +753,7 @@ public class DataToolRegressionTest extends AbstractDataTool {
Object[] row = i == 0 ? new Object[] {1L, 1.5, 2.5} : new Object[] {1L, 2.5, 1.5};
SessionDataSet result = dataSet(columns, List.of("TIMESTAMP", "DOUBLE", "DOUBLE"), row);
when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
- assertTrue(new ExportDataTable().exportBySql("select * from t", i));
+ exportTable(session, "select * from t", i, "-ft", "csv", "-dt", String.valueOf(typed));
long failedFiles = loadFileFailedNum.sum();
long successfulFiles = loadFileSuccessfulNum.sum();
ImportDataScanTool.putToQueue(directory.resolve("dump" + i + "_0.csv").toString());
@@ -750,14 +784,12 @@ public class DataToolRegressionTest extends AbstractDataTool {
})
.when(session)
.insert(any(Tablet.class));
- exportType = "csv";
for (boolean typed : List.of(false, true)) {
- needDataTypePrinted = typed;
SessionDataSet result =
dataSet(List.of("time", column), List.of("TIMESTAMP", "DOUBLE"), new Object[] {1L, 1.5});
when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(result);
int index = typed ? 1 : 0;
- assertTrue(new ExportDataTable().exportBySql("select * from t", index));
+ exportTable(session, "select * from t", index, "-ft", "csv", "-dt", String.valueOf(typed));
String csv = "dump" + index + "_0.csv";
assertEquals("time," + column + (typed ? "(DOUBLE)" : ""), firstLine(csv));
try (CSVParser records = readCsvFile(directory.resolve(csv).toString())) {
@@ -1001,20 +1033,109 @@ public class DataToolRegressionTest extends AbstractDataTool {
}
}
- private static void mockTree(SessionDataSet dataSet) throws Exception {
+ /** Exports one query to dump<index> files, as ExportData.run does for one -q statement. */
+ private void exportTree(Session session, String sql, int index, String... options)
+ throws Exception {
+ ExportConfig config = exportConfig(false, options);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(
+ new ExportTask(sql, config.targetDirectory.resolve(config.prefix + index), null, null));
+ }
+ }
+
+ /**
+ * Plans and exports like ExportData.run: the given query is written to dump<index> files, a null
+ * query exports the -q option or, without one, the tables of the database.
+ */
+ private void exportTable(ITableSession session, String sql, int index, String... options)
+ throws Exception {
+ ExportConfig config = exportConfig(true, options);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTable exporter = new ExportDataTable(config, progress, session)) {
+ List<ExportTask> tasks;
+ if (sql != null) {
+ tasks =
+ exporter.planQueries(
+ List.of(
+ new ExportTask(
+ sql, config.targetDirectory.resolve(config.prefix + index), null, null)));
+ } else if (config.query != null) {
+ tasks = exporter.planQueries(ExportData.queryTasks(config, config.query));
+ } else {
+ tasks = exporter.planTables(exporter.timeRange());
+ }
+ for (ExportTask task : tasks) {
+ exporter.export(task);
+ }
+ }
+ }
+
+ private ExportConfig exportConfig(boolean table, String... options) throws Exception {
+ List<String> args = new ArrayList<>(List.of("-t", directory.toString(), "-tf", "timestamp"));
+ if (table) {
+ args.addAll(List.of("-sql_dialect", "table", "-db", "db"));
+ }
+ args.addAll(Arrays.asList(options));
+ return ExportConfig.parse(args.toArray(new String[0]), ZonedDateTime.now());
+ }
+
+ private static ExportProgressReporter progress() {
+ return new ExportProgressReporter(
+ new StringWriter(),
+ "tree",
+ System.nanoTime(),
+ new PrintStream(OutputStream.nullOutputStream()));
+ }
+
+ private static Session mockTree(SessionDataSet dataSet) throws Exception {
Session session = mock(Session.class);
when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
- set(ExportDataTree.class, "session", session);
+ return session;
}
+ /** A table session whose SHOW TABLES lists the given tables. */
private static ITableSession mockTable(String... names) throws Exception {
ITableSession session = mock(ITableSession.class);
- set(ExportDataTable.class, "tableSession", session);
- set(ExportDataTable.class, "tables", new ArrayList<>(Arrays.asList(names)));
- set(ExportDataTable.class, "allTables", new ArrayList<>(Arrays.asList(names)));
+ String[][] rows = new String[names.length][];
+ for (int i = 0; i < names.length; i++) {
+ rows[i] = new String[] {names[i]};
+ }
+ when(session.executeQueryStatement(eq("show tables"), anyLong()))
+ .thenAnswer(invocation -> rows(rows));
return session;
}
+ /** Answers DESCRIBE for a table: its time column, then name, type and category per column. */
+ private static void describe(ITableSession session, String table, String... columns)
+ throws Exception {
+ List<String[]> rows = new ArrayList<>();
+ rows.add(new String[] {"time", "TIMESTAMP", "TIME"});
+ for (int i = 0; i < columns.length; i += 3) {
+ rows.add(Arrays.copyOfRange(columns, i, i + 3));
+ }
+ when(session.executeQueryStatement(
+ eq("DESCRIBE \"" + table.replace("\"", "\"\"") + "\""), anyLong()))
+ .thenAnswer(invocation -> rows(rows.toArray(new String[0][])));
+ }
+
+ /** A metadata result whose rows hold the given strings. */
+ private static SessionDataSet rows(String[]... rows) throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ AtomicInteger row = new AtomicInteger();
+ when(dataSet.hasNext()).thenAnswer(invocation -> row.get() < rows.length);
+ when(dataSet.next())
+ .thenAnswer(
+ invocation -> {
+ RowRecord record = new RowRecord(0);
+ for (String value : rows[row.getAndIncrement()]) {
+ record.addField(new Binary(value, StandardCharsets.UTF_8), TSDataType.TEXT);
+ }
+ return record;
+ });
+ return dataSet;
+ }
+
private static SessionDataSet dataSet(List<String> names, List<String> types, Object[]... rows)
throws Exception {
SessionDataSet dataSet = mock(SessionDataSet.class);
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataFeatureTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataFeatureTest.java
new file mode 100644
index 00000000000..1dd2ee68b7c
--- /dev/null
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataFeatureTest.java
@@ -0,0 +1,726 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.cli.i18n.CliMessages;
+import org.apache.iotdb.exception.ArgsErrorException;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.session.Session;
+import org.apache.iotdb.tool.common.Constants;
+
+import org.apache.commons.cli.ParseException;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.read.common.Field;
+import org.apache.tsfile.read.common.RowRecord;
+import org.apache.tsfile.utils.Binary;
+import org.jline.reader.EndOfFileException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Input, planning and lifecycle contracts exercised without a server. */
+public class ExportDataFeatureTest {
+ @Rule public TemporaryFolder folder = new TemporaryFolder();
+ private static final ZonedDateTime START = ZonedDateTime.parse("2026-10-14T12:34:56+08:00");
+
+ @Test
+ public void preservesPrefixOptionsAndRejectsFAlias() throws Exception {
+ ExportConfig defaults = config("-mf", "measurements.txt");
+ assertEquals("", defaults.prefix);
+ assertEquals("log_export_data_20261014123456.log", defaults.logFile.getFileName().toString());
+ assertEquals("backup", config("-mf", "measurements.txt", "-pfn", "backup").prefix);
+ assertEquals(
+ "backup", config("-mf", "measurements.txt", "--prefix_file_name", "backup").prefix);
+ assertEquals("dump", config("-q", "select * from root.**").prefix);
+ assertThrows(ParseException.class, () -> config("-f", "backup"));
+ assertThrows(ArgsErrorException.class, () -> config("-progress"));
+ assertThrows(
+ ArgsErrorException.class, () -> config("-mf", "file", "-q", "select s from root.sg.d"));
+ assertThrows(
+ ArgsErrorException.class,
+ () -> config("-sql_dialect", "table", "-db", "db", "-mf", "file"));
+ assertThrows(ArgsErrorException.class, () -> config("-lpf", "0"));
+ assertThrows(ArgsErrorException.class, () -> config("-dt", "maybe"));
+ assertThrows(ParseException.class, () -> config("-p"));
+ }
+
+ @Test
+ public void acceptsBomCommentsQuotedPathsAndIoTDBPatterns() throws Exception {
+ Path input =
+ input(
+ "\uFEFF# comment\r\n\n root.sg.d.pre* \nroot.sg.**\rroot.sg.d.`a.b`\nroot.sg.d.pre*\n");
+ MeasurementFile file = MeasurementFile.read(input);
+ assertEquals(6, file.lines());
+ assertEquals(3, file.patterns().size());
+ assertEquals(1, file.duplicates());
+ assertEquals("root.sg.d.pre*", file.patterns().get(0).path().getFullPath());
+ assertEquals("`a.b`", file.patterns().get(2).path().getMeasurement());
+ }
+
+ @Test
+ public void reportsInvalidLineAndUtf8AtActualLine() throws Exception {
+ Path input = input("root.sg.d.s\n# valid\nroot.sg..s\n");
+ ArgsErrorException invalid =
+ assertThrows(ArgsErrorException.class, () -> MeasurementFile.read(input));
+ assertTrue(invalid.getMessage().contains("3"));
+ assertTrue(invalid.getMessage().contains("root.sg..s"));
+ byte[] bytes = "root.sg.d.s\n# comment\n".getBytes(StandardCharsets.UTF_8);
+ byte[] bad = Arrays.copyOf(bytes, bytes.length + 2);
+ bad[bytes.length] = (byte) 0xc3;
+ bad[bytes.length + 1] = 0x28;
+ Files.write(input, bad);
+ ArgsErrorException utf8 =
+ assertThrows(ArgsErrorException.class, () -> MeasurementFile.read(input));
+ assertTrue(utf8.getMessage().contains("3"));
+ assertThrows(ArgsErrorException.class, () -> MeasurementFile.read(input("# empty\n \n")));
+ assertThrows(ArgsErrorException.class, () -> MeasurementFile.read(input("other.sg.s\n")));
+ }
+
+ @Test
+ public void plansEachDistinctPatternAndDeduplicatesActualMeasurements() throws Exception {
+ Path input = input("root.sg.d.pre*\nroot.sg.**\nroot.sg.d.pre*\nroot.empty.**\n");
+ ExportConfig config = config("-mf", input.toString(), "-pfn", "backup");
+ StringWriter log = new StringWriter();
+ Session session = mock(Session.class);
+ SessionDataSet first = metadata("root.sg.d.pre1");
+ SessionDataSet second = metadata("root.sg.d.pre1", "root.sg.d.pre2");
+ SessionDataSet empty = metadata();
+ when(session.executeQueryStatement("show timeseries root.sg.d.pre*", config.timeout))
+ .thenReturn(first);
+ when(session.executeQueryStatement("show timeseries root.sg.**", config.timeout))
+ .thenReturn(second);
+ when(session.executeQueryStatement("show timeseries root.empty.**", config.timeout))
+ .thenReturn(empty);
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ try (ExportProgressReporter reporter =
+ new ExportProgressReporter(
+ log,
+ "tree",
+ System.nanoTime(),
+ new PrintStream(console, true, StandardCharsets.UTF_8));
+ ExportDataTree exporter = new ExportDataTree(config, reporter, session)) {
+ List<ExportTask> tasks =
+ exporter.planMeasurements(
+ MeasurementFile.read(input),
+ ExportTimeRange.parse("100", "200", ZoneId.of("UTC"), "ms"));
+ assertEquals(2, tasks.size());
+ assertEquals(
+ "SELECT pre1 FROM root.sg.d WHERE time >= 100 AND time <= 200", tasks.get(0).sql());
+ assertEquals("backup_root.sg.d.pre1", tasks.get(0).outputBase().getFileName().toString());
+ assertEquals("root.sg.d.pre2", tasks.get(1).measurement().path().getFullPath());
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ verify(session, times(3)).executeQueryStatement(anyString(), anyLong());
+ verify(first).close();
+ verify(second).close();
+ verify(empty).close();
+ verify(session).close();
+ String content = log.toString();
+ String warning = event(log, "WARNING");
+ assertTrue(warning, warning.contains(member("line", 4)));
+ assertTrue(warning, warning.contains(member("pattern", "root.empty.**")));
+ // JSON escapes the backslashes of a Windows path.
+ assertTrue(warning, warning.contains(member("source", input.toString().replace("\\", "\\\\"))));
+ // The console names the unmatched pattern itself, without the long source path.
+ String expected =
+ String.format(
+ CliMessages.LOG_WARNING_ARG_05BE32E9,
+ String.format(
+ CliMessages.LOG_NO_MEASUREMENTS_MATCHED_ARG_MEASUREMENT_FILE_LINE_ARG_94C4EF10,
+ "root.empty.**",
+ 4));
+ String shown = console.toString(StandardCharsets.UTF_8);
+ assertTrue(shown, shown.lines().anyMatch(expected::equals));
+ // Measurement fields keep the requirement's order between the rows and the elapsed time.
+ assertTrue(
+ content,
+ content.contains(
+ String.join(
+ ",",
+ member("status", "RUNNING"),
+ member("exported_data_rows", 0),
+ member("exported_measurement_num", 0),
+ member("total_measurement_num", 2),
+ member("exported_percent", "0.00%"),
+ member("estimated_remaining", "UNKNOWN"))
+ + ",\"elapsed_time\":"));
+ assertTrue(content, content.contains(member("duplicate_matches", 1)));
+ }
+
+ @Test
+ public void failsOnlyWhenNoPatternMatches() throws Exception {
+ Path input = input("root.empty.**\n");
+ ExportConfig config = config("-mf", input.toString());
+ Session session = mock(Session.class);
+ SessionDataSet empty = metadata();
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(empty);
+ try (ExportProgressReporter reporter = reporter(new StringWriter());
+ ExportDataTree exporter = new ExportDataTree(config, reporter, session)) {
+ assertThrows(
+ ArgsErrorException.class,
+ () ->
+ exporter.planMeasurements(
+ MeasurementFile.read(input),
+ ExportTimeRange.parse(null, null, ZoneId.of("UTC"), "ms")));
+ }
+ }
+
+ @Test
+ public void timeRangesUsePrecisionTimezoneAndInclusiveBounds() throws Exception {
+ ZoneId zone = ZoneId.of("Asia/Shanghai");
+ assertEquals(
+ " WHERE time >= 100 AND time <= 100",
+ ExportTimeRange.parse("100", "100", zone, "ns").whereClause());
+ assertEquals(" WHERE time <= 99", ExportTimeRange.parse(null, "99", zone, "ms").whereClause());
+ assertEquals(
+ " WHERE time >= 1000",
+ ExportTimeRange.parse("1970-01-01T08:00:00.001", null, zone, "us").whereClause());
+ assertEquals(
+ " WHERE time >= 1000000",
+ ExportTimeRange.parse("1970-01-01T00:00:00.001Z", null, zone, "ns").whereClause());
+ assertThrows(ArgsErrorException.class, () -> ExportTimeRange.parse("200", "100", zone, "ms"));
+ assertThrows(ArgsErrorException.class, () -> ExportTimeRange.parse("oops", null, zone, "ms"));
+ ExportConfig config = config("-q", "select s from root.sg.d", "-start_time", "oops");
+ assertEquals(
+ "select s from root.sg.d", ExportData.queryTasks(config, config.query).get(0).sql());
+ }
+
+ @Test
+ public void fileNamesEscapeAndRejectCollisionsAndLogAliases() throws Exception {
+ ExportConfig config = config("-mf", "file");
+ ExportFileNames names = new ExportFileNames();
+ assertEquals(
+ "root.sg.d.`a%3Ab`", names.measurement(config, "root.sg.d.`a:b`").getFileName().toString());
+ names.measurement(config, "root.sg.d.A");
+ assertThrows(ArgsErrorException.class, () -> names.measurement(config, "root.sg.d.a"));
+ assertThrows(
+ ArgsErrorException.class, () -> names.measurement(config, "root." + "x".repeat(230)));
+ Path file = folder.newFile().toPath();
+ Path alias = folder.getRoot().toPath().resolve("alias");
+ Files.createLink(alias, file);
+ assertThrows(ArgsErrorException.class, () -> ExportFileNames.checkLogCollision(alias, file));
+ }
+
+ @Test
+ public void independentTimerReportsWithoutRowsAndStopsAfterOneTerminal() throws Exception {
+ CountDownLatch tick = new CountDownLatch(1);
+ StringWriter log =
+ new StringWriter() {
+ @Override
+ public void write(String value) {
+ super.write(value);
+ if (value.contains(member("event", "PROGRESS"))) {
+ tick.countDown();
+ }
+ }
+ };
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ try (ExportProgressReporter reporter =
+ new ExportProgressReporter(
+ log,
+ "tree",
+ System.nanoTime(),
+ new PrintStream(console, true, StandardCharsets.UTF_8))) {
+ reporter.start(
+ config("-q", "select * from root.**"), true, () -> fail("unexpected log error"));
+ assertTrue(tick.await(5, TimeUnit.SECONDS));
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ reporter.progress();
+ reporter.finish(ExportProgressReporter.Status.FAILED, new IOException("later"));
+ }
+ String content = log.toString();
+ assertTrue(
+ content,
+ content.startsWith(
+ "{" + member("progress_id", ProcessHandle.current().pid()) + ",\"timestamp\":"));
+ assertTrue(content, content.contains(member("mode", "query")));
+ // The requirement's fields lead each event, in its order; event details follow them.
+ assertTrue(
+ content,
+ Pattern.compile(
+ Pattern.quote(
+ String.join(
+ ",",
+ member("model", "tree"),
+ member("status", "RUNNING"),
+ member("exported_data_rows", 0)))
+ + ",\"elapsed_time\":\"00:00:0\\d\","
+ + Pattern.quote(
+ String.join(
+ ",",
+ member("event", "PROGRESS"),
+ member("level", "INFO"),
+ member("phase", "VALIDATING"))))
+ .matcher(content)
+ .find());
+ assertFalse(content.contains("exported_measurement_num"));
+ assertEquals(1, content.split(member("event", "FINISH"), -1).length - 1);
+ // Every event is one JSON object on its own line.
+ content.lines().forEach(line -> assertTrue(line, line.startsWith("{") && line.endsWith("}")));
+
+ // A query has no total, so the console shows rows, elapsed time and rate, then one summary.
+ String shown = console.toString(StandardCharsets.UTF_8);
+ assertTrue(
+ shown, printed(shown, CliMessages.LOG_EXPORT_STARTED_ARG_ARG_TARGET_ARG_LOG_ARG_BD58AECB));
+ assertTrue(shown, printed(shown, CliMessages.LOG_ARG_ROWS_ELAPSED_ARG_ARG_ROWS_S_AE3CF660));
+ assertTrue(
+ shown,
+ printed(shown, CliMessages.LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B));
+ assertFalse(shown, printed(shown, CliMessages.LOG_EXPORT_FAILED_AFTER_ARG_ARG_C123F0EF));
+ assertFalse(shown, shown.contains("{"));
+ // Scripts check the last line for the outcome, so the log path comes before the summary.
+ List<String> lines = shown.lines().collect(Collectors.toList());
+ assertTrue(
+ shown,
+ printed(
+ lines.get(lines.size() - 1),
+ CliMessages.LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B));
+ assertTrue(shown, printed(lines.get(lines.size() - 2), CliMessages.LOG_LOG_ARG_FBF61F63));
+ }
+
+ @Test
+ public void remainingTimeWaitsForSeveralMeasurements() throws Exception {
+ StringWriter log = new StringWriter();
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ ExportConfig config = config();
+ try (ExportProgressReporter reporter =
+ new ExportProgressReporter(
+ log,
+ "tree",
+ System.nanoTime(),
+ new PrintStream(console, true, StandardCharsets.UTF_8))) {
+ reporter.discovered(MeasurementFile.read(input("root.sg.**\n")), 40, 40, 0);
+ for (int i = 0; i < 3; i++) {
+ reporter.taskStarted(
+ new ExportTask(
+ "select s from root.sg.d" + i,
+ config.targetDirectory.resolve("m" + i),
+ null,
+ null));
+ reporter.taskCompleted();
+ reporter.progress();
+ }
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ List<String> progress =
+ log.toString()
+ .lines()
+ .filter(line -> line.contains(member("event", "PROGRESS")))
+ .collect(Collectors.toList());
+ // 5% of 40 is two measurements, yet at least three must complete before estimating.
+ assertTrue(progress.get(1), progress.get(1).contains(member("estimated_remaining", "UNKNOWN")));
+ assertFalse(
+ progress.get(2), progress.get(2).contains(member("estimated_remaining", "UNKNOWN")));
+ List<String> shown =
+ console
+ .toString(StandardCharsets.UTF_8)
+ .lines()
+ .filter(
+ line ->
+ printed(
+ line,
+ CliMessages
+ .LOG_ARG_ARG_ARG_MEASUREMENTS_ARG_ROWS_ELAPSED_ARG_ETA_ARG_CURRENT_ARG_B6DEA0BD))
+ .collect(Collectors.toList());
+ assertEquals(3, shown.size());
+ assertTrue(shown.get(0), shown.get(0).startsWith("[2%] 1/40 "));
+ assertTrue(shown.get(1), shown.get(1).contains(" --"));
+ assertFalse(shown.get(2), shown.get(2).contains(" --"));
+ }
+
+ @Test
+ public void terminalRedrawsOneFittedLineAndRedirectedOutputGetsLines() {
+ ByteArrayOutputStream terminalBytes = new ByteArrayOutputStream();
+ ExportConsole terminal =
+ new ExportConsole(new PrintStream(terminalBytes, true, StandardCharsets.UTF_8), true, 30);
+ terminal.progress("Exporting 10%", "root.factory.line19.s0", "unused");
+ terminal.progress("Exporting 20%", "", "unused");
+ terminal.line("done");
+ // The tail keeps whole path nodes from the right so the line, with its last column free, fits
+ // 30 columns. A shorter redraw blanks what the previous one left behind, and a lasting line
+ // first erases the progress line instead of continuing it.
+ assertEquals(
+ "\rExporting 10% ...line19.s0"
+ + "\rExporting 20%"
+ + " ".repeat(14)
+ + "\r"
+ + " ".repeat(13)
+ + "\rdone"
+ + System.lineSeparator(),
+ terminalBytes.toString(StandardCharsets.UTF_8));
+
+ ByteArrayOutputStream plainBytes = new ByteArrayOutputStream();
+ ExportConsole plain =
+ new ExportConsole(new PrintStream(plainBytes, true, StandardCharsets.UTF_8), false, 30);
+ plain.progress("Exporting 10%", "root.factory.line19.s0", "[10%] plain");
+ plain.line("done");
+ assertEquals(
+ "[10%] plain" + System.lineSeparator() + "done" + System.lineSeparator(),
+ plainBytes.toString(StandardCharsets.UTF_8));
+
+ // CJK characters take two columns, so cutting counts columns rather than characters.
+ assertEquals(10, ExportConsole.width("导出中 59%"));
+ assertEquals("...乙丙", ExportConsole.left("测点甲乙丙", 7));
+ assertEquals("root.sg", ExportConsole.left("root.sg", 7));
+ }
+
+ @Test
+ public void terminalTooNarrowForTheLineGetsLinesInsteadOfRedraws() {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ExportConsole console =
+ new ExportConsole(new PrintStream(bytes, true, StandardCharsets.UTF_8), true, 14);
+ console.progress("Exporting 10%", "root.factory.line19.s0", "[10%] first");
+ console.progress("Exporting 100%", "root.factory.line19.s0", "[100%] second");
+ console.line("done");
+ // 13 columns fit beside the free last column and are redrawn in place. A carriage return
+ // reaches only the last row of a wrapped line, so the 14-column line erases the redrawn one
+ // and becomes a line of its own.
+ assertEquals(
+ "\rExporting 10%"
+ + "\r"
+ + " ".repeat(13)
+ + "\r"
+ + "[100%] second"
+ + System.lineSeparator()
+ + "done"
+ + System.lineSeparator(),
+ bytes.toString(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void jsonEscapesStringsAndKeepsIntegersAsNumbers() {
+ assertEquals(
+ "{\"text\":\"a\\\"b\\\\c\\nd\\te\\u0001中\",\"rows\":3,\"code\":130,\"path\":\"x y\"}",
+ ExportProgressReporter.json(
+ ExportProgressReporter.fields(
+ "text",
+ "a\"b\\c\nd\te" + (char) 1 + "中",
+ "rows",
+ 3L,
+ "code",
+ 130,
+ "skipped",
+ null,
+ "path",
+ Path.of("x y"))));
+ }
+
+ @Test
+ public void interactiveQueryIsReadBeforeProgressAndLogStart() throws Exception {
+ Path target = folder.getRoot().toPath();
+ PrintStream originalOut = System.out;
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ AtomicBoolean prompted = new AtomicBoolean();
+ int code;
+ try {
+ System.setOut(new PrintStream(console, true, StandardCharsets.UTF_8));
+ code =
+ ExportData.run(
+ new String[] {"-ft", "csv", "-t", target.toString()},
+ config -> {
+ prompted.set(true);
+ // Neither progress events nor the log may exist while the user types the query.
+ assertEquals("", console.toString(StandardCharsets.UTF_8));
+ assertFalse(hasLog(target));
+ // End of input abandons the export before anything starts.
+ throw new EndOfFileException();
+ });
+ } finally {
+ System.setOut(originalOut);
+ }
+ assertTrue(prompted.get());
+ assertEquals(Constants.CODE_ERROR, code);
+ assertEquals(
+ CliMessages.EXCEPTION_EXPORT_CANCELLED_85D44F54 + System.lineSeparator(),
+ console.toString(StandardCharsets.UTF_8));
+ assertFalse(hasLog(target));
+ }
+
+ private static boolean hasLog(Path directory) throws IOException {
+ try (Stream<Path> files = Files.list(directory)) {
+ return files.anyMatch(file -> file.getFileName().toString().endsWith(".log"));
+ }
+ }
+
+ @Test
+ public void cancelledFinishReportsSignalStatusWithoutUnopenedOutput() throws Exception {
+ StringWriter log = new StringWriter();
+ ExportConfig config = config();
+ try (ExportProgressReporter reporter = reporter(log)) {
+ reporter.taskStarted(
+ new ExportTask(
+ "select s from root.sg.d", config.targetDirectory.resolve("dump0"), null, null));
+ reporter.cancel(130);
+ assertThrows(IOException.class, reporter::checkActive);
+ reporter.finish(ExportProgressReporter.Status.CANCELLED, null);
+ }
+ String content = log.toString();
+ // The task has not opened a file, so no output is named.
+ assertFalse(content, content.contains("\"output\":"));
+ assertTrue(content, content.contains(member("status", "CANCELLED")));
+ // The terminating signal decides the process status, which FINISH reports.
+ assertTrue(content, content.contains(member("exit_code", 130)));
+ }
+
+ @Test
+ public void queryModeReportsItsTimeRangeAsIgnored() throws Exception {
+ StringWriter queryLog = new StringWriter();
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ try (ExportProgressReporter reporter =
+ new ExportProgressReporter(
+ queryLog,
+ "tree",
+ System.nanoTime(),
+ new PrintStream(console, true, StandardCharsets.UTF_8))) {
+ reporter.start(
+ config("-q", "select * from root.**", "-start_time", "100"),
+ true,
+ () -> fail("unexpected log error"));
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ // The query runs as written, so START must not suggest a time filter.
+ String start = event(queryLog, "START");
+ assertFalse(start, start.contains("\"start_time\":"));
+ String warning = event(queryLog, "WARNING");
+ assertTrue(warning, warning.contains(member("level", "WARN")));
+ assertTrue(warning, warning.contains(member("start_time", "100")));
+ String shown = console.toString(StandardCharsets.UTF_8);
+ assertTrue(
+ shown,
+ shown
+ .lines()
+ .anyMatch(
+ String.format(
+ CliMessages.LOG_WARNING_ARG_05BE32E9,
+ CliMessages
+ .LOG_START_TIME_AND_END_TIME_DO_NOT_APPLY_TO_QUERIES_AND_ARE_IGNORED_02DC37CC)
+ ::equals));
+
+ StringWriter measurementLog = new StringWriter();
+ try (ExportProgressReporter reporter = reporter(measurementLog)) {
+ reporter.start(
+ config("-mf", input("root.sg.d.s\n").toString(), "-start_time", "100"),
+ false,
+ () -> fail("unexpected log error"));
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ assertTrue(event(measurementLog, "START").contains(member("start_time", "100")));
+ assertFalse(measurementLog.toString().contains(member("event", "WARNING")));
+ }
+
+ @Test
+ public void tableQueryReportsItsTableAsIgnored() throws Exception {
+ StringWriter queryLog = new StringWriter();
+ try (ExportProgressReporter reporter = reporter(queryLog)) {
+ reporter.start(
+ config("-sql_dialect", "table", "-db", "db", "-table", "t", "-q", "select * from t"),
+ true,
+ () -> fail("unexpected log error"));
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ // SQL and TsFile output take the table from the query, so -table has no effect.
+ String warning = event(queryLog, "WARNING");
+ assertTrue(
+ warning,
+ warning.contains(
+ member(
+ "message",
+ CliMessages.LOG_TABLE_DOES_NOT_APPLY_TO_QUERIES_AND_IS_IGNORED_DBA31209)));
+ assertTrue(warning, warning.contains(member("table", "t")));
+
+ StringWriter tableLog = new StringWriter();
+ try (ExportProgressReporter reporter = reporter(tableLog)) {
+ reporter.start(
+ config("-sql_dialect", "table", "-db", "db", "-table", "t"),
+ false,
+ () -> fail("unexpected log error"));
+ reporter.finish(ExportProgressReporter.Status.SUCCESS, null);
+ }
+ assertFalse(tableLog.toString().contains(member("event", "WARNING")));
+ }
+
+ private static String event(StringWriter log, String event) {
+ return log.toString()
+ .lines()
+ .filter(line -> line.contains(member("event", event)))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError(log.toString()));
+ }
+
+ /** A member as the JSON Lines log writes it, such as "status":"FAILED" or "exit_code":130. */
+ static String member(String key, Object value) {
+ return '"' + key + "\":" + (value instanceof Number ? value : "\"" + value + '"');
+ }
+
+ /** Whether some console line came from {@code template}, whatever values filled it in. */
+ static boolean printed(String console, String template) {
+ String pattern =
+ Arrays.stream(template.split("%[sd]", -1))
+ .map(Pattern::quote)
+ .collect(Collectors.joining(".*?"));
+ return Pattern.compile(pattern).matcher(console).find();
+ }
+
+ @Test
+ public void timerLogFailureIsVisibleToWorkerAndFailureHandler() throws Exception {
+ CountDownLatch failed = new CountDownLatch(1);
+ AtomicBoolean failWrites = new AtomicBoolean();
+ Writer log =
+ new Writer() {
+ @Override
+ public void write(char[] data, int offset, int length) throws IOException {
+ if (failWrites.get()) {
+ throw new IOException("disk failure");
+ }
+ }
+
+ @Override
+ public void flush() {}
+
+ @Override
+ public void close() {}
+ };
+ ByteArrayOutputStream console = new ByteArrayOutputStream();
+ try (ExportProgressReporter reporter =
+ new ExportProgressReporter(log, "tree", System.nanoTime(), new PrintStream(console))) {
+ reporter.start(config(), false, failed::countDown);
+ failWrites.set(true);
+ assertTrue(failed.await(5, TimeUnit.SECONDS));
+ assertThrows(IOException.class, reporter::checkActive);
+ assertThrows(
+ IOException.class, () -> reporter.finish(ExportProgressReporter.Status.SUCCESS, null));
+ assertEquals(ExportProgressReporter.Status.FAILED, reporter.status());
+ }
+ // The log cannot hold its own failure, so the console summary names it.
+ String shown = console.toString(StandardCharsets.UTF_8);
+ assertTrue(shown, printed(shown, CliMessages.LOG_EXPORT_FAILED_AFTER_ARG_ARG_C123F0EF));
+ assertTrue(shown, shown.contains("disk failure"));
+ assertFalse(
+ shown,
+ printed(shown, CliMessages.LOG_EXPORT_COMPLETELY_ROWS_ARG_FILES_ARG_ELAPSED_ARG_A94BBB8B));
+ }
+
+ @Test
+ public void resultCloseFailureNeverCompletesTheTask() throws Exception {
+ Session session = mock(Session.class);
+ SessionDataSet data = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(data.getColumnNames()).thenReturn(Arrays.asList("Time", "root.sg.d.s"));
+ when(data.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList()).thenReturn(Arrays.asList("INT64", "INT32"));
+ doThrow(new IOExceptionForTest()).when(data).close();
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(data);
+ StringWriter log = new StringWriter();
+ ExportConfig config = config();
+ try (ExportProgressReporter reporter = reporter(log);
+ ExportDataTree exporter = new ExportDataTree(config, reporter, session)) {
+ assertThrows(
+ org.apache.iotdb.rpc.IoTDBConnectionException.class,
+ () ->
+ exporter.export(
+ new ExportTask(
+ "select s from root.sg.d",
+ config.targetDirectory.resolve("dump0"),
+ null,
+ null)));
+ reporter.finish(ExportProgressReporter.Status.FAILED, new IOException("close failure"));
+ }
+ assertFalse(log.toString().contains(member("event", "TASK_COMPLETE")));
+ verify(data, times(1)).close();
+ verify(session).close();
+ }
+
+ private static class IOExceptionForTest extends org.apache.iotdb.rpc.IoTDBConnectionException {
+ IOExceptionForTest() {
+ super("close failure");
+ }
+ }
+
+ private ExportConfig config(String... extras) throws Exception {
+ List<String> args =
+ new ArrayList<>(Arrays.asList("-ft", "csv", "-t", folder.getRoot().toString()));
+ args.addAll(Arrays.asList(extras));
+ return ExportConfig.parse(args.toArray(new String[0]), START);
+ }
+
+ private Path input(String contents) throws IOException {
+ Path path = folder.newFile().toPath();
+ return Files.writeString(path, contents, StandardCharsets.UTF_8);
+ }
+
+ private ExportProgressReporter reporter(StringWriter log) {
+ return new ExportProgressReporter(
+ log, "tree", System.nanoTime(), new PrintStream(new ByteArrayOutputStream()));
+ }
+
+ private SessionDataSet metadata(String... paths) throws Exception {
+ SessionDataSet data = mock(SessionDataSet.class);
+ when(data.getColumnNames())
+ .thenReturn(Arrays.asList("Timeseries", "DataType", "Encoding", "Compression"));
+ List<RowRecord> records = new ArrayList<>();
+ for (String path : paths) {
+ List<Field> fields = new ArrayList<>();
+ for (String value : Arrays.asList(path, "INT32", "RLE", "LZ4")) {
+ Field field = new Field(TSDataType.TEXT);
+ field.setBinaryV(new Binary(value, StandardCharsets.UTF_8));
+ fields.add(field);
+ }
+ records.add(new RowRecord(0, fields));
+ }
+ java.util.concurrent.atomic.AtomicInteger cursor =
+ new java.util.concurrent.atomic.AtomicInteger();
+ when(data.hasNext()).thenAnswer(call -> cursor.get() < records.size());
+ when(data.next()).thenAnswer(call -> records.get(cursor.getAndIncrement()));
+ return data;
+ }
+}
diff --git a/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataRegressionTest.java b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataRegressionTest.java
new file mode 100644
index 00000000000..925c88cc99c
--- /dev/null
+++ b/iotdb-client/cli/src/test/java/org/apache/iotdb/tool/data/ExportDataRegressionTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.iotdb.tool.data;
+
+import org.apache.iotdb.isession.ITableSession;
+import org.apache.iotdb.isession.SessionDataSet;
+import org.apache.iotdb.session.Session;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/** Guards existing query output while the export lifecycle evolves. */
+public class ExportDataRegressionTest {
+
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void treeCsvKeepsHeaderNullValuesAndSplits() throws Exception {
+ SessionDataSet dataSet = treeDataSet();
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("csv", false);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 3));
+ }
+
+ assertEquals("Time,root.sg.d.s\r\n100,11\r\n200,\r\n", read("dump3_0.csv"));
+ assertEquals("Time,root.sg.d.s\r\n300,33\r\n", read("dump3_1.csv"));
+ verify(dataSet).close();
+ }
+
+ @Test
+ public void treeSqlKeepsTimestampAndSkipsRowsWithoutValues() throws Exception {
+ SessionDataSet dataSet = treeDataSet();
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("sql", false);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 0));
+ // The row at 200 has no value, so it writes no statement and is not counted.
+ assertEquals(2, progress.rows());
+ }
+
+ assertEquals(
+ "INSERT INTO root.sg.d(TIMESTAMP,s) VALUES (100,11);\n"
+ + "INSERT INTO root.sg.d(TIMESTAMP,s) VALUES (300,33);\n",
+ read("dump0_0.sql"));
+ assertFalse(Files.exists(temporaryFolder.getRoot().toPath().resolve("dump0_1.sql")));
+ }
+
+ @Test
+ public void treeSqlCountsAMultiDeviceRowOnceAndKeepsItInOneFile() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames())
+ .thenReturn(new ArrayList<>(Arrays.asList("Time", "root.sg.d1.s", "root.sg.d2.s")));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList()).thenReturn(Arrays.asList("INT64", "INT32", "INT32"));
+ AtomicInteger cursor = new AtomicInteger(-1);
+ when(iterator.next()).thenAnswer(call -> cursor.incrementAndGet() < 3);
+ when(iterator.getLong(1)).thenAnswer(call -> (cursor.get() + 1L) * 100);
+ when(iterator.getString(2)).thenAnswer(call -> "1" + cursor.get());
+ when(iterator.getString(3)).thenAnswer(call -> "2" + cursor.get());
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("sql", false);
+ StringWriter log = new StringWriter();
+ try (ExportProgressReporter progress = progress(log);
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 0));
+ assertEquals(3, progress.rows());
+ }
+
+ // -lpf 2 counts data rows, so the second row's statements share the first file.
+ assertEquals(
+ "INSERT INTO root.sg.d1(TIMESTAMP,s) VALUES (100,10);\n"
+ + "INSERT INTO root.sg.d2(TIMESTAMP,s) VALUES (100,20);\n"
+ + "INSERT INTO root.sg.d1(TIMESTAMP,s) VALUES (200,11);\n"
+ + "INSERT INTO root.sg.d2(TIMESTAMP,s) VALUES (200,21);\n",
+ read("dump0_0.sql"));
+ assertEquals(
+ "INSERT INTO root.sg.d1(TIMESTAMP,s) VALUES (300,12);\n"
+ + "INSERT INTO root.sg.d2(TIMESTAMP,s) VALUES (300,22);\n",
+ read("dump0_1.sql"));
+ String content = log.toString();
+ assertTrue(content, content.contains(ExportDataFeatureTest.member("file_rows", 2)));
+ assertTrue(content, content.contains(ExportDataFeatureTest.member("file_rows", 1)));
+ assertTrue(content, content.contains(ExportDataFeatureTest.member("task_rows", 3)));
+ }
+
+ @Test
+ public void treeSqlGroupsInterleavedColumnsByDevice() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames())
+ .thenReturn(
+ new ArrayList<>(Arrays.asList("Time", "root.sg.d1.a", "root.sg.d2.x", "root.sg.d1.b")));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList())
+ .thenReturn(Arrays.asList("INT64", "INT32", "INT32", "INT32"));
+ when(iterator.next()).thenReturn(true, false);
+ when(iterator.getLong(1)).thenReturn(100L);
+ when(iterator.getString(2)).thenReturn("1");
+ when(iterator.getString(3)).thenReturn("2");
+ when(iterator.getString(4)).thenReturn("3");
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("sql", false);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 0));
+ }
+
+ assertEquals(
+ "INSERT INTO root.sg.d1(TIMESTAMP,a,b) VALUES (100,1,3);\n"
+ + "INSERT INTO root.sg.d2(TIMESTAMP,x) VALUES (100,2);\n",
+ read("dump0_0.sql"));
+ }
+
+ @Test
+ public void treeSqlLeavesOutNullColumnsOfADevice() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames())
+ .thenReturn(
+ new ArrayList<>(Arrays.asList("Time", "root.sg.d.a", "root.sg.d.b", "root.sg.d.c")));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList())
+ .thenReturn(Arrays.asList("INT64", "INT32", "INT32", "INT32"));
+ String[][] rows = {{"1", null, "3"}, {null, null, "6"}, {"7", "8", "9"}};
+ AtomicInteger cursor = new AtomicInteger(-1);
+ when(iterator.next()).thenAnswer(call -> cursor.incrementAndGet() < rows.length);
+ when(iterator.getLong(1)).thenAnswer(call -> (cursor.get() + 1L) * 100);
+ when(iterator.isNull(anyInt()))
+ .thenAnswer(call -> rows[cursor.get()][(int) call.getArgument(0) - 2] == null);
+ when(iterator.getString(anyInt()))
+ .thenAnswer(call -> rows[cursor.get()][(int) call.getArgument(0) - 2]);
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("sql", false);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 0));
+ }
+
+ assertEquals(
+ "INSERT INTO root.sg.d(TIMESTAMP,a,c) VALUES (100,1,3);\n"
+ + "INSERT INTO root.sg.d(TIMESTAMP,c) VALUES (200,6);\n",
+ read("dump0_0.sql"));
+ assertEquals(
+ "INSERT INTO root.sg.d(TIMESTAMP,a,b,c) VALUES (300,7,8,9);\n", read("dump0_1.sql"));
+ }
+
+ @Test
+ public void treeSqlAlignedByDeviceNamesEachRowsDevice() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames())
+ .thenReturn(new ArrayList<>(Arrays.asList("Time", "Device", "s1", "s2")));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList()).thenReturn(Arrays.asList("INT64", "TEXT", "INT32", "TEXT"));
+ AtomicInteger cursor = new AtomicInteger(-1);
+ when(iterator.next()).thenAnswer(call -> cursor.incrementAndGet() < 2);
+ when(iterator.getLong(1)).thenAnswer(call -> (cursor.get() + 1L) * 100);
+ when(iterator.getString(2)).thenAnswer(call -> "root.sg.d" + (cursor.get() + 1));
+ when(iterator.getString(3)).thenAnswer(call -> String.valueOf(cursor.get()));
+ when(iterator.isNull(4)).thenAnswer(call -> cursor.get() == 1);
+ when(iterator.getString(4)).thenReturn("a\"b");
+ Session session = mock(Session.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("sql", false);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTree exporter = new ExportDataTree(config, progress, session)) {
+ exporter.export(task(config, 0));
+ }
+
+ assertEquals(
+ "INSERT INTO root.sg.d1(TIMESTAMP,s1,s2) ALIGNED VALUES (100,0,'a\"b');\n"
+ + "INSERT INTO root.sg.d2(TIMESTAMP,s1,s2) ALIGNED VALUES (200,1,NULL);\n",
+ read("dump0_0.sql"));
+ }
+
+ @Test
+ public void tableCsvKeepsColumnOrderAndNullValues() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames()).thenReturn(Arrays.asList("time", "value"));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList()).thenReturn(Arrays.asList("TIMESTAMP", "INT32"));
+ when(iterator.next()).thenReturn(true, true, true, false);
+ when(iterator.getLong(1)).thenReturn(100L, 200L, 300L);
+ when(iterator.isNull(2)).thenReturn(false, true, false);
+ when(iterator.getString(2)).thenReturn("11", "33");
+ ITableSession session = mock(ITableSession.class);
+ when(session.executeQueryStatement(anyString(), anyLong())).thenReturn(dataSet);
+ ExportConfig config = config("csv", true);
+ try (ExportProgressReporter progress = progress();
+ ExportDataTable exporter = new ExportDataTable(config, progress, session)) {
+ exporter.export(
+ new ExportTask(
+ "select * from metrics", config.targetDirectory.resolve("dump0"), null, "metrics"));
+ }
+
+ assertEquals("time,value\r\n100,11\r\n200,\r\n", read("dump0_0.csv"));
+ assertEquals("time,value\r\n300,33\r\n", read("dump0_1.csv"));
+ verify(dataSet).close();
+ }
+
+ private SessionDataSet treeDataSet() throws Exception {
+ SessionDataSet dataSet = mock(SessionDataSet.class);
+ SessionDataSet.DataIterator iterator = mock(SessionDataSet.DataIterator.class);
+ when(dataSet.getColumnNames())
+ .thenReturn(new ArrayList<>(Arrays.asList("Time", "root.sg.d.s")));
+ when(dataSet.iterator()).thenReturn(iterator);
+ when(iterator.getColumnTypeList()).thenReturn(Arrays.asList("INT64", "INT32"));
+ AtomicInteger cursor = new AtomicInteger(-1);
+ when(iterator.next()).thenAnswer(call -> cursor.incrementAndGet() < 3);
+ when(iterator.getLong(1)).thenAnswer(call -> (cursor.get() + 1L) * 100);
+ when(iterator.isNull(2)).thenAnswer(call -> cursor.get() == 1);
+ when(iterator.getString(2))
+ .thenAnswer(call -> cursor.get() == 0 ? "11" : cursor.get() == 1 ? null : "33");
+ return dataSet;
+ }
+
+ private String read(String name) throws Exception {
+ return Files.readString(temporaryFolder.getRoot().toPath().resolve(name));
+ }
+
+ private ExportConfig config(String format, boolean table) throws Exception {
+ List<String> args =
+ new ArrayList<>(
+ Arrays.asList(
+ "-ft",
+ format,
+ "-t",
+ temporaryFolder.getRoot().toString(),
+ "-lpf",
+ "2",
+ "-tf",
+ "timestamp"));
+ if (table) {
+ args.addAll(Arrays.asList("-sql_dialect", "table", "-db", "db"));
+ }
+ return ExportConfig.parse(args.toArray(new String[0]), ZonedDateTime.now());
+ }
+
+ private ExportProgressReporter progress() {
+ return progress(new StringWriter());
+ }
+
+ private ExportProgressReporter progress(StringWriter log) {
+ return new ExportProgressReporter(
+ log, "tree", System.nanoTime(), new PrintStream(OutputStream.nullOutputStream()));
+ }
+
+ private ExportTask task(ExportConfig config, int index) {
+ return new ExportTask(
+ "select s from root.sg.d", config.targetDirectory.resolve("dump" + index), null, null);
+ }
+}
diff --git a/scripts/tools/README.md b/scripts/tools/README.md
index cd6cea184a1..3ed99f76594 100644
--- a/scripts/tools/README.md
+++ b/scripts/tools/README.md
@@ -154,3 +154,133 @@ tools/import-wal.sh \
The process exits with `0` after a successful import and `1` when argument parsing, replay, or
source-file handling fails.
+
+# ExportData
+
+`tools/export-data.sh` (Windows: `tools/windows/export-data.bat`) exports Tree or Table results
+as CSV, SQL or TsFile. Run `-help csv`, `-help sql` or `-help tsfile` for the format's options.
+
+## Tree measurement files
+
+```bash
+tools/export-data.sh -h 127.0.0.1 -p 6667 -u root -pw secret \
+ -ft csv -t ./export -mf ./measurements.txt \
+ -start_time 2026-10-14T00:00:00+08:00 -end_time 2026-10-14T23:59:59+08:00
+```
+
+`-mf/--measurement_file` accepts a UTF-8 file containing one IoTDB path pattern per line:
+
+```text
+# Exact paths and IoTDB patterns can be combined.
+root.factory.device01.temperature
+root.factory.*.pre*
+root.factory.**
+```
+
+A first-line BOM, blank lines, surrounding whitespace and lines beginning with `#` are accepted.
+The entire file is validated before any server query. Each distinct pattern is resolved with
+`SHOW TIMESERIES`; `*`, `**` and `pre*` use IoTDB path syntax. Actual measurements are merged and
+deduplicated in first-discovery order, then exported sequentially. Each unmatched pattern produces
+a warning that names the pattern and its line, such as
+`WARNING: No measurements matched root.factory.missing.* (measurement file line 5).`; the log also
+records the source file. The command fails if no actual measurements remain.
+`-mf` is available only in Tree mode and cannot be combined with `-q`.
+
+Each measurement uses its actual full path as the output filename. CSV and SQL are split by
+`-lpf/--lines_per_file` (default 10,000 rows); TsFile produces one file per measurement:
+
+```text
+export/root.factory.device01.temperature_0.csv
+export/root.factory.device01.temperature_1.csv
+export/root.factory.device01.temperature.tsfile
+```
+
+The existing `-pfn/--prefix_file_name` supplies an optional extra prefix in measurement-file mode:
+`-pfn backup` produces `backup_root.factory.device01.temperature_0.csv`. No default `dump` prefix
+is added in this mode, and `-f` is not an alias. Query and Table exports retain names such as
+`dump0_0.csv`, `dump0_0.sql` and `dump0.tsfile`. Characters that are unsafe in filenames are encoded
+as `%HH`. Oversized filenames and case/normalization collisions fail before exporting data.
+
+`-start_time` and `-end_time` form an inclusive time interval. Values may be integer timestamps in
+the server's precision or date-time strings interpreted in the session's timezone (`-tz`). A
+single bound is supported; a reversed or invalid interval fails before data queries. These bounds
+also apply to automatic Table exports. Queries, whether given with `-q` or at the Tree prompt, run
+as written: the bounds are ignored and reported as a `WARNING`.
+A measurement with no rows in the interval still counts as completed; CSV/SQL create no data file
+for it. SQL exports contain INSERT statements for reimport into compatible existing schema;
+TsFile retains measurement types, encoding, compression and device alignment.
+
+## Queries and Table exports
+
+```bash
+tools/export-data.sh -ft csv -t ./export -q 'SELECT * FROM root.factory.**'
+tools/export-data.sh -sql_dialect table -db factory -table readings \
+ -ft tsfile -t ./export -start_time 100 -end_time 200
+tools/export-data.sh -sql_dialect table -db factory \
+ -ft sql -t ./export -q 'SELECT * FROM readings WHERE time >= 100'
+```
+
+Tree mode prompts for a query when neither `-mf` nor `-q` is supplied. Table mode automatically
+exports the selected table or all tables in `-db`. Each query in `-q` writes its own files
+(`dump0_0.csv`, `dump1_0.csv`, ...). A Table query exported as SQL or TsFile is written to the table
+it reads: it must read a single table of `-db` without joins, subqueries or CTEs, and return the
+time column and original column names; TsFile queries must include all tags to preserve device
+identity. All queries are checked before the first one runs. Like the time bounds, `-table` does
+not apply to queries and is reported as a `WARNING`.
+
+SQL output quotes strings and dates in single quotes, doubling a quote inside them. A Tree query
+row that spans several devices becomes one SQL INSERT per device that has values in the row, and
+columns without a value are left out. These statements stay in the same file and count as one data
+row for `-lpf`, `exported_data_rows` and `file_rows`. `ALIGN BY DEVICE` and Table rows write `NULL`
+for a missing value. With `-dt true`, CSV headers in both models add each value column's type, such
+as `s1(DOUBLE)`.
+
+## Progress, logs and failures
+
+Every invocation writes a UTF-8 append log and reports progress every two seconds, including while
+waiting for queries. When Tree mode prompts for a query, logging and progress start after the query
+is entered. The default log path uses the command's startup time:
+`<target_dir>/log_export_data_yyyyMMddHHmmss.log`. Override it with `-l/--export_data_log <path>`.
+The output directory is created automatically; an overridden log's parent directory must exist.
+
+The console is for people. In a terminal one progress line is redrawn in place, cutting the current
+measurement from the left to fit the terminal width. When output goes to a file, `nohup` or a
+container log, or when the progress line would not fit the terminal, each update is a line of its
+own:
+
+```text
+Exporting 59% 121/204 3,535,481 rows 00:00:02 ETA 00:00:01 ...line19.s0
+[59%] 121/204 measurements, 3,535,481 rows, elapsed 00:00:02, ETA 00:00:01, current root.factory.line19.s0
+```
+
+The shell launcher passes the terminal width to the tool as `COLUMNS`. The Windows launcher does
+not, so 80 columns are assumed there unless `COLUMNS` is set.
+
+The console also shows the start, warnings and the log path, then ends with one summary line such
+as `Export completely! Rows: 5, files: 1, elapsed: 00:00:00.` Query and Table modes have no total,
+so they show rows, elapsed time and rows per second instead of a percentage and ETA. The launcher
+applies `conf/logback-tool.xml` when present, so library logging shows errors only.
+
+The log is for programs: JSON Lines, one event object per line, with counts as JSON numbers. Each
+event starts with `progress_id` (the operating system process ID), `timestamp` (ISO 8601 with
+offset), `model` (`tree` or `table`), `status` (`RUNNING`, `SUCCESS`, `FAILED` or `CANCELLED`),
+`exported_data_rows`, `elapsed_time` (`HH:mm:ss`) and `output`, the file being written (omitted
+until a task opens one), followed by `event`, `level`, `phase` and the event's details. Events
+include `START`, `DISCOVERY`, `WARNING`, `TASK_START`, `TASK_COMPLETE`, `FILE_START`,
+`FILE_COMPLETE`, `PROGRESS` and one `FINISH`. The log records source patterns, queries, output
+files, rows, completed files, elapsed time and errors; command credentials are excluded. For
+example, `jq -c 'select(.event == "FINISH")' export/log_export_data_*.log` prints each result.
+
+Measurement-file mode adds `exported_measurement_num`, `total_measurement_num`, `exported_percent`
+and `estimated_remaining` between `exported_data_rows` and `elapsed_time`. A measurement completes
+only after all output files and its query handle close successfully. `estimated_remaining` uses the
+average measurement duration (`HH:mm:ss`). Measurements differ in size, so it stays `UNKNOWN`, and
+the console shows `ETA --`, until 5% of the measurements, and at least three, have completed. No
+extra `COUNT(*)` scan is made, and no `-progress` option is needed.
+
+The command returns `0` on success and `1` for invalid arguments, input, queries, output or log
+errors; failures record `FAILED`. Ctrl+C or `SIGTERM` records `CANCELLED`. The export stops at the
+next row and closes its query and session; a server call that is still blocked after three seconds
+is closed. The process then exits with the signal's status (`130` for Ctrl+C, `143` for `SIGTERM`),
+which `FINISH` reports as `exit_code`. Already generated files are retained for inspection, and
+incomplete files are not marked `FILE_COMPLETE`.
diff --git a/scripts/tools/export-data.sh b/scripts/tools/export-data.sh
index d98d1363aa4..72405e62ee2 100644
--- a/scripts/tools/export-data.sh
+++ b/scripts/tools/export-data.sh
@@ -57,5 +57,21 @@ done
MAIN_CLASS=org.apache.iotdb.tool.data.ExportData
-"$JAVA" $JVM_OPTS -DIOTDB_HOME=${IOTDB_HOME} -cp "$CLASSPATH" "$MAIN_CLASS" "$@"
+# Library logs would interleave with the progress line; the tool config keeps errors only.
+LOG_CONFIG="${IOTDB_HOME}/conf/logback-tool.xml"
+if [ ! -f "$LOG_CONFIG" ]; then
+ LOG_CONFIG=
+fi
+
+# Shells do not export COLUMNS. The tool redraws the progress line in place only while it fits.
+if [ -t 1 ]; then
+ if [ -z "${COLUMNS:-}" ]; then
+ COLUMNS=$(stty size </dev/tty 2>/dev/null | awk '{print $2}')
+ fi
+ if [ -n "${COLUMNS:-}" ]; then
+ export COLUMNS
+ fi
+fi
+
+"$JAVA" $JVM_OPTS -DIOTDB_HOME=${IOTDB_HOME} ${LOG_CONFIG:+"-Dlogback.configurationFile=$LOG_CONFIG"} -cp "$CLASSPATH" "$MAIN_CLASS" "$@"
exit $?
\ No newline at end of file
diff --git a/scripts/tools/windows/export-data.bat b/scripts/tools/windows/export-data.bat
index 06d489a2d30..43b61b8f82b 100644
--- a/scripts/tools/windows/export-data.bat
+++ b/scripts/tools/windows/export-data.bat
@@ -42,9 +42,13 @@ set JAVA_OPTS=-ea^
@REM ***** CLASSPATH library setting *****
set CLASSPATH=%CLASSPATH%;"%IOTDB_HOME%\lib\*"
+@REM Library logs would interleave with the progress line; the tool config keeps errors only.
+set LOG_CONFIG_OPTS=
+if exist "%IOTDB_HOME%\conf\logback-tool.xml" set LOG_CONFIG_OPTS=-Dlogback.configurationFile="%IOTDB_HOME%\conf\logback-tool.xml"
+
REM -----------------------------------------------------------------------------
-"%JAVA_HOME%\bin\java" -DIOTDB_HOME="%IOTDB_HOME%" %JAVA_OPTS% -cp %CLASSPATH% %MAIN_CLASS% %*
+"%JAVA_HOME%\bin\java" -DIOTDB_HOME="%IOTDB_HOME%" %JAVA_OPTS% %LOG_CONFIG_OPTS% -cp %CLASSPATH% %MAIN_CLASS% %*
set ret_code=%ERRORLEVEL%
goto finally