This is an automated email from the ASF dual-hosted git repository. jt2594838 pushed a commit to branch support_wal_in_import_data in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 0f2d66b5ca0fa88641b8047c91cf9fd3459e16e3 Author: Tian Jiang <[email protected]> AuthorDate: Thu Aug 13 16:36:54 2026 +0800 ver1 --- .../apache/iotdb/db/i18n/ImportWALMessages.java | 80 ++ .../apache/iotdb/db/i18n/ImportWALMessages.java | 79 ++ .../storageengine/dataregion/wal/io/WALReader.java | 9 + .../java/org/apache/iotdb/db/tools/ImportWAL.java | 977 +++++++++++++++++++++ .../org/apache/iotdb/db/tools/ImportWALTest.java | 506 +++++++++++ scripts/tools/import-wal.sh | 52 ++ scripts/tools/windows/import-wal.bat | 43 + 7 files changed, 1746 insertions(+) diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java new file mode 100644 index 00000000000..2ed53656ec0 --- /dev/null +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java @@ -0,0 +1,80 @@ +/* + * 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.db.i18n; + +/** Compile-time i18n constants for the WAL import tool (English). */ +public final class ImportWALMessages { + + public static final String MESSAGE_IMPORT_WAL_5E42804E = "import-wal"; + public static final String + MESSAGE_PATH_OF_A_WAL_FILE_OR_A_DIRECTORY_CONTAINING_WAL_FILES_473D0554 = + "Path of a WAL file or a directory containing WAL files."; + public static final String MESSAGE_TARGET_IOTDB_HOST_DEFAULT_127_0_0_1_3729156F = + "Target IoTDB host. Default: 127.0.0.1."; + public static final String MESSAGE_TARGET_IOTDB_RPC_PORT_DEFAULT_6667_FC0D345D = + "Target IoTDB RPC port. Default: 6667."; + public static final String MESSAGE_TARGET_IOTDB_USERNAME_DEFAULT_ROOT_EB91453B = + "Target IoTDB username. Default: root."; + public static final String + MESSAGE_TARGET_IOTDB_PASSWORD_PROMPTED_INTERACTIVELY_IF_OMITTED_29681961 = + "Target IoTDB password. Prompted interactively if omitted."; + public static final String MESSAGE_PASSWORD_PROMPT_F2D0E794 = "Password: "; + public static final String MESSAGE_TARGET_DATABASE_FOR_TABLE_MODEL_WAL_ENTRIES_27BACD1C = + "Target database for table-model WAL entries."; + public static final String MESSAGE_PRINT_THIS_HELP_MESSAGE_E800AF7A = + "Print this help message."; + public static final String MESSAGE_ARGUMENT_ERROR_ARG_A9767F62 = "Argument error: %s"; + public static final String MESSAGE_WAL_IMPORT_FAILED_ARG_55C014BA = "WAL import failed: %s"; + public static final String EXCEPTION_SOURCE_PATH_DOES_NOT_EXIST_ARG_7C806CA2 = + "Source path does not exist: %s"; + public static final String EXCEPTION_SOURCE_FILE_IS_NOT_A_WAL_FILE_ARG_14A43F76 = + "Source file is not a WAL file: %s"; + public static final String EXCEPTION_NO_WAL_FILES_FOUND_UNDER_ARG_45F7FA22 = + "No WAL files found under: %s"; + public static final String EXCEPTION_INVALID_PORT_ARG_A7CDD5AC = "Invalid port: %s"; + public static final String + MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A = + "Replayed %d operations from %d WAL files; skipped %d entries."; + public static final String + MESSAGE_PROGRESS_ARG_COMPLETED_FILES_ARG_TOTAL_FILES_ARG_PROCESSED_BYTES_ARG_TOTAL_BYTES_ARG_PERCENT_ARG_ELAPSED_SECONDS_ARG_RATE_ARG_MB_PER_SECOND_F1C1356F = + "Progress: %d/%d WAL files completed, %d/%d bytes (%.1f%%), elapsed %.1f s, rate %.1f MB/s."; + public static final String + MESSAGE_IMPORT_DURATION_ARG_SECONDS_TOTAL_SIZE_ARG_BYTES_AVERAGE_RATE_ARG_MB_PER_SECOND_4B4EA58D = + "Import duration: %.1f s; total size: %d bytes; average rate: %.1f MB/s."; + public static final String EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9 = + "Failed to replay WAL file %s at offset %d: %s"; + public static final String EXCEPTION_TABLE_MODEL_WAL_ENTRIES_REQUIRE_DB_DATABASE_F7597726 = + "Table-model WAL entries require -db/--database."; + public static final String EXCEPTION_UNSUPPORTED_WAL_OPERATION_ARG_ABD227A0 = + "Unsupported WAL operation: %s"; + public static final String MESSAGE_UNSUPPORTED_WAL_OPERATION_ARG_SKIP_THIS_ENTRY_Y_N_DAFBE650 = + "Unsupported WAL operation: %s. Skip this entry? [y/N]: "; + public static final String EXCEPTION_INSERT_NODE_ARG_CONTAINS_NO_REPLAYABLE_DATA_5DA13453 = + "Insert node %s contains no replayable data."; + public static final String EXCEPTION_UNSUPPORTED_SNAPSHOT_DATA_TYPE_ARG_7A32D312 = + "Unsupported snapshot data type: %s"; + public static final String EXCEPTION_THE_WAL_FILE_IS_TRUNCATED_OR_CORRUPTED_6B0734C5 = + "The WAL file is truncated or corrupted."; + public static final String + EXCEPTION_PASSWORD_WAS_NOT_PROVIDED_AND_INTERACTIVE_INPUT_IS_UNAVAILABLE_40F42BCD = + "Password was not provided and interactive input is unavailable. Specify -pw/--password."; + + private ImportWALMessages() {} +} diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java new file mode 100644 index 00000000000..c8dc2266dc8 --- /dev/null +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java @@ -0,0 +1,79 @@ +/* + * 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.db.i18n; + +/** WAL 导入工具的编译期国际化常量(中文)。 */ +public final class ImportWALMessages { + + public static final String MESSAGE_IMPORT_WAL_5E42804E = "import-wal"; + public static final String + MESSAGE_PATH_OF_A_WAL_FILE_OR_A_DIRECTORY_CONTAINING_WAL_FILES_473D0554 = + "WAL 文件或包含 WAL 文件的目录路径。"; + public static final String MESSAGE_TARGET_IOTDB_HOST_DEFAULT_127_0_0_1_3729156F = + "目标 IoTDB 主机。默认:127.0.0.1。"; + public static final String MESSAGE_TARGET_IOTDB_RPC_PORT_DEFAULT_6667_FC0D345D = + "目标 IoTDB RPC 端口。默认:6667。"; + public static final String MESSAGE_TARGET_IOTDB_USERNAME_DEFAULT_ROOT_EB91453B = + "目标 IoTDB 用户名。默认:root。"; + public static final String + MESSAGE_TARGET_IOTDB_PASSWORD_PROMPTED_INTERACTIVELY_IF_OMITTED_29681961 = + "目标 IoTDB 密码。未提供时将交互式询问。"; + public static final String MESSAGE_PASSWORD_PROMPT_F2D0E794 = "密码:"; + public static final String MESSAGE_TARGET_DATABASE_FOR_TABLE_MODEL_WAL_ENTRIES_27BACD1C = + "表模型 WAL 条目的目标数据库。"; + public static final String MESSAGE_PRINT_THIS_HELP_MESSAGE_E800AF7A = "打印帮助信息。"; + public static final String MESSAGE_ARGUMENT_ERROR_ARG_A9767F62 = "参数错误:%s"; + public static final String MESSAGE_WAL_IMPORT_FAILED_ARG_55C014BA = "WAL 导入失败:%s"; + public static final String EXCEPTION_SOURCE_PATH_DOES_NOT_EXIST_ARG_7C806CA2 = + "源路径不存在:%s"; + public static final String EXCEPTION_SOURCE_FILE_IS_NOT_A_WAL_FILE_ARG_14A43F76 = + "源文件不是 WAL 文件:%s"; + public static final String EXCEPTION_NO_WAL_FILES_FOUND_UNDER_ARG_45F7FA22 = + "路径下未找到 WAL 文件:%s"; + public static final String EXCEPTION_INVALID_PORT_ARG_A7CDD5AC = "无效端口:%s"; + public static final String + MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A = + "已重放 %d 个操作(来自 %d 个 WAL 文件);跳过 %d 个条目。"; + public static final String + MESSAGE_PROGRESS_ARG_COMPLETED_FILES_ARG_TOTAL_FILES_ARG_PROCESSED_BYTES_ARG_TOTAL_BYTES_ARG_PERCENT_ARG_ELAPSED_SECONDS_ARG_RATE_ARG_MB_PER_SECOND_F1C1356F = + "进度:已完成 %d/%d 个 WAL 文件,已处理 %d/%d 字节(%.1f%%),耗时 %.1f 秒,速率 %.1f MB/s。"; + public static final String + MESSAGE_IMPORT_DURATION_ARG_SECONDS_TOTAL_SIZE_ARG_BYTES_AVERAGE_RATE_ARG_MB_PER_SECOND_4B4EA58D = + "导入耗时:%.1f 秒;文件总大小:%d 字节;平均速率:%.1f MB/s。"; + public static final String EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9 = + "重放 WAL 文件 %s 时失败,偏移量 %d:%s"; + public static final String EXCEPTION_TABLE_MODEL_WAL_ENTRIES_REQUIRE_DB_DATABASE_F7597726 = + "表模型 WAL 条目要求指定 -db/--database。"; + public static final String EXCEPTION_UNSUPPORTED_WAL_OPERATION_ARG_ABD227A0 = + "不支持的 WAL 操作:%s"; + public static final String MESSAGE_UNSUPPORTED_WAL_OPERATION_ARG_SKIP_THIS_ENTRY_Y_N_DAFBE650 = + "不支持的 WAL 操作:%s。是否跳过此条目?[y/N]:"; + public static final String EXCEPTION_INSERT_NODE_ARG_CONTAINS_NO_REPLAYABLE_DATA_5DA13453 = + "Insert node %s 不包含可重放数据。"; + public static final String EXCEPTION_UNSUPPORTED_SNAPSHOT_DATA_TYPE_ARG_7A32D312 = + "Unsupported snapshot data type: %s"; + public static final String EXCEPTION_THE_WAL_FILE_IS_TRUNCATED_OR_CORRUPTED_6B0734C5 = + "WAL 文件被截断或已损坏。"; + public static final String + EXCEPTION_PASSWORD_WAS_NOT_PROVIDED_AND_INTERACTIVE_INPUT_IS_UNAVAILABLE_40F42BCD = + "未提供密码且当前环境不支持交互式输入,请指定 -pw/--password。"; + + private ImportWALMessages() {} +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java index befcf58c632..0a2c994eb54 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALReader.java @@ -72,6 +72,11 @@ public class WALReader implements Closeable { return false; } try { + // An active WAL has no end marker until its writer closes. Reaching EOF exactly between + // entries is therefore valid, while EOF during deserialization still marks a partial entry. + if (walInputStream.available() == 0) { + return false; + } nextEntry = WALEntry.deserialize(logStream); if (nextEntry.getType() == WALEntryType.WAL_FILE_INFO_END_MARKER) { nextEntry = null; @@ -98,6 +103,10 @@ public class WALReader implements Closeable { return walInputStream.getFileCurrentPos(); } + public boolean isFileCorrupted() { + return fileCorrupted; + } + /** * Like {@link Iterator#next()}. * diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java new file mode 100644 index 00000000000..f8316499bdb --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java @@ -0,0 +1,977 @@ +/* + * 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.db.tools; + +import org.apache.iotdb.commons.path.MeasurementPath; +import org.apache.iotdb.db.i18n.ImportWALMessages; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.ObjectNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowsNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; +import org.apache.iotdb.db.storageengine.dataregion.memtable.AlignedWritableMemChunk; +import org.apache.iotdb.db.storageengine.dataregion.memtable.IMemTable; +import org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunk; +import org.apache.iotdb.db.storageengine.dataregion.memtable.IWritableMemChunkGroup; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType; +import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALReader; +import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils; +import org.apache.iotdb.db.subscription.broker.consensus.ConsensusLogToTabletConverter; +import org.apache.iotdb.db.subscription.columnfilter.ColumnFilterMatcher; +import org.apache.iotdb.db.utils.datastructure.AlignedTVList; +import org.apache.iotdb.db.utils.datastructure.TVList; +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.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.IDeviceID; +import org.apache.tsfile.utils.Binary; +import org.apache.tsfile.utils.BitMap; +import org.apache.tsfile.utils.DateUtils; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; + +import java.io.Console; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ImportWAL { + + private static final int CODE_OK = 0; + private static final int CODE_ERROR = 1; + private static final String DEFAULT_HOST = "127.0.0.1"; + private static final int DEFAULT_PORT = 6667; + private static final String DEFAULT_USER = "root"; + private static final int SNAPSHOT_TABLET_ROW_LIMIT = 1024; + + private ImportWAL() {} + + public static void main(final String[] args) { + System.exit(run(args, System.out, System.err)); + } + + static int run(final String[] args, final PrintStream out, final PrintStream err) { + final Options options = createOptions(); + if (containsHelpOption(args)) { + printHelp(options, out); + return CODE_OK; + } + + final CommandLine commandLine; + try { + commandLine = new DefaultParser().parse(options, args); + } catch (final ParseException e) { + err.printf(ImportWALMessages.MESSAGE_ARGUMENT_ERROR_ARG_A9767F62, e.getMessage()); + err.println(); + printHelp(options, err); + return CODE_ERROR; + } + + try { + final Path source = Paths.get(commandLine.getOptionValue("file")); + final List<Path> walFiles = collectWALFiles(source); + final String database = commandLine.getOptionValue("database"); + final String password = getPassword(commandLine); + final Session treeSession = + createSession( + commandLine.getOptionValue("host", DEFAULT_HOST), + parsePort(commandLine.getOptionValue("port", String.valueOf(DEFAULT_PORT))), + commandLine.getOptionValue("username", DEFAULT_USER), + password, + null); + final Session tableSession = + database == null + ? null + : createSession( + commandLine.getOptionValue("host", DEFAULT_HOST), + parsePort(commandLine.getOptionValue("port", String.valueOf(DEFAULT_PORT))), + commandLine.getOptionValue("username", DEFAULT_USER), + password, + database); + try { + treeSession.open(false); + if (tableSession != null) { + tableSession.open(false); + } + final ReplayStatistics statistics = + replayWALFiles(walFiles, new WALReplayer(treeSession, tableSession, database), out); + out.printf( + ImportWALMessages + .MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A, + statistics.replayedOperationCount, + walFiles.size(), + statistics.skippedEntryCount); + out.println(); + out.printf( + ImportWALMessages + .MESSAGE_IMPORT_DURATION_ARG_SECONDS_TOTAL_SIZE_ARG_BYTES_AVERAGE_RATE_ARG_MB_PER_SECOND_4B4EA58D, + statistics.getElapsedSeconds(), + statistics.getTotalBytes(), + statistics.getAverageRateMbPerSecond()); + out.println(); + } finally { + closeSession(tableSession); + closeSession(treeSession); + } + return CODE_OK; + } catch (final Exception e) { + err.printf(ImportWALMessages.MESSAGE_WAL_IMPORT_FAILED_ARG_55C014BA, e.getMessage()); + err.println(); + return CODE_ERROR; + } + } + + private static Options createOptions() { + final Options options = new Options(); + options.addOption( + Option.builder("f") + .longOpt("file") + .hasArg() + .required() + .desc( + ImportWALMessages + .MESSAGE_PATH_OF_A_WAL_FILE_OR_A_DIRECTORY_CONTAINING_WAL_FILES_473D0554) + .build()); + options.addOption( + Option.builder("h") + .longOpt("host") + .hasArg() + .desc(ImportWALMessages.MESSAGE_TARGET_IOTDB_HOST_DEFAULT_127_0_0_1_3729156F) + .build()); + options.addOption( + Option.builder("p") + .longOpt("port") + .hasArg() + .desc(ImportWALMessages.MESSAGE_TARGET_IOTDB_RPC_PORT_DEFAULT_6667_FC0D345D) + .build()); + options.addOption( + Option.builder("u") + .longOpt("username") + .hasArg() + .desc(ImportWALMessages.MESSAGE_TARGET_IOTDB_USERNAME_DEFAULT_ROOT_EB91453B) + .build()); + options.addOption( + Option.builder("pw") + .longOpt("password") + .hasArg() + .desc( + ImportWALMessages + .MESSAGE_TARGET_IOTDB_PASSWORD_PROMPTED_INTERACTIVELY_IF_OMITTED_29681961) + .build()); + options.addOption( + Option.builder("db") + .longOpt("database") + .hasArg() + .desc(ImportWALMessages.MESSAGE_TARGET_DATABASE_FOR_TABLE_MODEL_WAL_ENTRIES_27BACD1C) + .build()); + options.addOption( + Option.builder() + .longOpt("help") + .desc(ImportWALMessages.MESSAGE_PRINT_THIS_HELP_MESSAGE_E800AF7A) + .build()); + return options; + } + + private static String getPassword(final CommandLine commandLine) { + return getPassword(commandLine, System.console()); + } + + static String getPassword(final CommandLine commandLine, final Console console) { + if (commandLine.hasOption("password")) { + return commandLine.getOptionValue("password"); + } + if (console == null) { + throw new IllegalArgumentException( + ImportWALMessages + .EXCEPTION_PASSWORD_WAS_NOT_PROVIDED_AND_INTERACTIVE_INPUT_IS_UNAVAILABLE_40F42BCD); + } + final char[] password = + console.readPassword(ImportWALMessages.MESSAGE_PASSWORD_PROMPT_F2D0E794); + if (password == null) { + throw new IllegalArgumentException( + ImportWALMessages + .EXCEPTION_PASSWORD_WAS_NOT_PROVIDED_AND_INTERACTIVE_INPUT_IS_UNAVAILABLE_40F42BCD); + } + return new String(password); + } + + private static boolean containsHelpOption(final String[] args) { + if (args == null) { + return false; + } + for (final String arg : args) { + if ("--help".equals(arg) || "-help".equals(arg)) { + return true; + } + } + return false; + } + + private static void printHelp(final Options options, final PrintStream stream) { + final HelpFormatter formatter = new HelpFormatter(); + formatter.setWidth(120); + formatter.printHelp( + new PrintWriter(stream, true), + 120, + ImportWALMessages.MESSAGE_IMPORT_WAL_5E42804E, + null, + options, + 2, + 2, + null, + true); + } + + private static int parsePort(final String port) { + try { + final int value = Integer.parseInt(port); + if (value <= 0 || value > 65535) { + throw new NumberFormatException(port); + } + return value; + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + String.format(ImportWALMessages.EXCEPTION_INVALID_PORT_ARG_A7CDD5AC, port), e); + } + } + + private static Session createSession( + final String host, + final int port, + final String username, + final String password, + final String database) { + final Session.Builder builder = + new Session.Builder().host(host).port(port).username(username).password(password); + if (database != null) { + builder.sqlDialect("table").database(database); + } + return builder.build(); + } + + private static void closeSession(final Session session) { + if (session == null) { + return; + } + try { + session.close(); + } catch (final IoTDBConnectionException ignored) { + // The import result has already been determined; closing failure must not hide it. + } + } + + static List<Path> collectWALFiles(final Path source) throws IOException { + if (!Files.exists(source)) { + throw new IOException( + String.format( + ImportWALMessages.EXCEPTION_SOURCE_PATH_DOES_NOT_EXIST_ARG_7C806CA2, source)); + } + if (Files.isRegularFile(source)) { + if (!isWALFile(source)) { + throw new IOException( + String.format( + ImportWALMessages.EXCEPTION_SOURCE_FILE_IS_NOT_A_WAL_FILE_ARG_14A43F76, source)); + } + return List.of(source.toAbsolutePath().normalize()); + } + if (!Files.isDirectory(source)) { + throw new IOException( + String.format( + ImportWALMessages.EXCEPTION_SOURCE_PATH_DOES_NOT_EXIST_ARG_7C806CA2, source)); + } + + final List<Path> walFiles; + try (Stream<Path> stream = Files.walk(source)) { + walFiles = + stream + .filter(Files::isRegularFile) + .filter(ImportWAL::isWALFile) + .map(path -> path.toAbsolutePath().normalize()) + .sorted(WAL_FILE_COMPARATOR) + .collect(Collectors.toList()); + } + if (walFiles.isEmpty()) { + throw new IOException( + String.format(ImportWALMessages.EXCEPTION_NO_WAL_FILES_FOUND_UNDER_ARG_45F7FA22, source)); + } + return walFiles; + } + + private static boolean isWALFile(final Path path) { + return path.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".wal"); + } + + private static final Comparator<Path> WAL_FILE_COMPARATOR = + Comparator.comparing((Path path) -> Objects.toString(path.getParent(), "")) + .thenComparingLong(ImportWAL::getWALVersion) + .thenComparing(path -> path.getFileName().toString()); + + private static long getWALVersion(final Path path) { + final String filename = path.getFileName().toString(); + return WALFileUtils.WAL_FILE_NAME_PATTERN.matcher(filename).find() + ? WALFileUtils.parseVersionId(filename) + : Long.MAX_VALUE; + } + + static ReplayStatistics replayWALFiles(final List<Path> walFiles, final WALReplayer replayer) + throws IOException { + return replayWALFiles(walFiles, replayer, null); + } + + static ReplayStatistics replayWALFiles( + final List<Path> walFiles, final WALReplayer replayer, final PrintStream progressStream) + throws IOException { + final ReplayStatistics statistics = new ReplayStatistics(); + final long startNanos = System.nanoTime(); + for (final Path walFile : walFiles) { + statistics.totalBytes += Files.size(walFile); + } + for (final Path walFile : walFiles) { + try (WALReader reader = new WALReader(walFile.toFile())) { + long offset = reader.getWALCurrentReadOffset(); + while (reader.hasNext()) { + final WALEntry entry = reader.next(); + try { + if (replayer.replay(entry)) { + statistics.replayedOperationCount++; + } else { + statistics.skippedEntryCount++; + } + } catch (final IoTDBConnectionException | StatementExecutionException e) { + throw new WALReplayException( + String.format( + ImportWALMessages + .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, + walFile, + offset, + e.getMessage()), + e); + } + offset = reader.getWALCurrentReadOffset(); + } + if (reader.isFileCorrupted()) { + throw new WALReplayException( + String.format( + ImportWALMessages + .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, + walFile, + reader.getWALCurrentReadOffset(), + ImportWALMessages.EXCEPTION_THE_WAL_FILE_IS_TRUNCATED_OR_CORRUPTED_6B0734C5), + null); + } + statistics.completedFileCount++; + statistics.processedBytes += Files.size(walFile); + statistics.elapsedNanos = System.nanoTime() - startNanos; + if (progressStream != null) { + progressStream.printf( + ImportWALMessages + .MESSAGE_PROGRESS_ARG_COMPLETED_FILES_ARG_TOTAL_FILES_ARG_PROCESSED_BYTES_ARG_TOTAL_BYTES_ARG_PERCENT_ARG_ELAPSED_SECONDS_ARG_RATE_ARG_MB_PER_SECOND_F1C1356F, + statistics.completedFileCount, + walFiles.size(), + statistics.processedBytes, + statistics.totalBytes, + statistics.getProgressPercent(), + statistics.getElapsedSeconds(), + statistics.getAverageRateMbPerSecond()); + progressStream.println(); + } + } catch (final IOException e) { + if (e instanceof WALReplayException walReplayException) { + throw walReplayException; + } + throw new WALReplayException( + String.format( + ImportWALMessages + .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, + walFile, + 0, + e.getMessage()), + e); + } + } + return statistics; + } + + static class WALReplayer { + + private final Session treeSession; + private final Session tableSession; + private final ConsensusLogToTabletConverter converter; + private final UnsupportedEntryPrompt unsupportedEntryPrompt; + private final Map<String, List<IMeasurementSchema>> tableTagSchemas = new HashMap<>(); + + WALReplayer( + final Session treeSession, final Session tableSession, final String tableDatabaseName) { + this( + treeSession, + tableSession, + tableDatabaseName, + createUnsupportedEntryPrompt(System.console())); + } + + WALReplayer( + final Session treeSession, + final Session tableSession, + final String tableDatabaseName, + final UnsupportedEntryPrompt unsupportedEntryPrompt) { + this.treeSession = treeSession; + this.tableSession = tableSession; + this.unsupportedEntryPrompt = unsupportedEntryPrompt; + converter = + new ConsensusLogToTabletConverter( + null, null, ColumnFilterMatcher.matchAll(), tableDatabaseName); + } + + boolean replay(final WALEntry entry) + throws IoTDBConnectionException, StatementExecutionException { + if (entry.getType() == WALEntryType.MEMORY_TABLE_SNAPSHOT + || entry.getType() == WALEntryType.OLD_MEMORY_TABLE_SNAPSHOT) { + return replayMemTableSnapshot((IMemTable) entry.getValue()); + } + if (entry.getValue() instanceof InsertNode insertNode) { + replayInsert(insertNode); + return true; + } + if (entry.getValue() instanceof DeleteDataNode deleteDataNode) { + replayTreeDelete(deleteDataNode); + return true; + } + if (entry.getValue() instanceof RelationalDeleteDataNode + || entry.getValue() instanceof ObjectNode) { + // A null prompt means no interactive console is available, so preserve fail-fast behavior. + if (unsupportedEntryPrompt != null && unsupportedEntryPrompt.shouldSkip(entry)) { + return false; + } + throw unsupportedOperation(entry); + } + return false; + } + + private static UnsupportedEntryPrompt createUnsupportedEntryPrompt(final Console console) { + if (console == null) { + return null; + } + return entry -> { + final String answer = + console.readLine( + ImportWALMessages + .MESSAGE_UNSUPPORTED_WAL_OPERATION_ARG_SKIP_THIS_ENTRY_Y_N_DAFBE650, + entry.getType()); + return isSkipConfirmation(answer); + }; + } + + static boolean isSkipConfirmation(final String answer) { + return answer != null + && ("y".equalsIgnoreCase(answer.trim()) || "yes".equalsIgnoreCase(answer.trim())); + } + + @FunctionalInterface + interface UnsupportedEntryPrompt { + + boolean shouldSkip(WALEntry entry); + } + + private static StatementExecutionException unsupportedOperation(final WALEntry entry) { + return new StatementExecutionException( + String.format( + ImportWALMessages.EXCEPTION_UNSUPPORTED_WAL_OPERATION_ARG_ABD227A0, entry.getType())); + } + + private void replayInsert(final InsertNode node) + throws IoTDBConnectionException, StatementExecutionException { + if (node instanceof InsertRowsNode insertRowsNode + && !(node instanceof RelationalInsertRowsNode)) { + for (final InsertNode rowNode : insertRowsNode.getInsertRowNodeList()) { + replayInsert(rowNode); + } + return; + } + final List<Tablet> tablets = converter.convert(node); + if (tablets.isEmpty()) { + throw new StatementExecutionException( + String.format( + ImportWALMessages.EXCEPTION_INSERT_NODE_ARG_CONTAINS_NO_REPLAYABLE_DATA_5DA13453, + node.getType())); + } + final boolean tableModel = isTableModelInsert(node); + if (tableModel && tableSession == null) { + throw new StatementExecutionException( + ImportWALMessages.EXCEPTION_TABLE_MODEL_WAL_ENTRIES_REQUIRE_DB_DATABASE_F7597726); + } + for (final Tablet tablet : tablets) { + if (tableModel) { + tableSession.insertRelationalTablet(tablet); + } else if (node.isAligned()) { + treeSession.insertAlignedTablet(tablet); + } else { + treeSession.insertTablet(tablet); + } + } + } + + private static boolean isTableModelInsert(final InsertNode node) { + return node instanceof RelationalInsertRowNode + || node instanceof RelationalInsertRowsNode + || node instanceof RelationalInsertTabletNode; + } + + private void replayTreeDelete(final DeleteDataNode node) + throws IoTDBConnectionException, StatementExecutionException { + final List<String> paths = new ArrayList<>(node.getPathList().size()); + for (final MeasurementPath path : node.getPathList()) { + paths.add(path.getFullPath()); + } + treeSession.deleteData(paths, node.getDeleteStartTime(), node.getDeleteEndTime()); + } + + private boolean replayMemTableSnapshot(final IMemTable memTable) + throws IoTDBConnectionException, StatementExecutionException { + if (memTable == null || memTable.isSignalMemTable()) { + return false; + } + boolean replayed = false; + for (Map.Entry<IDeviceID, IWritableMemChunkGroup> deviceEntry : + memTable.getMemTableMap().entrySet()) { + final IDeviceID deviceId = deviceEntry.getKey(); + final IWritableMemChunkGroup group = deviceEntry.getValue(); + for (IWritableMemChunk chunk : group.getMemChunkMap().values()) { + if (chunk == null || chunk.isEmpty()) { + continue; + } + if (chunk instanceof AlignedWritableMemChunk) { + replayed |= replayAlignedMemChunk(deviceId, (AlignedWritableMemChunk) chunk); + } else { + replayed |= replayNonAlignedMemChunk(deviceId, chunk); + } + } + } + return replayed; + } + + private boolean replayNonAlignedMemChunk( + final IDeviceID deviceId, final IWritableMemChunk chunk) + throws IoTDBConnectionException, StatementExecutionException { + final List<IMeasurementSchema> schemas = Collections.singletonList(chunk.getSchema()); + final boolean tableModel = deviceId.isTableModel(); + requireTableSessionIfNeeded(tableModel); + final TableTabletSchema tabletSchema = createTableTabletSchema(deviceId, schemas); + final List<TVList> lists = new ArrayList<>(); + lists.addAll(chunk.getSortedList()); + lists.add(chunk.getWorkingTVList()); + boolean replayed = false; + for (TVList list : lists) { + if (list == null || list.rowCount() == 0) { + continue; + } + if (!list.isSorted()) { + list.sort(); + } + for (int start = 0; start < list.rowCount(); start += SNAPSHOT_TABLET_ROW_LIMIT) { + final int end = Math.min(start + SNAPSHOT_TABLET_ROW_LIMIT, list.rowCount()); + final Tablet tablet = + buildNonAlignedTablet(deviceId, tabletSchema, schemas, list, start, end); + sendTablet(tablet, tableModel, false); + replayed = true; + } + } + return replayed; + } + + private boolean replayAlignedMemChunk( + final IDeviceID deviceId, final AlignedWritableMemChunk chunk) + throws IoTDBConnectionException, StatementExecutionException { + final boolean tableModel = deviceId.isTableModel(); + requireTableSessionIfNeeded(tableModel); + final List<IMeasurementSchema> schemas = chunk.getSchemaList(); + final TableTabletSchema tabletSchema = createTableTabletSchema(deviceId, schemas); + final List<AlignedTVList> lists = new ArrayList<>(); + lists.addAll(chunk.getSortedList()); + lists.add(chunk.getWorkingTVList()); + boolean replayed = false; + for (AlignedTVList list : lists) { + if (list == null || list.rowCount() == 0) { + continue; + } + if (!list.isSorted()) { + list.sort(); + } + final List<Integer> replayableRows = getReplayableAlignedRows(list); + for (int start = 0; start < replayableRows.size(); start += SNAPSHOT_TABLET_ROW_LIMIT) { + final int end = Math.min(start + SNAPSHOT_TABLET_ROW_LIMIT, replayableRows.size()); + final Tablet tablet = + buildAlignedTablet(deviceId, tabletSchema, schemas, list, replayableRows, start, end); + sendTablet(tablet, tableModel, true); + replayed = true; + } + } + return replayed; + } + + private static List<Integer> getReplayableAlignedRows(final AlignedTVList list) { + final List<Integer> replayableRows = new ArrayList<>(list.rowCount()); + for (int row = 0; row < list.rowCount(); row++) { + if (!list.isTimeDeleted(row)) { + replayableRows.add(row); + } + } + return replayableRows; + } + + private void requireTableSessionIfNeeded(final boolean tableModel) + throws StatementExecutionException { + if (tableModel && tableSession == null) { + throw new StatementExecutionException( + ImportWALMessages.EXCEPTION_TABLE_MODEL_WAL_ENTRIES_REQUIRE_DB_DATABASE_F7597726); + } + } + + private TableTabletSchema createTableTabletSchema( + final IDeviceID deviceId, final List<IMeasurementSchema> fieldSchemas) + throws IoTDBConnectionException, StatementExecutionException { + if (!deviceId.isTableModel()) { + return new TableTabletSchema(fieldSchemas, null, 0); + } + final String tableName = deviceId.getTableName(); + List<IMeasurementSchema> tagSchemas = tableTagSchemas.get(tableName); + if (tagSchemas == null) { + tagSchemas = new ArrayList<>(); + try (SessionDataSet dataSet = + tableSession.executeQueryStatement("DESCRIBE " + quoteIdentifier(tableName))) { + final SessionDataSet.DataIterator iterator = dataSet.iterator(); + while (iterator.next()) { + final String category = iterator.getString(3); + if ("TAG".equalsIgnoreCase(category)) { + tagSchemas.add( + new MeasurementSchema( + iterator.getString(1), TSDataType.valueOf(iterator.getString(2)))); + } + } + } + tableTagSchemas.put(tableName, tagSchemas); + } + final List<IMeasurementSchema> schemas = + new ArrayList<>(tagSchemas.size() + fieldSchemas.size()); + schemas.addAll(tagSchemas); + schemas.addAll(fieldSchemas); + final List<ColumnCategory> categories = new ArrayList<>(schemas.size()); + categories.addAll(Collections.nCopies(tagSchemas.size(), ColumnCategory.TAG)); + categories.addAll(Collections.nCopies(fieldSchemas.size(), ColumnCategory.FIELD)); + return new TableTabletSchema(schemas, categories, tagSchemas.size()); + } + + static String quoteIdentifier(final String identifier) { + return "\"" + identifier.replace("\"", "\"\"") + "\""; + } + + private void sendTablet(final Tablet tablet, final boolean tableModel, final boolean aligned) + throws IoTDBConnectionException, StatementExecutionException { + if (tableModel) { + tableSession.insertRelationalTablet(tablet); + } else if (aligned) { + treeSession.insertAlignedTablet(tablet); + } else { + treeSession.insertTablet(tablet); + } + } + + private static Tablet buildNonAlignedTablet( + final IDeviceID deviceId, + final TableTabletSchema tabletSchema, + final List<IMeasurementSchema> sourceSchemas, + final TVList list, + final int start, + final int end) { + final int rowCount = end - start; + final long[] times = new long[rowCount]; + final Object[] values = createValueArrays(tabletSchema.schemas, rowCount); + final BitMap[] bitMaps = new BitMap[tabletSchema.schemas.size()]; + final int fieldColumnIndex = tabletSchema.tagCount; + final TSDataType type = sourceSchemas.get(0).getType(); + for (int i = 0; i < rowCount; i++) { + final int scanIndex = start + i; + final int valueIndex = list.getValueIndex(scanIndex); + times[i] = list.getTime(scanIndex); + if (list.isNullValue(valueIndex)) { + if (bitMaps[fieldColumnIndex] == null) { + bitMaps[fieldColumnIndex] = new BitMap(rowCount); + } + bitMaps[fieldColumnIndex].mark(i); + } else { + putValue(values[fieldColumnIndex], i, type, list, scanIndex); + } + } + populateTableTags(deviceId, tabletSchema.categories, values, bitMaps, rowCount); + return tabletSchema.categories == null + ? new Tablet(deviceId.toString(), tabletSchema.schemas, times, values, bitMaps, rowCount) + : new Tablet( + deviceId.getTableName(), + tabletSchema.schemas, + tabletSchema.categories, + times, + values, + bitMaps, + rowCount); + } + + private static Tablet buildAlignedTablet( + final IDeviceID deviceId, + final TableTabletSchema tabletSchema, + final List<IMeasurementSchema> sourceSchemas, + final AlignedTVList list, + final List<Integer> replayableRows, + final int start, + final int end) { + final int rowCount = end - start; + final long[] times = new long[rowCount]; + final Object[] values = createValueArrays(tabletSchema.schemas, rowCount); + final BitMap[] bitMaps = new BitMap[tabletSchema.schemas.size()]; + final List<TSDataType> types = list.getTsDataTypes(); + for (int i = 0; i < rowCount; i++) { + final int scanIndex = replayableRows.get(start + i); + final int valueIndex = list.getValueIndex(scanIndex); + times[i] = list.getTime(scanIndex); + for (int c = 0; c < sourceSchemas.size(); c++) { + final int targetColumnIndex = tabletSchema.tagCount + c; + if (c >= types.size() || list.isNullValue(valueIndex, c)) { + if (bitMaps[targetColumnIndex] == null) { + bitMaps[targetColumnIndex] = new BitMap(rowCount); + } + bitMaps[targetColumnIndex].mark(i); + } else { + putValue(values[targetColumnIndex], i, types.get(c), list, valueIndex, c); + } + } + } + populateTableTags(deviceId, tabletSchema.categories, values, bitMaps, rowCount); + return tabletSchema.categories == null + ? new Tablet(deviceId.toString(), tabletSchema.schemas, times, values, bitMaps, rowCount) + : new Tablet( + deviceId.getTableName(), + tabletSchema.schemas, + tabletSchema.categories, + times, + values, + bitMaps, + rowCount); + } + + private static void populateTableTags( + final IDeviceID deviceId, + final List<ColumnCategory> columnCategories, + final Object[] values, + final BitMap[] bitMaps, + final int rowCount) { + if (columnCategories == null) { + return; + } + int tagSegmentIndex = 1; + for (int columnIndex = 0; columnIndex < columnCategories.size(); columnIndex++) { + if (columnCategories.get(columnIndex) != ColumnCategory.TAG) { + continue; + } + final Object segment = + tagSegmentIndex < deviceId.segmentNum() ? deviceId.segment(tagSegmentIndex) : null; + tagSegmentIndex++; + final Binary tagValue = + segment == null ? null : new Binary(segment.toString(), TSFileConfig.STRING_CHARSET); + for (int row = 0; row < rowCount; row++) { + if (tagValue == null) { + if (bitMaps[columnIndex] == null) { + bitMaps[columnIndex] = new BitMap(rowCount); + } + bitMaps[columnIndex].mark(row); + } else { + ((Binary[]) values[columnIndex])[row] = tagValue; + if (bitMaps[columnIndex] != null) { + bitMaps[columnIndex].unmark(row); + } + } + } + } + } + + private static Object createValueArray(final TSDataType type, final int rowCount) { + return switch (type) { + case BOOLEAN -> new boolean[rowCount]; + case INT32 -> new int[rowCount]; + case DATE -> new LocalDate[rowCount]; + case INT64, TIMESTAMP -> new long[rowCount]; + case FLOAT -> new float[rowCount]; + case DOUBLE -> new double[rowCount]; + case TEXT, STRING, BLOB, OBJECT -> new Binary[rowCount]; + case VECTOR, UNKNOWN -> throw unsupportedSnapshotDataType(type); + }; + } + + private static Object[] createValueArrays( + final List<IMeasurementSchema> schemas, final int rowCount) { + final Object[] values = new Object[schemas.size()]; + for (int column = 0; column < schemas.size(); column++) { + values[column] = createValueArray(schemas.get(column).getType(), rowCount); + } + return values; + } + + private static void putValue( + final Object target, + final int targetIndex, + final TSDataType type, + final TVList list, + final int sourceIndex) { + switch (type) { + case BOOLEAN -> ((boolean[]) target)[targetIndex] = list.getBoolean(sourceIndex); + case INT32 -> ((int[]) target)[targetIndex] = list.getInt(sourceIndex); + case DATE -> + ((LocalDate[]) target)[targetIndex] = + DateUtils.parseIntToLocalDate(list.getInt(sourceIndex)); + case INT64, TIMESTAMP -> ((long[]) target)[targetIndex] = list.getLong(sourceIndex); + case FLOAT -> ((float[]) target)[targetIndex] = list.getFloat(sourceIndex); + case DOUBLE -> ((double[]) target)[targetIndex] = list.getDouble(sourceIndex); + case TEXT, STRING, BLOB, OBJECT -> + ((Binary[]) target)[targetIndex] = list.getBinary(sourceIndex); + case VECTOR, UNKNOWN -> throw unsupportedSnapshotDataType(type); + } + } + + private static void putValue( + final Object target, + final int targetIndex, + final TSDataType type, + final AlignedTVList list, + final int sourceIndex, + final int columnIndex) { + switch (type) { + case BOOLEAN -> + ((boolean[]) target)[targetIndex] = + list.getBooleanByValueIndex(sourceIndex, columnIndex); + case INT32 -> + ((int[]) target)[targetIndex] = list.getIntByValueIndex(sourceIndex, columnIndex); + case DATE -> + ((LocalDate[]) target)[targetIndex] = + DateUtils.parseIntToLocalDate(list.getIntByValueIndex(sourceIndex, columnIndex)); + case INT64, TIMESTAMP -> + ((long[]) target)[targetIndex] = list.getLongByValueIndex(sourceIndex, columnIndex); + case FLOAT -> + ((float[]) target)[targetIndex] = list.getFloatByValueIndex(sourceIndex, columnIndex); + case DOUBLE -> + ((double[]) target)[targetIndex] = list.getDoubleByValueIndex(sourceIndex, columnIndex); + case TEXT, STRING, BLOB, OBJECT -> + ((Binary[]) target)[targetIndex] = list.getBinaryByValueIndex(sourceIndex, columnIndex); + case VECTOR, UNKNOWN -> throw unsupportedSnapshotDataType(type); + } + } + + private static IllegalArgumentException unsupportedSnapshotDataType(final TSDataType type) { + return new IllegalArgumentException( + String.format( + ImportWALMessages.EXCEPTION_UNSUPPORTED_SNAPSHOT_DATA_TYPE_ARG_7A32D312, type)); + } + + private static class TableTabletSchema { + private final List<IMeasurementSchema> schemas; + private final List<ColumnCategory> categories; + private final int tagCount; + + private TableTabletSchema( + final List<IMeasurementSchema> schemas, + final List<ColumnCategory> categories, + final int tagCount) { + this.schemas = schemas; + this.categories = categories; + this.tagCount = tagCount; + } + } + } + + static class ReplayStatistics { + private long replayedOperationCount; + private long skippedEntryCount; + private long totalBytes; + private long processedBytes; + private long completedFileCount; + private long elapsedNanos; + + long getReplayedOperationCount() { + return replayedOperationCount; + } + + long getSkippedEntryCount() { + return skippedEntryCount; + } + + long getTotalBytes() { + return totalBytes; + } + + long getCompletedFileCount() { + return completedFileCount; + } + + double getElapsedSeconds() { + return elapsedNanos / 1_000_000_000.0; + } + + double getProgressPercent() { + return totalBytes == 0 ? 100.0 : processedBytes * 100.0 / totalBytes; + } + + double getAverageRateMbPerSecond() { + final double elapsedSeconds = getElapsedSeconds(); + return elapsedSeconds <= 0 ? 0.0 : processedBytes / elapsedSeconds / (1024.0 * 1024.0); + } + } + + private static class WALReplayException extends IOException { + private WALReplayException(final String message, final Throwable cause) { + super(message, cause); + } + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java new file mode 100644 index 00000000000..787e06e1975 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java @@ -0,0 +1,506 @@ +/* + * 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.db.tools; + +import org.apache.iotdb.commons.path.MeasurementPath; +import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode; +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode; +import org.apache.iotdb.db.storageengine.dataregion.memtable.IMemTable; +import org.apache.iotdb.db.storageengine.dataregion.memtable.PrimitiveMemTable; +import org.apache.iotdb.db.storageengine.dataregion.wal.WALTestUtils; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry; +import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALSignalEntry; +import org.apache.iotdb.db.storageengine.dataregion.wal.io.ILogWriter; +import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileTest; +import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALWriter; +import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALByteBufferForTest; +import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus; +import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils; +import org.apache.iotdb.rpc.StatementExecutionException; +import org.apache.iotdb.session.Session; + +import org.apache.commons.cli.CommandLine; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.StringArrayDeviceID; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ImportWALTest { + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + /** + * Covers recursive directory discovery with WAL versions 2 and 10 in separate node folders. The + * result must ignore non-WAL files and preserve parent-folder then numeric-version order. + */ + @Test + public void testCollectWALFilesRecursivelyAndSortByVersion() throws IOException { + final Path source = temporaryFolder.newFolder("wal-root").toPath(); + final Path nodeA = Files.createDirectory(source.resolve("node-a")); + final Path nodeB = Files.createDirectory(source.resolve("node-b")); + final Path a10 = createWALFile(nodeA, 10); + final Path a2 = createWALFile(nodeA, 2); + final Path b1 = createWALFile(nodeB, 1); + Files.createFile(nodeA.resolve("ignore.txt")); + + final List<Path> files = ImportWAL.collectWALFiles(source); + + assertEquals( + Arrays.asList( + a2.toAbsolutePath().normalize(), + a10.toAbsolutePath().normalize(), + b1.toAbsolutePath().normalize()), + files); + } + + /** + * Covers a real WAL file containing one tree insert and one internal signal. The insert must be + * sent once as a Tablet, while the signal is counted as skipped and no corruption is reported. + */ + @Test + public void testReplayWALFileReplaysInsertAndSkipsInternalEntry() throws Exception { + final File walFile = createWALFile(0); + final InsertRowNode rowNode = WALTestUtils.getInsertRowNode("root.sg.d1", 100); + writeWAL(walFile, new WALInfoEntry(1, rowNode), new WALSignalEntry(WALEntryType.CLOSE_SIGNAL)); + final Session treeSession = mock(Session.class); + + final ImportWAL.ReplayStatistics statistics = + ImportWAL.replayWALFiles( + Collections.singletonList(walFile.toPath()), + new ImportWAL.WALReplayer(treeSession, null, null)); + + assertEquals(1, statistics.getReplayedOperationCount()); + assertEquals(1, statistics.getSkippedEntryCount()); + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(treeSession).insertTablet(tabletCaptor.capture()); + assertEquals("root.sg.d1", tabletCaptor.getValue().getDeviceId()); + assertEquals(100, tabletCaptor.getValue().getTimestamp(0)); + } + + @Test + public void testReplayReportsProgressAndFileStatistics() throws Exception { + final File walFile = createWALFile(0); + writeWAL( + walFile, + new WALInfoEntry(1, WALTestUtils.getInsertRowNode("root.sg.progress", 100)), + new WALSignalEntry(WALEntryType.CLOSE_SIGNAL)); + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + + final ImportWAL.ReplayStatistics statistics = + ImportWAL.replayWALFiles( + Collections.singletonList(walFile.toPath()), + new ImportWAL.WALReplayer(mock(Session.class), null, null), + new PrintStream(output)); + + assertEquals(1, statistics.getCompletedFileCount()); + assertEquals(Files.size(walFile.toPath()), statistics.getTotalBytes()); + assertTrue(statistics.getElapsedSeconds() >= 0); + assertTrue(output.toString().contains("1/1")); + } + + /** + * Covers an active WAL containing one complete entry but no end marker or metadata. Replay must + * treat EOF at the entry boundary as clean and import the entry without waiting for writer close. + */ + @Test + public void testReplayActiveWALFileAtEntryBoundary() throws Exception { + final File walFile = createWALFile(0); + final InsertRowNode rowNode = WALTestUtils.getInsertRowNode("root.sg.active", 102); + final Session treeSession = mock(Session.class); + + try (WALWriter writer = new WALWriter(walFile)) { + writer.write(serializeWAL(new WALInfoEntry(1, rowNode))); + writer.force(); + + final ImportWAL.ReplayStatistics statistics = + ImportWAL.replayWALFiles( + Collections.singletonList(walFile.toPath()), + new ImportWAL.WALReplayer(treeSession, null, null)); + + assertEquals(1, statistics.getReplayedOperationCount()); + assertEquals(0, statistics.getSkippedEntryCount()); + verify(treeSession).insertTablet(any(Tablet.class)); + } + } + + /** + * Covers an aligned tree row. The replay must use the aligned Session API and must not fall back + * to the non-aligned Tablet API. + */ + @Test + public void testReplayAlignedTreeInsertUsesAlignedSessionAPI() throws Exception { + final InsertRowNode rowNode = WALTestUtils.getInsertRowNode("root.sg.aligned", 101); + rowNode.setAligned(true); + final Session treeSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, rowNode)); + + verify(treeSession).insertAlignedTablet(any(Tablet.class)); + verify(treeSession, never()).insertTablet(any(Tablet.class)); + } + + /** + * Covers a table-model WAL tablet with an explicit target database. Replay must use the table + * Session and retain the source table name in the converted Tablet. + */ + @Test + public void testReplayTableInsertUsesRelationalSessionAPI() throws Exception { + final RelationalInsertTabletNode node = WALFileTest.getRelationalInsertTabletNode("table1"); + final Session treeSession = mock(Session.class); + final Session tableSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, tableSession, "db").replay(new WALInfoEntry(1, node)); + + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(tableSession).insertRelationalTablet(tabletCaptor.capture()); + assertEquals("table1", tabletCaptor.getValue().getTableName()); + verify(treeSession, never()).insertTablet(any(Tablet.class)); + } + + /** + * Covers a table-model WAL insert without a target database. Replay must fail before issuing any + * write because WAL insert entries do not carry their source database name. + */ + @Test + public void testReplayTableInsertRequiresDatabase() throws Exception { + final RelationalInsertTabletNode node = WALFileTest.getRelationalInsertTabletNode("table1"); + final Session treeSession = mock(Session.class); + + assertThrows( + StatementExecutionException.class, + () -> new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, node))); + + verify(treeSession, never()).insertTablet(any(Tablet.class)); + } + + /** + * Covers a tree deletion with multiple paths and a bounded time range. Replay must pass the + * original paths and inclusive time bounds to Session.deleteData. + */ + @Test + public void testReplayTreeDeletePreservesPathsAndTimeRange() throws Exception { + final DeleteDataNode deleteNode = + new DeleteDataNode( + new PlanNodeId(""), + Arrays.asList( + new MeasurementPath("root.sg.d1.s1"), new MeasurementPath("root.sg.d2.*")), + 10, + 20); + final Session treeSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, deleteNode)); + + verify(treeSession) + .deleteData(eq(Arrays.asList("root.sg.d1.s1", "root.sg.d2.*")), eq(10L), eq(20L)); + } + + /** Covers an unsupported entry when the interactive user explicitly chooses to skip it. */ + @Test + public void testReplayUnsupportedEntrySkipsAfterConfirmation() throws Exception { + final WALEntry entry = mockUnsupportedEntry(); + + final boolean replayed = + new ImportWAL.WALReplayer(mock(Session.class), null, null, ignored -> true).replay(entry); + + assertFalse(replayed); + } + + /** Covers an unsupported entry when the interactive user declines the skip prompt. */ + @Test + public void testReplayUnsupportedEntryFailsAfterDecliningSkip() { + final WALEntry entry = mockUnsupportedEntry(); + + assertThrows( + StatementExecutionException.class, + () -> + new ImportWAL.WALReplayer(mock(Session.class), null, null, ignored -> false) + .replay(entry)); + } + + /** Covers non-interactive execution, which must retain the original fail-fast behavior. */ + @Test + public void testReplayUnsupportedEntryFailsWithoutInteractiveInput() { + final WALEntry entry = mockUnsupportedEntry(); + + assertThrows( + StatementExecutionException.class, + () -> new ImportWAL.WALReplayer(mock(Session.class), null, null, null).replay(entry)); + } + + /** Covers accepted confirmations and the safe default for all other prompt answers. */ + @Test + public void testUnsupportedEntrySkipConfirmationParsing() { + assertTrue(ImportWAL.WALReplayer.isSkipConfirmation("y")); + assertTrue(ImportWAL.WALReplayer.isSkipConfirmation(" YES ")); + assertFalse(ImportWAL.WALReplayer.isSkipConfirmation("n")); + assertFalse(ImportWAL.WALReplayer.isSkipConfirmation("")); + assertFalse(ImportWAL.WALReplayer.isSkipConfirmation(null)); + } + + /** Covers a non-aligned snapshot whose measurements have independent time axes. */ + @Test + public void testReplayNonAlignedMemTableSnapshotAsTablets() throws Exception { + final PrimitiveMemTable memTable = new PrimitiveMemTable("root.sg", "0"); + final List<IMeasurementSchema> schemas = + Arrays.asList( + new MeasurementSchema("s1", TSDataType.INT32), + new MeasurementSchema("s2", TSDataType.INT64)); + final StringArrayDeviceID deviceId = new StringArrayDeviceID("root.sg.d1"); + memTable.write(deviceId, schemas, 3, new Object[] {30, 300L}); + memTable.write(deviceId, schemas, 1, new Object[] {10, null}); + final Session treeSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, memTable)); + + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(treeSession, times(2)).insertTablet(tabletCaptor.capture()); + final Tablet s1Tablet = + tabletCaptor.getAllValues().stream() + .filter(tablet -> "s1".equals(tablet.getSchemas().get(0).getMeasurementName())) + .findFirst() + .orElseThrow(AssertionError::new); + assertEquals(2, s1Tablet.getRowSize()); + assertEquals(1, s1Tablet.getTimestamp(0)); + assertEquals(3, s1Tablet.getTimestamp(1)); + assertArrayEquals(new int[] {10, 30}, (int[]) s1Tablet.getValues()[0]); + } + + /** Covers an aligned snapshot with nulls and verifies the aligned Session API is used. */ + @Test + public void testReplayAlignedMemTableSnapshotPreservesNulls() throws Exception { + final PrimitiveMemTable memTable = new PrimitiveMemTable("root.sg", "0"); + final List<IMeasurementSchema> schemas = + Arrays.asList( + new MeasurementSchema("s1", TSDataType.INT32), + new MeasurementSchema("s2", TSDataType.INT64)); + final StringArrayDeviceID deviceId = new StringArrayDeviceID("root.sg.d1"); + memTable.writeAlignedRow(deviceId, schemas, 2, new Object[] {20, null}); + memTable.writeAlignedRow(deviceId, schemas, 1, new Object[] {10, 100L}); + final Session treeSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, memTable)); + + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(treeSession).insertAlignedTablet(tabletCaptor.capture()); + verify(treeSession, never()).insertTablet(any(Tablet.class)); + final Tablet tablet = tabletCaptor.getValue(); + assertEquals(2, tablet.getRowSize()); + assertEquals(1, tablet.getTimestamp(0)); + assertEquals(2, tablet.getTimestamp(1)); + assertArrayEquals(new int[] {10, 20}, (int[]) tablet.getValues()[0]); + assertTrue(tablet.getBitMaps()[1].isMarked(1)); + } + + /** Covers snapshot serialization and deserialization through a real WAL file. */ + @Test + public void testReplaySerializedMemTableSnapshot() throws Exception { + final PrimitiveMemTable memTable = new PrimitiveMemTable("root.sg", "0"); + memTable.write( + new StringArrayDeviceID("root.sg.d1"), + Collections.singletonList(new MeasurementSchema("s1", TSDataType.INT32)), + 7, + new Object[] {70}); + final File walFile = createWALFile(0); + writeWAL(walFile, new WALInfoEntry(1, memTable)); + final Session treeSession = mock(Session.class); + + final ImportWAL.ReplayStatistics statistics = + ImportWAL.replayWALFiles( + Collections.singletonList(walFile.toPath()), + new ImportWAL.WALReplayer(treeSession, null, null)); + + assertEquals(1, statistics.getReplayedOperationCount()); + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(treeSession).insertTablet(tabletCaptor.capture()); + assertEquals(7, tabletCaptor.getValue().getTimestamp(0)); + assertEquals(70, ((int[]) tabletCaptor.getValue().getValues()[0])[0]); + } + + /** Covers a snapshot larger than the replay batch limit. */ + @Test + public void testReplayMemTableSnapshotSplitsLargeChunk() throws Exception { + final PrimitiveMemTable memTable = new PrimitiveMemTable("root.sg", "0"); + final List<IMeasurementSchema> schemas = + Collections.singletonList(new MeasurementSchema("s1", TSDataType.INT32)); + final StringArrayDeviceID deviceId = new StringArrayDeviceID("root.sg.d1"); + for (int i = 0; i < 1025; i++) { + memTable.write(deviceId, schemas, i, new Object[] {i}); + } + final Session treeSession = mock(Session.class); + + new ImportWAL.WALReplayer(treeSession, null, null).replay(new WALInfoEntry(1, memTable)); + + final ArgumentCaptor<Tablet> tabletCaptor = ArgumentCaptor.forClass(Tablet.class); + verify(treeSession, times(2)).insertTablet(tabletCaptor.capture()); + assertEquals(1024, tabletCaptor.getAllValues().get(0).getRowSize()); + assertEquals(1, tabletCaptor.getAllValues().get(1).getRowSize()); + } + + /** Covers a signal snapshot, which carries no user data and must be skipped. */ + @Test + public void testReplaySignalMemTableSnapshotIsSkipped() throws Exception { + final IMemTable signalMemTable = mock(IMemTable.class); + when(signalMemTable.isSignalMemTable()).thenReturn(true); + final Session treeSession = mock(Session.class); + + final boolean replayed = + new ImportWAL.WALReplayer(treeSession, null, null) + .replay(new WALInfoEntry(1, signalMemTable)); + + assertFalse(replayed); + verify(treeSession, never()).insertTablet(any(Tablet.class)); + verify(treeSession, never()).insertAlignedTablet(any(Tablet.class)); + } + + /** Covers a table-model snapshot without a target database. */ + @Test + public void testReplayTableMemTableSnapshotRequiresDatabase() throws Exception { + final PrimitiveMemTable memTable = new PrimitiveMemTable("db", "0"); + memTable.writeAlignedRow( + new StringArrayDeviceID("table1", "device1"), + Collections.singletonList(new MeasurementSchema("temperature", TSDataType.FLOAT)), + 1, + new Object[] {1.0F}); + final Session treeSession = mock(Session.class); + + assertThrows( + StatementExecutionException.class, + () -> + new ImportWAL.WALReplayer(treeSession, null, null) + .replay(new WALInfoEntry(1, memTable))); + + verify(treeSession, never()).insertAlignedTablet(any(Tablet.class)); + } + + /** Covers table-model identifier quoting, including an embedded double quote. */ + @Test + public void testQuoteTableIdentifierForDescribe() { + assertEquals("\"table\"", ImportWAL.WALReplayer.quoteIdentifier("table")); + assertEquals("\"table\"\"name\"", ImportWAL.WALReplayer.quoteIdentifier("table\"name")); + } + + /** + * Covers a truncated WAL that cannot yield a complete entry. The file-level replay must fail so + * callers cannot mistake a partial replay for success. + */ + @Test + public void testReplayWALFileFailsOnCorruption() throws Exception { + final File walFile = createWALFile(0); + Files.write(walFile.toPath(), new byte[] {WALEntryType.INSERT_ROW_NODE.getCode()}); + + final IOException exception = + assertThrows( + IOException.class, + () -> + ImportWAL.replayWALFiles( + Collections.singletonList(walFile.toPath()), + new ImportWAL.WALReplayer(mock(Session.class), null, null))); + + assertTrue(exception.getMessage().contains(walFile.getName())); + } + + @Test + public void testPasswordIsRequiredWhenInteractiveInputIsUnavailable() { + final CommandLine commandLine = mock(CommandLine.class); + when(commandLine.hasOption("password")).thenReturn(false); + + assertThrows(IllegalArgumentException.class, () -> ImportWAL.getPassword(commandLine, null)); + } + + @Test + public void testExplicitPasswordTakesPrecedence() { + final CommandLine commandLine = mock(CommandLine.class); + when(commandLine.hasOption("password")).thenReturn(true); + when(commandLine.getOptionValue("password")).thenReturn("secret"); + + assertEquals("secret", ImportWAL.getPassword(commandLine, null)); + } + + private Path createWALFile(final Path parent, final long version) throws IOException { + return Files.createFile( + parent.resolve( + WALFileUtils.getLogFileName(version, 0, WALFileStatus.CONTAINS_SEARCH_INDEX))); + } + + private File createWALFile(final long version) throws IOException { + return temporaryFolder.newFile( + WALFileUtils.getLogFileName(version, 0, WALFileStatus.CONTAINS_SEARCH_INDEX)); + } + + private static void writeWAL(final File walFile, final WALEntry... entries) throws IOException { + try (ILogWriter writer = new WALWriter(walFile)) { + writer.write(serializeWAL(entries)); + } + } + + private static ByteBuffer serializeWAL(final WALEntry... entries) { + int serializedSize = 0; + for (final WALEntry entry : entries) { + serializedSize += entry.serializedSize(); + } + final WALByteBufferForTest buffer = + new WALByteBufferForTest(ByteBuffer.allocate(serializedSize)); + for (final WALEntry entry : entries) { + entry.serialize(buffer); + } + return buffer.getBuffer(); + } + + private static WALEntry mockUnsupportedEntry() { + final WALEntry entry = mock(WALEntry.class); + when(entry.getType()).thenReturn(WALEntryType.RELATIONAL_DELETE_DATA_NODE); + when(entry.getValue()).thenReturn(mock(RelationalDeleteDataNode.class)); + return entry; + } +} diff --git a/scripts/tools/import-wal.sh b/scripts/tools/import-wal.sh new file mode 100644 index 00000000000..58bfe63c9ee --- /dev/null +++ b/scripts/tools/import-wal.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# +# 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. +# + +if [ -z "${IOTDB_INCLUDE}" ]; then + : +elif [ -r "$IOTDB_INCLUDE" ]; then + . "$IOTDB_INCLUDE" +fi + +if [ -z "${IOTDB_HOME}" ]; then + export IOTDB_HOME="$(cd "$(dirname "$0")"/..; pwd)" +fi + +if [ -n "$JAVA_HOME" ]; then + for java in "$JAVA_HOME"/bin/amd64/java "$JAVA_HOME"/bin/java; do + if [ -x "$java" ]; then + JAVA="$java" + break + fi + done +else + JAVA=java +fi + +if [ -z "$JAVA" ]; then + echo "Unable to find java executable. Check JAVA_HOME and PATH environment variables." > /dev/stderr + exit 1 +fi + +JVM_OPTS="-Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8" +CLASSPATH="${IOTDB_HOME}/lib/*" +MAIN_CLASS=org.apache.iotdb.db.tools.ImportWAL + +"$JAVA" $JVM_OPTS -DIOTDB_HOME="${IOTDB_HOME}" -cp "$CLASSPATH" "$MAIN_CLASS" "$@" +exit $? diff --git a/scripts/tools/windows/import-wal.bat b/scripts/tools/windows/import-wal.bat new file mode 100644 index 00000000000..dc69c440d81 --- /dev/null +++ b/scripts/tools/windows/import-wal.bat @@ -0,0 +1,43 @@ +@REM +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM + +@echo off +if "%OS%" == "Windows_NT" setlocal + +pushd %~dp0..\.. +if NOT DEFINED IOTDB_HOME set IOTDB_HOME=%CD% +popd + +if NOT DEFINED MAIN_CLASS set MAIN_CLASS=org.apache.iotdb.db.tools.ImportWAL +if NOT DEFINED JAVA_HOME goto :err + +set JAVA_OPTS=-ea^ + -DIOTDB_HOME="%IOTDB_HOME%" -Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8 +set CLASSPATH="%IOTDB_HOME%\lib\*" + +"%JAVA_HOME%\bin\java" %JAVA_OPTS% -cp %CLASSPATH% %MAIN_CLASS% %* +set ret_code=%ERRORLEVEL% +goto finally + +:err +echo JAVA_HOME environment variable must be set! +set ret_code=1 + +:finally +ENDLOCAL & EXIT /B %ret_code%
