@@ -100,20 +100,26 @@ public Set<Pair<String, String>> copyPlan() {
100100 * Create a new table metadata object, replacing path references
101101 *
102102 * @param metadata source table metadata
103- * @param sourcePrefix source prefix that will be replaced
104- * @param targetPrefix target prefix that will replace it
103+ * @param prefixMappings source prefix amd target prefix mappings
105104 * @return copy of table metadata with paths replaced
106105 */
107106 public static TableMetadata replacePaths (
108- TableMetadata metadata , String sourcePrefix , String targetPrefix ) {
109- String newLocation = metadata .location ().replaceFirst (sourcePrefix , targetPrefix );
110- List <Snapshot > newSnapshots = updatePathInSnapshots (metadata , sourcePrefix , targetPrefix );
107+ TableMetadata metadata , Map <String , String > prefixMappings ) {
108+ Preconditions .checkArgument (
109+ metadata .partitionStatisticsFiles ().isEmpty (),
110+ "Partition statistics files are not supported yet" );
111+ Map .Entry <String , String > prefixMap = lookupPrefixMappings (metadata .location (), prefixMappings );
112+ if (prefixMap == null ) {
113+ throw new IllegalArgumentException (
114+ "unable to find prefix mapping for path: " + metadata .location ());
115+ }
116+ String newLocation = newPath (metadata .location (), prefixMappings );
117+ List <Snapshot > newSnapshots = updatePathInSnapshots (metadata , prefixMappings );
111118 List <TableMetadata .MetadataLogEntry > metadataLogEntries =
112- updatePathInMetadataLogs (metadata , sourcePrefix , targetPrefix );
119+ updatePathInMetadataLogs (metadata , prefixMappings );
113120 long snapshotId =
114121 metadata .currentSnapshot () == null ? -1 : metadata .currentSnapshot ().snapshotId ();
115- Map <String , String > properties =
116- updateProperties (metadata .properties (), sourcePrefix , targetPrefix );
122+ Map <String , String > properties = updateProperties (metadata .properties (), prefixMappings );
117123
118124 return new TableMetadata (
119125 null ,
@@ -137,25 +143,29 @@ public static TableMetadata replacePaths(
137143 metadata .snapshotLog (),
138144 metadataLogEntries ,
139145 metadata .refs (),
140- updatePathInStatisticsFiles (metadata .statisticsFiles (), sourcePrefix , targetPrefix ),
141- updatePathInPartitionStatisticsFiles (
142- metadata .partitionStatisticsFiles (), sourcePrefix , targetPrefix ),
146+ updatePathInStatisticsFiles (metadata .statisticsFiles (), prefixMappings ),
147+ updatePathInPartitionStatisticsFiles (metadata .partitionStatisticsFiles (), prefixMappings ),
143148 metadata .nextRowId (),
144149 metadata .encryptionKeys (),
145150 metadata .changes ());
146151 }
147152
148153 private static Map <String , String > updateProperties (
149- Map <String , String > tableProperties , String sourcePrefix , String targetPrefix ) {
154+ Map <String , String > tableProperties , Map < String , String > prefixMappings ) {
150155 Map <String , String > properties = Maps .newHashMap (tableProperties );
151- updatePathInProperty (properties , sourcePrefix , targetPrefix , TableProperties .OBJECT_STORE_PATH );
152- updatePathInProperty (
153- properties , sourcePrefix , targetPrefix , TableProperties .WRITE_FOLDER_STORAGE_LOCATION );
154- updatePathInProperty (
155- properties , sourcePrefix , targetPrefix , TableProperties .WRITE_DATA_LOCATION );
156- updatePathInProperty (
157- properties , sourcePrefix , targetPrefix , TableProperties .WRITE_METADATA_LOCATION );
158-
156+ for (Map .Entry <String , String > entry : prefixMappings .entrySet ()) {
157+ updatePathInProperty (
158+ properties , entry .getKey (), entry .getValue (), TableProperties .OBJECT_STORE_PATH );
159+ updatePathInProperty (
160+ properties ,
161+ entry .getKey (),
162+ entry .getValue (),
163+ TableProperties .WRITE_FOLDER_STORAGE_LOCATION );
164+ updatePathInProperty (
165+ properties , entry .getKey (), entry .getValue (), TableProperties .WRITE_DATA_LOCATION );
166+ updatePathInProperty (
167+ properties , entry .getKey (), entry .getValue (), TableProperties .WRITE_METADATA_LOCATION );
168+ }
159169 return properties ;
160170 }
161171
@@ -171,13 +181,13 @@ private static void updatePathInProperty(
171181 }
172182
173183 private static List <StatisticsFile > updatePathInStatisticsFiles (
174- List <StatisticsFile > statisticsFiles , String sourcePrefix , String targetPrefix ) {
184+ List <StatisticsFile > statisticsFiles , Map < String , String > prefixMappings ) {
175185 return statisticsFiles .stream ()
176186 .map (
177187 existing ->
178188 new GenericStatisticsFile (
179189 existing .snapshotId (),
180- newPath (existing .path (), sourcePrefix , targetPrefix ),
190+ newPath (existing .path (), prefixMappings ),
181191 existing .fileSizeInBytes (),
182192 existing .fileFooterSizeInBytes (),
183193 existing .blobMetadata ()))
@@ -189,46 +199,41 @@ private static List<StatisticsFile> updatePathInStatisticsFiles(
189199 * sourcePrefix in the file paths with the targetPrefix.
190200 *
191201 * @param partitionStatisticsFiles The list of PartitionStatisticsFile to update.
192- * @param sourcePrefix The prefix to be replaced in the file paths.
193- * @param targetPrefix The new prefix to replace the sourcePrefix in the file paths.
202+ * @param prefixMappings The mappings between source prefix and destination prefix.
194203 * @return A new list of PartitionStatisticsFile with updated file paths.
195204 */
196205 private static List <PartitionStatisticsFile > updatePathInPartitionStatisticsFiles (
197- List <PartitionStatisticsFile > partitionStatisticsFiles ,
198- String sourcePrefix ,
199- String targetPrefix ) {
206+ List <PartitionStatisticsFile > partitionStatisticsFiles , Map <String , String > prefixMappings ) {
200207
201208 return partitionStatisticsFiles .stream ()
202209 .map (
203210 existing ->
204211 ImmutableGenericPartitionStatisticsFile .builder ()
205212 .snapshotId (existing .snapshotId ())
206- .path (newPath (existing .path (), sourcePrefix , targetPrefix ))
213+ .path (newPath (existing .path (), prefixMappings ))
207214 .fileSizeInBytes (existing .fileSizeInBytes ())
208215 .build ())
209216 .collect (Collectors .toList ());
210217 }
211218
212219 private static List <TableMetadata .MetadataLogEntry > updatePathInMetadataLogs (
213- TableMetadata metadata , String sourcePrefix , String targetPrefix ) {
220+ TableMetadata metadata , Map < String , String > prefixMappings ) {
214221 List <TableMetadata .MetadataLogEntry > metadataLogEntries =
215222 Lists .newArrayListWithCapacity (metadata .previousFiles ().size ());
216223 for (TableMetadata .MetadataLogEntry metadataLog : metadata .previousFiles ()) {
217224 TableMetadata .MetadataLogEntry newMetadataLog =
218225 new TableMetadata .MetadataLogEntry (
219- metadataLog .timestampMillis (),
220- newPath (metadataLog .file (), sourcePrefix , targetPrefix ));
226+ metadataLog .timestampMillis (), newPath (metadataLog .file (), prefixMappings ));
221227 metadataLogEntries .add (newMetadataLog );
222228 }
223229 return metadataLogEntries ;
224230 }
225231
226232 private static List <Snapshot > updatePathInSnapshots (
227- TableMetadata metadata , String sourcePrefix , String targetPrefix ) {
233+ TableMetadata metadata , Map < String , String > prefixMappings ) {
228234 List <Snapshot > newSnapshots = Lists .newArrayListWithCapacity (metadata .snapshots ().size ());
229235 for (Snapshot snapshot : metadata .snapshots ()) {
230- String newManifestListLocation =
231- newPath (snapshot .manifestListLocation (), sourcePrefix , targetPrefix );
236+ String newManifestListLocation = newPath (snapshot .manifestListLocation (), prefixMappings );
232237 Snapshot newSnapshot =
233238 new BaseSnapshot (
234239 snapshot .sequenceNumber (),
@@ -254,8 +259,7 @@ private static List<Snapshot> updatePathInSnapshots(
254259 * @param io file io
255260 * @param tableMetadata metadata of table
256261 * @param manifestsToRewrite a list of manifest files to filter for rewrite
257- * @param sourcePrefix source prefix that will be replaced
258- * @param targetPrefix target prefix that will replace it
262+ * @param prefixMappings map of source prefix to target prefix mappings
259263 * @param stagingDir staging directory
260264 * @param outputPath location to write the manifest list
261265 * @return a copy plan for manifest files whose metadata were contained in the rewritten manifest
@@ -266,21 +270,22 @@ public static RewriteResult<ManifestFile> rewriteManifestList(
266270 FileIO io ,
267271 TableMetadata tableMetadata ,
268272 Set <String > manifestsToRewrite ,
269- String sourcePrefix ,
270- String targetPrefix ,
273+ Map <String , String > prefixMappings ,
271274 String stagingDir ,
272275 String outputPath ) {
273276 RewriteResult <ManifestFile > result = new RewriteResult <>();
274277 OutputFile outputFile = io .newOutputFile (outputPath );
275278
276279 List <ManifestFile > manifestFiles = manifestFilesInSnapshot (io , snapshot );
280+ // Validate that all manifest files can be matched by at least one prefix
277281 manifestFiles .forEach (
278- mf ->
279- Preconditions .checkArgument (
280- mf .path ().startsWith (sourcePrefix ),
281- "Encountered manifest file %s not under the source prefix %s" ,
282- mf .path (),
283- sourcePrefix ));
282+ mf -> {
283+ Preconditions .checkArgument (
284+ lookupPrefixMappings (mf .path (), prefixMappings ) != null ,
285+ "Encountered manifest file %s not matching any source prefix in %s" ,
286+ mf .path (),
287+ prefixMappings .keySet ());
288+ });
284289
285290 EncryptionManager encryptionManager =
286291 (io instanceof EncryptingFileIO )
@@ -299,14 +304,16 @@ public static RewriteResult<ManifestFile> rewriteManifestList(
299304
300305 for (ManifestFile file : manifestFiles ) {
301306 ManifestFile newFile = file .copy ();
302- ((StructLike ) newFile ).set (0 , newPath (newFile .path (), sourcePrefix , targetPrefix ));
307+ String newPath = newPath (newFile .path (), prefixMappings );
308+ ((StructLike ) newFile ).set (0 , newPath );
303309 writer .add (newFile );
304310
305311 if (manifestsToRewrite .contains (file .path ())) {
306312 result .toRewrite ().add (file );
313+ String stagingFilePath = stagingPath (file .path (), prefixMappings , stagingDir );
307314 result
308315 .copyPlan ()
309- .add (Pair .of (stagingPath ( file . path (), sourcePrefix , stagingDir ), newFile .path ()));
316+ .add (Pair .of (stagingFilePath != null ? stagingFilePath : file .path (), newPath ));
310317 }
311318 }
312319 return result ;
@@ -604,28 +611,27 @@ PositionDeleteWriter<Record> writer(
604611 * @param outputFile output file to rewrite delete file to
605612 * @param io file io
606613 * @param spec spec of delete file
607- * @param sourcePrefix source prefix that will be replaced
608- * @param targetPrefix target prefix to replace it
614+ * @param prefixMappings source prefix and target prefix mappings
609615 * @param posDeleteReaderWriter class to read and write position delete files
610616 */
611617 public static void rewritePositionDeleteFile (
612618 DeleteFile deleteFile ,
613619 OutputFile outputFile ,
614620 FileIO io ,
615621 PartitionSpec spec ,
616- String sourcePrefix ,
617- String targetPrefix ,
622+ Map <String , String > prefixMappings ,
618623 PositionDeleteReaderWriter posDeleteReaderWriter )
619624 throws IOException {
620625 String path = deleteFile .location ();
621- if (!path .startsWith (sourcePrefix )) {
626+ Map .Entry <String , String > matchEntry = lookupPrefixMappings (path , prefixMappings );
627+ if (matchEntry == null ) {
622628 throw new UnsupportedOperationException (
623- String .format ("Expected delete file %s to start with prefix: %s " , path , sourcePrefix ));
629+ String .format ("Expected delete file %s to find in prefixes " , path ));
624630 }
625631
626632 // DV files (Puffin format for v3+) need special handling to rewrite internal blob metadata
627633 if (ContentFileUtil .isDV (deleteFile )) {
628- rewriteDVFile (deleteFile , outputFile , io , sourcePrefix , targetPrefix );
634+ rewriteDVFile (deleteFile , outputFile , io , matchEntry . getKey (), matchEntry . getValue () );
629635 return ;
630636 }
631637
@@ -647,12 +653,13 @@ record = recordIt.next();
647653 posDeleteReaderWriter .writer (
648654 outputFile , deleteFile .format (), spec , deleteFile .partition (), rowSchema )) {
649655
650- writer .write (newPositionDeleteRecord (record , sourcePrefix , targetPrefix ));
656+ writer .write (newPositionDeleteRecord (record , matchEntry . getKey (), matchEntry . getValue () ));
651657
652658 while (recordIt .hasNext ()) {
653659 record = recordIt .next ();
654660 if (record != null ) {
655- writer .write (newPositionDeleteRecord (record , sourcePrefix , targetPrefix ));
661+ writer .write (
662+ newPositionDeleteRecord (record , matchEntry .getKey (), matchEntry .getValue ()));
656663 }
657664 }
658665 }
@@ -728,7 +735,42 @@ private static PositionDelete newPositionDeleteRecord(
728735 }
729736
730737 /**
731- * Rewrite a path by replacing its source prefix with a target prefix.
738+ * Lookup the longest matching prefix mapping for a given path.
739+ *
740+ * @param path the path to find a prefix mapping for
741+ * @param prefixMappings map of source prefix to target prefix mappings
742+ * @return the Map.Entry with the longest matching source prefix, or null if no match found
743+ */
744+ public static Map .Entry <String , String > lookupPrefixMappings (
745+ String path , Map <String , String > prefixMappings ) {
746+ if (prefixMappings == null || prefixMappings .isEmpty () || path == null ) {
747+ return null ;
748+ }
749+
750+ String normalizedPath = maybeAppendFileSeparator (path );
751+ return prefixMappings .entrySet ().stream ()
752+ .filter (entry -> normalizedPath .startsWith (maybeAppendFileSeparator (entry .getKey ())))
753+ .max (java .util .Comparator .comparing (entry -> entry .getKey ().length ()))
754+ .orElse (null );
755+ }
756+
757+ public static String newPath (String path , Map <String , String > prefixMappings ) {
758+ if (prefixMappings == null || prefixMappings .isEmpty ()) {
759+ return path ;
760+ }
761+
762+ Map .Entry <String , String > entry = lookupPrefixMappings (path , prefixMappings );
763+ if (entry != null ) {
764+ String sourcePrefix = entry .getKey ();
765+ String targetPrefix = entry .getValue ();
766+ return combinePaths (targetPrefix , relativize (path , sourcePrefix ));
767+ }
768+
769+ return path ;
770+ }
771+
772+ /**
773+ * Replace path reference
732774 *
733775 * <p>If the path equals the source prefix (representing a directory location), the result will be
734776 * the target prefix with a trailing separator.
@@ -814,4 +856,23 @@ public static String stagingPath(String originalPath, String sourcePrefix, Strin
814856 String relativePath = relativize (originalPath , sourcePrefix );
815857 return combinePaths (stagingDir , relativePath );
816858 }
859+
860+ /**
861+ * Construct a staging path under a given staging directory, preserving relative directory
862+ * structure to avoid conflicts when multiple files have the same name but different paths.
863+ *
864+ * @param originalPath source path
865+ * @param prefixMappings source prefix and target prefix mappings
866+ * @param stagingDir staging directory
867+ * @return a staging path under the staging directory that preserves the relative path structure
868+ */
869+ public static String stagingPath (
870+ String originalPath , Map <String , String > prefixMappings , String stagingDir ) {
871+ Map .Entry <String , String > entry = lookupPrefixMappings (originalPath , prefixMappings );
872+ if (entry == null ) {
873+ throw new IllegalArgumentException ("unable to find prefix mapping for path: " + originalPath );
874+ }
875+ String relativePath = relativize (originalPath , entry .getKey ());
876+ return combinePaths (stagingDir , relativePath );
877+ }
817878}
0 commit comments