diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java index 7054896f8821..e7c5e5be4292 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManager.java @@ -81,11 +81,6 @@ public class JdbcOwnershipAnalyticsTableManager extends AbstractEventJdbcTableManager { private final JdbcConfiguration jdbcConfiguration; - private static final String HISTORY_TABLE_ID = "1001-01-01"; - - // Must be later than the dummy HISTORY_TABLE_ID for SQL query order. - private static final String TRACKED_ENTITY_OWN_TABLE_ID = "2002-02-02"; - protected static final List FIXED_COLS = List.of( AnalyticsTableColumn.builder() @@ -97,12 +92,12 @@ public class JdbcOwnershipAnalyticsTableManager extends AbstractEventJdbcTableMa AnalyticsTableColumn.builder() .name("startdate") .dataType(DATE) - .selectExpression("a.startdate") + .selectExpression("null") .build(), AnalyticsTableColumn.builder() .name("enddate") .dataType(DATE) - .selectExpression("a.enddate") + .selectExpression("h.enddate") .build(), AnalyticsTableColumn.builder() .name("ou") @@ -213,6 +208,7 @@ private void populateOwnershipTableInternal(AnalyticsTablePartition partition, S writer.write(getRowMap(columnNames, resultSet)); queryRowCount.getAndIncrement(); }); + writer.flush(); log.info( "OwnershipAnalytics query row count was {} for table '{}'", queryRowCount, tableName); @@ -223,19 +219,12 @@ private void populateOwnershipTableInternal(AnalyticsTablePartition partition, S } /** - * Returns a SQL select query. For the from clause, for tracked entities in this program in - * programownershiphistory, get one row for each programownershiphistory row and then get a final - * row from the trackedentityprogramowner table to show the final owner. - * - *

The start date values are dummy so that all the history table rows will be ordered first and - * the tracked entity owner table row will come last. + * Returns a SQL select query. * *

The start date in the analytics table will be a far past date for the first row for each * tracked entity, or the previous row's end date plus one day in subsequent rows for that tracked * entity. * - *

Rows in programownershiphistory that don't have organisationunitid will be filtered out. - * * @param program the {@link Program}. * @return a SQL select query. */ @@ -248,29 +237,15 @@ private String getInputSql(Program program) { sql.append( replaceQualify( """ - \sfrom (\ - select h.trackedentityid, '${historyTableId}' as startdate, h.enddate as enddate, h.organisationunitid \ - from ${programownershiphistory} h \ + \sfrom ${programownershiphistory} h \ + inner join ${trackedentity} te on h.trackedentityid = te.trackedentityid \ + inner join ${organisationunit} ou on h.organisationunitid = ou.organisationunitid \ + left join analytics_rs_orgunitstructure ous on h.organisationunitid = ous.organisationunitid \ + left join analytics_rs_organisationunitgroupsetstructure ougs on h.organisationunitid = ougs.organisationunitid \ where h.programid = ${programId} \ and h.organisationunitid is not null \ - union distinct \ - select o.trackedentityid, '${trackedEntityOwnTableId}' as startdate, null as enddate, o.organisationunitid \ - from ${trackedentityprogramowner} o \ - where o.programid = ${programId} \ - and o.trackedentityid in (\ - select distinct p.trackedentityid \ - from ${programownershiphistory} p \ - where p.programid = ${programId} \ - and p.organisationunitid is not null)) a \ - inner join ${trackedentity} te on a.trackedentityid = te.trackedentityid \ - inner join ${organisationunit} ou on a.organisationunitid = ou.organisationunitid \ - left join analytics_rs_orgunitstructure ous on a.organisationunitid = ous.organisationunitid \ - left join analytics_rs_organisationunitgroupsetstructure ougs on a.organisationunitid = ougs.organisationunitid \ - order by te.uid, a.startdate, a.enddate""", - Map.of( - "historyTableId", HISTORY_TABLE_ID, - "trackedEntityOwnTableId", TRACKED_ENTITY_OWN_TABLE_ID, - "programId", String.valueOf(program.getId())))); + order by te.uid, h.enddate""", + Map.of("programId", String.valueOf(program.getId())))); return sql.toString(); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/writer/JdbcOwnershipWriter.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/writer/JdbcOwnershipWriter.java index 663f1688009d..5c3fada77dcc 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/writer/JdbcOwnershipWriter.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/writer/JdbcOwnershipWriter.java @@ -27,7 +27,6 @@ */ package org.hisp.dhis.analytics.table.writer; -import static java.util.Calendar.DECEMBER; import static java.util.Calendar.JANUARY; import static org.apache.commons.lang3.time.DateUtils.truncate; import static org.hisp.dhis.util.DateUtils.addDays; @@ -51,12 +50,15 @@ public class JdbcOwnershipWriter { private final MappingBatchHandler batchHandler; - /** Previous row for this TRACKED ENTITY, if any. */ - private Map prevRow = null; - /** Row of the current write, possibly modified. */ private Map newRow; + /** Previous row, if any. */ + private Map prevRow; + + /** Does the previous row exist? */ + private boolean previousRowExists = false; + public static final String TRACKEDENTITY = "teuid"; public static final String STARTDATE = "startdate"; @@ -65,9 +67,7 @@ public class JdbcOwnershipWriter { public static final String OU = "ou"; - private static final Date FAR_PAST_DATE = new GregorianCalendar(1000, JANUARY, 1).getTime(); - - private static final Date FAR_FUTURE_DATE = new GregorianCalendar(9999, DECEMBER, 31).getTime(); + private static Date FAR_PAST_DATE = new GregorianCalendar(1001, JANUARY, 1).getTime(); /** Gets instance by a factory method (so it can be mocked). */ public static JdbcOwnershipWriter getInstance(MappingBatchHandler batchHandler) { @@ -76,27 +76,28 @@ public static JdbcOwnershipWriter getInstance(MappingBatchHandler batchHandler) /** * Write a row to an analytics_ownership staging table. Work on a copy of the row, so we do not - * change the original row. We cannot use immutable maps because the orgUnit levels contain nulls - * when the orgUnit is not at the lowest level, and immutable maps do not allow null values. Also, - * the end date is null in the last record for each TRACKED ENTITY. + * change the original row. We cannot use immutable maps because some of the orgUnit levels are + * null when the orgUnit isn't at the lowest level. Immutable maps don't allow null values. * * @param row map of values to write */ public void write(Map row) { newRow = new HashMap<>(row); + adjustNewRowDates(); - if (newRow.get(ENDDATE) != null) { - // Remove the time of day portion of the ENDDATE. - newRow.put(ENDDATE, truncate(newRow.get(ENDDATE), Calendar.DATE)); - } - - if (prevRow == null) { - startNewTrackedEntity(); - } else if (sameValue(OU) || sameValue(ENDDATE)) { - combineWithPreviousRow(); + if (shouldContinuePreviousRow()) { + continuePreviousRow(); } else { - writePreviousRow(); + writePreviousRowIfExists(); } + + prevRow = newRow; + previousRowExists = true; + } + + /** Flush the last row to the output. We will have a row to flush unless we had no input. */ + public void flush() { + writePreviousRowIfExists(); } // ------------------------------------------------------------------------- @@ -104,75 +105,54 @@ public void write(Map row) { // ------------------------------------------------------------------------- /** - * Process the first row for a TRACKED ENTITY. Save it as the previous row and set the start date - * for far in the past. If the previous row does not have an "enddate", we enforce a default one. - */ - private void startNewTrackedEntity() { - prevRow = newRow; - - // Ensure a default "enddate" value. - prevRow.putIfAbsent(ENDDATE, FAR_FUTURE_DATE); - - prevRow.put(STARTDATE, FAR_PAST_DATE); - } - - /** - * Combine the current row with the previous row by updating the previous row's OU and ENDDATE. If - * the ENDDATE is the same this means there were multiple assignments during a day, and we want to - * record only the last OU assignment for the day. If the OU is the same this means there were - * successive assignments to the same OU (possibly after collapsing the same ENDDATE assignments - * such as if a TRACKED ENTITY was switched during a day to a different OU and then switched back - * again.) In this case we can combine the two successive records for the same OU into a single - * record. + * Adjust the dates in the new row. + * + *

If we are continuing entries for a tracked entity, then set the start date equal to the + * previous end date plus one day, otherwise we are recording values for a different tacked entity + * so set the start date equal to a far past date. * - *

If this is the last (and not only) row for this TRACKED ENTITY, write it out. + *

Remove the time portion of the end date, so it is just the date. */ - private void combineWithPreviousRow() { - prevRow.put(OU, newRow.get(OU)); - prevRow.put(ENDDATE, newRow.get(ENDDATE)); - - writeRowIfLast(prevRow); + private void adjustNewRowDates() { + newRow.put(STARTDATE, sameTrackedEntity() ? startAfterPrevious() : FAR_PAST_DATE); + newRow.put(ENDDATE, truncate(newRow.get(ENDDATE), Calendar.DATE)); } - /** - * The new row is for a different ownership period of the same TRACKED ENTITY. So write out the - * old row and start the new row after the old row's end date. Then also write out the new row if - * it is the last for this TRACKED ENTITY. - */ - private void writePreviousRow() { - batchHandler.addObject(prevRow); - - newRow.put(STARTDATE, addDays((Date) prevRow.get(ENDDATE), 1)); - - prevRow = newRow; + /** Do we have a previous row with the same tracked entity? */ + private boolean sameTrackedEntity() { + return previousRowExists && sameValue(TRACKEDENTITY); + } - writeRowIfLast(prevRow); + /** Start the new row one day after the previous one ends. */ + private Date startAfterPrevious() { + return addDays((Date) prevRow.get(ENDDATE), 1); } /** - * If the passed row is the last for this TRACKED ENTITY (no end date), then set the end date to - * far in the future and write it out. However, if this is the only row for this TRACKED ENTITY - * (from the beginning of time to the end of time), then don't write it because the ownership - * never changed and analytics queries can always use the enrollment orgUnit. + * Should we continue the previous row? + * + *

It should be continued if the previous row is the same tracked entity and either the OU or + * the ENDDATE has the same value. + * + *

If the ENDDATE is the same (now that the time of day has been removed), this means that + * there were multiple ownership changes within the same day. In this event, we want to record + * only a single ownership period that starts with the previous ownership start date and ends at + * the end of the day. * - *

After, there will be no previous row for this TRACKED ENTITY. + *

If the OU is the same (perhaps after combining rows with the same ENDDATE), then also we + * only want to record a single ownership that starts with the previous ownership start date and + * ends with the new ownership end date. */ - private void writeRowIfLast(Map row) { - if (hasNullValue(row, ENDDATE)) // If the last row - { - row.put(ENDDATE, FAR_FUTURE_DATE); - - if (!FAR_PAST_DATE.equals(row.get(STARTDATE))) { - batchHandler.addObject(row); - } - - prevRow = null; - } + private boolean shouldContinuePreviousRow() { + return sameTrackedEntity() && (sameValue(ENDDATE) || sameValue(OU)); } - /** Returns true if the map has a null value. */ - private boolean hasNullValue(Map row, String colName) { - return row.get(colName) == null; + /** + * Continue the previous row by transferring the previous row's start date to the new row. All + * other properties such as OU and ENDDATE come from the new row. + */ + private void continuePreviousRow() { + newRow.put(STARTDATE, prevRow.get(STARTDATE)); } /** @@ -182,4 +162,11 @@ private boolean hasNullValue(Map row, String colName) { private boolean sameValue(String colName) { return Objects.equals(prevRow.get(colName), newRow.get(colName)); } + + /** Write out the previous row to the batch handler. */ + private void writePreviousRowIfExists() { + if (previousRowExists) { + batchHandler.addObject(prevRow); + } + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java index d18d0fb6353e..99807aa59906 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java @@ -275,33 +275,24 @@ void testPopulateTable() throws SQLException { "lastupdated <= 'yyyy-mm-ddThh:mm:ss'"); assertEquals( """ - select te.uid,a.startdate,a.enddate,ou.uid from (\ - select h.trackedentityid, '1001-01-01' as startdate, h.enddate as enddate, h.organisationunitid \ + select te.uid,null,h.enddate,ou.uid \ from "programownershiphistory" h \ + inner join "trackedentity" te on h.trackedentityid = te.trackedentityid \ + inner join "organisationunit" ou on h.organisationunitid = ou.organisationunitid \ + left join analytics_rs_orgunitstructure ous on h.organisationunitid = ous.organisationunitid \ + left join analytics_rs_organisationunitgroupsetstructure ougs on h.organisationunitid = ougs.organisationunitid \ where h.programid = 0 \ and h.organisationunitid is not null \ - union distinct \ - select o.trackedentityid, '2002-02-02' as startdate, null as enddate, o.organisationunitid \ - from "trackedentityprogramowner" o \ - where o.programid = 0 \ - and o.trackedentityid in (\ - select distinct p.trackedentityid \ - from "programownershiphistory" p \ - where p.programid = 0 \ - and p.organisationunitid is not null)) a \ - inner join "trackedentity" te on a.trackedentityid = te.trackedentityid \ - inner join "organisationunit" ou on a.organisationunitid = ou.organisationunitid \ - left join analytics_rs_orgunitstructure ous on a.organisationunitid = ous.organisationunitid \ - left join analytics_rs_organisationunitgroupsetstructure ougs on a.organisationunitid = ougs.organisationunitid \ - order by te.uid, a.startdate, a.enddate""", + order by te.uid, h.enddate""", sqlMasked); List writerInvocations = getInvocations(writer); - assertEquals(3, writerInvocations.size()); + assertEquals(4, writerInvocations.size()); assertEquals("write", writerInvocations.get(0).getMethod().getName()); assertEquals("write", writerInvocations.get(1).getMethod().getName()); assertEquals("write", writerInvocations.get(2).getMethod().getName()); + assertEquals("flush", writerInvocations.get(3).getMethod().getName()); Map map0 = writerInvocations.get(0).getArgument(0); Map map1 = writerInvocations.get(1).getArgument(0); @@ -319,17 +310,18 @@ void testGetFixedColumns() { AnalyticsTableColumn.builder() .name("teuid") .dataType(CHARACTER_11) + .nullable(NOT_NULL) .selectExpression("te.uid") .build(), AnalyticsTableColumn.builder() .name("startdate") .dataType(DATE) - .selectExpression("a.startdate") + .selectExpression("null") .build(), AnalyticsTableColumn.builder() .name("enddate") .dataType(DATE) - .selectExpression("a.enddate") + .selectExpression("h.enddate") .build(), AnalyticsTableColumn.builder() .name("ou") diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipWriterTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipWriterTest.java index b049038389cb..600d2a2a2b33 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipWriterTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipWriterTest.java @@ -29,17 +29,12 @@ import static java.util.Calendar.FEBRUARY; import static java.util.Calendar.JANUARY; -import static org.apache.commons.lang3.reflect.FieldUtils.writeField; import static org.hisp.dhis.analytics.table.writer.JdbcOwnershipWriter.ENDDATE; import static org.hisp.dhis.analytics.table.writer.JdbcOwnershipWriter.OU; import static org.hisp.dhis.analytics.table.writer.JdbcOwnershipWriter.STARTDATE; import static org.hisp.dhis.analytics.table.writer.JdbcOwnershipWriter.TRACKEDENTITY; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mockingDetails; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.sql.Connection; @@ -48,7 +43,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; -import java.util.HashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; @@ -115,104 +109,92 @@ public void setUp() throws SQLException { @Test void testWriteNoOwnershipChanges() throws SQLException { - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, null)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouB, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouB, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouB, ENDDATE, null)); - + writer.flush(); batchHandler.flush(); - verify(statement, never()).executeUpdate(any()); + List invocations = new ArrayList<>(mockingDetails(statement).getInvocations()); + assertEquals(1, invocations.size()); + assertEquals("close", invocations.get(0).getMethod().getName()); } - /** Note that {@link BatchHandler} does not quote column names. */ @Test void testWriteOneOwnershipChange() { - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouB, ENDDATE, null)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, null)); + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); + writer.flush(); batchHandler.flush(); assertEquals( "insert into analytics_ownership_programUidA (teuid,ou,startdate,enddate) values " - + "('teAaaaaaaa','ouAaaaaaaaa','1000-01-01','2022-02-01')," - + "('teAaaaaaaa','ouBbbbbbbbb','2022-02-02','9999-12-31')", + + "('teAaaaaaaa','ouAaaaaaaaa','1001-01-01','2022-01-01')", getUpdateSql()); } @Test - void testWriteTwoOwnershipChanges() { - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouB, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, null)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, null)); + void testWriteOneCollapsedSameOuOwnershipChange() { + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_02)); + writer.flush(); batchHandler.flush(); assertEquals( "insert into analytics_ownership_programUidA (teuid,ou,startdate,enddate) values " - + "('teAaaaaaaa','ouAaaaaaaaa','1000-01-01','2022-01-01')," - + "('teAaaaaaaa','ouBbbbbbbbb','2022-01-02','2022-02-01')," - + "('teAaaaaaaa','ouAaaaaaaaa','2022-02-02','9999-12-31')", + + "('teAaaaaaaa','ouAaaaaaaaa','1001-01-01','2022-02-01')", getUpdateSql()); } @Test - void testWriteThreeOwnershipChanges() { - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouB, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teA, OU, ouA, ENDDATE, null)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_01)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_02)); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouB, ENDDATE, null)); + void testWriteOneCollapsedSameDateOwnershipChange() { + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouB, ENDDATE, date_2022_01)); + writer.flush(); batchHandler.flush(); assertEquals( "insert into analytics_ownership_programUidA (teuid,ou,startdate,enddate) values " - + "('teAaaaaaaa','ouAaaaaaaaa','1000-01-01','2022-01-01')," - + "('teAaaaaaaa','ouBbbbbbbbb','2022-01-02','2022-02-01')," - + "('teAaaaaaaa','ouAaaaaaaaa','2022-02-02','9999-12-31')," - + "('teBbbbbbbb','ouAaaaaaaaa','1000-01-01','2022-02-01')," - + "('teBbbbbbbb','ouBbbbbbbbb','2022-02-02','9999-12-31')", + + "('teAaaaaaaa','ouBbbbbbbbb','1001-01-01','2022-01-01')", getUpdateSql()); } @Test - void testWriteWhenEndDateIsNull() throws IllegalAccessException { - JdbcOwnershipWriter writer = JdbcOwnershipWriter.getInstance(batchHandler); - Map prevRow = new HashMap<>(); - writeField(writer, "prevRow", prevRow, true); + void testWriteTwoOwnershipChanges() { + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouB, ENDDATE, date_2022_02)); + writer.flush(); - writer.write(mapOf(TRACKEDENTITY, teB, OU, ouB, ENDDATE, null)); + batchHandler.flush(); - assertNotNull(prevRow.get(ENDDATE)); + assertEquals( + "insert into analytics_ownership_programUidA (teuid,ou,startdate,enddate) values " + + "('teAaaaaaaa','ouAaaaaaaaa','1001-01-01','2022-01-01')," + + "('teAaaaaaaa','ouBbbbbbbbb','2022-01-02','2022-02-01')", + getUpdateSql()); + } + + @Test + void testWriteThreeOwnershipChanges() { + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_01)); + writer.write(Map.of(TRACKEDENTITY, teA, OU, ouA, ENDDATE, date_2022_02)); + writer.write(Map.of(TRACKEDENTITY, teB, OU, ouA, ENDDATE, date_2022_01)); + writer.write(Map.of(TRACKEDENTITY, teB, OU, ouB, ENDDATE, date_2022_02)); + writer.flush(); + + batchHandler.flush(); + + assertEquals( + "insert into analytics_ownership_programUidA (teuid,ou,startdate,enddate) values " + + "('teAaaaaaaa','ouAaaaaaaaa','1001-01-01','2022-02-01')," + + "('teBbbbbbbb','ouAaaaaaaaa','1001-01-01','2022-01-01')," + + "('teBbbbbbbb','ouBbbbbbbbb','2022-01-02','2022-02-01')", + getUpdateSql()); } // ------------------------------------------------------------------------- // Supportive methods // ------------------------------------------------------------------------- - /** - * Creates a map of three key/value pairs that allows nulls (because the database can return nulls - * and the logic relies on that). - */ - private Map mapOf(K key1, V value1, K key2, V value2, K key3, V value3) { - HashMap map = new HashMap<>(); - map.put(key1, value1); - map.put(key2, value2); - map.put(key3, value3); - return map; - } - /** Returns the invoked SQL statement. */ private String getUpdateSql() { List invocations = new ArrayList<>(mockingDetails(statement).getInvocations());