From aab8048b02140fd8bd5a2ec569f640f9ab40d3d9 Mon Sep 17 00:00:00 2001 From: Suguru ARAKAWA Date: Sun, 5 Jan 2025 17:34:16 +0900 Subject: [PATCH 1/2] feat: tgdump can now use any SQL statement by `--sql`. --- .../tools/tgdump/cli/CommandArgumentSet.java | 69 ++- .../tools/tgdump/cli/CommandUtil.java | 13 +- .../tools/tgdump/cli/ConsoleDumpMonitor.java | 17 +- .../com/tsurugidb/tools/tgdump/cli/Main.java | 2 +- .../tools/tgdump/cli/package-info.java | 2 +- .../tgdump/cli/CommandArgumentSetTest.java | 18 +- .../tsurugidb/tools/tgdump/cli/MainTest.java | 9 + .../tgdump/core/engine/BasicDumpMonitor.java | 37 +- .../tgdump/core/engine/BasicDumpSession.java | 158 ++---- .../core/engine/CompositeDumpMonitor.java | 10 + .../tools/tgdump/core/engine/DumpMonitor.java | 24 +- .../tgdump/core/engine/DumpOperation.java | 78 +++ .../core/engine/DumpOperationDispatch.java | 104 ++++ .../tools/tgdump/core/engine/DumpSession.java | 4 +- .../core/engine/DumpTargetSelector.java | 231 ++------ .../tgdump/core/engine/NameNormalizer.java | 186 +++++++ .../core/engine/QueryDumpOperation.java | 189 +++++++ .../core/engine/QueryDumpTargetSelector.java | 348 ++++++++++++ .../core/engine/TableDumpOperation.java | 219 ++++++++ .../core/engine/TableDumpTargetSelector.java | 115 ++++ .../tools/tgdump/core/model/DumpTarget.java | 94 +++- .../tools/tgdump/core/model/package-info.java | 2 +- .../core/engine/BasicDumpSessionTest.java | 509 +++--------------- .../core/engine/DumpTargetSelectorTest.java | 143 ----- .../tgdump/core/engine/MockDumpMonitor.java | 6 + .../core/engine/MockPreparedStatement.java | 41 ++ .../tgdump/core/engine/MockResultSet.java | 79 +++ .../tgdump/core/engine/MockSqlClient.java | 57 ++ .../tgdump/core/engine/MockTransaction.java | 49 ++ .../core/engine/NameNormalizerTest.java | 78 +++ .../core/engine/QueryDumpOperationTest.java | 429 +++++++++++++++ .../engine/QueryDumpTargetSelectorTest.java | 176 ++++++ .../core/engine/TableDumpOperationTest.java | 442 +++++++++++++++ .../engine/TableDumpTargetSelectorTest.java | 86 +++ 34 files changed, 3103 insertions(+), 921 deletions(-) create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperation.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizer.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java create mode 100644 modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelector.java delete mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelectorTest.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockPreparedStatement.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockResultSet.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockSqlClient.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockTransaction.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizerTest.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperationTest.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelectorTest.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperationTest.java create mode 100644 modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelectorTest.java diff --git a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSet.java b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSet.java index d489fd2..e991455 100644 --- a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSet.java +++ b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSet.java @@ -38,8 +38,9 @@ import com.beust.jcommander.ParameterException; import com.tsurugidb.tools.common.connection.ConnectionProvider; import com.tsurugidb.tools.tgdump.core.engine.DumpTargetSelector; +import com.tsurugidb.tools.tgdump.core.engine.QueryDumpTargetSelector; +import com.tsurugidb.tools.tgdump.core.engine.TableDumpTargetSelector; import com.tsurugidb.tools.tgdump.core.model.TransactionSettings; -import com.tsurugidb.tools.tgdump.core.model.TransactionSettings.Type; import com.tsurugidb.tools.tgdump.profile.DumpProfileBundleLoader; import com.tsurugidb.tools.tgdump.profile.DumpProfileReader; @@ -123,7 +124,7 @@ public TransactionTypeConverter(String optionName) { "read-only", DEFAULT_TRANSACTION_TYPE); @Override - public Type convert(String value) { + public TransactionSettings.Type convert(String value) { var result = NAMES.get(value.toLowerCase(Locale.ENGLISH)); if (result == null) { throw new ParameterException(MessageFormat.format( @@ -143,7 +144,7 @@ public Type convert(String value) { /** * The default transaction type ({@link com.tsurugidb.tools.tgdump.core.model.TransactionSettings.Type#RTX RTX}). */ - public static final Type DEFAULT_TRANSACTION_TYPE = TransactionSettings.Type.RTX; + public static final TransactionSettings.Type DEFAULT_TRANSACTION_TYPE = TransactionSettings.Type.RTX; /** * The default number of dump operation worker threads. @@ -164,6 +165,8 @@ public Type convert(String value) { required = true) private List tableNames; + private boolean queryMode = false; + private Path destinationPath; private Path profile = Path.of(DEFAULT_PROFILE); @@ -198,10 +201,10 @@ public Type convert(String value) { private ConnectionProvider connectionProvider; - /** * Returns the dump target table names. * @return the table name list + * @see #isQueryMode() */ public List getTableNames() { if (tableNames == null) { @@ -220,6 +223,29 @@ public void setTableNames(@Nonnull List nameList) { this.tableNames = List.copyOf(nameList); } + /** + * Returns whether to specify query text instead of table names. + * @return {@code true} if use query text, or {@code false} if use table names + */ + public boolean isQueryMode() { + return queryMode; + } + + /** + * Sets whether to specify query text instead of table names. + * @param enable {@code true} to allow query text, {@code false} to use table names + */ + @Parameter( + order = 10, + names = { "--sql" }, + arity = 0, + description = "specify SQL text instead of table names", + required = false) + public void setQueryMode(boolean enable) { + LOG.trace("argument: --sql: {}", enable); //$NON-NLS-1$ + this.queryMode = enable; + } + /** * Returns the dump files destination path. * @return the destination path @@ -236,7 +262,7 @@ public Path getDestinationPath() { order = 10, names = { "--to" }, arity = 1, - description = "Destination directory of table dump files.", + description = "Destination directory of dump files.", required = true) public void setDestinationPath(@Nonnull Path path) { Objects.requireNonNull(path); @@ -525,10 +551,10 @@ public void setPrintVersion(boolean enable) { * @return the dump profile reader */ protected DumpProfileReader getProfileReader() { - if (profileReader == null) { - this.profileReader = new DumpProfileReader(); + if (profileReader != null) { + return profileReader; } - return profileReader; + return new DumpProfileReader(); } /** @@ -552,13 +578,13 @@ protected void setProfileReader(@Nullable DumpProfileReader value) { * @return the dump profile bundle loader */ protected DumpProfileBundleLoader getProfileBundleLoader() { - if (profileBundleLoader == null) { - this.profileBundleLoader = new DumpProfileBundleLoader( - getProfileReader(), - CommandArgumentSet.class.getClassLoader(), - true); + if (profileBundleLoader != null) { + return profileBundleLoader; } - return profileBundleLoader; + return new DumpProfileBundleLoader( + getProfileReader(), + CommandArgumentSet.class.getClassLoader(), + true); } /** @@ -582,10 +608,13 @@ protected void setProfileBundleLoader(@Nullable DumpProfileBundleLoader value) { * @return the dump target selector */ protected DumpTargetSelector getTargetSelector() { - if (targetSelector == null) { - this.targetSelector = new DumpTargetSelector(); + if (targetSelector != null) { + return this.targetSelector; + } + if (queryMode) { + return new QueryDumpTargetSelector(); } - return targetSelector; + return new TableDumpTargetSelector(); } /** @@ -609,10 +638,10 @@ protected void setTargetSelector(@Nullable DumpTargetSelector value) { * @return the connection provider */ protected ConnectionProvider getConnectionProvider() { - if (connectionProvider == null) { - this.connectionProvider = new ConnectionProvider(); + if (connectionProvider != null) { + return connectionProvider; } - return connectionProvider; + return new ConnectionProvider(); } /** diff --git a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandUtil.java b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandUtil.java index 29eed64..243d42d 100644 --- a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandUtil.java +++ b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/CommandUtil.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.common.diagnostic.DiagnosticUtil; import com.tsurugidb.tools.common.monitoring.CompositeMonitor; import com.tsurugidb.tools.common.monitoring.JsonMonitor; import com.tsurugidb.tools.common.monitoring.LoggingMonitor; @@ -84,6 +85,7 @@ static void printArgumentSet(@Nonnull Printer printer, @Nonnull CommandArgumentS // dump core settings printArgument(printer, "(positional)", args.getTableNames()); + printArgument(printer, "--sql", args.isQueryMode()); //$NON-NLS-1$ printArgument(printer, "--to", args.getDestinationPath()); //$NON-NLS-1$ printArgument(printer, "--profile", args.getProfile()); //$NON-NLS-1$ @@ -171,8 +173,15 @@ static List prepareDestination( e); } - LOG.debug("compute individual table dump output directories"); - var targets = selector.getTargets(destination, tableNames); + LOG.debug("compute individual dump output directories"); + List targets; + try { + targets = selector.getTargets(destination, tableNames); + } catch (IllegalArgumentException e) { + throw new CliException(CliDiagnosticCode.INVALID_PARAMETER, + List.of(DiagnosticUtil.getMessage(e)), + e); + } LOG.debug("dump targets: {}", targets); return targets; } diff --git a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/ConsoleDumpMonitor.java b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/ConsoleDumpMonitor.java index abbd58a..51709d2 100644 --- a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/ConsoleDumpMonitor.java +++ b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/ConsoleDumpMonitor.java @@ -25,6 +25,7 @@ import com.google.protobuf.Message; import com.google.protobuf.TextFormat; +import com.tsurugidb.tools.common.monitoring.MonitoringException; import com.tsurugidb.tools.tgdump.core.engine.DumpMonitor; import com.tsurugidb.tsubakuro.sql.TableMetadata; @@ -88,29 +89,37 @@ public void onDumpInfo(@Nonnull String tableName, @Nonnull TableMetadata tableIn .map(TextFormat::shortDebugString) .collect(Collectors.joining(", "))).append("]"); //$NON-NLS-1$ //$NON-NLS-2$ metadata.append('}'); - verbose("found table: table={0}, metadata={1}", tableName, metadata.toString()); + verbose("found table: target={0}, metadata={1}", tableName, metadata.toString()); } } + @Override + public void onDumpInfo(String label, String query, Path dumpDirectory) throws MonitoringException { + Objects.requireNonNull(label); + Objects.requireNonNull(query); + Objects.requireNonNull(dumpDirectory); + verbose("checked query: target={0}, query={1}", label, query); + } + @Override public void onDumpStart(@Nonnull String tableName, @Nonnull Path dumpDirectory) { Objects.requireNonNull(tableName); Objects.requireNonNull(dumpDirectory); - print("table dump operation was started: table={0}, output={1}", tableName, dumpDirectory); + print("dump operation was started: target={0}, output={1}", tableName, dumpDirectory); } @Override public void onDumpFile(@Nonnull String tableName, @Nonnull Path dumpFile) { Objects.requireNonNull(tableName); Objects.requireNonNull(dumpFile); - verbose("generated a part of table dump file: table={0}, output={1}", tableName, dumpFile); + verbose("generated a part of dump file: target={0}, output={1}", tableName, dumpFile); } @Override public void onDumpFinish(@Nonnull String tableName, @Nonnull Path dumpDirectory) { Objects.requireNonNull(tableName); Objects.requireNonNull(dumpDirectory); - print("table dump operation was finished: table={0}, output={1}", tableName, dumpDirectory); + print("dump operation was finished: target={0}, output={1}", tableName, dumpDirectory); } @Override diff --git a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/Main.java b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/Main.java index 46180ef..9f77f15 100644 --- a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/Main.java +++ b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/Main.java @@ -46,7 +46,7 @@ import com.tsurugidb.tsubakuro.sql.SqlClient; /** - * The program entry for Tsurugi Table Dump Tool ({@literal a.k.a.} {@code tgdump}}). + * The program entry for Tsurugi Dump Tool ({@literal a.k.a.} {@code tgdump}}). * @see CommandArgumentSet */ public class Main { diff --git a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/package-info.java b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/package-info.java index f70f08f..ad12c9b 100644 --- a/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/package-info.java +++ b/modules/tgdump/cli/src/main/java/com/tsurugidb/tools/tgdump/cli/package-info.java @@ -14,6 +14,6 @@ * limitations under the License. */ /** - * CLI classes for Tsurugi Table Dump Tool. + * CLI classes for Tsurugi Dump Tool. */ package com.tsurugidb.tools.tgdump.cli; diff --git a/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSetTest.java b/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSetTest.java index 5c4c88f..2250338 100644 --- a/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSetTest.java +++ b/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/CommandArgumentSetTest.java @@ -15,10 +15,13 @@ */ package com.tsurugidb.tools.tgdump.cli; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; +import com.tsurugidb.tools.tgdump.core.engine.QueryDumpTargetSelector; +import com.tsurugidb.tools.tgdump.core.engine.TableDumpTargetSelector; + class CommandArgumentSetTest { @Test @@ -36,7 +39,18 @@ void getProfileBundleLoader() { @Test void getTargetSelector() { var args = new CommandArgumentSet(); - assertNotNull(args.getTargetSelector()); + var selector = args.getTargetSelector(); + assertNotNull(selector); + assertInstanceOf(TableDumpTargetSelector.class, selector); + } + + @Test + void getTargetSelector_queryMode() { + var args = new CommandArgumentSet(); + args.setQueryMode(true); + var selector = args.getTargetSelector(); + assertNotNull(selector); + assertInstanceOf(QueryDumpTargetSelector.class, selector); } @Test diff --git a/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/MainTest.java b/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/MainTest.java index 460e9e5..866a9a9 100644 --- a/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/MainTest.java +++ b/modules/tgdump/cli/src/test/java/com/tsurugidb/tools/tgdump/cli/MainTest.java @@ -258,6 +258,7 @@ void parseArguments_simple() { assertEquals(URI.create("ipc:testing"), args.getConnectionUri()); // defaults + assertFalse(args.isQueryMode()); assertEquals(Path.of("default"), args.getProfile()); assertNull(args.getConnectionLabel()); assertEquals(0, args.getConnectionTimeoutMillis()); @@ -292,6 +293,14 @@ void parseArguments_table_missing() { () -> app.parseArguments("--connection", "ipc:testing", "--to", "output")); } + @Test + void parseArguments_query() { + var app = new Main(); + var args = app.parseArguments("--connection", "ipc:testing", "--sql", "A", "B", "C", "--to", "output"); + assertTrue(args.isQueryMode()); + assertEquals(List.of("A", "B", "C"), args.getTableNames()); + } + @Test void parseArguments_connection_unsupported() { var app = new Main(); diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpMonitor.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpMonitor.java index 2ae234c..618cac2 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpMonitor.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpMonitor.java @@ -42,7 +42,7 @@ public class BasicDumpMonitor implements DumpMonitor { public static final String FORMAT_DUMP_INFO = "dump-info"; /** - * The monitoring format name that a table dump was started. + * The monitoring format name that individual dump operations were started. */ public static final String FORMAT_DUMP_START = "dump-start"; @@ -52,15 +52,35 @@ public class BasicDumpMonitor implements DumpMonitor { public static final String FORMAT_DUMP_FILE = "dump-file"; /** - * The monitoring format name that a table dump was finished. + * The monitoring format name that individual dump operations were finished. */ public static final String FORMAT_DUMP_FINISH = "dump-finish"; + /** + * The monitoring property of the target type. + */ + public static final String PROPERTY_TYPE = "type"; + + /** + * The monitoring value of the table type. + */ + public static final String TYPE_TABLE = "table"; + + /** + * The monitoring value of the query type. + */ + public static final String TYPE_QUERY = "query"; + /** * The monitoring property of the table name. */ public static final String PROPERTY_TABLE_NAME = "table"; + /** + * The monitoring property of the query text. + */ + public static final String PROPERTY_QUERY_TEXT = "query"; + /** * The monitoring property of the dump destination path. */ @@ -106,6 +126,7 @@ public void onDumpInfo(@Nonnull String tableName, @Nonnull TableMetadata tableIn Property.of(PROPERTY_COLUMN_TYPE, getTypeName(column)))); } monitor.onData(FORMAT_DUMP_INFO, List.of( + Property.of(PROPERTY_TYPE, Value.of(TYPE_TABLE)), Property.of(PROPERTY_TABLE_NAME, Value.of(tableName)), Property.of(PROPERTY_COLUMNS, Value.of(Array.fromList(columns))), Property.of(PROPERTY_DESTINATION, Value.of(dumpDirectory.toString())))); @@ -122,6 +143,18 @@ private static Value getTypeName(SqlCommon.Column column) { } } + @Override + public void onDumpInfo(String label, String query, Path dumpDirectory) throws MonitoringException { + Objects.requireNonNull(label); + Objects.requireNonNull(query); + Objects.requireNonNull(dumpDirectory); + monitor.onData(FORMAT_DUMP_INFO, List.of( + Property.of(PROPERTY_TYPE, Value.of(TYPE_QUERY)), + Property.of(PROPERTY_TABLE_NAME, Value.of(label)), + Property.of(PROPERTY_QUERY_TEXT, Value.of(query)), + Property.of(PROPERTY_DESTINATION, Value.of(dumpDirectory.toString())))); + } + @Override public void onDumpStart(@Nonnull String tableName, @Nonnull Path dumpDirectory) throws MonitoringException { Objects.requireNonNull(tableName); diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSession.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSession.java index 409252d..fbb9107 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSession.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSession.java @@ -16,23 +16,17 @@ package com.tsurugidb.tools.tgdump.core.engine; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.text.MessageFormat; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Pattern; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.protobuf.TextFormat; -import com.tsurugidb.sql.proto.SqlCommon; import com.tsurugidb.tools.common.diagnostic.DiagnosticException; import com.tsurugidb.tools.common.diagnostic.DiagnosticUtil; import com.tsurugidb.tools.tgdump.core.model.DumpProfile; @@ -40,9 +34,7 @@ import com.tsurugidb.tools.tgdump.core.model.TransactionSettings; import com.tsurugidb.tsubakuro.exception.ServerException; import com.tsurugidb.tsubakuro.sql.SqlClient; -import com.tsurugidb.tsubakuro.sql.TableMetadata; import com.tsurugidb.tsubakuro.sql.Transaction; -import com.tsurugidb.tsubakuro.sql.exception.TargetNotFoundException; /** * A basic implementation of {@link DumpSession}. @@ -90,24 +82,18 @@ enum State { CLOSED, } - private static final String SQL_DUMP_QUERY = "SELECT * FROM %s"; //$NON-NLS-1$ - private static final Logger LOG = LoggerFactory.getLogger(BasicDumpSession.class); private final SqlClient client; private final TransactionSettings transactionSettings; - private final DumpProfile dumpProfile; + private final DumpOperationDispatch operation; private final AtomicReference stateRef = new AtomicReference<>(State.PREPARING); - private final Map registered = new ConcurrentHashMap<>(); - private final AtomicReference transactionRef = new AtomicReference<>(); - private final boolean createTargetDirectories; - /** * Creates a new instance. *

@@ -123,13 +109,40 @@ public BasicDumpSession( @Nonnull TransactionSettings transactionSettings, @Nonnull DumpProfile dumpProfile, boolean createTargetDirectories) { + this(client, transactionSettings, createOperationsMap(dumpProfile, createTargetDirectories)); + } + + private static @Nonnull Map createOperationsMap( + @Nonnull DumpProfile dumpProfile, + boolean createTargetDirectories) { + Objects.requireNonNull(dumpProfile); + return Map.ofEntries( + Map.entry(DumpTarget.TargetType.TABLE, new TableDumpOperation(dumpProfile, createTargetDirectories)), + Map.entry(DumpTarget.TargetType.QUERY, new QueryDumpOperation(dumpProfile, createTargetDirectories))); + } + + /** + * Creates a new instance. + *

+ * This will invoke {@link SqlClient#close()} during this object is closed. + *

+ *

+ * This is designed for testing mainly. + *

+ * @param client the SQL client to execute the series of operations + * @param transactionSettings the transaction settings for dump operations + * @param operations the dump operations + */ + BasicDumpSession( + @Nonnull SqlClient client, + @Nonnull TransactionSettings transactionSettings, + @Nonnull Map operations) { Objects.requireNonNull(client); Objects.requireNonNull(transactionSettings); - Objects.requireNonNull(dumpProfile); + Objects.requireNonNull(operations); this.client = client; this.transactionSettings = transactionSettings; - this.dumpProfile = dumpProfile; - this.createTargetDirectories = createTargetDirectories; + this.operation = new DumpOperationDispatch(operations); } /** @@ -150,6 +163,7 @@ public void register(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(monitor); Objects.requireNonNull(target); + LOG.trace("enter: register: {}", target); //$NON-NLS-1$ if (stateRef.get() != State.PREPARING) { throw new IllegalStateException(MessageFormat.format( @@ -157,26 +171,7 @@ public void register(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) stateRef.get(), State.PREPARING)); } - if (registered.containsKey(target.getTableName())) { - monitor.verbose("skip already registerd table: {0}", target.getTableName()); //$NON-NLS-1$ - LOG.trace("enter: register: {} (already registered)", target); //$NON-NLS-1$ - return; - } - monitor.verbose("inspecting dump table: {0} ({1})", target.getTableName(), target.getDestination()); //$NON-NLS-1$ - try { - var metadata = client.getTableMetadata(target.getTableName()).await(); - registered.put(target.getTableName(), metadata); - monitor.onDumpInfo(target.getTableName(), metadata, target.getDestination()); - } catch (TargetNotFoundException e) { - LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.TABLE_NOT_FOUND, List.of(target.getTableName()), e); - } catch (IOException e) { - LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); - } catch (ServerException e) { - LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.SERVER_ERROR, List.of(DiagnosticUtil.getMessage(e)), e); - } + operation.register(client, monitor, target); LOG.trace("exit: register: {}", target); //$NON-NLS-1$ } @@ -193,10 +188,10 @@ public void begin(@Nonnull DumpMonitor monitor) throws InterruptedException, Dia } boolean complete = false; try { - if (registered.isEmpty()) { + if (operation.isEmpty()) { throw new IllegalStateException("dump targets are empty"); } - var tables = List.copyOf(registered.keySet()); + var tables = operation.getTargetTables(); var txOptions = transactionSettings.toProtocolBuffer(tables); monitor.verbose("starting a new transaction: {0}", txOptions); //$NON-NLS-1$ try { @@ -224,11 +219,6 @@ public void execute(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) Objects.requireNonNull(monitor); Objects.requireNonNull(target); LOG.trace("enter: execute: {}", target); //$NON-NLS-1$ - if (!registered.containsKey(target.getTableName())) { - throw new IllegalArgumentException(MessageFormat.format( - "the table {0} has not been prepared", - target.getTableName())); - } if (stateRef.get() != State.RUNNING) { throw new IllegalStateException(MessageFormat.format( "inconsistent operation state: {0} (expected: {1})", @@ -240,61 +230,7 @@ public void execute(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) // may not occur in general cases throw new IllegalStateException("transaction object is missing"); } - - var statement = createStatement(target.getTableName()); - monitor.verbose("preparing dump command: {0} ({1}: {2})", //$NON-NLS-1$ - target.getTableName(), transaction.getTransactionId(), statement); - var dumpOptions = dumpProfile.toProtocolBuffer(); - if (LOG.isDebugEnabled()) { - LOG.debug("dump options: {}", TextFormat.shortDebugString(dumpOptions)); - } - try (var prepared = client.prepare(statement, List.of()).await()) { - monitor.onDumpStart(target.getTableName(), target.getDestination()); - - // create target directory - if (createTargetDirectories) { - LOG.debug("creating dump target directory: {} ({})", target.getTableName(), target.getDestination()); - Files.createDirectories(target.getDestination()); - } - - try (var rs = transaction.executeDump(prepared, List.of(), target.getDestination(), dumpOptions).await()) { - monitor.verbose("start retrieving dump results: {0} ({1})", //$NON-NLS-1$ - target.getTableName(), transaction.getTransactionId()); - // NOTE: we assume the first column has provided dump file path (from operation specification). - var meta = rs.getMetadata(); - if (meta.getColumns().isEmpty()) { - // may not occur in general cases - throw new IllegalStateException("invalid result set format: colum list is empty"); - } - var column = meta.getColumns().get(0); - if (column.getTypeInfoCase() != SqlCommon.Column.TypeInfoCase.ATOM_TYPE - || column.getAtomType() != SqlCommon.AtomType.CHARACTER) { - // may not occur otherwise dump operation specification was changed - throw new IllegalStateException(MessageFormat.format( - "unexpected dump result type: {0} (expected: {1})", - column, - SqlCommon.AtomType.CHARACTER)); - } - while (rs.nextRow()) { - if (!rs.nextColumn()) { - throw new IllegalStateException("broken dump result (less columns in the result set)"); - } - var file = Path.of(rs.fetchCharacterValue()); - monitor.onDumpFile(target.getTableName(), file); - } - } catch (ServerException e) { - LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.OPERATION_FAILURE, - List.of(target.getTableName(), statement), e); - } - monitor.onDumpFinish(target.getTableName(), target.getDestination()); - } catch (IOException e) { - LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); - } catch (ServerException e) { - LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ - throw new DumpException(DumpDiagnosticCode.PREPARE_FAILURE, List.of(target.getTableName(), statement), e); - } + operation.execute(client, transaction, monitor, target); LOG.trace("exit: execute: {}", target); //$NON-NLS-1$ } @@ -362,26 +298,4 @@ public void close() throws InterruptedException, DiagnosticException { } LOG.trace("exit: close"); //$NON-NLS-1$ } - - private static final Pattern PATTERN_REGULAR_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); //$NON-NLS-1$ - - private static String createStatement(String tableName) { - var matcher = PATTERN_REGULAR_IDENTIFIER.matcher(tableName); - if (matcher.matches()) { - return String.format(SQL_DUMP_QUERY, tableName); - } - var string = new StringBuilder(); - string.append('"'); - for (int i = 0; i < tableName.length(); i++) { - var c = tableName.charAt(i); - if (c == '"') { - string.append('"'); - string.append('"'); - } else { - string.append(c); - } - } - string.append('"'); - return String.format(SQL_DUMP_QUERY, string.toString()); - } } diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/CompositeDumpMonitor.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/CompositeDumpMonitor.java index 81bbd9f..0662f60 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/CompositeDumpMonitor.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/CompositeDumpMonitor.java @@ -70,6 +70,16 @@ public void onDumpInfo(@Nonnull String tableName, @Nonnull TableMetadata tableIn } } + @Override + public void onDumpInfo(String label, String query, Path dumpDirectory) throws MonitoringException { + Objects.requireNonNull(label); + Objects.requireNonNull(query); + Objects.requireNonNull(dumpDirectory); + for (var element : elements) { + element.onDumpInfo(label, query, dumpDirectory); + } + } + @Override public void onDumpStart(@Nonnull String tableName, @Nonnull Path dumpDirectory) throws MonitoringException { Objects.requireNonNull(tableName); diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpMonitor.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpMonitor.java index b61cfd5..14b9fbe 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpMonitor.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpMonitor.java @@ -50,25 +50,35 @@ void onDumpInfo(@Nonnull String tableName, @Nonnull TableMetadata tableInfo, @No throws MonitoringException; /** - * Invoked when a table dump operation was started. - * @param tableName the target table name - * @param dumpDirectory the dump destination directory for the table + * Invoked when a query text was validated. + * @param label the query label + * @param query the query text + * @param dumpDirectory the dump destination directory for the operation + * @throws MonitoringException if error was occurred while monitoring the event + */ + void onDumpInfo(@Nonnull String label, @Nonnull String query, @Nonnull Path dumpDirectory) + throws MonitoringException; + + /** + * Invoked when a dump operation was started. + * @param tableName the table name or label for the operation + * @param dumpDirectory the dump destination directory for the operation * @throws MonitoringException if error was occurred while monitoring the event */ void onDumpStart(@Nonnull String tableName, @Nonnull Path dumpDirectory) throws MonitoringException; /** * Invoked when a dump file is provided. - * @param tableName the target table name + * @param tableName the table name or label for the operation * @param dumpFile the provided file path * @throws MonitoringException if error was occurred while monitoring the event */ void onDumpFile(@Nonnull String tableName, @Nonnull Path dumpFile) throws MonitoringException; /** - * Invoked when a table dump operation was finished for the table. - * @param tableName the target table name - * @param dumpDirectory the dump destination directory for the table + * Invoked when each dump operation was finished. + * @param tableName the table name or label for the operation + * @param dumpDirectory the dump destination directory for the operation * @throws MonitoringException if error was occurred while monitoring the event */ void onDumpFinish(@Nonnull String tableName, @Nonnull Path dumpDirectory) throws MonitoringException; diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperation.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperation.java new file mode 100644 index 0000000..4a012a0 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperation.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.util.List; + +import javax.annotation.Nonnull; + +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tsubakuro.sql.SqlClient; +import com.tsurugidb.tsubakuro.sql.Transaction; + +/** + * Individual dump operations for target types, used in {@link BasicDumpSession}. + * @see BasicDumpSession + */ +interface DumpOperation { + + /** + * Returns whether this has no registered operations. + * @return {@code true} if this has no registered operations, otherwise {@code false} + * @see #register(SqlClient, DumpMonitor, DumpTarget) + */ + boolean isEmpty(); + + /** + * Returns a list of the dump target table names. + * @return the table names, or empty if they are not sure in this operation type + */ + default List getTargetTables() { + return List.of(); + } + + /** + * Registers a dump operation. + * @param client the SQL client to access the database + * @param monitor the operation monitor + * @param target the dump target table information + * @throws InterruptedException if interrupted during the operation + * @throws DiagnosticException if the target table is not found in the database + * @throws DiagnosticException if error was occurred + * @throws IllegalStateException if the transaction have been already started + */ + void register(@Nonnull SqlClient client, @Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) + throws InterruptedException, DiagnosticException; + + /** + * Executes a dump operation for the table. + * @param client the SQL client to access the database + * @param transaction the transaction where the operation is executed + * @param monitor the operation monitor + * @param target the dump target information + * @throws InterruptedException if interrupted during the operation + * @throws DiagnosticException if error was occurred + * @throws IllegalArgumentException if the dump target is not {@link #register(SqlClient, DumpMonitor, DumpTarget) registered} + * @throws IllegalStateException if the transaction have not been started. or already finished + */ + void execute( + @Nonnull SqlClient client, + @Nonnull Transaction transaction, + @Nonnull DumpMonitor monitor, + @Nonnull DumpTarget target) + throws InterruptedException, DiagnosticException; +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java new file mode 100644 index 0000000..ac79b12 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java @@ -0,0 +1,104 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.text.MessageFormat; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget.TargetType; +import com.tsurugidb.tsubakuro.sql.SqlClient; +import com.tsurugidb.tsubakuro.sql.Transaction; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An implementation of {@link DumpOperation} for dispatching by its tarrget type. + */ +class DumpOperationDispatch implements DumpOperation { + + private final Map elements; + + /** + * Creates a new instance. + * @param elements the elements to dispatch + */ + DumpOperationDispatch(@NonNull Map elements) { + Objects.requireNonNull(elements); + this.elements = elements.isEmpty() ? Map.of() : new EnumMap<>(elements); + } + + /** + * Returns the operation for the specified target type. + * @param type the target type + * @return the operation for the specified target type, or {@code empty} if not found + */ + public Optional getOperation(@NonNull DumpTarget.TargetType type) { + Objects.requireNonNull(type); + return Optional.ofNullable(elements.get(type)); + } + + private DumpOperation getOperationStrict(@NonNull DumpTarget target) { + return getOperation(target.getTargetType()) + .orElseThrow(() -> new UnsupportedOperationException(MessageFormat.format( + "unsupported target type: {0} in {1}", + target.getTargetType(), + target.getLabel()))); + } + + @Override + public List getTargetTables() { + return elements.values().stream() + .flatMap(e -> e.getTargetTables().stream()) + .sorted() + .distinct() + .collect(Collectors.toList()); + } + + @Override + public boolean isEmpty() { + return elements.values().stream().allMatch(DumpOperation::isEmpty); + } + + @Override + public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + getOperationStrict(target).register(client, monitor, target); + } + + @Override + public void execute( + @NonNull SqlClient client, + @NonNull Transaction transaction, + @NonNull DumpMonitor monitor, + @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(transaction); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + getOperationStrict(target).execute(client, transaction, monitor, target); + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpSession.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpSession.java index e168a51..6da9b69 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpSession.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpSession.java @@ -26,7 +26,7 @@ public interface DumpSession extends AutoCloseable { /** - * Registers dump target table, and returns its table metadata. + * Registers dump target table. *

* This cannot use after calling {@link #begin(DumpMonitor)}. *

@@ -36,6 +36,7 @@ public interface DumpSession extends AutoCloseable { * @throws DiagnosticException if the target table is not found in the database * @throws DiagnosticException if error was occurred * @throws IllegalStateException if the transaction have been already started + * @throws UnsupportedOperationException if the {@code target} is not supported * @see #begin(DumpMonitor) */ void register(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) @@ -59,6 +60,7 @@ void register(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) * @throws DiagnosticException if error was occurred * @throws IllegalArgumentException if the dump target is not {@link #register(DumpMonitor, DumpTarget) registered} * @throws IllegalStateException if the transaction have not been started. or already finished + * @throws UnsupportedOperationException if the {@code target} is not supported */ void execute(@Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException; diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelector.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelector.java index 04149d5..982bfce 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelector.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelector.java @@ -18,206 +18,81 @@ import java.nio.file.Path; import java.text.MessageFormat; import java.util.ArrayList; -import java.util.BitSet; -import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.annotation.Nonnull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.tsurugidb.tools.tgdump.core.model.DumpTarget; /** - * Computes table dump destination directories from each table name. + * Computes {@link DumpTarget dump targets} for each commands. */ -public class DumpTargetSelector { - - /** - * Default table name length limit. - */ - public static final int DEFAULT_TABLE_NAME_LIMIT = 50; - - /** - * Default restricted characters. - */ - public static final String DEFAULT_RESTRICTED_CHARACTERS = ".<>:/\\|?*\""; - - /** - * Default replacement character. - */ - public static final char DEFAULT_REPLACEMENT = '_'; - - /** - * Default delimiter character. - */ - public static final char DEFAULT_DELIMITER = '-'; - - private static final int ASCII_COUNT = 128; - - private static final Logger LOG = LoggerFactory.getLogger(DumpTargetSelector.class); - - private final int tableNameLimit; - - private final BitSet escapeTargets; - - private final char replacement; - - private final char delimiter; +public interface DumpTargetSelector { /** - * Creates a new instance with default settings. - * @see #DEFAULT_TABLE_NAME_LIMIT - * @see #DEFAULT_RESTRICTED_CHARACTERS - * @see #DEFAULT_REPLACEMENT - * @see #DEFAULT_DELIMITER + * Default delimiter character before destination path sequence. */ - public DumpTargetSelector() { - this(DEFAULT_TABLE_NAME_LIMIT, DEFAULT_RESTRICTED_CHARACTERS, DEFAULT_REPLACEMENT, DEFAULT_DELIMITER); - } + char DEFAULT_SEQUENCE_DELIMITER = '-'; /** - * Creates a new instance. - * @param tableNameLimit the maximum table name length. - * If exceeded, the table name will be trimmed the trailing characters to the length. - * @param escapeTargets the escape target characters, each must be ASCII character - * @param replacement the replacement character, must be ASCII character - * @param delimiter the delimiter character, must be ASCII character - * @throws IllegalArgumentException if {@code tableNameLimit} is less than or equal to 0 - * @throws IllegalArgumentException if {@code escapeTargets} or {@code delimiter} contains non-ASCII character - */ - public DumpTargetSelector( - int tableNameLimit, - @Nonnull CharSequence escapeTargets, - char replacement, - char delimiter) { - if (tableNameLimit <= 0) { - throw new IllegalArgumentException(MessageFormat.format( - "table name limit must be >= 0 ({0})", - tableNameLimit)); - } - Objects.requireNonNull(escapeTargets); - escapeTargets.codePoints().forEach(it -> checkAscii("escape target", it)); - checkAscii("replacement", replacement); - checkAscii("delimiter", delimiter); - this.tableNameLimit = tableNameLimit; - this.escapeTargets = escapeTargets.codePoints() - .collect( - () -> new BitSet(ASCII_COUNT), - (r, c) -> r.set(c), - (a, b) -> a.or(b)); - this.replacement = replacement; - this.delimiter = delimiter; - } - - private static void checkAscii(String name, int value) { - if (value >= ASCII_COUNT) { - throw new IllegalArgumentException(MessageFormat.format( - "{0} must be an ASCII character: {1}", - name, - String.format("U+%04X", value))); //$NON-NLS-1$ - } - } - - /** - * Normalizes the given table name. - * @param name the table name - * @return the normalized name - */ - public String normalize(@Nonnull String name) { - Objects.requireNonNull(name); - var codePoints = name.codePoints() - .sequential() - .map(c -> { - if (Character.isISOControl(c) - || Character.isWhitespace(c) - || Character.isSupplementaryCodePoint(c) - || c == delimiter - || escapeTargets.get(c)) { - return replacement; - } else if (Character.isUpperCase(c)) { - return Character.toLowerCase(c); - } - return c; - }) - .limit(tableNameLimit) - .toArray(); - return new String(codePoints, 0, codePoints.length); - } - - /** - * Computes {@link DumpTarget dump targets} for each table. + * Computes {@link DumpTarget dump targets} for each commands. * @param destinationDirectory the base destination directory, * each dump target will be placed under it. - * @param tableNames the table name list - * @return the dump targets for the tables + * @param commands the command list + * @return the dump targets for the commands + * @throws IllegalArgumentException if some commands are not valid */ - public List getTargets(@Nonnull Path destinationDirectory, @Nonnull List tableNames) { - Objects.requireNonNull(destinationDirectory); - Objects.requireNonNull(tableNames); - LOG.trace("enter: getTargets: {}, {}", destinationDirectory, tableNames); //$NON-NLS-1$ - - var conflictCounts = new HashMap(); - var results = new ArrayList(tableNames.size()); + List getTargets(@Nonnull Path destinationDirectory, @Nonnull List commands); - for (var name : tableNames) { - var normalized = normalize(name); - int conflicts = conflictCounts.computeIfAbsent(normalized, k -> new AtomicInteger()).getAndIncrement(); - Path destination; - if (conflicts <= 0) { - destination = destinationDirectory.resolve(normalized); + /** + * Removes conflict from the dump target destinations, by appending unique suffixes. + * @param targets the dump target list + * @param delimiter the delimiter character + * @return the dump target list without duplicates + * @throws IllegalArgumentException if any destination name includes the delimiter character + */ + static List resolveConflicts(@Nonnull List targets, char delimiter) { + Objects.requireNonNull(targets); + // SELECT destination, 0 FROM GROUP BY destination HAVING count(*) >= 2 + var conflicts = targets.stream() + .peek(it -> { + var name = it.getDestination().getFileName(); + Objects.requireNonNull(name); + if (name.toString().indexOf(delimiter) >= 0) { + throw new IllegalArgumentException(MessageFormat.format( + "dump target destination must not include the delimiter character ({0}) : {1}", + delimiter, + it.getDestination())); + } + }) + .collect(Collectors.groupingBy(DumpTarget::getDestination, Collectors.counting())) + .entrySet().stream() + .filter(e -> e.getValue() >= 2) + .collect(Collectors.toMap(Map.Entry::getKey, e -> new AtomicInteger())); + + // if conflicts are found, rewrite its destination by appending the delimiter and suffixes + var results = new ArrayList(targets.size()); + for (var target : targets) { + var destination = target.getDestination(); + var counter = conflicts.get(target.getDestination()); + if (counter == null) { + // no counflicts + results.add(target); } else { - destination = destinationDirectory.resolve(addSuffix(normalized, conflicts)); + // has conflicts, append delimiter and unique index to its tail + var index = counter.incrementAndGet(); + var suffixed = String.format("%s%s%d", destination.getFileName(), delimiter, index); //$NON-NLS-1$ + results.add(new DumpTarget( + target.getTargetType(), + target.getLabel(), + target.getTarget(), + destination.resolveSibling(suffixed))); } - var target = new DumpTarget(name, destination); - LOG.trace("element: getTargets: {}", target); //$NON-NLS-1$ - results.add(target); } - - LOG.trace("exit: getTargets: {}", results); //$NON-NLS-1$ return results; } - - private String addSuffix(String base, int index) { - return String.format("%s%s%d", base, delimiter, index); //$NON-NLS-1$ - } - - @Override - public int hashCode() { - return Objects.hash(tableNameLimit, escapeTargets, delimiter, replacement); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - DumpTargetSelector other = (DumpTargetSelector) obj; - return delimiter == other.delimiter - && Objects.equals(escapeTargets, other.escapeTargets) - && replacement == other.replacement - && tableNameLimit == other.tableNameLimit; - } - - @Override - public String toString() { - return String.format( - "DumpTargetPlacer(tableNameLimit=%s, escapeTargets=%s, replacement=%s, delimiter=%s)", - tableNameLimit, - escapeTargets.stream() - .mapToObj(Character::toString) - .collect(Collectors.joining(", ", "{", "}")), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - replacement, - delimiter); - } -} +} \ No newline at end of file diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizer.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizer.java new file mode 100644 index 0000000..de0830d --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizer.java @@ -0,0 +1,186 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.text.MessageFormat; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +/** + * Normalizes name of tables or queries. + */ +public class NameNormalizer implements Function { + + /** + * Default name length limit. + */ + public static final int DEFAULT_NAME_LIMIT = 50; + + /** + * Default restricted characters. + */ + public static final String DEFAULT_RESTRICTED_CHARACTERS = ".<>:/\\|?*\""; + + /** + * Default replacement character. + */ + public static final char DEFAULT_REPLACEMENT = '_'; + + /** + * Default delimiter character. + */ + public static final char DEFAULT_DELIMITER = '-'; + + private static final int ASCII_COUNT = 128; + + private final int nameLimit; + + private final BitSet escapeTargets; + + private final char replacement; + + private final char delimiter; + + /** + * Creates a new instance with default settings. + * @see #DEFAULT_NAME_LIMIT + * @see #DEFAULT_RESTRICTED_CHARACTERS + * @see #DEFAULT_REPLACEMENT + * @see #DEFAULT_DELIMITER + */ + public NameNormalizer() { + this(DEFAULT_NAME_LIMIT, DEFAULT_RESTRICTED_CHARACTERS, DEFAULT_REPLACEMENT, DEFAULT_DELIMITER); + } + + /** + * Creates a new instance. + * @param nameLimit the maximum name length. + * If exceeded, the name will be trimmed the trailing characters to the length. + * @param escapeTargets the escape target characters, each must be ASCII character + * @param replacement the replacement character, must be ASCII character + * @param delimiter the delimiter character, must be ASCII character + * @throws IllegalArgumentException if {@code nameLimit} is less than or equal to 0 + * @throws IllegalArgumentException if {@code escapeTargets} or {@code delimiter} contains non-ASCII character + */ + public NameNormalizer( + int nameLimit, + @Nonnull CharSequence escapeTargets, + char replacement, + char delimiter) { + if (nameLimit <= 0) { + throw new IllegalArgumentException(MessageFormat.format( + "name limit must be >= 0 ({0})", + nameLimit)); + } + Objects.requireNonNull(escapeTargets); + escapeTargets.codePoints().forEach(it -> checkAscii("escape target", it)); + checkAscii("replacement", replacement); + checkAscii("delimiter", delimiter); + this.nameLimit = nameLimit; + this.escapeTargets = toBitSet(escapeTargets); + this.replacement = replacement; + this.delimiter = delimiter; + } + + static BitSet toBitSet(CharSequence elements) { + return elements.codePoints() + .collect( + () -> new BitSet(ASCII_COUNT), + (r, c) -> r.set(c), + (a, b) -> a.or(b)); + } + + static List fromBitSet(BitSet elements) { + return elements.stream() + .sorted() + .mapToObj(Character::toString) + .collect(Collectors.toList()); + } + + private static void checkAscii(String name, int value) { + if (value >= ASCII_COUNT) { + throw new IllegalArgumentException(MessageFormat.format( + "{0} must be an ASCII character: {1}", + name, + String.format("U+%04X", value))); //$NON-NLS-1$ + } + } + + /** + * Normalizes the given table or query name. + * @param name the name + * @return the normalized name + */ + @Override + public String apply(@Nonnull String name) { + Objects.requireNonNull(name); + var codePoints = name.codePoints() + .sequential() + .map(c -> { + if (Character.isISOControl(c) + || Character.isWhitespace(c) + || Character.isSupplementaryCodePoint(c) + || c == delimiter + || escapeTargets.get(c)) { + return replacement; + } else if (Character.isUpperCase(c)) { + return Character.toLowerCase(c); + } + return c; + }) + .limit(nameLimit) + .toArray(); + return new String(codePoints, 0, codePoints.length); + } + + @Override + public int hashCode() { + return Objects.hash(nameLimit, escapeTargets, delimiter, replacement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + var other = (NameNormalizer) obj; + return delimiter == other.delimiter + && Objects.equals(escapeTargets, other.escapeTargets) + && replacement == other.replacement + && nameLimit == other.nameLimit; + } + + @Override + public String toString() { + return String.format( + "NamelNormalizer(nameLimit=%s, escapeTargets=%s, replacement=%s, delimiter=%s)", + nameLimit, + fromBitSet(escapeTargets), + replacement, + delimiter); + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java new file mode 100644 index 0000000..552bab4 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java @@ -0,0 +1,189 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.protobuf.TextFormat; +import com.tsurugidb.sql.proto.SqlCommon; +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.common.diagnostic.DiagnosticUtil; +import com.tsurugidb.tools.tgdump.core.model.DumpProfile; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tsubakuro.exception.ServerException; +import com.tsurugidb.tsubakuro.sql.SqlClient; +import com.tsurugidb.tsubakuro.sql.Transaction; +import com.tsurugidb.tsubakuro.sql.exception.CompileException; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An implementation of {@link DumpOperation} for SQL text. + */ +class QueryDumpOperation implements DumpOperation { + + private static final Logger LOG = LoggerFactory.getLogger(QueryDumpOperation.class); + + private final Set registered = ConcurrentHashMap.newKeySet(); + + private final DumpProfile dumpProfile; + + private final boolean createTargetDirectories; + + /** + * Creates a new instance. + * @param dumpProfile the dump operation settings + * @param createTargetDirectories whether or not to create dump target directories before the dump operations + */ + QueryDumpOperation(@NonNull DumpProfile dumpProfile, boolean createTargetDirectories) { + Objects.requireNonNull(dumpProfile); + this.dumpProfile = dumpProfile; + this.createTargetDirectories = createTargetDirectories; + } + + @Override + public boolean isEmpty() { + return registered.isEmpty(); + } + + private static void checkTargetType(DumpTarget target) { + if (target.getTargetType() != DumpTarget.TargetType.QUERY) { + throw new UnsupportedOperationException(MessageFormat.format( + "target type must be {0}: {1} ({2})", + DumpTarget.TargetType.QUERY, + target.getTargetType(), + target.getLabel())); + } + } + + @Override + public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + checkTargetType(target); + var label = target.getLabel(); + var statement = target.getTarget(); + if (registered.contains(statement)) { + monitor.verbose("skip already registerd query: {1} ({0})", label, statement); //$NON-NLS-1$ + return; + } + monitor.verbose("validating query: {0} ({1})", label, target.getDestination()); //$NON-NLS-1$ + try (var prepared = client.prepare(statement, List.of()).await()) { + // NOTE: we don't keep the prepared SQL statement to simplify the server-side resource management + registered.add(statement); + monitor.onDumpInfo(label, statement, target.getDestination()); + } catch (CompileException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.PREPARE_FAILURE, List.of(label, statement), e); + } catch (IOException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); + } catch (ServerException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.SERVER_ERROR, List.of(DiagnosticUtil.getMessage(e)), e); + } + } + + @Override + public void execute( + @NonNull SqlClient client, + @NonNull Transaction transaction, + @NonNull DumpMonitor monitor, + @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(transaction); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + checkTargetType(target); + var label = target.getLabel(); + var statement = target.getTarget(); + if (!registered.contains(statement)) { + throw new IllegalArgumentException(MessageFormat.format( + "the query \"{1}\" ({0}) has not been prepared", + label, + statement)); + } + monitor.verbose("preparing dump command: {0} ({1}: {2})", //$NON-NLS-1$ + label, transaction.getTransactionId(), statement); + var dumpOptions = dumpProfile.toProtocolBuffer(); + if (LOG.isDebugEnabled()) { + LOG.debug("dump options: {}", TextFormat.shortDebugString(dumpOptions)); + } + try (var prepared = client.prepare(statement, List.of()).await()) { + monitor.onDumpStart(label, target.getDestination()); + + // create target directory + if (createTargetDirectories) { + LOG.debug("creating dump target directory: {} ({})", label, target.getDestination()); + Files.createDirectories(target.getDestination()); + } + + try (var rs = transaction.executeDump(prepared, List.of(), target.getDestination(), dumpOptions).await()) { + monitor.verbose("start retrieving dump results: {0} ({1})", //$NON-NLS-1$ + label, transaction.getTransactionId()); + // NOTE: we assume the first column has provided dump file path (from operation specification). + var meta = rs.getMetadata(); + if (meta.getColumns().isEmpty()) { + // may not occur in general cases + throw new IllegalStateException("invalid result set format: colum list is empty"); + } + var column = meta.getColumns().get(0); + if (column.getTypeInfoCase() != SqlCommon.Column.TypeInfoCase.ATOM_TYPE + || column.getAtomType() != SqlCommon.AtomType.CHARACTER) { + // may not occur otherwise dump operation specification was changed + throw new IllegalStateException(MessageFormat.format( + "unexpected dump result type: {0} (expected: {1})", + column, + SqlCommon.AtomType.CHARACTER)); + } + while (rs.nextRow()) { + if (!rs.nextColumn()) { + throw new IllegalStateException("broken dump result (less columns in the result set)"); + } + var file = Path.of(rs.fetchCharacterValue()); + monitor.onDumpFile(label, file); + } + } catch (ServerException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.OPERATION_FAILURE, + List.of(label, statement), e); + } + monitor.onDumpFinish(label, target.getDestination()); + } catch (IOException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); + } catch (CompileException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.PREPARE_FAILURE, List.of(label, statement), e); + } catch (ServerException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.SERVER_ERROR, List.of(DiagnosticUtil.getMessage(e)), e); + } + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java new file mode 100644 index 0000000..0e79b67 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java @@ -0,0 +1,348 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; + +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Computes dump destination directories from each SQL text. + */ +public class QueryDumpTargetSelector implements DumpTargetSelector { + + private static final Logger LOG = LoggerFactory.getLogger(QueryDumpTargetSelector.class); + + private static final Function DEFAULT_NORMALIZER = new NameNormalizer(); + + /** + * Default restricted characters. + */ + public static final String DEFAULT_STOP_CHARACTERS = "\"'"; + + /** + * Default value of the default label prefix. + */ + public static final String DEFAULT_DEFAULT_PREFIX = "sql"; + + /** + * Default delimiter character between label and statement. + */ + public static final char DEFAULT_LABEL_DELIMITER = ':'; + + private static final String EMPTY_TEXT = ""; //$NON-NLS-1$ + + enum State { + INITIAL, + BODY, + PADDING, + FINISH, + } + + enum CharType { + NORMAL, + SPACE, + STOP, + DELIMITER, + } + + static class LabelAndStatement { + + private final @Nullable String label; + + private final String statement; + + LabelAndStatement(@Nonnull String statement) { + this(null, statement); + } + + LabelAndStatement(String label, @Nonnull String statement) { + Objects.requireNonNull(statement); + this.label = label; + this.statement = statement; + } + + Optional getLabel() { + return Optional.ofNullable(label); + } + + String getStatement() { + return statement; + } + + @Override + public int hashCode() { + return Objects.hash(label, statement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + var other = (LabelAndStatement) obj; + return Objects.equals(label, other.label) && Objects.equals(statement, other.statement); + } + + @Override + public String toString() { + return String.format("(%s, %s)", label, statement); //$NON-NLS-1$ + } + } + + private Function normalizer; + + private final char labelDelimiter; + + private final String defaultPrefix; + + private final BitSet stopCharacters; + + private final char sequenceDelimiter; + + /** + * Creates a new instance with default settings. + */ + public QueryDumpTargetSelector() { + this( + DEFAULT_NORMALIZER, + DEFAULT_LABEL_DELIMITER, + DEFAULT_STOP_CHARACTERS, + DEFAULT_DEFAULT_PREFIX, + DEFAULT_SEQUENCE_DELIMITER); + } + + /** + * Creates a new instance. + * @param normalizer the destination name normalizer + * @param delimiter the delimiter character between label and statement + * @param stopCharacters the characters which stops the label + * @param defaultPrefix the prefix if label is not specified + * @param sequenceDelimiter the delimiter character for path and its sequence + * @throws IllegalArgumentException if {@code stopCharacters} contains non-ASCII character + */ + public QueryDumpTargetSelector( + @Nonnull Function normalizer, + char delimiter, + @Nonnull CharSequence stopCharacters, + @Nonnull String defaultPrefix, + char sequenceDelimiter) { + Objects.requireNonNull(normalizer); + Objects.requireNonNull(stopCharacters); + Objects.requireNonNull(defaultPrefix); + this.normalizer = normalizer; + this.labelDelimiter = delimiter; + this.defaultPrefix = defaultPrefix; + this.stopCharacters = NameNormalizer.toBitSet(stopCharacters); + this.sequenceDelimiter = sequenceDelimiter; + } + + LabelAndStatement parseCommand(String command) { + LOG.trace("enter: parseCommand: {}", command); //$NON-NLS-1$ + var result = parseCommand0(command); + LOG.trace("exit: parseCommand: {}", result); //$NON-NLS-1$ + return result; + } + + private LabelAndStatement parseCommand0(String command) { + var state = State.INITIAL; + int labelStart = -1; + int labelEnd = -1; + for (int position = 0, n = command.length(); position < n; position++) { + var c = command.charAt(position); + var type = classifyChar(c); + if (LOG.isTraceEnabled()) { + LOG.trace("step: parseCommand: position={}, state={}, type={}", position, state, type); //$NON-NLS-1$ + } + switch (state) { + case INITIAL: + switch (type) { + case NORMAL: + state = State.BODY; + labelStart = position; + break; + case SPACE: + // continue + break; + case DELIMITER: + state = State.FINISH; + labelStart = position; + labelEnd = position; + break; + default: + return new LabelAndStatement(command); + } + break; + case BODY: + switch (type) { + case NORMAL: + // continue + break; + case SPACE: + state = State.PADDING; + labelEnd = position; + break; + case DELIMITER: + state = State.FINISH; + labelEnd = position; + break; + default: + return new LabelAndStatement(command); + } + break; + case PADDING: + switch (type) { + case NORMAL: + return new LabelAndStatement(command); + case SPACE: + // continue + break; + case DELIMITER: + state = State.FINISH; + break; + default: + return new LabelAndStatement(command); + } + break; + case FINISH: + switch (type) { + case SPACE: + // continue + break; + default: + return new LabelAndStatement( + command.substring(labelStart, labelEnd), + command.substring(position)); + } + break; + default: + throw new AssertionError(); // never reach + } + } + if (state == State.FINISH) { + // statement is empty + return new LabelAndStatement( + command.substring(labelStart, labelEnd), + EMPTY_TEXT); + } + // delimiter not found + return new LabelAndStatement(command); + } + + private CharType classifyChar(char c) { + if (Character.isWhitespace(c)) { + return CharType.SPACE; + } else if (Character.isISOControl(c) || stopCharacters.get(c)) { + return CharType.STOP; + } else if (c == labelDelimiter) { + return CharType.DELIMITER; + } + return CharType.NORMAL; + } + + /** + * Computes {@link DumpTarget dump targets} for each table. + * @param destinationDirectory the base destination directory, + * each dump target will be placed under it. + * @param commands the table name list + * @return the dump targets for the tables + */ + @Override + public List getTargets(@Nonnull Path destinationDirectory, @Nonnull List commands) { + Objects.requireNonNull(destinationDirectory); + Objects.requireNonNull(commands); + LOG.trace("enter: getTargets: {}, {}", destinationDirectory, commands); //$NON-NLS-1$ + var targets = new ArrayList(); + for (var command : commands) { + int position = targets.size() + 1; + var parsed = parseCommand(command); + var label = parsed.getLabel() + .orElseGet(() -> { + return String.format("%s%d", defaultPrefix, position); //$NON-NLS-1$ + }); //$NON-NLS-1$ + var statement = parsed.getStatement(); + if (label.isEmpty()) { + throw new IllegalArgumentException(MessageFormat.format( + "label at {0} must not be empty: {1}", + position, + command)); + } + if (statement.isEmpty()) { + throw new IllegalArgumentException(MessageFormat.format( + "statement at {0} must not be empty: {1}", + position, + command)); + } + var destination = destinationDirectory.resolve(normalizer.apply(label)); + targets.add(new DumpTarget(DumpTarget.TargetType.QUERY, label, statement, destination)); + } + var results = DumpTargetSelector.resolveConflicts(targets, sequenceDelimiter); + LOG.trace("exit: getTargets: {}", results); //$NON-NLS-1$ + return results; + } + + @Override + public int hashCode() { + return Objects.hash(normalizer, labelDelimiter, defaultPrefix, stopCharacters); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + var other = (QueryDumpTargetSelector) obj; + return Objects.equals(normalizer, other.normalizer) && labelDelimiter == other.labelDelimiter + && Objects.equals(defaultPrefix, other.defaultPrefix) + && Objects.equals(stopCharacters, other.stopCharacters); + } + + @Override + public String toString() { + return String.format( + "QueryDumpTargetSelector(normalizer=%s, delimiter=%s, defaultPrefix=%s, stopCharacters=%s)", + normalizer, + labelDelimiter, + defaultPrefix, + NameNormalizer.fromBitSet(stopCharacters)); + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java new file mode 100644 index 0000000..c75f128 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java @@ -0,0 +1,219 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.protobuf.TextFormat; +import com.tsurugidb.sql.proto.SqlCommon; +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.common.diagnostic.DiagnosticUtil; +import com.tsurugidb.tools.tgdump.core.model.DumpProfile; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tsubakuro.exception.ServerException; +import com.tsurugidb.tsubakuro.sql.SqlClient; +import com.tsurugidb.tsubakuro.sql.TableMetadata; +import com.tsurugidb.tsubakuro.sql.Transaction; +import com.tsurugidb.tsubakuro.sql.exception.CompileException; +import com.tsurugidb.tsubakuro.sql.exception.TargetNotFoundException; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * An implementation of {@link DumpOperation} for table dump. + */ +class TableDumpOperation implements DumpOperation { + + private static final Logger LOG = LoggerFactory.getLogger(TableDumpOperation.class); + + private static final String SQL_DUMP_QUERY = "SELECT * FROM %s"; //$NON-NLS-1$ + + private final Map registered = new ConcurrentHashMap<>(); + + private final DumpProfile dumpProfile; + + private final boolean createTargetDirectories; + + /** + * Creates a new instance. + * @param dumpProfile the dump operation settings + * @param createTargetDirectories whether or not to create dump target directories before the dump operations + */ + TableDumpOperation(@NonNull DumpProfile dumpProfile, boolean createTargetDirectories) { + Objects.requireNonNull(dumpProfile); + this.dumpProfile = dumpProfile; + this.createTargetDirectories = createTargetDirectories; + } + + @Override + public boolean isEmpty() { + return registered.isEmpty(); + } + + @Override + public List getTargetTables() { + return List.copyOf(registered.keySet()); + } + + private static void checkTargetType(DumpTarget target) { + if (target.getTargetType() != DumpTarget.TargetType.TABLE) { + throw new UnsupportedOperationException(MessageFormat.format( + "target type must be {0}: {1} ({2})", + DumpTarget.TargetType.TABLE, + target.getTargetType(), + target.getLabel())); + } + } + + @Override + public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + checkTargetType(target); + String table = target.getTableName(); + if (registered.containsKey(table)) { + monitor.verbose("skip already registerd table: {0}", table); //$NON-NLS-1$ + return; + } + monitor.verbose("inspecting dump table: {0} ({1})", table, target.getDestination()); //$NON-NLS-1$ + try { + var metadata = client.getTableMetadata(table).await(); + registered.put(table, metadata); + monitor.onDumpInfo(table, metadata, target.getDestination()); + } catch (TargetNotFoundException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.TABLE_NOT_FOUND, List.of(table), e); + } catch (IOException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); + } catch (ServerException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.SERVER_ERROR, List.of(DiagnosticUtil.getMessage(e)), e); + } + } + + @Override + public void execute( + @NonNull SqlClient client, + @NonNull Transaction transaction, + @NonNull DumpMonitor monitor, + @NonNull DumpTarget target) + throws InterruptedException, DiagnosticException { + Objects.requireNonNull(client); + Objects.requireNonNull(transaction); + Objects.requireNonNull(monitor); + Objects.requireNonNull(target); + checkTargetType(target); + var table = target.getTableName(); + if (!registered.containsKey(table)) { + throw new IllegalArgumentException(MessageFormat.format( + "the table {0} has not been prepared", + table)); + } + var statement = createStatement(table); + monitor.verbose("preparing dump command: {0} ({1}: {2})", //$NON-NLS-1$ + table, transaction.getTransactionId(), statement); + var dumpOptions = dumpProfile.toProtocolBuffer(); + if (LOG.isDebugEnabled()) { + LOG.debug("dump options: {}", TextFormat.shortDebugString(dumpOptions)); + } + try (var prepared = client.prepare(statement, List.of()).await()) { + monitor.onDumpStart(table, target.getDestination()); + + // create target directory + if (createTargetDirectories) { + LOG.debug("creating dump target directory: {} ({})", table, target.getDestination()); + Files.createDirectories(target.getDestination()); + } + + try (var rs = transaction.executeDump(prepared, List.of(), target.getDestination(), dumpOptions).await()) { + monitor.verbose("start retrieving dump results: {0} ({1})", //$NON-NLS-1$ + table, transaction.getTransactionId()); + // NOTE: we assume the first column has provided dump file path (from operation specification). + var meta = rs.getMetadata(); + if (meta.getColumns().isEmpty()) { + // may not occur in general cases + throw new IllegalStateException("invalid result set format: column list is empty"); + } + var column = meta.getColumns().get(0); + if (column.getTypeInfoCase() != SqlCommon.Column.TypeInfoCase.ATOM_TYPE + || column.getAtomType() != SqlCommon.AtomType.CHARACTER) { + // may not occur otherwise dump operation specification was changed + throw new IllegalStateException(MessageFormat.format( + "unexpected dump result type: {0} (expected: {1})", + column, + SqlCommon.AtomType.CHARACTER)); + } + while (rs.nextRow()) { + if (!rs.nextColumn()) { + throw new IllegalStateException("broken dump result (less columns in the result set)"); + } + var file = Path.of(rs.fetchCharacterValue()); + monitor.onDumpFile(table, file); + } + } catch (ServerException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.OPERATION_FAILURE, + List.of(table, statement), e); + } + monitor.onDumpFinish(table, target.getDestination()); + } catch (IOException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.IO_ERROR, List.of(e.toString()), e); + } catch (CompileException e) { + LOG.debug("exception was occurred in prepare", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.PREPARE_FAILURE, List.of(table, statement), e); + } catch (ServerException e) { + LOG.debug("exception was occurred in execute", e); //$NON-NLS-1$ + throw new DumpException(DumpDiagnosticCode.SERVER_ERROR, List.of(DiagnosticUtil.getMessage(e)), e); + } + } + + private static final Pattern PATTERN_REGULAR_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); //$NON-NLS-1$ + + private static String createStatement(String tableName) { + var matcher = PATTERN_REGULAR_IDENTIFIER.matcher(tableName); + if (matcher.matches()) { + return String.format(SQL_DUMP_QUERY, tableName); + } + var string = new StringBuilder(); + string.append('"'); + for (int i = 0; i < tableName.length(); i++) { + var c = tableName.charAt(i); + if (c == '"') { + string.append('"'); + string.append('"'); + } else { + string.append(c); + } + } + string.append('"'); + return String.format(SQL_DUMP_QUERY, string.toString()); + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelector.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelector.java new file mode 100644 index 0000000..3b1ecc0 --- /dev/null +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelector.java @@ -0,0 +1,115 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; + +/** + * Computes table dump destination directories from each table name. + */ +public class TableDumpTargetSelector implements DumpTargetSelector { + + private static final Logger LOG = LoggerFactory.getLogger(TableDumpTargetSelector.class); + + private static final Function DEFAULT_NORMALIZER = new NameNormalizer(); + + private Function normalizer; + + private final char delimiter; + + /** + * Creates a new instance with default settings. + */ + public TableDumpTargetSelector() { + this(DEFAULT_NORMALIZER, DEFAULT_SEQUENCE_DELIMITER); + } + + /** + * Creates a new instance. + * @param normalizer the destination name normalizer + * @param delimiter the separator character between name and sequence number, must be escaped within the normalizer + */ + public TableDumpTargetSelector(@Nonnull Function normalizer, char delimiter) { + Objects.requireNonNull(normalizer); + this.normalizer = normalizer; + this.delimiter = delimiter; + } + + /** + * Computes {@link DumpTarget dump targets} for each table. + * @param destinationDirectory the base destination directory, + * each dump target will be placed under it. + * @param commands the table name list + * @return the dump targets for the tables + */ + @Override + public List getTargets(@Nonnull Path destinationDirectory, @Nonnull List commands) { + Objects.requireNonNull(destinationDirectory); + Objects.requireNonNull(commands); + LOG.trace("enter: getTargets: {}, {}", destinationDirectory, commands); //$NON-NLS-1$ + var results = commands.stream() + .map(it -> it.strip()) + .peek(it -> { + if (it.isEmpty()) { + throw new IllegalArgumentException("table name must not be empty"); + } + }) + .map(tableName -> new DumpTarget( + tableName, + destinationDirectory.resolve(normalizer.apply(tableName)))) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + it -> DumpTargetSelector.resolveConflicts(it, delimiter))); + LOG.trace("exit: getTargets: {}", results); //$NON-NLS-1$ + return results; + } + + @Override + public int hashCode() { + return Objects.hash(normalizer, delimiter); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + var other = (TableDumpTargetSelector) obj; + return Objects.equals(normalizer, other.normalizer) && delimiter == other.delimiter; + } + + @Override + public String toString() { + return String.format("TableDumpTargetSelector(normalizer=%s, delimiter=%s)", normalizer, delimiter); + } +} diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/DumpTarget.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/DumpTarget.java index 18865ca..5a048b7 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/DumpTarget.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/DumpTarget.java @@ -21,32 +21,101 @@ import javax.annotation.Nonnull; /** - * Represents a table dump target. + * Represents target of dump operation. */ public class DumpTarget { - private final String tableName; + /** + * Represents a target type. + */ + public enum TargetType { + + /** + * Operation was represented as a query text. + */ + QUERY, + + /** + * Represents a table target. + */ + TABLE, + } + + private final TargetType targetType; + + private final String label; + + private final String target; private final Path destination; /** * Creates a new instance. - * @param tableName the source table name + * @param targetType the target type + * @param label the target label + * @param target the target text depending on the target type * @param destination the dump destination path (directory) */ - public DumpTarget(@Nonnull String tableName, Path destination) { - Objects.requireNonNull(tableName); + public DumpTarget( + @Nonnull TargetType targetType, + @Nonnull String label, + @Nonnull String target, + @Nonnull Path destination) { + Objects.requireNonNull(targetType); + Objects.requireNonNull(label); + Objects.requireNonNull(target); Objects.requireNonNull(destination); - this.tableName = tableName; + this.targetType = targetType; + this.label = label; + this.target = target; this.destination = destination; } + /** + * Creates a new instance for a dump target. + * @param tableName the source table name + * @param destination the dump destination path (directory) + */ + public DumpTarget(@Nonnull String tableName, Path destination) { + this(TargetType.TABLE, tableName, tableName, destination); + } + + /** + * Returns the target type. + * @return the target type + */ + public TargetType getTargetType() { + return targetType; + } + + /** + * Returns the target text for the database dump operation. + * This can either be the name of a table or a SQL text, depending on {@link #getTargetType()}. + * @return the target text + * @see #getTargetType() + */ + public String getTarget() { + return target; + } + + /** + * Returns the target label. + * @return the target label + */ + public String getLabel() { + return label; + } + /** * Returns the source table name. * @return the table name + * @throws IllegalStateException if the {@link #getTargetType() target type} is not {@link TargetType#TABLE} */ public String getTableName() { - return tableName; + if (targetType != TargetType.TABLE) { + throw new IllegalStateException("Target type must be TABLE"); + } + return target; } /** @@ -59,7 +128,7 @@ public Path getDestination() { @Override public int hashCode() { - return Objects.hash(destination, tableName); + return Objects.hash(targetType, label, target, destination); } @Override @@ -74,11 +143,16 @@ public boolean equals(Object obj) { return false; } DumpTarget other = (DumpTarget) obj; - return Objects.equals(destination, other.destination) && Objects.equals(tableName, other.tableName); + return targetType == other.targetType + && Objects.equals(label, other.label) + && Objects.equals(target, other.target) + && Objects.equals(destination, other.destination); } @Override public String toString() { - return String.format("DumpTarget [tableName=%s, destination=%s]", tableName, destination); //$NON-NLS-1$ + return String.format( + "DumpTarget [targetType=%s, label=%s, target=%s, destination=%s]", //$NON-NLS1$ + targetType, label, target, destination); } } diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/package-info.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/package-info.java index 24164ec..3825106 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/package-info.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/model/package-info.java @@ -14,6 +14,6 @@ * limitations under the License. */ /** - * Model classes for Tsurugi Table Dump Tool. + * Model classes for Tsurugi Dump Tool. */ package com.tsurugidb.tools.tgdump.core.model; \ No newline at end of file diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSessionTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSessionTest.java index d1224d3..d2a8950 100644 --- a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSessionTest.java +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/BasicDumpSessionTest.java @@ -15,21 +15,20 @@ */ package com.tsurugidb.tools.tgdump.core.engine; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; -import com.tsurugidb.sql.proto.SqlCommon; import com.tsurugidb.sql.proto.SqlRequest; import com.tsurugidb.tools.common.diagnostic.DiagnosticException; import com.tsurugidb.tools.tgdump.core.model.DumpProfile; @@ -40,158 +39,26 @@ import com.tsurugidb.tsubakuro.exception.ServerException; import com.tsurugidb.tsubakuro.sql.PreparedStatement; import com.tsurugidb.tsubakuro.sql.ResultSet; -import com.tsurugidb.tsubakuro.sql.ResultSetMetadata; -import com.tsurugidb.tsubakuro.sql.SqlClient; import com.tsurugidb.tsubakuro.sql.SqlServiceCode; import com.tsurugidb.tsubakuro.sql.TableMetadata; import com.tsurugidb.tsubakuro.sql.Transaction; -import com.tsurugidb.tsubakuro.sql.Types; -import com.tsurugidb.tsubakuro.sql.exception.EvaluationException; import com.tsurugidb.tsubakuro.sql.exception.OccException; -import com.tsurugidb.tsubakuro.sql.exception.SyntaxException; -import com.tsurugidb.tsubakuro.sql.exception.TargetNotFoundException; -import com.tsurugidb.tsubakuro.sql.impl.EmptyRelationCursor; import com.tsurugidb.tsubakuro.util.FutureResponse; class BasicDumpSessionTest { - static class MockSqlClient implements SqlClient { - - private final Transaction tx; - - MockSqlClient() { - this(new MockTransaction()); - } - - MockSqlClient(Transaction tx) { - this.tx = tx; - } - - @Override - public FutureResponse getTableMetadata(String tableName) throws IOException { - return FutureResponse.returns(new MockTableMetadata("T1")); - } - - @Override - public FutureResponse prepare( - String source, - Collection placeholders) - throws IOException { - return FutureResponse.returns(new MockPreparedStatement(source)); - } - - @Override - public FutureResponse createTransaction(SqlRequest.TransactionOption option) throws IOException { - return FutureResponse.returns(tx); - } - } - - static class MockTransaction implements Transaction { - - @Override - public FutureResponse executeDump( - PreparedStatement statement, - Collection parameters, - Path directory, - SqlRequest.DumpOption option) throws IOException { - return FutureResponse.returns(new MockResultSet(List.of(dumpFile(directory, 1).toString()))); - } - - @Override - public FutureResponse commit(SqlRequest.CommitStatus status) throws IOException { - return FutureResponse.returns(null); - } - - @Override - public String getTransactionId() { - return "TXID-TESTING"; - } - } - - static class MockPreparedStatement implements PreparedStatement { - - private final String statement; - - MockPreparedStatement(String statement) { - this.statement = statement; - } - - public String getStatement() { - return statement; - } - - @Override - public boolean hasResultRecords() { - return true; - } - - @Override - public void close() { - return; - } - } - - static class MockResultSet extends EmptyRelationCursor implements ResultSet { - - private final List results; - - private int rowPosition; - - private int columnPosition; - - MockResultSet(List results) { - this.results = results; - this.rowPosition = -1; - this.columnPosition = -1; - } - - @Override - public ResultSetMetadata getMetadata() throws IOException, ServerException, InterruptedException { - // just assumes single character column. - return new ResultSetMetadata() { - @Override - public List getColumns() { - return List.of(Types.column(String.class)); - } - }; - } - - @Override - public boolean nextRow() { - if (rowPosition + 1 < results.size()) { - rowPosition++; - columnPosition = -1; - return true; - } - return false; - } - - @Override - public boolean nextColumn() { - if (columnPosition < 0) { - columnPosition++; - return true; - } - return false; - } - - @Override - public String fetchCharacterValue() { - if (rowPosition < 0 || rowPosition >= results.size() || columnPosition != 0) { - throw new IllegalStateException(); - } - return results.get(rowPosition); - } - } - private final TransactionSettings.Builder tx = TransactionSettings.newBuilder(); private final DumpProfile.Builder profile = DumpProfile.newBuilder(); private final MockDumpMonitor monitor = new MockDumpMonitor(); - private static DumpTarget target(String tableName) { - return new DumpTarget(tableName, destination(tableName)); + private static DumpTarget table(String tableName) { + return new DumpTarget(DumpTarget.TargetType.TABLE, tableName, tableName, destination(tableName)); + } + + private static DumpTarget query(String label, String sql) { + return new DumpTarget(DumpTarget.TargetType.QUERY, label, sql, destination(label)); } private static Path destination(String tableName) { @@ -219,7 +86,7 @@ public FutureResponse getTableMetadata(String tableName) throws I }; var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); assertEquals(BasicDumpSession.State.PREPARING, session.getState()); assertEquals(List.of("T1"), registered); assertEquals(Map.of("T1", destination("T1")), monitor.getInfo()); @@ -227,8 +94,8 @@ public FutureResponse getTableMetadata(String tableName) throws I } @Test - void register_multiple() throws Exception { - var registered = new ArrayList(); + void register_mixed() throws Exception { + var registered = new HashSet(); try ( var client = new MockSqlClient() { @Override @@ -236,85 +103,23 @@ public FutureResponse getTableMetadata(String tableName) throws I registered.add(tableName); return super.getTableMetadata(tableName); } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T1")); - session.register(monitor, target("T2")); - session.register(monitor, target("T3")); - assertEquals(BasicDumpSession.State.PREPARING, session.getState()); - assertEquals(List.of("T1", "T2", "T3"), registered); - assertEquals( - Map.of("T1", destination("T1"), "T2", destination("T2"), "T3", destination("T3")), - monitor.getInfo()); - } - } - - @Test - void register_duplicate() throws Exception { - var registered = new ArrayList(); - try ( - var client = new MockSqlClient() { @Override - public FutureResponse getTableMetadata(String tableName) throws IOException { - registered.add(tableName); - return super.getTableMetadata(tableName); + public FutureResponse prepare(String source, Collection placeholders) throws IOException { + registered.add(source); + return super.prepare(source, placeholders); } }; var session = createSession(client); ) { - session.register(monitor, target("T1")); - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); + session.register(monitor, query("Q1", "SELECT 1")); assertEquals(BasicDumpSession.State.PREPARING, session.getState()); - assertEquals(List.of("T1"), registered); - } - } - - @Test - void register_not_found() throws Exception { - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse getTableMetadata(String tableName) throws IOException { - return FutureResponse.raises(new TargetNotFoundException(SqlServiceCode.TARGET_NOT_FOUND_EXCEPTION)); - } - }; - var session = createSession(client); - ) { - var e = assertThrows(DiagnosticException.class, () -> session.register(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.TABLE_NOT_FOUND, e.getDiagnosticCode()); - } - } - - @Test - void register_io_error() throws Exception { - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse getTableMetadata(String tableName) throws IOException { - throw new IOException(); - } - }; - var session = createSession(client); - ) { - var e = assertThrows(DiagnosticException.class, () -> session.register(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); - } - } - - @Test - void register_server_error() throws Exception { - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse getTableMetadata(String tableName) throws IOException { - return FutureResponse.raises(new CoreServiceException(CoreServiceCode.PERMISSION_ERROR)); - } - }; - var session = createSession(client); - ) { - var e = assertThrows(DiagnosticException.class, () -> session.register(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.SERVER_ERROR, e.getDiagnosticCode()); + assertEquals(Set.of("T1", "SELECT 1"), registered); + assertEquals( + Map.of( + "T1", destination("T1"), + "Q1", destination("Q1")), + monitor.getInfo()); } } @@ -332,7 +137,7 @@ public FutureResponse createTransaction(SqlRequest.TransactionOptio }; var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); assertEquals(BasicDumpSession.State.RUNNING, session.getState()); } @@ -350,7 +155,7 @@ public FutureResponse createTransaction(SqlRequest.TransactionOptio }; var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); var e = assertThrows(DumpException.class, () -> session.begin(monitor)); assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); assertEquals(BasicDumpSession.State.FAILED, session.getState()); @@ -369,7 +174,7 @@ public FutureResponse createTransaction(SqlRequest.TransactionOptio }; var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); var e = assertThrows(DumpException.class, () -> session.begin(monitor)); assertEquals(DumpDiagnosticCode.BEGIN_FAILURE, e.getDiagnosticCode()); assertEquals(BasicDumpSession.State.FAILED, session.getState()); @@ -399,9 +204,9 @@ void register_after_begin() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - assertThrows(IllegalStateException.class, () -> session.register(monitor, target("T1"))); + assertThrows(IllegalStateException.class, () -> session.register(monitor, table("T1"))); } } @@ -412,7 +217,7 @@ void begin_after_begin() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); assertThrows(IllegalStateException.class, () -> session.begin(monitor)); assertEquals(BasicDumpSession.State.RUNNING, session.getState()); @@ -422,67 +227,6 @@ void begin_after_begin() throws Exception { @Test void execute() throws Exception { var statements = new ArrayList(); - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse prepare( - String source, - Collection placeholders) throws IOException { - statements.add(source); - return super.prepare(source, placeholders); - } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T1")); - session.begin(monitor); - session.execute(monitor, target("T1")); - assertEquals(Map.of("T1", List.of(dumpFile(destination("T1"), 1))), monitor.getFiles()); - assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); - assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); - assertEquals(BasicDumpSession.State.RUNNING, session.getState()); - - assertEquals(List.of("SELECT * FROM T1"), statements); - } - } - - @Test - void execute_multiple_tables() throws Exception { - try ( - var client = new MockSqlClient(); - var session = createSession(client); - ) { - session.register(monitor, target("T1")); - session.register(monitor, target("T2")); - session.register(monitor, target("T3")); - session.begin(monitor); - session.execute(monitor, target("T3")); - session.execute(monitor, target("T2")); - session.execute(monitor, target("T1")); - assertEquals( - Map.of( - "T1", List.of(dumpFile(destination("T1"), 1)), - "T2", List.of(dumpFile(destination("T2"), 1)), - "T3", List.of(dumpFile(destination("T3"), 1))), - monitor.getFiles()); - assertEquals( - Map.of( - "T1", destination("T1"), - "T2", destination("T2"), - "T3", destination("T3")), - monitor.getFinish()); - assertEquals( - Map.of( - "T1", destination("T1"), - "T2", destination("T2"), - "T3", destination("T3")), - monitor.getStart()); - assertEquals(BasicDumpSession.State.RUNNING, session.getState()); - } - } - - @Test - void execute_multiple_files() throws Exception { try ( var client = new MockSqlClient(new MockTransaction() { @Override @@ -491,117 +235,27 @@ public FutureResponse executeDump( Collection parameters, Path directory, SqlRequest.DumpOption option) throws IOException { - return FutureResponse.returns(new MockResultSet(List.of( - dumpFile(directory, 1).toString(), - dumpFile(directory, 2).toString(), - dumpFile(directory, 3).toString()))); + statements.add(((MockPreparedStatement) statement).getStatement()); + return super.executeDump(statement, parameters, directory, option); } }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); - assertEquals( - Map.of( - "T1", - List.of( - dumpFile(destination("T1"), 1), - dumpFile(destination("T1"), 2), - dumpFile(destination("T1"), 3))), - monitor.getFiles()); + session.execute(monitor, table("T1")); + assertEquals(Map.of("T1", List.of(dumpFile(destination("T1"), 1))), monitor.getFiles()); assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); assertEquals(BasicDumpSession.State.RUNNING, session.getState()); - } - } - - @Test - void execute_table_name_non_regular() throws Exception { - var statements = new ArrayList(); - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse prepare( - String source, - Collection placeholders) throws IOException { - statements.add(source); - return super.prepare(source, placeholders); - } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T 1")); - session.begin(monitor); - session.execute(monitor, target("T 1")); - assertEquals(List.of("SELECT * FROM \"T 1\""), statements); - } - } - - @Test - void execute_table_name_delimiter() throws Exception { - var statements = new ArrayList(); - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse prepare( - String source, - Collection placeholders) throws IOException { - statements.add(source); - return super.prepare(source, placeholders); - } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T\"1")); - session.begin(monitor); - session.execute(monitor, target("T\"1")); - assertEquals(List.of("SELECT * FROM \"T\"\"1\""), statements); - } - } - @Test - void execute_prepare_failure() throws Exception { - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse prepare( - String source, - Collection placeholders) throws IOException { - return FutureResponse.raises(new SyntaxException(SqlServiceCode.SYNTAX_EXCEPTION)); - } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T1")); - session.begin(monitor); - var e = assertThrows(DiagnosticException.class, () -> session.execute(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.PREPARE_FAILURE, e.getDiagnosticCode()); - } - } - - @Test - void execute_prepare_failure_io_error() throws Exception { - try ( - var client = new MockSqlClient() { - @Override - public FutureResponse prepare( - String source, - Collection placeholders) throws IOException { - throw new IOException(); - } - }; - var session = createSession(client); - ) { - session.register(monitor, target("T1")); - session.begin(monitor); - var e = assertThrows(DiagnosticException.class, () -> session.execute(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + assertEquals(List.of("SELECT * FROM T1"), statements); } } @Test - void execute_operation_failure() throws Exception { + void execute_mixed() throws Exception { + var statements = new HashSet(); try ( var client = new MockSqlClient(new MockTransaction() { @Override @@ -610,37 +264,38 @@ public FutureResponse executeDump( Collection parameters, Path directory, SqlRequest.DumpOption option) throws IOException { - return FutureResponse.raises(new EvaluationException(SqlServiceCode.EVALUATION_EXCEPTION)); + statements.add(((MockPreparedStatement) statement).getStatement()); + return super.executeDump(statement, parameters, directory, option); } }); var session = createSession(client); ) { - session.register(monitor, target("T1")); - session.begin(monitor); - var e = assertThrows(DiagnosticException.class, () -> session.execute(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.OPERATION_FAILURE, e.getDiagnosticCode()); - } - } + var t1 = table("T1"); + var t2 = query("Q1", "SELECT 1"); - @Test - void execute_operation_failure_io_error() throws Exception { - try ( - var client = new MockSqlClient(new MockTransaction() { - @Override - public FutureResponse executeDump( - PreparedStatement statement, - Collection parameters, - Path directory, - SqlRequest.DumpOption option) throws IOException { - throw new IOException(); - } - }); - var session = createSession(client); - ) { - session.register(monitor, target("T1")); + session.register(monitor, t1); + session.register(monitor, t2); session.begin(monitor); - var e = assertThrows(DiagnosticException.class, () -> session.execute(monitor, target("T1"))); - assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + session.execute(monitor, t1); + session.execute(monitor, t2); + assertEquals( + Map.of( + "T1", List.of(dumpFile(destination("T1"), 1)), + "Q1", List.of(dumpFile(destination("Q1"), 1))), + monitor.getFiles()); + assertEquals( + Map.of( + "T1", destination("T1"), + "Q1", destination("Q1")), + monitor.getStart()); + assertEquals( + Map.of( + "T1", destination("T1"), + "Q1", destination("Q1")), + monitor.getFinish()); + assertEquals(BasicDumpSession.State.RUNNING, session.getState()); + + assertEquals(Set.of("SELECT * FROM T1", "SELECT 1"), statements); } } @@ -650,8 +305,8 @@ void execute_before_begin() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); - assertThrows(IllegalStateException.class, () -> session.execute(monitor, target("T1"))); + session.register(monitor, table("T1")); + assertThrows(IllegalStateException.class, () -> session.execute(monitor, table("T1"))); } } @@ -661,9 +316,9 @@ void execute_without_register() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - assertThrows(IllegalArgumentException.class, () -> session.execute(monitor, target("XXX"))); + assertThrows(IllegalArgumentException.class, () -> session.execute(monitor, table("XXX"))); } } @@ -680,9 +335,9 @@ public FutureResponse commit(SqlRequest.CommitStatus status) throws IOExce }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); session.commit(monitor); assertEquals(BasicDumpSession.State.COMMITTED, session.getState()); assertTrue(committedRef.get()); @@ -700,9 +355,9 @@ public FutureResponse commit(SqlRequest.CommitStatus status) throws IOExce }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); var e = assertThrows(DiagnosticException.class, () -> session.commit(monitor)); assertEquals(DumpDiagnosticCode.COMMIT_FAILURE, e.getDiagnosticCode()); assertEquals(BasicDumpSession.State.FAILED, session.getState()); @@ -720,9 +375,9 @@ public FutureResponse commit(SqlRequest.CommitStatus status) throws IOExce }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); var e = assertThrows(DiagnosticException.class, () -> session.commit(monitor)); assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); assertEquals(BasicDumpSession.State.FAILED, session.getState()); @@ -735,7 +390,7 @@ void commit_before_begin() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); assertThrows(IllegalStateException.class, () -> session.commit(monitor)); } } @@ -746,9 +401,9 @@ void close() throws Exception { var client = new MockSqlClient(); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); session.commit(monitor); session.close(); assertEquals(BasicDumpSession.State.CLOSED, session.getState()); @@ -770,9 +425,9 @@ public void close() { }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); session.close(); assertTrue(closedRef.get()); } @@ -789,9 +444,9 @@ public void close() throws ServerException { }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); session.commit(monitor); var e = assertThrows(DiagnosticException.class, () -> session.close()); assertEquals(DumpDiagnosticCode.SERVER_ERROR, e.getDiagnosticCode()); @@ -809,9 +464,9 @@ public void close() throws IOException { }); var session = createSession(client); ) { - session.register(monitor, target("T1")); + session.register(monitor, table("T1")); session.begin(monitor); - session.execute(monitor, target("T1")); + session.execute(monitor, table("T1")); session.commit(monitor); var e = assertThrows(DiagnosticException.class, () -> session.close()); assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelectorTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelectorTest.java deleted file mode 100644 index 47ee021..0000000 --- a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/DumpTargetSelectorTest.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2023-2024 Project Tsurugi. - * - * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.File; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import com.tsurugidb.tools.tgdump.core.model.DumpTarget; - -class DumpTargetSelectorTest { - - @Test - void normalize_identity() { - var selector = new DumpTargetSelector(); - assertEquals("testing", selector.normalize("testing")); - } - - @Test - void normalize_iso_control() { - var selector = new DumpTargetSelector(); - assertEquals("testing____", selector.normalize("testing\0\t\r\n")); - } - - @Test - void normalize_supplementary() { - var selector = new DumpTargetSelector(); - assertEquals("testing_", selector.normalize("testing" + String.valueOf(Character.toChars(0x1F600)))); - } - - @Test - void normalize_whitespace() { - var selector = new DumpTargetSelector(); - assertEquals("hello_world", selector.normalize("hello world")); - } - - @Test - void normalize_delimiter() { - var selector = new DumpTargetSelector(100, "", '@', ':'); - assertEquals("hello@world", selector.normalize("hello:world")); - } - - @Test - void normalize_escape_targets() { - var selector = new DumpTargetSelector(100, "[]", '@', ':'); - assertEquals("hello@3@", selector.normalize("hello[3]")); - } - - @Test - void normalize_upper_case() { - var selector = new DumpTargetSelector(); - assertEquals("testing", selector.normalize("TESTING")); - } - - @Test - void normalize_escape_shorten() { - var selector = new DumpTargetSelector(10, "[]", '@', ':'); - assertEquals("a".repeat(10), selector.normalize("a".repeat(100))); - } - - @Test - void getTargets_simple() { - var selector = new DumpTargetSelector(); - var targets = selector.getTargets(Path.of("p"), List.of("a")); - assertEquals(Map.of("a", "p/a"), toMap(targets)); - } - - @Test - void getTargets_multiple() { - var selector = new DumpTargetSelector(); - var targets = selector.getTargets(Path.of("p"), List.of("a", "b", "c")); - assertEquals(Map.of("a", "p/a", "b", "p/b", "c", "p/c"), toMap(targets)); - } - - @Test - void getTargets_conflict() { - var selector = new DumpTargetSelector(); - var targets = selector.getTargets(Path.of("p"), List.of("a", "A")); - assertEquals(Map.of("a", "p/a", "A", "p/a-*"), toMap(targets)); - } - - @Test - void getTargets_conflict_multiple() { - var selector = new DumpTargetSelector(); - var targets = selector.getTargets(Path.of("p"), List.of("a", "A", "a_", "A_", "A?")); - assertEquals(Map.of( - "a", "p/a", - "A", "p/a-*", - "a_", "p/a_", - "A_", "p/a_-*", - "A?", "p/a_-*"), toMap(targets)); - } - - @Test - void equivalent() { - var a = new DumpTargetSelector(); - var b = new DumpTargetSelector(); - assertEquals(a, b, String.format("%s = %s", a, b)); - } - - private static Map toMap(List targets) { - // SELECT destination - // GROUP BY destination - // HAVING count(*) >= 2 - // ORDER BY destination - var conflicts = targets.stream() - .map(DumpTarget::getDestination) - .collect(Collectors.groupingBy(k -> k, Collectors.counting())) - .entrySet().stream() - .filter(e -> e.getValue() >= 2) - .map(e -> e.getKey()) - .sorted() - .collect(Collectors.toList()); - assertEquals(List.of(), conflicts); - return targets.stream() - .collect(Collectors.toMap( - DumpTarget::getTableName, - t -> { - var r = t.getDestination().toString().replace(File.separatorChar, '/'); - // replace suffix with "-*" - return r.replaceFirst("-\\d+$", "-*"); - })); - } -} diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockDumpMonitor.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockDumpMonitor.java index 896e042..58a7582 100644 --- a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockDumpMonitor.java +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockDumpMonitor.java @@ -83,6 +83,12 @@ public void onDumpInfo(String tableName, TableMetadata tableInfo, Path dumpDirec Assertions.assertNull(prev); } + @Override + public void onDumpInfo(String label, String query, Path dumpDirectory) throws MonitoringException { + var prev = info.putIfAbsent(label, dumpDirectory); + Assertions.assertNull(prev); + } + @Override public void onDumpStart(String tableName, Path dumpDirectory) throws MonitoringException { var prev = start.putIfAbsent(tableName, dumpDirectory); diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockPreparedStatement.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockPreparedStatement.java new file mode 100644 index 0000000..2854c71 --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockPreparedStatement.java @@ -0,0 +1,41 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import com.tsurugidb.tsubakuro.sql.PreparedStatement; + +class MockPreparedStatement implements PreparedStatement { + + private final String statement; + + MockPreparedStatement(String statement) { + this.statement = statement; + } + + public String getStatement() { + return statement; + } + + @Override + public boolean hasResultRecords() { + return true; + } + + @Override + public void close() { + return; + } +} \ No newline at end of file diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockResultSet.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockResultSet.java new file mode 100644 index 0000000..4b79015 --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockResultSet.java @@ -0,0 +1,79 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.io.IOException; +import java.util.List; + +import com.tsurugidb.sql.proto.SqlCommon; +import com.tsurugidb.tsubakuro.exception.ServerException; +import com.tsurugidb.tsubakuro.sql.ResultSet; +import com.tsurugidb.tsubakuro.sql.ResultSetMetadata; +import com.tsurugidb.tsubakuro.sql.Types; +import com.tsurugidb.tsubakuro.sql.impl.EmptyRelationCursor; + +class MockResultSet extends EmptyRelationCursor implements ResultSet { + + private final List results; + + private int rowPosition; + + private int columnPosition; + + MockResultSet(List results) { + this.results = results; + this.rowPosition = -1; + this.columnPosition = -1; + } + + @Override + public ResultSetMetadata getMetadata() throws IOException, ServerException, InterruptedException { + // just assumes single character column. + return new ResultSetMetadata() { + @Override + public List getColumns() { + return List.of(Types.column(String.class)); + } + }; + } + + @Override + public boolean nextRow() { + if (rowPosition + 1 < results.size()) { + rowPosition++; + columnPosition = -1; + return true; + } + return false; + } + + @Override + public boolean nextColumn() { + if (columnPosition < 0) { + columnPosition++; + return true; + } + return false; + } + + @Override + public String fetchCharacterValue() { + if (rowPosition < 0 || rowPosition >= results.size() || columnPosition != 0) { + throw new IllegalStateException(); + } + return results.get(rowPosition); + } +} \ No newline at end of file diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockSqlClient.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockSqlClient.java new file mode 100644 index 0000000..c0fe67b --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockSqlClient.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.io.IOException; +import java.util.Collection; + +import com.tsurugidb.sql.proto.SqlRequest; +import com.tsurugidb.tsubakuro.sql.PreparedStatement; +import com.tsurugidb.tsubakuro.sql.SqlClient; +import com.tsurugidb.tsubakuro.sql.TableMetadata; +import com.tsurugidb.tsubakuro.sql.Transaction; +import com.tsurugidb.tsubakuro.util.FutureResponse; + +class MockSqlClient implements SqlClient { + + private final Transaction tx; + + MockSqlClient() { + this(new MockTransaction()); + } + + MockSqlClient(Transaction tx) { + this.tx = tx; + } + + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + return FutureResponse.returns(new MockTableMetadata("T1")); + } + + @Override + public FutureResponse prepare( + String source, + Collection placeholders) + throws IOException { + return FutureResponse.returns(new MockPreparedStatement(source)); + } + + @Override + public FutureResponse createTransaction(SqlRequest.TransactionOption option) throws IOException { + return FutureResponse.returns(tx); + } +} \ No newline at end of file diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockTransaction.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockTransaction.java new file mode 100644 index 0000000..47fcc14 --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/MockTransaction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; + +import com.tsurugidb.sql.proto.SqlRequest; +import com.tsurugidb.tsubakuro.sql.PreparedStatement; +import com.tsurugidb.tsubakuro.sql.ResultSet; +import com.tsurugidb.tsubakuro.sql.Transaction; +import com.tsurugidb.tsubakuro.util.FutureResponse; + +class MockTransaction implements Transaction { + + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + return FutureResponse.returns(new MockResultSet(List.of(BasicDumpSessionTest.dumpFile(directory, 1).toString()))); + } + + @Override + public FutureResponse commit(SqlRequest.CommitStatus status) throws IOException { + return FutureResponse.returns(null); + } + + @Override + public String getTransactionId() { + return "TXID-TESTING"; + } +} \ No newline at end of file diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizerTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizerTest.java new file mode 100644 index 0000000..653c3e8 --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/NameNormalizerTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class NameNormalizerTest { + + @Test + void normalize_identity() { + var normalizer = new NameNormalizer(); + assertEquals("testing", normalizer.apply("testing")); + } + + @Test + void normalize_iso_control() { + var normalizer = new NameNormalizer(); + assertEquals("testing____", normalizer.apply("testing\0\t\r\n")); + } + + @Test + void normalize_supplementary() { + var normalizer = new NameNormalizer(); + assertEquals("testing_", normalizer.apply("testing" + String.valueOf(Character.toChars(0x1F600)))); + } + + @Test + void normalize_whitespace() { + var normalizer = new NameNormalizer(); + assertEquals("hello_world", normalizer.apply("hello world")); + } + + @Test + void normalize_delimiter() { + var normalizer = new NameNormalizer(100, "", '@', ':'); + assertEquals("hello@world", normalizer.apply("hello:world")); + } + + @Test + void normalize_escape_targets() { + var normalizer = new NameNormalizer(100, "[]", '@', ':'); + assertEquals("hello@3@", normalizer.apply("hello[3]")); + } + + @Test + void normalize_upper_case() { + var normalizer = new NameNormalizer(); + assertEquals("testing", normalizer.apply("TESTING")); + } + + @Test + void normalize_escape_shorten() { + var normalizer = new NameNormalizer(10, "[]", '@', ':'); + assertEquals("a".repeat(10), normalizer.apply("a".repeat(100))); + } + + @Test + void equivalent() { + var a = new NameNormalizer(); + var b = new NameNormalizer(); + assertEquals(a, b, String.format("%s = %s", a, b)); + } +} diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperationTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperationTest.java new file mode 100644 index 0000000..991403f --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperationTest.java @@ -0,0 +1,429 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.tsurugidb.sql.proto.SqlRequest; +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.tgdump.core.model.DumpProfile; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tsubakuro.exception.CoreServiceCode; +import com.tsurugidb.tsubakuro.exception.CoreServiceException; +import com.tsurugidb.tsubakuro.sql.PreparedStatement; +import com.tsurugidb.tsubakuro.sql.ResultSet; +import com.tsurugidb.tsubakuro.sql.SqlServiceCode; +import com.tsurugidb.tsubakuro.sql.exception.EvaluationException; +import com.tsurugidb.tsubakuro.sql.exception.SyntaxException; +import com.tsurugidb.tsubakuro.util.FutureResponse; + +class QueryDumpOperationTest { + + private final DumpProfile.Builder profile = DumpProfile.newBuilder(); + + private final MockDumpMonitor monitor = new MockDumpMonitor(); + + private static DumpTarget target(String label, String query) { + return new DumpTarget(DumpTarget.TargetType.QUERY, label, query, destination(label)); + } + + private static DumpTarget target(String tableName) { + return target(tableName, query(tableName)); + } + + private static String query(String tableName) { + return String.format("SELECT * FROM %s", tableName); + } + + private static Path destination(String tableName) { + return Path.of(String.valueOf(tableName.hashCode())).toAbsolutePath(); + } + + static Path dumpFile(Path directory, int index) { + return directory.resolve(String.valueOf(index)); + } + + private QueryDumpOperation createOperation() { + return new QueryDumpOperation(profile.build(), false); + } + + @Test + void register() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + registered.add(source); + return super.prepare(source, placeholders); + } + }; + ) { + assertTrue(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + operation.register(client, monitor, target("T1")); + assertEquals(List.of(query("T1")), registered); + assertEquals(Map.of("T1", destination("T1")), monitor.getInfo()); + assertFalse(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + } + } + + @Test + void register_multiple() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + registered.add(source); + return super.prepare(source, placeholders); + } + } + ) { + assertTrue(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + operation.register(client, monitor, target("T1")); + operation.register(client, monitor, target("T2")); + operation.register(client, monitor, target("T3")); + assertEquals( + List.of(query("T1"), query("T2"), query("T3")), + registered); + assertEquals( + Map.of("T1", destination("T1"), "T2", destination("T2"), "T3", destination("T3")), + monitor.getInfo()); + assertFalse(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + } + } + + @Test + void register_duplicate() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + registered.add(source); + return super.prepare(source, placeholders); + } + } + ) { + operation.register(client, monitor, target("a", "SELECT 1")); + operation.register(client, monitor, target("b", "SELECT 1")); + assertEquals(List.of("SELECT 1"), registered); + } + } + + @Test + void register_compile_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + return FutureResponse.raises(new SyntaxException(SqlServiceCode.SYNTAX_EXCEPTION)); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.PREPARE_FAILURE, e.getDiagnosticCode()); + } + } + + @Test + void register_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + throw new IOException(); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void register_server_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + return FutureResponse.raises(new CoreServiceException(CoreServiceCode.PERMISSION_ERROR)); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.SERVER_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void register_unsupported_target_type() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + ) { + var target = new DumpTarget(DumpTarget.TargetType.TABLE, "T1", "T1", destination("mismatch")); + assertThrows(UnsupportedOperationException.class, () -> operation.register(client, monitor, target)); + } + } + + @Test + void execute() throws Exception { + var operation = createOperation(); + var statements = new HashSet(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + statements.add(source); + return super.prepare(source, placeholders); + } + }; + var tx = new MockTransaction(); + ) { + operation.register(client, monitor, target("T1")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals(Map.of("T1", List.of(dumpFile(destination("T1"), 1))), monitor.getFiles()); + assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); + assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); + + assertEquals(Set.of("SELECT * FROM T1"), statements); + } + } + + @Test + void execute_multiple_tables() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction(); + ) { + operation.register(client, monitor, target("T1")); + operation.register(client, monitor, target("T2")); + operation.register(client, monitor, target("T3")); + operation.execute(client, tx, monitor, target("T3")); + operation.execute(client, tx, monitor, target("T2")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals( + Map.of( + "T1", List.of(dumpFile(destination("T1"), 1)), + "T2", List.of(dumpFile(destination("T2"), 1)), + "T3", List.of(dumpFile(destination("T3"), 1))), + monitor.getFiles()); + assertEquals( + Map.of( + "T1", destination("T1"), + "T2", destination("T2"), + "T3", destination("T3")), + monitor.getFinish()); + assertEquals( + Map.of( + "T1", destination("T1"), + "T2", destination("T2"), + "T3", destination("T3")), + monitor.getStart()); + } + } + + @Test + void execute_multiple_files() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + return FutureResponse.returns(new MockResultSet(List.of( + dumpFile(directory, 1).toString(), + dumpFile(directory, 2).toString(), + dumpFile(directory, 3).toString()))); + } + }; + ) { + operation.register(client, monitor, target("T1")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals( + Map.of( + "T1", + List.of( + dumpFile(destination("T1"), 1), + dumpFile(destination("T1"), 2), + dumpFile(destination("T1"), 3))), + monitor.getFiles()); + assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); + assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); + } + } + + @Test + void execute_prepare_failure() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + boolean first = true; + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + if (first) { + first = false; + return super.prepare(source, placeholders); + } + return FutureResponse.raises(new SyntaxException(SqlServiceCode.SYNTAX_EXCEPTION)); + } + }; + var tx = new MockTransaction(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.PREPARE_FAILURE, e.getDiagnosticCode()); + } + } + + @Test + void execute_prepare_failure_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + boolean first = true; + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + if (first) { + first = false; + return super.prepare(source, placeholders); + } + throw new IOException(); + } + }; + var tx = new MockTransaction(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void execute_operation_failure() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + return FutureResponse.raises(new EvaluationException(SqlServiceCode.EVALUATION_EXCEPTION)); + } + }; + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.OPERATION_FAILURE, e.getDiagnosticCode()); + } + } + + @Test + void execute_operation_failure_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + throw new IOException(); + } + }; + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void execute_without_register() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction(); + ) { + operation.register(client, monitor, target("T1")); + assertThrows(IllegalArgumentException.class, () -> operation.execute(client, tx, monitor, target("XXX"))); + } + } + + @Test + void execute_unsupported_target_type() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = new MockTransaction(); + ) { + var t1 = new DumpTarget(DumpTarget.TargetType.QUERY, "mismatch", "mismatch", destination("mismatch")); + var t2 = new DumpTarget(DumpTarget.TargetType.TABLE, "mismatch", "mismatch", destination("mismatch")); + operation.register(client, monitor, t1); + assertThrows(UnsupportedOperationException.class, () -> operation.execute(client, tx, monitor, t2)); + } + } +} diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelectorTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelectorTest.java new file mode 100644 index 0000000..04b004d --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelectorTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; + +class QueryDumpTargetSelectorTest { + + private static QueryDumpTargetSelector selector() { + return new QueryDumpTargetSelector(); + } + + private static QueryDumpTargetSelector.LabelAndStatement parsed(String label, String statement) { + return new QueryDumpTargetSelector.LabelAndStatement(label, statement); + } + + @Test + void parseCommand_simple() { + var result = selector().parseCommand("q:SELECT 1"); + assertEquals(parsed("q", "SELECT 1"), result); + } + + @Test + void parseCommand_word() { + var result = selector().parseCommand("query:SELECT 1"); + assertEquals(parsed("query", "SELECT 1"), result); + } + + @Test + void parseCommand_default() { + var result = selector().parseCommand("SELECT 1"); + assertEquals(parsed(null, "SELECT 1"), result); + } + + @Test + void parseCommand_spaces() { + var result = selector().parseCommand(" q : SELECT 1"); + assertEquals(parsed("q", "SELECT 1"), result); + } + + @Test + void parseCommand_empty_label() { + var result = selector().parseCommand(":SELECT 1"); + assertEquals(parsed("", "SELECT 1"), result); + } + + @Test + void parseCommand_empty_label_spaces() { + var result = selector().parseCommand(" : SELECT 1"); + assertEquals(parsed("", "SELECT 1"), result); + } + + @Test + void parseCommand_stop() { + var result = selector().parseCommand("'a':SELECT 1"); + assertEquals(parsed(null, "'a':SELECT 1"), result); + } + + @Test + void parseCommand_control() { + var result = selector().parseCommand("a\0:SELECT 1"); + assertEquals(parsed(null, "a\0:SELECT 1"), result); + } + + @Test + void parseCommand_body_stop() { + var result = selector().parseCommand("q'a':SELECT 1"); + assertEquals(parsed(null, "q'a':SELECT 1"), result); + } + + @Test + void parseCommand_padding_stop() { + var result = selector().parseCommand("q 'a':SELECT 1"); + assertEquals(parsed(null, "q 'a':SELECT 1"), result); + } + + @Test + void parseCommand_empty_statement() { + var result = selector().parseCommand("q:"); + assertEquals(parsed("q", ""), result); + } + + @Test + void parseCommand_empty_command() { + var result = selector().parseCommand(""); + assertEquals(parsed(null, ""), result); + } + + @Test + void getTargets_simple() { + var results = selector().getTargets(Path.of("p"), List.of("q:SELECT 1")); + assertEquals(Map.of("q:SELECT 1", "p/q"), toMap(results)); + } + + @Test + void getTargets_multiple() { + var results = selector().getTargets(Path.of("p"), List.of("q1:SELECT 1", "q2:SELECT 2", "q3:SELECT 3")); + assertEquals(Map.of( + "q1:SELECT 1", "p/q1", + "q2:SELECT 2", "p/q2", + "q3:SELECT 3", "p/q3"), toMap(results)); + } + + @Test + void getTargets_default_label() { + var results = selector().getTargets(Path.of("p"), List.of("SELECT 1")); + assertEquals(Map.of("sql1:SELECT 1", "p/sql1"), toMap(results)); + } + + @Test + void getTargets_default_label_multiple() { + var results = selector().getTargets(Path.of("p"), List.of("SELECT 1", "SELECT 2", "SELECT 3")); + assertEquals(Map.of( + "sql1:SELECT 1", "p/sql1", + "sql2:SELECT 2", "p/sql2", + "sql3:SELECT 3", "p/sql3"), toMap(results)); + } + + @Test + void getTargets_conflict_label() { + var results = selector().getTargets(Path.of("p"), List.of("q:SELECT 1", "q:SELECT 2")); + assertEquals(Map.of( + "q:SELECT 1", "p/q-1", + "q:SELECT 2", "p/q-2"), toMap(results)); + } + + @Test + void getTargets_empty_label() { + assertThrows(IllegalArgumentException.class, () -> selector().getTargets(Path.of("p"), List.of(":SELECT 1"))); + } + + @Test + void getTargets_empty_statement() { + assertThrows(IllegalArgumentException.class, () -> selector().getTargets(Path.of("p"), List.of("q:"))); + } + + @Test + void equivalent() { + var a = selector(); + var b = selector(); + assertEquals(a, b, String.format("%s = %s", a, b)); + } + + private static Map toMap(List targets) { + return targets.stream() + .peek(it -> assertEquals(DumpTarget.TargetType.QUERY, it.getTargetType())) + .peek(it -> assertEquals(it.getLabel(), it.getLabel())) + .peek(it -> assertEquals(it.getTarget(), it.getTarget())) + .collect(Collectors.toMap( + t -> String.format("%s:%s", t.getLabel(), t.getTarget()), + t -> t.getDestination().toString().replace(File.separatorChar, '/'))); + } +} diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperationTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperationTest.java new file mode 100644 index 0000000..6e2a254 --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperationTest.java @@ -0,0 +1,442 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.tsurugidb.sql.proto.SqlRequest; +import com.tsurugidb.tools.common.diagnostic.DiagnosticException; +import com.tsurugidb.tools.tgdump.core.model.DumpProfile; +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; +import com.tsurugidb.tsubakuro.exception.CoreServiceCode; +import com.tsurugidb.tsubakuro.exception.CoreServiceException; +import com.tsurugidb.tsubakuro.sql.PreparedStatement; +import com.tsurugidb.tsubakuro.sql.ResultSet; +import com.tsurugidb.tsubakuro.sql.SqlServiceCode; +import com.tsurugidb.tsubakuro.sql.TableMetadata; +import com.tsurugidb.tsubakuro.sql.exception.EvaluationException; +import com.tsurugidb.tsubakuro.sql.exception.SyntaxException; +import com.tsurugidb.tsubakuro.sql.exception.TargetNotFoundException; +import com.tsurugidb.tsubakuro.util.FutureResponse; + +class TableDumpOperationTest { + + private final DumpProfile.Builder profile = DumpProfile.newBuilder(); + + private final MockDumpMonitor monitor = new MockDumpMonitor(); + + private static DumpTarget target(String tableName) { + return new DumpTarget(tableName, destination(tableName)); + } + + private static Path destination(String tableName) { + return Path.of(String.valueOf(tableName.hashCode())).toAbsolutePath(); + } + + static Path dumpFile(Path directory, int index) { + return directory.resolve(String.valueOf(index)); + } + + private TableDumpOperation createOperation() { + return new TableDumpOperation(profile.build(), false); + } + + @Test + void register() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + registered.add(tableName); + return super.getTableMetadata(tableName); + } + }; + ) { + assertTrue(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + operation.register(client, monitor, target("T1")); + assertEquals(List.of("T1"), registered); + assertEquals(Map.of("T1", destination("T1")), monitor.getInfo()); + assertFalse(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of("T1")); + } + } + + @Test + void register_multiple() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + registered.add(tableName); + return super.getTableMetadata(tableName); + } + } + ) { + assertTrue(operation.isEmpty()); + assertEquals(operation.getTargetTables(), List.of()); + operation.register(client, monitor, target("T1")); + operation.register(client, monitor, target("T2")); + operation.register(client, monitor, target("T3")); + assertEquals(List.of("T1", "T2", "T3"), registered); + assertEquals( + Map.of("T1", destination("T1"), "T2", destination("T2"), "T3", destination("T3")), + monitor.getInfo()); + assertFalse(operation.isEmpty()); + assertEquals(Set.copyOf(operation.getTargetTables()), Set.of("T1", "T2", "T3")); + } + } + + @Test + void register_duplicate() throws Exception { + var operation = createOperation(); + var registered = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + registered.add(tableName); + return super.getTableMetadata(tableName); + } + } + ) { + operation.register(client, monitor, target("T1")); + operation.register(client, monitor, target("T1")); + assertEquals(List.of("T1"), registered); + } + } + + @Test + void register_not_found() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + return FutureResponse.raises(new TargetNotFoundException(SqlServiceCode.TARGET_NOT_FOUND_EXCEPTION)); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.TABLE_NOT_FOUND, e.getDiagnosticCode()); + } + } + + @Test + void register_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + throw new IOException(); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void register_server_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse getTableMetadata(String tableName) throws IOException { + return FutureResponse.raises(new CoreServiceException(CoreServiceCode.PERMISSION_ERROR)); + } + } + ) { + var e = assertThrows(DiagnosticException.class, () -> operation.register(client, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.SERVER_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void register_unsupported_target_type() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + ) { + var target = new DumpTarget(DumpTarget.TargetType.QUERY, "mismatch", "SELECT 1", destination("mismatch")); + assertThrows(UnsupportedOperationException.class, () -> operation.register(client, monitor, target)); + } + } + + @Test + void execute() throws Exception { + var operation = createOperation(); + var statements = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + statements.add(source); + return super.prepare(source, placeholders); + } + }; + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals(Map.of("T1", List.of(dumpFile(destination("T1"), 1))), monitor.getFiles()); + assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); + assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); + + assertEquals(List.of("SELECT * FROM T1"), statements); + } + } + + @Test + void execute_multiple_tables() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + operation.register(client, monitor, target("T2")); + operation.register(client, monitor, target("T3")); + operation.execute(client, tx, monitor, target("T3")); + operation.execute(client, tx, monitor, target("T2")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals( + Map.of( + "T1", List.of(dumpFile(destination("T1"), 1)), + "T2", List.of(dumpFile(destination("T2"), 1)), + "T3", List.of(dumpFile(destination("T3"), 1))), + monitor.getFiles()); + assertEquals( + Map.of( + "T1", destination("T1"), + "T2", destination("T2"), + "T3", destination("T3")), + monitor.getFinish()); + assertEquals( + Map.of( + "T1", destination("T1"), + "T2", destination("T2"), + "T3", destination("T3")), + monitor.getStart()); + } + } + + @Test + void execute_multiple_files() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + return FutureResponse.returns(new MockResultSet(List.of( + dumpFile(directory, 1).toString(), + dumpFile(directory, 2).toString(), + dumpFile(directory, 3).toString()))); + } + }); + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + operation.execute(client, tx, monitor, target("T1")); + assertEquals( + Map.of( + "T1", + List.of( + dumpFile(destination("T1"), 1), + dumpFile(destination("T1"), 2), + dumpFile(destination("T1"), 3))), + monitor.getFiles()); + assertEquals(Map.of("T1", destination("T1")), monitor.getStart()); + assertEquals(Map.of("T1", destination("T1")), monitor.getFinish()); + } + } + + @Test + void execute_table_name_non_regular() throws Exception { + var operation = createOperation(); + var statements = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + statements.add(source); + return super.prepare(source, placeholders); + } + }; + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T 1")); + operation.execute(client, tx, monitor, target("T 1")); + assertEquals(List.of("SELECT * FROM \"T 1\""), statements); + } + } + + @Test + void execute_table_name_delimiter() throws Exception { + var operation = createOperation(); + var statements = new ArrayList(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + statements.add(source); + return super.prepare(source, placeholders); + } + }; + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T\"1")); + operation.execute(client, tx, monitor, target("T\"1")); + assertEquals(List.of("SELECT * FROM \"T\"\"1\""), statements); + } + } + + @Test + void execute_prepare_failure() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + return FutureResponse.raises(new SyntaxException(SqlServiceCode.SYNTAX_EXCEPTION)); + } + }; + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.PREPARE_FAILURE, e.getDiagnosticCode()); + } + } + + @Test + void execute_prepare_failure_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient() { + @Override + public FutureResponse prepare( + String source, + Collection placeholders) throws IOException { + throw new IOException(); + } + }; + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void execute_operation_failure() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + return FutureResponse.raises(new EvaluationException(SqlServiceCode.EVALUATION_EXCEPTION)); + } + }); + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.OPERATION_FAILURE, e.getDiagnosticCode()); + } + } + + @Test + void execute_operation_failure_io_error() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(new MockTransaction() { + @Override + public FutureResponse executeDump( + PreparedStatement statement, + Collection parameters, + Path directory, + SqlRequest.DumpOption option) throws IOException { + throw new IOException(); + } + }); + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + var e = assertThrows(DiagnosticException.class, () -> operation.execute(client, tx, monitor, target("T1"))); + assertEquals(DumpDiagnosticCode.IO_ERROR, e.getDiagnosticCode()); + } + } + + @Test + void execute_without_register() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = client.createTransaction().await(); + ) { + operation.register(client, monitor, target("T1")); + assertThrows(IllegalArgumentException.class, () -> operation.execute(client, tx, monitor, target("XXX"))); + } + } + + @Test + void execute_unsupported_target_type() throws Exception { + var operation = createOperation(); + try ( + var client = new MockSqlClient(); + var tx = client.createTransaction().await(); + ) { + var t1 = new DumpTarget(DumpTarget.TargetType.TABLE, "mismatch", "SELECT 1", destination("mismatch")); + var t2 = new DumpTarget(DumpTarget.TargetType.QUERY, "mismatch", "SELECT 1", destination("mismatch")); + operation.register(client, monitor, t1); + assertThrows(UnsupportedOperationException.class, () -> operation.execute(client, tx, monitor, t2)); + } + } +} diff --git a/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelectorTest.java b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelectorTest.java new file mode 100644 index 0000000..1b5bede --- /dev/null +++ b/modules/tgdump/core/src/test/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpTargetSelectorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2023-2024 Project Tsurugi. + * + * Licensed 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 com.tsurugidb.tools.tgdump.core.engine; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.tsurugidb.tools.tgdump.core.model.DumpTarget; + +class TableDumpTargetSelectorTest { + + private static TableDumpTargetSelector selector() { + return new TableDumpTargetSelector(); + } + + @Test + void getTargets_simple() { + var targets = selector().getTargets(Path.of("p"), List.of("a")); + assertEquals(Map.of("a", "p/a"), toMap(targets)); + } + + @Test + void getTargets_multiple() { + var targets = selector().getTargets(Path.of("p"), List.of("a", "b", "c")); + assertEquals(Map.of("a", "p/a", "b", "p/b", "c", "p/c"), toMap(targets)); + } + + @Test + void getTargets_empty_name() { + assertThrows(IllegalArgumentException.class, () -> selector().getTargets(Path.of("p"), List.of(""))); + } + + @Test + void getTargets_conflict() { + var targets = selector().getTargets(Path.of("p"), List.of("a", "A")); + assertEquals(Map.of("a", "p/a-1", "A", "p/a-2"), toMap(targets)); + } + + @Test + void getTargets_conflict_multiple() { + var targets = selector().getTargets(Path.of("p"), List.of("a", "A", "a_", "A_", "A?")); + assertEquals(Map.of( + "a", "p/a-1", + "A", "p/a-2", + "a_", "p/a_-1", + "A_", "p/a_-2", + "A?", "p/a_-3"), toMap(targets)); + } + + @Test + void equivalent() { + var a = selector(); + var b = selector(); + assertEquals(a, b, String.format("%s = %s", a, b)); + } + + private static Map toMap(List targets) { + return targets.stream() + .peek(it -> assertEquals(DumpTarget.TargetType.TABLE, it.getTargetType())) + .peek(it -> assertEquals(it.getTableName(), it.getTarget())) + .peek(it -> assertEquals(it.getTableName(), it.getLabel())) + .collect(Collectors.toMap( + DumpTarget::getTableName, + t -> t.getDestination().toString().replace(File.separatorChar, '/'))); + } +} From 38a03732e6bad65fad2610d7d679d38fd2ed28b7 Mon Sep 17 00:00:00 2001 From: Suguru ARAKAWA Date: Tue, 7 Jan 2025 16:35:01 +0900 Subject: [PATCH 2/2] fix: use `java.annotation` instead of findgbugs. --- .../core/engine/DumpOperationDispatch.java | 20 +++++++++---------- .../core/engine/QueryDumpOperation.java | 16 +++++++-------- .../core/engine/QueryDumpTargetSelector.java | 3 +-- .../core/engine/TableDumpOperation.java | 16 +++++++-------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java index ac79b12..c7ad421 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/DumpOperationDispatch.java @@ -23,14 +23,14 @@ import java.util.Optional; import java.util.stream.Collectors; +import javax.annotation.Nonnull; + import com.tsurugidb.tools.common.diagnostic.DiagnosticException; import com.tsurugidb.tools.tgdump.core.model.DumpTarget; import com.tsurugidb.tools.tgdump.core.model.DumpTarget.TargetType; import com.tsurugidb.tsubakuro.sql.SqlClient; import com.tsurugidb.tsubakuro.sql.Transaction; -import edu.umd.cs.findbugs.annotations.NonNull; - /** * An implementation of {@link DumpOperation} for dispatching by its tarrget type. */ @@ -42,7 +42,7 @@ class DumpOperationDispatch implements DumpOperation { * Creates a new instance. * @param elements the elements to dispatch */ - DumpOperationDispatch(@NonNull Map elements) { + DumpOperationDispatch(@Nonnull Map elements) { Objects.requireNonNull(elements); this.elements = elements.isEmpty() ? Map.of() : new EnumMap<>(elements); } @@ -52,12 +52,12 @@ class DumpOperationDispatch implements DumpOperation { * @param type the target type * @return the operation for the specified target type, or {@code empty} if not found */ - public Optional getOperation(@NonNull DumpTarget.TargetType type) { + public Optional getOperation(@Nonnull DumpTarget.TargetType type) { Objects.requireNonNull(type); return Optional.ofNullable(elements.get(type)); } - private DumpOperation getOperationStrict(@NonNull DumpTarget target) { + private DumpOperation getOperationStrict(@Nonnull DumpTarget target) { return getOperation(target.getTargetType()) .orElseThrow(() -> new UnsupportedOperationException(MessageFormat.format( "unsupported target type: {0} in {1}", @@ -80,7 +80,7 @@ public boolean isEmpty() { } @Override - public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + public void register(@Nonnull SqlClient client, @Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(monitor); @@ -90,10 +90,10 @@ public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @N @Override public void execute( - @NonNull SqlClient client, - @NonNull Transaction transaction, - @NonNull DumpMonitor monitor, - @NonNull DumpTarget target) + @Nonnull SqlClient client, + @Nonnull Transaction transaction, + @Nonnull DumpMonitor monitor, + @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(transaction); diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java index 552bab4..4053490 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpOperation.java @@ -24,6 +24,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,8 +40,6 @@ import com.tsurugidb.tsubakuro.sql.Transaction; import com.tsurugidb.tsubakuro.sql.exception.CompileException; -import edu.umd.cs.findbugs.annotations.NonNull; - /** * An implementation of {@link DumpOperation} for SQL text. */ @@ -58,7 +58,7 @@ class QueryDumpOperation implements DumpOperation { * @param dumpProfile the dump operation settings * @param createTargetDirectories whether or not to create dump target directories before the dump operations */ - QueryDumpOperation(@NonNull DumpProfile dumpProfile, boolean createTargetDirectories) { + QueryDumpOperation(@Nonnull DumpProfile dumpProfile, boolean createTargetDirectories) { Objects.requireNonNull(dumpProfile); this.dumpProfile = dumpProfile; this.createTargetDirectories = createTargetDirectories; @@ -80,7 +80,7 @@ private static void checkTargetType(DumpTarget target) { } @Override - public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + public void register(@Nonnull SqlClient client, @Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(monitor); @@ -111,10 +111,10 @@ public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @N @Override public void execute( - @NonNull SqlClient client, - @NonNull Transaction transaction, - @NonNull DumpMonitor monitor, - @NonNull DumpTarget target) + @Nonnull SqlClient client, + @Nonnull Transaction transaction, + @Nonnull DumpMonitor monitor, + @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(transaction); diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java index 0e79b67..00ff629 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/QueryDumpTargetSelector.java @@ -25,14 +25,13 @@ import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.tsurugidb.tools.tgdump.core.model.DumpTarget; -import edu.umd.cs.findbugs.annotations.Nullable; - /** * Computes dump destination directories from each SQL text. */ diff --git a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java index c75f128..b715cb8 100644 --- a/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java +++ b/modules/tgdump/core/src/main/java/com/tsurugidb/tools/tgdump/core/engine/TableDumpOperation.java @@ -25,6 +25,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; +import javax.annotation.Nonnull; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,8 +43,6 @@ import com.tsurugidb.tsubakuro.sql.exception.CompileException; import com.tsurugidb.tsubakuro.sql.exception.TargetNotFoundException; -import edu.umd.cs.findbugs.annotations.NonNull; - /** * An implementation of {@link DumpOperation} for table dump. */ @@ -63,7 +63,7 @@ class TableDumpOperation implements DumpOperation { * @param dumpProfile the dump operation settings * @param createTargetDirectories whether or not to create dump target directories before the dump operations */ - TableDumpOperation(@NonNull DumpProfile dumpProfile, boolean createTargetDirectories) { + TableDumpOperation(@Nonnull DumpProfile dumpProfile, boolean createTargetDirectories) { Objects.requireNonNull(dumpProfile); this.dumpProfile = dumpProfile; this.createTargetDirectories = createTargetDirectories; @@ -90,7 +90,7 @@ private static void checkTargetType(DumpTarget target) { } @Override - public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @NonNull DumpTarget target) + public void register(@Nonnull SqlClient client, @Nonnull DumpMonitor monitor, @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(monitor); @@ -120,10 +120,10 @@ public void register(@NonNull SqlClient client, @NonNull DumpMonitor monitor, @N @Override public void execute( - @NonNull SqlClient client, - @NonNull Transaction transaction, - @NonNull DumpMonitor monitor, - @NonNull DumpTarget target) + @Nonnull SqlClient client, + @Nonnull Transaction transaction, + @Nonnull DumpMonitor monitor, + @Nonnull DumpTarget target) throws InterruptedException, DiagnosticException { Objects.requireNonNull(client); Objects.requireNonNull(transaction);