66import dev .mhh .cloner .api .Cloner ;
77import dev .mhh .cloner .api .verification .ConfigurationVerifier ;
88import dev .mhh .cloner .impl .select .CloneConfigurationTransformer ;
9+ import dev .mhh .cloner .impl .select .DeletePreviousTransformer ;
910import org .postgresql .PGConnection ;
1011import org .postgresql .copy .CopyManager ;
1112
1213import java .io .InputStream ;
1314import java .io .OutputStream ;
1415import java .io .Serializable ;
16+ import java .nio .charset .StandardCharsets ;
1517import java .sql .Connection ;
1618import java .sql .SQLException ;
1719import java .util .List ;
2022import java .util .zip .ZipOutputStream ;
2123
2224public class ClonerImpl <T extends Serializable > implements Cloner <T > {
25+ private static final String CONFIG_ENTRY_NAME = "__config__.txt" ;
26+
2327 @ Override
2428 public void exportClone (
2529 final OutputStream outputStream ,
@@ -35,8 +39,21 @@ public void exportClone(
3539
3640 final var zos = new ZipOutputStream (outputStream );
3741
38- for (final var select : selects ) {
42+ final var configEntry = new ZipEntry (CONFIG_ENTRY_NAME );
43+ CloneException .wrapKnownExceptions (
44+ () -> zos .putNextEntry (configEntry ),
45+ CloneError .ZIP_ENTRY_CREATION_FAILED
46+ );
47+ CloneException .wrapKnownExceptions (
48+ () -> zos .write (configuration .toString ().getBytes (StandardCharsets .UTF_8 )),
49+ CloneError .ZIP_ENTRY_CREATION_FAILED
50+ );
51+ CloneException .wrapKnownExceptions (
52+ zos ::closeEntry ,
53+ CloneError .ZIP_ENTRY_CLOSE_FAILED
54+ );
3955
56+ for (final var select : selects ) {
4057 final var zipEntry = new ZipEntry (select .tableName () + ".csv" );
4158 CloneException .wrapKnownExceptions (
4259 () -> zos .putNextEntry (zipEntry ),
@@ -73,12 +90,28 @@ private static CopyManager getCopyManager(final Connection connection) throws Cl
7390 }
7491
7592 @ Override
76- public void importClone (final InputStream inputStream , final Connection connection ) throws CloneException {
93+ public void importClone (
94+ final InputStream inputStream ,
95+ final Connection connection ,
96+ final CloneConfiguration <T > configuration
97+ ) throws CloneException {
7798 final var copyManager = getCopyManager (connection );
7899
100+ final var deleteScopeByTable = DeletePreviousTransformer .transform (configuration );
101+
79102 final var zis = new ZipInputStream (inputStream );
80103
81- ZipEntry entry ;
104+ var entry = CloneException .wrapKnownExceptions (
105+ zis ::getNextEntry ,
106+ CloneError .ZIP_ENTRY_NEXT_FAILED
107+ );
108+
109+ if (entry .getName ().equals (CONFIG_ENTRY_NAME )) {
110+ verifyConfigFingerprint (zis , configuration );
111+ CloneException .wrapKnownExceptions (zis ::closeEntry , CloneError .ZIP_ENTRY_CLOSE_FAILED );
112+ } else {
113+ throw CloneError .CONFIG_FINGERPRINT_WAS_NOT_FIRST_ENTRY .toException ();
114+ }
82115
83116 while ((entry = CloneException .wrapKnownExceptions (
84117 zis ::getNextEntry ,
@@ -88,7 +121,7 @@ public void importClone(final InputStream inputStream, final Connection connecti
88121
89122 final var tableName = fileName .substring (0 , fileName .length () - 4 );
90123
91- final var tempTableSql = "create temp table temp_" + tableName + " (like " + tableName + " including defaults)" ;
124+ final var tempTableSql = "create temp table temp_" + tableName + " (like " + tableName + " including defaults) on commit drop " ;
92125 CloneException .wrapKnownExceptions (
93126 () -> execute (connection , tempTableSql ),
94127 CloneError .CREATE_TEMP_TABLE_FAILED
@@ -100,7 +133,20 @@ public void importClone(final InputStream inputStream, final Connection connecti
100133 CloneError .COPY_IN_FAILED
101134 );
102135
103- final var moveSql = "insert into " + tableName + " select * from temp_" + tableName ;
136+ final var deleteScope = deleteScopeByTable .get (tableName );
137+ if (deleteScope != null ) {
138+ final var idColumn = configuration .idColumnName ();
139+ final var deleteSql = "delete from " + tableName
140+ + " where " + idColumn + " not in (select " + idColumn + " from temp_" + tableName + ")"
141+ + " and " + deleteScope ;
142+ CloneException .wrapKnownExceptions (
143+ () -> execute (connection , deleteSql ),
144+ CloneError .DELETE_STALE_DATA_FAILED
145+ );
146+ }
147+
148+ final var conflictClause = buildConflictClause (connection , tableName , configuration );
149+ final var moveSql = "insert into " + tableName + " select * from temp_" + tableName + " " + conflictClause ;
104150 CloneException .wrapKnownExceptions (
105151 () -> execute (connection , moveSql ),
106152 CloneError .MOVE_DATA_FAILED
@@ -113,6 +159,41 @@ public void importClone(final InputStream inputStream, final Connection connecti
113159 }
114160 }
115161
162+ private String buildConflictClause (final Connection connection , final String tableName , final CloneConfiguration <T > configuration ) throws CloneException {
163+ final var idColumn = configuration .idColumnName ();
164+ final var schema = configuration .schema ();
165+ final var sql = "select 'on conflict (" + idColumn + ") do update set ' || "
166+ + "string_agg(column_name || ' = excluded.' || column_name, ', ') "
167+ + "from information_schema.columns "
168+ + "where table_schema = '" + schema + "' "
169+ + "and table_name = '" + tableName + "' "
170+ + "and column_name != '" + idColumn + "'" ;
171+
172+ return CloneException .wrapKnownExceptions (() -> {
173+ try (final var stmt = connection .createStatement ();
174+ final var rs = stmt .executeQuery (sql )) {
175+ if (rs .next ()) {
176+ final var result = rs .getString (1 );
177+ if (result != null ) {
178+ return result ;
179+ }
180+ }
181+ throw CloneError .GET_CONFLICT_COLUMNS_RETURNED_NOTHING .toException ();
182+ }
183+ }, CloneError .GET_CONFLICT_COLUMNS_FAILED );
184+ }
185+
186+ private void verifyConfigFingerprint (final ZipInputStream zis , final CloneConfiguration <T > configuration ) throws CloneException {
187+ final var storedFingerprint = CloneException .wrapKnownExceptions (
188+ () -> new String (zis .readAllBytes (), StandardCharsets .UTF_8 ),
189+ CloneError .CONFIG_FINGERPRINT_READ_FAILED
190+ );
191+
192+ if (!configuration .toString ().equals (storedFingerprint )) {
193+ throw new CloneException (CloneError .CONFIG_FINGERPRINT_MISMATCH );
194+ }
195+ }
196+
116197 private void execute (final Connection connection , final String sql ) throws SQLException {
117198 try (final var stmt = connection .createStatement ()) {
118199 stmt .execute (sql );
0 commit comments