1010 */
1111package tools .dynamia .modules .saas .migration .pipeline ;
1212
13+ import jakarta .persistence .EntityGraph ;
1314import jakarta .persistence .EntityManager ;
1415import jakarta .persistence .EntityManagerFactory ;
1516import jakarta .persistence .metamodel .Attribute .PersistentAttributeType ;
4647import java .time .LocalDateTime ;
4748import java .util .ArrayList ;
4849import java .util .Collections ;
49- import java .util .LinkedHashMap ;
5050import java .util .List ;
5151import java .util .Map ;
5252import java .util .concurrent .ConcurrentHashMap ;
@@ -100,17 +100,21 @@ public class ExportPipeline {
100100
101101 private static final LoggingService logger = LoggingService .get (ExportPipeline .class );
102102
103- /** ZIP compression level — BEST_SPEED gives good ratio for JSON with minimal CPU overhead. */
103+ /**
104+ * ZIP compression level — BEST_SPEED gives good ratio for JSON with minimal CPU overhead.
105+ */
104106 private static final int ZIP_LEVEL = Deflater .BEST_SPEED ;
105107
106- /** ZIP output buffer size. */
108+ /**
109+ * ZIP output buffer size.
110+ */
107111 private static final int ZIP_BUFFER_SIZE = 256 * 1024 ;
108112
109- /** Per-entity file write buffer size. */
113+ /**
114+ * Per-entity file write buffer size.
115+ */
110116 private static final int ENTITY_BUFFER_SIZE = 64 * 1024 ;
111-
112- /** Standard Jakarta Persistence fetch-graph hint key. */
113- private static final String HINT_FETCH_GRAPH = "jakarta.persistence.fetchgraph" ;
117+ public static final String JAKARTA_PERSISTENCE_FETCHGRAPH = "jakarta.persistence.fetchgraph" ;
114118
115119 /**
116120 * Column definitions cached per entity class; built once on first export,
@@ -378,12 +382,14 @@ private void writeEmptyEntityFile(Path filePath, Class<?> entityClass) throws IO
378382 }
379383
380384 /**
381- * Pages through all rows for {@code entityClass} using keyset pagination
382- * ({@code id > lastId}) and writes each row to {@code gen}.
385+ * Pages through all rows for {@code entityClass} using keyset pagination and writes
386+ * each row to {@code gen}.
387+ *
388+ * <p>ID type-agnostic: {@code null} is used as the first-page sentinel so no
389+ * type-specific "zero" value is required. The JPA provider receives the actual
390+ * ID object (Long, UUID, String, …) on subsequent pages and handles coercion.
383391 *
384- * <p>{@link Account} is skipped — its data lives in the manifest.
385- * Each parallel task passes its own {@code localEm} so there is no
386- * cross-thread EntityManager sharing.
392+ * <p>{@link Account} rows are skipped — account data lives in the manifest.
387393 */
388394 private long writeEntityRows (JsonGenerator gen ,
389395 Class <?> entityClass ,
@@ -396,28 +402,40 @@ private long writeEntityRows(JsonGenerator gen,
396402 return 0 ;
397403 }
398404
405+ String simpleName = entityClass .getSimpleName ();
399406 int chunkSize = resolveChunkSize (options );
400- long lastId = 0 ;
407+ Object lastId = null ; // null = first page; avoids assuming ID type
401408 long processed = 0 ;
402409
410+ EntityGraph <?> emptyGraph = localEm .createEntityGraph (entityClass ); //to avoid errors with multiple eagers calls
411+
412+
403413 do {
404414 @ SuppressWarnings ("unchecked" )
405- List <Object > page = localEm .createQuery (
406- "SELECT e FROM " + entityClass .getSimpleName () +
415+ List <Object > page = (lastId == null )
416+ ? localEm .createQuery (
417+ "SELECT e FROM " + simpleName +
418+ " e WHERE e.accountId = :accountId ORDER BY e.id ASC" )
419+ .setParameter ("accountId" , accountId )
420+ .setMaxResults (chunkSize )
421+ .setHint (JAKARTA_PERSISTENCE_FETCHGRAPH , emptyGraph )
422+ .getResultList ()
423+ : localEm .createQuery (
424+ "SELECT e FROM " + simpleName +
407425 " e WHERE e.accountId = :accountId AND e.id > :lastId ORDER BY e.id ASC" )
408426 .setParameter ("accountId" , accountId )
409427 .setParameter ("lastId" , lastId )
410428 .setMaxResults (chunkSize )
411- .setHint (HINT_FETCH_GRAPH , localEm . createEntityGraph ( entityClass ) )
429+ .setHint (JAKARTA_PERSISTENCE_FETCHGRAPH , emptyGraph )
412430 .getResultList ();
413431
414432 for (Object entity : page ) {
415433 if (token != null && token .isCancelled ()) break ;
416434 if (entity != null ) {
417435 writeEntityRow (gen , entity , columns );
418436 Object idVal = JpaUtils .getJPAIdValue (entity );
419- if (idVal instanceof Number n ) {
420- lastId = n . longValue ();
437+ if (idVal != null ) {
438+ lastId = idVal ; // preserve exact type: Long, UUID, String, …
421439 }
422440 }
423441 processed ++;
@@ -443,7 +461,12 @@ private long writeEntityRows(JsonGenerator gen,
443461 private void zipToOutput (Path tempDir , List <Class <?>> ordered , Long accountId ,
444462 OutputStream output ) throws IOException {
445463
446- ZipOutputStream zipOut = new ZipOutputStream (new BufferedOutputStream (output , ZIP_BUFFER_SIZE ));
464+ // Keep reference to the buffer so we can flush it after finish().
465+ // ZipOutputStream.finish() writes the central directory into the BufferedOutputStream
466+ // buffer but does NOT flush it — without the explicit flush() below the last bytes
467+ // never reach `output` and the ZIP is corrupt.
468+ BufferedOutputStream buffered = new BufferedOutputStream (output , ZIP_BUFFER_SIZE );
469+ ZipOutputStream zipOut = new ZipOutputStream (buffered );
447470 zipOut .setLevel (ZIP_LEVEL );
448471
449472 addFileToZip (zipOut , tempDir .resolve (ExportConstants .MANIFEST_FILE ), ExportConstants .MANIFEST_FILE );
@@ -456,7 +479,8 @@ private void zipToOutput(Path tempDir, List<Class<?>> ordered, Long accountId,
456479 }
457480 }
458481
459- zipOut .finish (); // writes ZIP central directory; does not close the caller's stream
482+ zipOut .finish (); // writes ZIP central directory into buffered
483+ buffered .flush (); // pushes buffered bytes to the caller's output stream
460484 }
461485
462486 private static void addFileToZip (ZipOutputStream zipOut , Path file , String entryName )
0 commit comments