This is an automated email from the ASF dual-hosted git repository.

karan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 3df00aef9d2 Add manifest file for MSQ export (#15953)
3df00aef9d2 is described below

commit 3df00aef9d27962b7f2262cd0055431273a5d067
Author: Adarsh Sanjeev <[email protected]>
AuthorDate: Mon Apr 15 11:37:31 2024 +0530

    Add manifest file for MSQ export (#15953)
    
    Currently, export creates the files at the provided destination. The 
addition of the manifest file will provide a list of files created as part of 
the manifest. This will allow easier consumption of the data exported from 
Druid, especially for automated data pipelines
---
 docs/multi-stage-query/reference.md                | 10 +++
 .../google/output/GoogleExportStorageProvider.java |  9 +++
 .../org/apache/druid/msq/exec/ControllerImpl.java  | 32 ++++++--
 .../druid/msq/exec/ExportMetadataManager.java      | 93 ++++++++++++++++++++++
 .../druid/msq/indexing/MSQControllerTask.java      |  6 ++
 .../indexing/destination/ExportMSQDestination.java |  2 -
 .../results/ExportResultsFrameProcessor.java       |  3 +-
 .../ExportResultsFrameProcessorFactory.java        | 58 ++++++++++++--
 .../org/apache/druid/msq/exec/MSQExportTest.java   | 55 ++++++++++---
 .../org/apache/druid/msq/test/MSQTestBase.java     |  9 ---
 .../storage/s3/output/S3ExportStorageProvider.java |  9 +++
 .../s3/output/S3ExportStorageProviderTest.java     |  9 +++
 .../druid/storage/ExportStorageProvider.java       |  2 +
 .../local/LocalFileExportStorageProvider.java      |  8 ++
 .../local/LocalFileExportStorageProviderTest.java  | 13 +++
 .../druid/sql/calcite/CalciteExportTest.java       | 57 ++++++++-----
 .../druid/sql/calcite/export/TestExportModule.java | 52 ------------
 .../calcite/export/TestExportStorageConnector.java | 92 ---------------------
 .../export/TestExportStorageConnectorProvider.java | 46 -----------
 19 files changed, 321 insertions(+), 244 deletions(-)

diff --git a/docs/multi-stage-query/reference.md 
b/docs/multi-stage-query/reference.md
index f9c54b8e2e3..20d0ef2f7e6 100644
--- a/docs/multi-stage-query/reference.md
+++ b/docs/multi-stage-query/reference.md
@@ -99,6 +99,16 @@ For more information, see [Read external data with 
EXTERN](concepts.md#read-exte
 This variation of EXTERN requires one argument, the details of the destination 
as specified below.
 This variation additionally requires an `AS` clause to specify the format of 
the exported rows.
 
+While exporting data, some metadata files will also be created at the 
destination in addition to the data. These files will be created in a directory 
`_symlink_format_manifest`.
+- `_symlink_format_manifest/manifest`: Lists the files which were created as 
part of the export. The file is in the symlink manifest format, and consists of 
a list of absolute paths to the files created.
+```text
+s3://export-bucket/export/query-6564a32f-2194-423a-912e-eead470a37c4-worker2-partition2.csv
+s3://export-bucket/export/query-6564a32f-2194-423a-912e-eead470a37c4-worker1-partition1.csv
+s3://export-bucket/export/query-6564a32f-2194-423a-912e-eead470a37c4-worker0-partition0.csv
+...
+s3://export-bucket/export/query-6564a32f-2194-423a-912e-eead470a37c4-worker0-partition24.csv
+```
+
 Keep the following in mind when using EXTERN to export rows:
 - Only INSERT statements are supported.
 - Only `CSV` format is supported as an export format.
diff --git 
a/extensions-core/google-extensions/src/main/java/org/apache/druid/storage/google/output/GoogleExportStorageProvider.java
 
b/extensions-core/google-extensions/src/main/java/org/apache/druid/storage/google/output/GoogleExportStorageProvider.java
index 480b80e118a..8d0c6b50b31 100644
--- 
a/extensions-core/google-extensions/src/main/java/org/apache/druid/storage/google/output/GoogleExportStorageProvider.java
+++ 
b/extensions-core/google-extensions/src/main/java/org/apache/druid/storage/google/output/GoogleExportStorageProvider.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import org.apache.druid.data.input.google.GoogleCloudStorageInputSource;
 import org.apache.druid.data.input.impl.CloudObjectLocation;
 import org.apache.druid.error.DruidException;
@@ -44,6 +45,8 @@ import java.util.List;
 public class GoogleExportStorageProvider implements ExportStorageProvider
 {
   public static final String TYPE_NAME = 
GoogleCloudStorageInputSource.TYPE_KEY;
+  private static final String DELIM = "/";
+  private static final Joiner JOINER = Joiner.on(DELIM).skipNulls();
   @JsonProperty
   private final String bucket;
   @JsonProperty
@@ -146,4 +149,10 @@ public class GoogleExportStorageProvider implements 
ExportStorageProvider
   {
     return new CloudObjectLocation(bucket, 
prefix).toUri(GoogleStorageDruidModule.SCHEME_GS).toString();
   }
+
+  @Override
+  public String getFilePathForManifest(String fileName)
+  {
+    return new CloudObjectLocation(bucket, JOINER.join(prefix, 
fileName)).toUri(GoogleStorageDruidModule.SCHEME_GS).toString();
+  }
 }
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
index 918a4fd2969..878492d8a6e 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ControllerImpl.java
@@ -433,7 +433,7 @@ public class ControllerImpl implements Controller
       queryKernel = Preconditions.checkNotNull(queryRunResult.lhs);
       workerTaskRunnerFuture = Preconditions.checkNotNull(queryRunResult.rhs);
       resultsYielder = getFinalResultsYielder(queryDef, queryKernel);
-      publishSegmentsIfNeeded(queryDef, queryKernel);
+      handleQueryResults(queryDef, queryKernel);
     }
     catch (Throwable e) {
       exceptionEncountered = e;
@@ -1746,12 +1746,16 @@ public class ControllerImpl implements Controller
     }
   }
 
-  private void publishSegmentsIfNeeded(
+  private void handleQueryResults(
       final QueryDefinition queryDef,
       final ControllerQueryKernel queryKernel
   ) throws IOException
   {
-    if (queryKernel.isSuccess() && 
MSQControllerTask.isIngestion(task.getQuerySpec())) {
+    if (!queryKernel.isSuccess()) {
+      return;
+    }
+    if (MSQControllerTask.isIngestion(task.getQuerySpec())) {
+      // Publish segments if needed.
       final StageId finalStageId = 
queryKernel.getStageId(queryDef.getFinalStageDefinition().getStageNumber());
 
       //noinspection unchecked
@@ -1790,6 +1794,25 @@ public class ControllerImpl implements Controller
       }
       log.info("Query [%s] publishing %d segments.", queryDef.getQueryId(), 
segments.size());
       publishAllSegments(segments);
+    } else if (MSQControllerTask.isExport(task.getQuerySpec())) {
+      // Write manifest file.
+      ExportMSQDestination destination = (ExportMSQDestination) 
task.getQuerySpec().getDestination();
+      ExportMetadataManager exportMetadataManager = new 
ExportMetadataManager(destination.getExportStorageProvider());
+
+      final StageId finalStageId = 
queryKernel.getStageId(queryDef.getFinalStageDefinition().getStageNumber());
+      //noinspection unchecked
+
+
+      Object resultObjectForStage = 
queryKernel.getResultObjectForStage(finalStageId);
+      if (!(resultObjectForStage instanceof List)) {
+        // This might occur if all workers are running on an older version. We 
are not able to write a manifest file in this case.
+        log.warn("Was unable to create manifest file due to ");
+        return;
+      }
+      @SuppressWarnings("unchecked")
+      List<String> exportedFiles = (List<String>) 
queryKernel.getResultObjectForStage(finalStageId);
+      log.info("Query [%s] exported %d files.", queryDef.getQueryId(), 
exportedFiles.size());
+      exportMetadataManager.writeMetadata(exportedFiles);
     }
   }
 
@@ -2018,7 +2041,7 @@ public class ControllerImpl implements Controller
       } else {
         return queryDef;
       }
-    } else if (querySpec.getDestination() instanceof ExportMSQDestination) {
+    } else if (MSQControllerTask.isExport(querySpec)) {
       final ExportMSQDestination exportMSQDestination = (ExportMSQDestination) 
querySpec.getDestination();
       final ExportStorageProvider exportStorageProvider = 
exportMSQDestination.getExportStorageProvider();
 
@@ -2063,7 +2086,6 @@ public class ControllerImpl implements Controller
     }
   }
 
-
   private static DataSchema generateDataSchema(
       MSQSpec querySpec,
       RowSignature querySignature,
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ExportMetadataManager.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ExportMetadataManager.java
new file mode 100644
index 00000000000..3b9d0296de5
--- /dev/null
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/exec/ExportMetadataManager.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.msq.exec;
+
+import org.apache.druid.error.DruidException;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.java.util.common.logger.Logger;
+import org.apache.druid.storage.ExportStorageProvider;
+import org.apache.druid.storage.StorageConnector;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Manages writing of metadata files during export queries.
+ */
+public class ExportMetadataManager
+{
+  public static final String SYMLINK_DIR = "_symlink_format_manifest";
+  public static final String MANIFEST_FILE = SYMLINK_DIR + "/manifest";
+  public static final String META_FILE = SYMLINK_DIR + "/druid_export_meta";
+  public static final int MANIFEST_FILE_VERSION = 1;
+  private static final Logger log = new Logger(ExportMetadataManager.class);
+  private final ExportStorageProvider exportStorageProvider;
+
+  public ExportMetadataManager(final ExportStorageProvider 
exportStorageProvider)
+  {
+    this.exportStorageProvider = exportStorageProvider;
+  }
+
+  public void writeMetadata(List<String> exportedFiles) throws IOException
+  {
+    final StorageConnector storageConnector = exportStorageProvider.get();
+    log.info("Writing manifest file at location [%s]", 
exportStorageProvider.getBasePath());
+
+    if (storageConnector.pathExists(MANIFEST_FILE) || 
storageConnector.pathExists(META_FILE)) {
+      throw DruidException.forPersona(DruidException.Persona.USER)
+                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                          .build("Found existing manifest file already present 
at path.");
+    }
+
+    createManifestFile(storageConnector, exportedFiles);
+    createDruidMetadataFile(storageConnector);
+  }
+
+  /**
+   * Creates a manifest file containing the list of files created by the 
export query. The manifest file consists of a
+   * new line separated list. Each line contains the absolute path to a file 
created by the export.
+   */
+  public void createManifestFile(StorageConnector storageConnector, 
List<String> exportedFiles) throws IOException
+  {
+    try (PrintWriter printWriter = new PrintWriter(new 
OutputStreamWriter(storageConnector.write(MANIFEST_FILE), 
StandardCharsets.UTF_8))) {
+      for (String exportedFile : exportedFiles) {
+        
printWriter.println(exportStorageProvider.getFilePathForManifest(exportedFile));
+      }
+    }
+  }
+
+  /**
+   * Creates a druid metadata file at the export location. This file contains 
extra information about the export, which
+   * cannot be stored in the manifest directly, so that it can follow the 
symlink format.
+   * <br>
+   * Currently, this only contains the manifest file version.
+   */
+  private void createDruidMetadataFile(StorageConnector storageConnector) 
throws IOException
+  {
+    // Write the export manifest metadata information.
+    // This includes only the version number currently.
+    try (PrintWriter printWriter = new PrintWriter(new 
OutputStreamWriter(storageConnector.write(META_FILE), StandardCharsets.UTF_8))) 
{
+      printWriter.println(StringUtils.format("version: %s", 
MANIFEST_FILE_VERSION));
+    }
+  }
+}
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQControllerTask.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQControllerTask.java
index 7eb455ca842..64cecdfbf25 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQControllerTask.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQControllerTask.java
@@ -50,6 +50,7 @@ import org.apache.druid.msq.exec.ControllerImpl;
 import org.apache.druid.msq.exec.MSQTasks;
 import org.apache.druid.msq.indexing.destination.DataSourceMSQDestination;
 import org.apache.druid.msq.indexing.destination.DurableStorageMSQDestination;
+import org.apache.druid.msq.indexing.destination.ExportMSQDestination;
 import org.apache.druid.msq.indexing.destination.MSQDestination;
 import org.apache.druid.msq.util.MultiStageQueryContext;
 import org.apache.druid.query.QueryContext;
@@ -286,6 +287,11 @@ public class MSQControllerTask extends AbstractTask 
implements ClientTaskQuery
     return querySpec.getDestination() instanceof DataSourceMSQDestination;
   }
 
+  public static boolean isExport(final MSQSpec querySpec)
+  {
+    return querySpec.getDestination() instanceof ExportMSQDestination;
+  }
+
   /**
    * Returns true if the task reads from the same table as the destionation. 
In this case, we would prefer to fail
    * instead of reading any unused segments to ensure that old data is not 
read.
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/ExportMSQDestination.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/ExportMSQDestination.java
index 3187ace349b..14ac0ce4c2e 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/ExportMSQDestination.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/destination/ExportMSQDestination.java
@@ -52,8 +52,6 @@ public class ExportMSQDestination implements MSQDestination
     this.resultFormat = resultFormat;
   }
 
-
-
   @JsonProperty("exportStorageProvider")
   public ExportStorageProvider getExportStorageProvider()
   {
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
index 52697578b07..56b287781c2 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessor.java
@@ -33,7 +33,6 @@ import org.apache.druid.frame.processor.ReturnOrAwait;
 import org.apache.druid.frame.read.FrameReader;
 import org.apache.druid.frame.segment.FrameStorageAdapter;
 import org.apache.druid.java.util.common.Intervals;
-import org.apache.druid.java.util.common.Unit;
 import org.apache.druid.java.util.common.granularity.Granularities;
 import org.apache.druid.java.util.common.guava.Sequence;
 import org.apache.druid.msq.counters.ChannelCounters;
@@ -130,7 +129,7 @@ public class ExportResultsFrameProcessor implements 
FrameProcessor<Object>
     }
 
     if (inputChannel.isFinished()) {
-      return ReturnOrAwait.returnObject(Unit.instance());
+      return ReturnOrAwait.returnObject(exportFilePath);
     } else {
       exportFrame(inputChannel.read());
       return ReturnOrAwait.awaitAll(1);
diff --git 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessorFactory.java
 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessorFactory.java
index 5fe9b52191c..beb626f0fce 100644
--- 
a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessorFactory.java
+++ 
b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/querykit/results/ExportResultsFrameProcessorFactory.java
@@ -23,11 +23,13 @@ import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.core.type.TypeReference;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.frame.processor.FrameProcessor;
 import org.apache.druid.frame.processor.OutputChannelFactory;
 import org.apache.druid.frame.processor.OutputChannels;
 import org.apache.druid.frame.processor.manager.ProcessorManagers;
+import org.apache.druid.java.util.common.ISE;
 import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.java.util.common.guava.Sequence;
 import org.apache.druid.java.util.common.guava.Sequences;
@@ -38,21 +40,24 @@ import org.apache.druid.msq.input.InputSlice;
 import org.apache.druid.msq.input.InputSliceReader;
 import org.apache.druid.msq.input.ReadableInput;
 import org.apache.druid.msq.input.stage.StageInputSlice;
+import org.apache.druid.msq.kernel.ExtraInfoHolder;
 import org.apache.druid.msq.kernel.FrameContext;
+import org.apache.druid.msq.kernel.FrameProcessorFactory;
+import org.apache.druid.msq.kernel.NilExtraInfoHolder;
 import org.apache.druid.msq.kernel.ProcessorsAndChannels;
 import org.apache.druid.msq.kernel.StageDefinition;
-import org.apache.druid.msq.querykit.BaseFrameProcessorFactory;
 import org.apache.druid.sql.calcite.planner.ColumnMappings;
 import org.apache.druid.sql.http.ResultFormat;
 import org.apache.druid.storage.ExportStorageProvider;
 import org.apache.druid.utils.CollectionUtils;
 
 import javax.annotation.Nullable;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 
 @JsonTypeName("exportResults")
-public class ExportResultsFrameProcessorFactory extends 
BaseFrameProcessorFactory
+public class ExportResultsFrameProcessorFactory implements 
FrameProcessorFactory<Object, Object, Object>
 {
   private final String queryId;
   private final ExportStorageProvider exportStorageProvider;
@@ -101,7 +106,7 @@ public class ExportResultsFrameProcessorFactory extends 
BaseFrameProcessorFactor
   }
 
   @Override
-  public ProcessorsAndChannels<Object, Long> makeProcessors(
+  public ProcessorsAndChannels<Object, Object> makeProcessors(
       StageDefinition stageDefinition,
       int workerNumber,
       List<InputSlice> inputSlices,
@@ -120,7 +125,11 @@ public class ExportResultsFrameProcessorFactory extends 
BaseFrameProcessorFactor
     );
 
     if (inputSliceReader.numReadableInputs(slice) == 0) {
-      return new ProcessorsAndChannels<>(ProcessorManagers.none(), 
OutputChannels.none());
+      return new ProcessorsAndChannels<>(
+          ProcessorManagers.of(Sequences.<ExportResultsFrameProcessor>empty())
+                           .withAccumulation(new ArrayList<String>(), (acc, 
file) -> acc),
+          OutputChannels.none()
+      );
     }
 
     ChannelCounters channelCounter = 
counters.channel(CounterNames.outputChannel());
@@ -141,11 +150,50 @@ public class ExportResultsFrameProcessorFactory extends 
BaseFrameProcessorFactor
     );
 
     return new ProcessorsAndChannels<>(
-        ProcessorManagers.of(processors),
+        ProcessorManagers.of(processors)
+                         .withAccumulation(new ArrayList<String>(), (acc, 
file) -> {
+                           ((ArrayList<String>) acc).add((String) file);
+                           return acc;
+                         }),
         OutputChannels.none()
     );
   }
 
+  @Nullable
+  @Override
+  public TypeReference<Object> getResultTypeReference()
+  {
+    return new TypeReference<Object>() {};
+  }
+
+  @Override
+  public Object mergeAccumulatedResult(Object accumulated, Object 
otherAccumulated)
+  {
+    // If a worker does not return a list, fail the query
+    if (!(accumulated instanceof List)) {
+      throw DruidException.forPersona(DruidException.Persona.USER)
+                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                          .build("Expected a list result from worker, received 
[%s] instead. This might be due to workers having an older version.", 
accumulated.getClass());
+    }
+    if (!(otherAccumulated instanceof List)) {
+      throw DruidException.forPersona(DruidException.Persona.USER)
+                          .ofCategory(DruidException.Category.RUNTIME_FAILURE)
+                          .build("Expected a list result from worker, received 
[%s] instead. This might be due to workers having an older version.", 
otherAccumulated.getClass());
+    }
+    ((List<String>) accumulated).addAll((List<String>) otherAccumulated);
+    return accumulated;
+  }
+
+  @Override
+  public ExtraInfoHolder makeExtraInfoHolder(@Nullable Object extra)
+  {
+    if (extra != null) {
+      throw new ISE("Expected null 'extra'");
+    }
+
+    return NilExtraInfoHolder.instance();
+  }
+
   private static String getExportFilePath(String queryId, int workerNumber, 
int partitionNumber, ResultFormat exportFormat)
   {
     return StringUtils.format("%s-worker%s-partition%s.%s", queryId, 
workerNumber, partitionNumber, exportFormat.toString());
diff --git 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQExportTest.java
 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQExportTest.java
index 4481d046c92..edc98dcea98 100644
--- 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQExportTest.java
+++ 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQExportTest.java
@@ -62,7 +62,7 @@ public class MSQExportTest extends MSQTestBase
                      .verifyResults();
 
     Assert.assertEquals(
-        1,
+         2, // result file and manifest file
         Objects.requireNonNull(new 
File(exportDir.getAbsolutePath()).listFiles()).length
     );
 
@@ -93,16 +93,19 @@ public class MSQExportTest extends MSQTestBase
                      .verifyResults();
 
     Assert.assertEquals(
-        1,
+        2,
         Objects.requireNonNull(new 
File(exportDir.getAbsolutePath()).listFiles()).length
     );
 
+
     File resultFile = new File(exportDir, 
"query-test-query-worker0-partition0.csv");
     List<String> results = readResultsFromFile(resultFile);
     Assert.assertEquals(
         expectedFoo2FileContents(true),
         results
     );
+
+    verifyManifestFile(exportDir, ImmutableList.of(resultFile));
   }
 
   @Test
@@ -129,7 +132,7 @@ public class MSQExportTest extends MSQTestBase
                      .verifyResults();
 
     Assert.assertEquals(
-        expectedFooFileContents(false).size(),
+        expectedFooFileContents(false).size() + 1, // + 1 for the manifest file
         Objects.requireNonNull(new 
File(exportDir.getAbsolutePath()).listFiles()).length
     );
   }
@@ -141,14 +144,13 @@ public class MSQExportTest extends MSQTestBase
       expectedResults.add("cnt,dim");
     }
     expectedResults.addAll(ImmutableList.of(
-                               "1,",
-                               "1,10.1",
-                               "1,2",
-                               "1,1",
-                               "1,def",
-                               "1,abc"
-                           )
-    );
+        "1,",
+        "1,10.1",
+        "1,2",
+        "1,1",
+        "1,def",
+        "1,abc"
+    ));
     return expectedResults;
   }
 
@@ -173,4 +175,35 @@ public class MSQExportTest extends MSQTestBase
       return results;
     }
   }
+
+  private void verifyManifestFile(File exportDir, List<File> resultFiles) 
throws IOException
+  {
+    final File manifestFile = new File(exportDir, 
ExportMetadataManager.MANIFEST_FILE);
+    try (
+        BufferedReader bufferedReader = new BufferedReader(
+            new InputStreamReader(Files.newInputStream(manifestFile.toPath()), 
StringUtils.UTF8_STRING)
+        )
+    ) {
+      for (File file : resultFiles) {
+        Assert.assertEquals(
+            StringUtils.format("file:%s", file.getAbsolutePath()),
+            bufferedReader.readLine()
+        );
+      }
+      Assert.assertNull(bufferedReader.readLine());
+    }
+
+    final File metaFile = new File(exportDir, ExportMetadataManager.META_FILE);
+    try (
+        BufferedReader bufferedReader = new BufferedReader(
+            new InputStreamReader(Files.newInputStream(metaFile.toPath()), 
StringUtils.UTF8_STRING)
+        )
+    ) {
+      Assert.assertEquals(
+          StringUtils.format("version: %s", 
ExportMetadataManager.MANIFEST_FILE_VERSION),
+          bufferedReader.readLine()
+      );
+      Assert.assertNull(bufferedReader.readLine());
+    }
+  }
 }
diff --git 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
index 57f052e6f36..a3b6fa3d458 100644
--- 
a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
+++ 
b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java
@@ -159,8 +159,6 @@ import org.apache.druid.sql.SqlQueryPlus;
 import org.apache.druid.sql.SqlStatementFactory;
 import org.apache.druid.sql.SqlToolbox;
 import org.apache.druid.sql.calcite.BaseCalciteQueryTest;
-import org.apache.druid.sql.calcite.export.TestExportStorageConnector;
-import org.apache.druid.sql.calcite.export.TestExportStorageConnectorProvider;
 import org.apache.druid.sql.calcite.external.ExternalDataSource;
 import org.apache.druid.sql.calcite.external.ExternalOperatorConversion;
 import org.apache.druid.sql.calcite.external.HttpOperatorConversion;
@@ -318,7 +316,6 @@ public class MSQTestBase extends BaseCalciteQueryTest
   protected SqlStatementFactory sqlStatementFactory;
   protected AuthorizerMapper authorizerMapper;
   private IndexIO indexIO;
-  protected TestExportStorageConnectorProvider exportStorageConnectorProvider 
= new TestExportStorageConnectorProvider();
   // Contains the metadata of loaded segments
   protected List<ImmutableSegmentLoadInfo> loadedSegmentsMetadata = new 
ArrayList<>();
   // Mocks the return of data from data servers
@@ -512,12 +509,6 @@ public class MSQTestBase extends BaseCalciteQueryTest
         .build();
 
     objectMapper = setupObjectMapper(injector);
-    objectMapper.registerModule(
-        new SimpleModule(StorageConnector.class.getSimpleName())
-            .registerSubtypes(
-                new NamedType(TestExportStorageConnectorProvider.class, 
TestExportStorageConnector.TYPE_NAME)
-            )
-    );
     objectMapper.registerModules(new 
StorageConnectorModule().getJacksonModules());
     objectMapper.registerModules(sqlModule.getJacksonModules());
 
diff --git 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
index 7577f56f76f..9b03a4f07c7 100644
--- 
a/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
+++ 
b/extensions-core/s3-extensions/src/main/java/org/apache/druid/storage/s3/output/S3ExportStorageProvider.java
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonTypeName;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
 import org.apache.druid.data.input.impl.CloudObjectLocation;
 import org.apache.druid.data.input.s3.S3InputSource;
 import org.apache.druid.error.DruidException;
@@ -43,6 +44,8 @@ import java.util.List;
 public class S3ExportStorageProvider implements ExportStorageProvider
 {
   public static final String TYPE_NAME = S3InputSource.TYPE_KEY;
+  private static final String DELIM = "/";
+  private static final Joiner JOINER = Joiner.on(DELIM).skipNulls();
   @JsonProperty
   private final String bucket;
   @JsonProperty
@@ -143,4 +146,10 @@ public class S3ExportStorageProvider implements 
ExportStorageProvider
   {
     return new CloudObjectLocation(bucket, 
prefix).toUri(S3StorageDruidModule.SCHEME).toString();
   }
+
+  @Override
+  public String getFilePathForManifest(String fileName)
+  {
+    return new CloudObjectLocation(bucket, JOINER.join(prefix, 
fileName)).toUri(S3StorageDruidModule.SCHEME).toString();
+  }
 }
diff --git 
a/extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/output/S3ExportStorageProviderTest.java
 
b/extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/output/S3ExportStorageProviderTest.java
index 362f8583fd1..0a87c3a4c01 100644
--- 
a/extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/output/S3ExportStorageProviderTest.java
+++ 
b/extensions-core/s3-extensions/src/test/java/org/apache/druid/storage/s3/output/S3ExportStorageProviderTest.java
@@ -57,4 +57,13 @@ public class S3ExportStorageProviderTest
         () -> S3ExportStorageProvider.validateS3Prefix(validPrefixes, 
"bucket-name", "validPath123")
     );
   }
+
+  @Test
+  public void testExportManifestFilePath()
+  {
+    Assert.assertEquals(
+        "s3://export-bucket/export/table/file1",
+        new S3ExportStorageProvider("export-bucket", 
"export/table").getFilePathForManifest("file1")
+    );
+  }
 }
diff --git 
a/processing/src/main/java/org/apache/druid/storage/ExportStorageProvider.java 
b/processing/src/main/java/org/apache/druid/storage/ExportStorageProvider.java
index 890ac577b1a..173544e2f8d 100644
--- 
a/processing/src/main/java/org/apache/druid/storage/ExportStorageProvider.java
+++ 
b/processing/src/main/java/org/apache/druid/storage/ExportStorageProvider.java
@@ -31,4 +31,6 @@ public interface ExportStorageProvider extends 
Provider<StorageConnector>
    * Return a URI representation of the base path. This is used to be used for 
logging and error messages.
    */
   String getBasePath();
+
+  String getFilePathForManifest(String fileName);
 }
diff --git 
a/processing/src/main/java/org/apache/druid/storage/local/LocalFileExportStorageProvider.java
 
b/processing/src/main/java/org/apache/druid/storage/local/LocalFileExportStorageProvider.java
index f0d4c87b41f..74b099aef88 100644
--- 
a/processing/src/main/java/org/apache/druid/storage/local/LocalFileExportStorageProvider.java
+++ 
b/processing/src/main/java/org/apache/druid/storage/local/LocalFileExportStorageProvider.java
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonTypeName;
 import org.apache.druid.data.input.impl.LocalInputSource;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.storage.ExportStorageProvider;
 import org.apache.druid.storage.StorageConfig;
 import org.apache.druid.storage.StorageConnector;
@@ -83,6 +84,13 @@ public class LocalFileExportStorageProvider implements 
ExportStorageProvider
     return exportPath;
   }
 
+  @Override
+  public String getFilePathForManifest(String fileName)
+  {
+    final File exportFile = new File(exportPath, fileName);
+    return StringUtils.format("file:%s", exportFile.toPath().normalize());
+  }
+
   @Override
   public boolean equals(Object o)
   {
diff --git 
a/processing/src/test/java/org/apache/druid/storage/local/LocalFileExportStorageProviderTest.java
 
b/processing/src/test/java/org/apache/druid/storage/local/LocalFileExportStorageProviderTest.java
index 4daef2f9cd9..752720dcff7 100644
--- 
a/processing/src/test/java/org/apache/druid/storage/local/LocalFileExportStorageProviderTest.java
+++ 
b/processing/src/test/java/org/apache/druid/storage/local/LocalFileExportStorageProviderTest.java
@@ -90,4 +90,17 @@ public class LocalFileExportStorageProviderTest
         () -> LocalFileExportStorageProvider.validateAndGetPath("/base", 
"/base1")
     );
   }
+
+  @Test
+  public void testExportManifestFilePath()
+  {
+    Assert.assertEquals(
+        "file:/base/path1/file1",
+        new 
LocalFileExportStorageProvider("/base/path1").getFilePathForManifest("file1")
+    );
+    Assert.assertEquals(
+        "file:/base/path1/file1",
+        new 
LocalFileExportStorageProvider("/base/../base/path1").getFilePathForManifest("file1")
+    );
+  }
 }
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java
index eaf6af9e77b..c78aa536d30 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java
@@ -20,6 +20,7 @@
 package org.apache.druid.sql.calcite;
 
 import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.jsontype.NamedType;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.google.common.collect.ImmutableList;
 import com.google.inject.Binder;
@@ -33,14 +34,13 @@ import org.apache.druid.query.scan.ScanQuery;
 import org.apache.druid.segment.column.ColumnType;
 import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.server.security.ForbiddenException;
-import org.apache.druid.sql.calcite.export.TestExportModule;
-import org.apache.druid.sql.calcite.export.TestExportStorageConnector;
 import org.apache.druid.sql.calcite.filtration.Filtration;
 import org.apache.druid.sql.calcite.util.CalciteTests;
 import org.apache.druid.sql.destination.ExportDestination;
 import org.apache.druid.sql.http.SqlParameter;
 import org.apache.druid.storage.StorageConfig;
 import org.apache.druid.storage.StorageConnector;
+import org.apache.druid.storage.StorageConnectorProvider;
 import org.apache.druid.storage.local.LocalFileExportStorageProvider;
 import org.apache.druid.storage.local.LocalFileStorageConnectorProvider;
 import org.hamcrest.CoreMatchers;
@@ -57,7 +57,24 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void configureGuice(DruidInjectorBuilder builder)
   {
     super.configureGuice(builder);
-    builder.addModule(new TestExportModule());
+    builder.addModule(
+        new DruidModule()
+        {
+          @Override
+          public void configure(Binder binder)
+          {
+          }
+
+          @Override
+          public List<? extends Module> getJacksonModules()
+          {
+            return ImmutableList.of(
+                new 
SimpleModule(StorageConnectorProvider.class.getSimpleName()).registerSubtypes(
+                    new NamedType(LocalFileExportStorageProvider.class, 
CalciteTests.FORBIDDEN_DESTINATION)
+                )
+            );
+          }
+        });
     builder.addModule(new DruidModule()
     {
       @Override
@@ -84,10 +101,10 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testReplaceIntoExtern()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("REPLACE INTO EXTERN(%s(basePath => 'export')) 
"
+        .sql(StringUtils.format("REPLACE INTO EXTERN(%s(exportPath => 
'export')) "
                                 + "AS CSV "
                                 + "OVERWRITE ALL "
-                                + "SELECT dim2 FROM foo", 
TestExportStorageConnector.TYPE_NAME))
+                                + "SELECT dim2 FROM foo", 
LocalFileExportStorageProvider.TYPE_NAME))
         .expectQuery(
             Druids.newScanQueryBuilder()
                   .dataSource(
@@ -99,7 +116,7 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
                   .legacy(false)
                   .build()
         )
-        .expectResources(dataSourceRead("foo"), 
externalWrite(TestExportStorageConnector.TYPE_NAME))
+        .expectResources(dataSourceRead("foo"), 
externalWrite(LocalFileExportStorageProvider.TYPE_NAME))
         .expectTarget(ExportDestination.TYPE_KEY, 
RowSignature.builder().add("dim2", ColumnType.STRING).build())
         .verify();
   }
@@ -108,10 +125,10 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testReplaceIntoExternShouldThrowUnsupportedException()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("REPLACE INTO EXTERN(%s(basePath => 'export')) 
"
+        .sql(StringUtils.format("REPLACE INTO EXTERN(%s(exportPath => 
'export')) "
                                 + "AS CSV "
                                 + "OVERWRITE ALL "
-                                + "SELECT dim2 FROM foo", 
TestExportStorageConnector.TYPE_NAME))
+                                + "SELECT dim2 FROM foo", 
LocalFileExportStorageProvider.TYPE_NAME))
         .expectValidationError(
             CoreMatchers.allOf(
                 CoreMatchers.instanceOf(DruidException.class),
@@ -145,10 +162,10 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testExportWithPartitionedBy()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("INSERT INTO EXTERN(%s()) "
+        .sql(StringUtils.format("INSERT INTO 
EXTERN(%s(exportPath=>'/tmp/export')) "
                                 + "AS CSV "
                                 + "SELECT dim2 FROM foo "
-                                + "PARTITIONED BY ALL", 
TestExportStorageConnector.TYPE_NAME))
+                                + "PARTITIONED BY ALL", 
LocalFileStorageConnectorProvider.TYPE_NAME))
         .expectValidationError(
             DruidException.class,
             "Export statements do not support a PARTITIONED BY or CLUSTERED BY 
clause."
@@ -160,9 +177,9 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testInsertIntoExtern()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("INSERT INTO EXTERN(%s()) "
+        .sql(StringUtils.format("INSERT INTO 
EXTERN(%s(exportPath=>'/tmp/export')) "
                                 + "AS CSV "
-                                + "SELECT dim2 FROM foo", 
TestExportStorageConnector.TYPE_NAME))
+                                + "SELECT dim2 FROM foo", 
LocalFileStorageConnectorProvider.TYPE_NAME))
         .expectQuery(
             Druids.newScanQueryBuilder()
                   .dataSource(
@@ -174,7 +191,7 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
                   .legacy(false)
                   .build()
         )
-        .expectResources(dataSourceRead("foo"), 
externalWrite(TestExportStorageConnector.TYPE_NAME))
+        .expectResources(dataSourceRead("foo"), 
externalWrite(LocalFileStorageConnectorProvider.TYPE_NAME))
         .expectTarget(ExportDestination.TYPE_KEY, 
RowSignature.builder().add("dim2", ColumnType.STRING).build())
         .verify();
   }
@@ -184,9 +201,9 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testInsertIntoExternParameterized()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("INSERT INTO EXTERN(%s()) "
+        .sql(StringUtils.format("INSERT INTO 
EXTERN(%s(exportPath=>'/tmp/export')) "
                                 + "AS CSV "
-                                + "SELECT dim2 FROM foo WHERE dim2=?", 
TestExportStorageConnector.TYPE_NAME))
+                                + "SELECT dim2 FROM foo WHERE dim2=?", 
LocalFileStorageConnectorProvider.TYPE_NAME))
         .parameters(Collections.singletonList(new 
SqlParameter(SqlType.VARCHAR, "val")))
         .expectQuery(
             Druids.newScanQueryBuilder()
@@ -200,7 +217,7 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
                 .legacy(false)
                 .build()
         )
-        .expectResources(dataSourceRead("foo"), 
externalWrite(TestExportStorageConnector.TYPE_NAME))
+        .expectResources(dataSourceRead("foo"), 
externalWrite(LocalFileStorageConnectorProvider.TYPE_NAME))
         .expectTarget(ExportDestination.TYPE_KEY, 
RowSignature.builder().add("dim2", ColumnType.STRING).build())
         .verify();
   }
@@ -211,9 +228,9 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testReplaceIntoExternParameterized()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("REPLACE INTO EXTERN(%s()) "
+        .sql(StringUtils.format("REPLACE INTO 
EXTERN(%s(exportPath=>'/tmp/export')) "
                                 + "AS CSV "
-                                + "SELECT dim2 FROM foo WHERE dim2=?", 
TestExportStorageConnector.TYPE_NAME))
+                                + "SELECT dim2 FROM foo WHERE dim2=?", 
LocalFileStorageConnectorProvider.TYPE_NAME))
         .parameters(Collections.singletonList(new 
SqlParameter(SqlType.VARCHAR, "val")))
         .expectQuery(
             Druids.newScanQueryBuilder()
@@ -227,7 +244,7 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
                 .legacy(false)
                 .build()
         )
-        .expectResources(dataSourceRead("foo"), 
externalWrite(TestExportStorageConnector.TYPE_NAME))
+        .expectResources(dataSourceRead("foo"), 
externalWrite(LocalFileStorageConnectorProvider.TYPE_NAME))
         .expectTarget(ExportDestination.TYPE_KEY, 
RowSignature.builder().add("dim2", ColumnType.STRING).build())
         .verify();
   }
@@ -263,7 +280,7 @@ public class CalciteExportTest extends 
CalciteIngestionDmlTest
   public void testWithForbiddenDestination()
   {
     testIngestionQuery()
-        .sql(StringUtils.format("insert into extern(%s()) as csv select  
__time, dim1 from foo", CalciteTests.FORBIDDEN_DESTINATION))
+        .sql(StringUtils.format("insert into 
extern(%s(exportPath=>'/tmp/export')) as csv select  __time, dim1 from foo", 
CalciteTests.FORBIDDEN_DESTINATION))
         .expectValidationError(ForbiddenException.class)
         .verify();
   }
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportModule.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportModule.java
deleted file mode 100644
index b6969f4c165..00000000000
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportModule.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.sql.calcite.export;
-
-import com.fasterxml.jackson.databind.Module;
-import com.fasterxml.jackson.databind.jsontype.NamedType;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.google.common.collect.ImmutableList;
-import com.google.inject.Binder;
-import org.apache.druid.initialization.DruidModule;
-import org.apache.druid.sql.calcite.util.CalciteTests;
-import org.apache.druid.storage.StorageConnectorProvider;
-
-import java.util.List;
-
-public class TestExportModule implements DruidModule
-{
-  @Override
-  public List<? extends Module> getJacksonModules()
-  {
-    return ImmutableList.of(
-        new SimpleModule(StorageConnectorProvider.class.getSimpleName())
-            .registerSubtypes(
-                new NamedType(TestExportStorageConnectorProvider.class, 
TestExportStorageConnector.TYPE_NAME),
-                new NamedType(TestExportStorageConnectorProvider.class, 
CalciteTests.FORBIDDEN_DESTINATION)
-            )
-    );
-  }
-
-  @Override
-  public void configure(Binder binder)
-  {
-
-  }
-}
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnector.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnector.java
deleted file mode 100644
index b81b22ceb87..00000000000
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnector.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.sql.calcite.export;
-
-import com.google.common.collect.ImmutableList;
-import org.apache.druid.storage.StorageConnector;
-
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Iterator;
-
-public class TestExportStorageConnector implements StorageConnector
-{
-  public static final String TYPE_NAME = "testStorage";
-  private final ByteArrayOutputStream byteArrayOutputStream;
-
-  public TestExportStorageConnector()
-  {
-    this.byteArrayOutputStream = new ByteArrayOutputStream();
-  }
-
-  public ByteArrayOutputStream getByteArrayOutputStream()
-  {
-    return byteArrayOutputStream;
-  }
-
-  @Override
-  public boolean pathExists(String path)
-  {
-    return true;
-  }
-
-  @Override
-  public InputStream read(String path)
-  {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public InputStream readRange(String path, long from, long size)
-  {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public OutputStream write(String path)
-  {
-    return byteArrayOutputStream;
-  }
-
-  @Override
-  public void deleteFile(String path)
-  {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void deleteFiles(Iterable<String> paths)
-  {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void deleteRecursively(String path)
-  {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public Iterator<String> listDir(String dirName)
-  {
-    return ImmutableList.<String>of().stream().iterator();
-  }
-}
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnectorProvider.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnectorProvider.java
deleted file mode 100644
index b1ca59e2ccc..00000000000
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/export/TestExportStorageConnectorProvider.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.druid.sql.calcite.export;
-
-import org.apache.druid.storage.ExportStorageProvider;
-import org.apache.druid.storage.StorageConnector;
-
-public class TestExportStorageConnectorProvider implements 
ExportStorageProvider
-{
-  private static final StorageConnector STORAGE_CONNECTOR = new 
TestExportStorageConnector();
-
-  @Override
-  public StorageConnector get()
-  {
-    return STORAGE_CONNECTOR;
-  }
-
-  @Override
-  public String getResourceType()
-  {
-    return "testExport";
-  }
-
-  @Override
-  public String getBasePath()
-  {
-    return "testExport";
-  }
-}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to