/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.history; import java.io.Serializable; import java.util.*; import org.eclipse.persistence.descriptors.ClassDescriptor; import org.eclipse.persistence.expressions.*; import org.eclipse.persistence.internal.databaseaccess.*; import org.eclipse.persistence.internal.expressions.*; import org.eclipse.persistence.internal.history.*; import org.eclipse.persistence.internal.helper.*; import org.eclipse.persistence.internal.queries.*; import org.eclipse.persistence.internal.sessions.AbstractRecord; import org.eclipse.persistence.internal.sessions.AbstractSession; import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; import org.eclipse.persistence.mappings.*; import org.eclipse.persistence.mappings.DatabaseMapping.WriteType; import org.eclipse.persistence.sessions.DatabaseRecord; import org.eclipse.persistence.queries.*; /** * <b>Purpose:</b>Expresses how historical data is saved on the data store. * <p>This information is used to both maintain a history of all objects * modified through TopLink and to enable point in time querying. * <p>If Oracle 9R2 or later Flashback is used this policy is not required, as * the preservation of history is automatic. * <p>Descriptors, ManyToManyMappings, DirectCollectionMappings, * and DirectMapMappings only can have a history policy, as only they have associated * database tables. * @author Stephen McRitchie * @since 10 */ public class HistoryPolicy implements Cloneable, Serializable { protected ClassDescriptor descriptor; protected DatabaseMapping mapping; protected List<DatabaseTable> historicalTables; protected List<DatabaseField> startFields; protected List<DatabaseField> endFields; protected boolean shouldHandleWrites = true; protected boolean usesLocalTime = true; public HistoryPolicy() { } /** * INTERNAL: * Add any temporal querying conditions to this object expression. */ public Expression additionalHistoryExpression(Expression context, Expression base) { return additionalHistoryExpression(context, base, null); } /** * INTERNAL: * Add any temporal querying conditions to this object expression. * @param tableIndex not null indicates that only expression for a single table should be returned. */ public Expression additionalHistoryExpression(Expression context, Expression base, Integer tableIndex) { // AsOfClause clause = base.getAsOfClause(); Object value = clause.getValue(); Expression join = null; Expression subJoin = null; Expression start = null; Expression end = null; if (value == null) { return null; // for now nothing as assume mirroring historical tables. } else { if (value instanceof Expression) { // Sort of an implementation of native sql. // Print AS OF TIMESTAMP (SYSDATE - 1000*60*10) not AS OF ('SYSDATE - 1000*60*10'). if ((value instanceof ConstantExpression) && (((ConstantExpression)value).getValue() instanceof String)) { value = (((ConstantExpression)value).getValue()); } } else { ConversionManager converter = ConversionManager.getDefaultManager(); value = converter.convertObject(value, ClassConstants.TIMESTAMP); } if (getMapping() != null) { if (tableIndex != null && tableIndex.intValue() > 0) { return null; } TableExpression tableExp = null; DatabaseTable historicalTable = getHistoricalTables().get(0); tableExp = (TableExpression)((ObjectExpression)base).existingDerivedTable(historicalTable); start = tableExp.getField(getStart()); end = tableExp.getField(getEnd()); join = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value))); // We also need to do step two here in advance. tableExp.setTable(historicalTable); return join; } int iFirst, iLast; if (tableIndex == null) { // loop through all history tables iFirst = 0; iLast = getHistoricalTables().size() - 1; } else { // only return expression for the specified table iFirst = tableIndex.intValue(); iLast = iFirst; } for (int i = iFirst; i <= iLast ; i++) { start = base.getField(getStart(i)); end = base.getField(getEnd(i)); subJoin = start.lessThanEqual(value).and(end.isNull().or(end.greaterThan(value))); join = ((join == null) ? subJoin : join.and(subJoin)); } return join; } } /** * PUBLIC: * Performs a sufficiently deep clone. * Use to quickly setup standard policies on multiple descriptors. */ public Object clone() { HistoryPolicy clone = null; try { clone = (HistoryPolicy)super.clone(); } catch (CloneNotSupportedException ignore) { } if (startFields != null) { clone.setStartFields(new ArrayList(startFields.size())); for (DatabaseField field : startFields) { clone.getStartFields().add(field.clone()); } } if (endFields != null) { clone.setEndFields(new ArrayList(endFields.size())); for (DatabaseField field : endFields) { clone.getEndFields().add(field.clone()); } } if (historicalTables != null) { clone.setHistoricalTables(new ArrayList(historicalTables)); } return clone; } /** * PUBLIC: * Whenever a historical record is logically deleted (updated) or inserted, * the end and start fields respectively will be set to this value. */ public Object getCurrentTime(AbstractSession session) { if (shouldUseLocalTime()) { return new java.sql.Timestamp(System.currentTimeMillis()); } if (shouldUseDatabaseTime()) { AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass()); while (readSession.isUnitOfWork()) { readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass()); } return readSession.getDatasourceLogin().getDatasourcePlatform().getTimestampFromServer(session, readSession.getName()); } return null; } /** * INTERNAL: * Return a minimal time increment supported by the platform. */ public long getMinimumTimeIncrement(AbstractSession session) { AbstractSession readSession = session.getSessionForClass(getDescriptor().getJavaClass()); while (readSession.isUnitOfWork()) { readSession = ((UnitOfWorkImpl)readSession).getParent().getSessionForClass(getDescriptor().getJavaClass()); } return readSession.getPlatform().minimumTimeIncrement(); } /** * PUBLIC: * Return the descriptor of the policy. */ public ClassDescriptor getDescriptor() { return descriptor; } /** * INTERNAL: */ public final List<DatabaseTable> getHistoricalTables() { if (historicalTables == null) { historicalTables = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(1); } return historicalTables; } /** * PUBLIC: */ public List<String> getHistoryTableNames() { List<String> names = new ArrayList(getHistoricalTables().size()); for (DatabaseTable table : getHistoricalTables()) { names.add(table.getQualifiedName()); } return names; } /** * PUBLIC: */ public DatabaseMapping getMapping() { return mapping; } /** * INTERNAL: */ protected DatabaseField getStart() { if (startFields != null) { return startFields.get(0); } else { return null; } } /** * INTERNAL: */ protected DatabaseField getStart(int i) { return startFields.get(i); } /** * PUBLIC: * Answers the name of the start field. Assumes that multiple tables * for a descriptor have the same field names. */ public String getStartFieldName() { if (getStart() != null) { return getStart().getName(); } else { return null; } } /** * INTERNAL: */ public List<DatabaseField> getStartFields() { return startFields; } /** * INTERNAL: */ protected DatabaseField getEnd() { if (endFields != null) { return endFields.get(0); } else { return null; } } /** * INTERNAL: */ protected DatabaseField getEnd(int i) { return endFields.get(i); } /** * PUBLIC: */ public String getEndFieldName() { if (getEnd() != null) { return getEnd().getName(); } else { return null; } } /** * INTERNAL: */ public List<DatabaseField> getEndFields() { return endFields; } /** * PUBLIC: */ public void setDescriptor(ClassDescriptor descriptor) { this.descriptor = descriptor; } /** * INTERNAL: * Initialize a HistoryPolicy. */ public void initialize(AbstractSession session) { if (getMapping() != null) { setDescriptor(getMapping().getDescriptor()); if (getMapping().isDirectCollectionMapping()) { DatabaseTable refTable = ((DirectCollectionMapping)getMapping()).getReferenceTable(); DatabaseTable histTable = getHistoricalTables().get(0); histTable.setName(refTable.getName()); histTable.setTableQualifier(refTable.getTableQualifier()); getStart().setTable(histTable); getEnd().setTable(histTable); } else if (getMapping().isManyToManyMapping()) { DatabaseTable relationTable = ((ManyToManyMapping)getMapping()).getRelationTable(); DatabaseTable histTable = getHistoricalTables().get(0); histTable.setName(relationTable.getName()); histTable.setTableQualifier(relationTable.getTableQualifier()); getStart().setTable(histTable); getEnd().setTable(histTable); } verifyTableQualifiers(session.getPlatform()); return; } // Some historicalTables will be inherited from a parent policy. int offset = getDescriptor().getTables().size() - getHistoricalTables().size(); // In this configuration descriptor tables, history tables, and start/end fields // are all in the same order. if (!getHistoricalTables().isEmpty() && getHistoricalTables().get(0).getName().equals("")) { for (int i = 0; i < getHistoricalTables().size(); i++) { DatabaseTable table = getHistoricalTables().get(i); if (table.getName().equals("")) { DatabaseTable mirrored = getDescriptor().getTables().get(i + offset); table.setName(mirrored.getName()); table.setTableQualifier(mirrored.getTableQualifier()); } if (getStartFields().size() < (i + 1)) { DatabaseField startField = getStart(0).clone(); startField.setTable(table); getStartFields().add(startField); } else { DatabaseField startField = getStart(i); startField.setTable(table); } if (getEndFields().size() < (i + 1)) { DatabaseField endField = getEnd(0).clone(); endField.setTable(table); getEndFields().add(endField); } else { DatabaseField endField = getEnd(i); endField.setTable(table); } } } else { // The user did not specify history tables/fields in order, so // initialize will take a little longer. List<DatabaseTable> unsortedTables = getHistoricalTables(); List<DatabaseTable> sortedTables = new ArrayList(unsortedTables.size()); List<DatabaseField> sortedStartFields = new ArrayList(unsortedTables.size()); List<DatabaseField> sortedEndFields = new ArrayList(unsortedTables.size()); boolean universalStartField = ((getStartFields().size() == 1) && (!(getStartFields().get(0)).hasTableName())); boolean universalEndField = ((getEndFields().size() == 1) && (!(getEndFields().get(0)).hasTableName())); DatabaseTable descriptorTable = null; DatabaseTable historicalTable = null; DatabaseField historyField = null; List<DatabaseTable> descriptorTables = getDescriptor().getTables(); for (int i = offset; i < descriptorTables.size(); i++) { descriptorTable = descriptorTables.get(i); int index = unsortedTables.indexOf(descriptorTable); if (index == -1) { // this is a configuration error! } historicalTable = unsortedTables.get(index); historicalTable.setTableQualifier(descriptorTable.getTableQualifier()); sortedTables.add(historicalTable); if (universalStartField) { historyField = getStart(0).clone(); historyField.setTable(historicalTable); sortedStartFields.add(historyField); } else { for (DatabaseField field : getStartFields()) { if (field.getTable().equals(historicalTable)) { sortedStartFields.add(field); break; } } } if (universalEndField) { historyField = getEnd(0).clone(); historyField.setTable(historicalTable); sortedEndFields.add(historyField); } else { for (DatabaseField field : getEndFields()) { if (field.getTable().equals(historicalTable)) { sortedEndFields.add(field); break; } } } } setHistoricalTables(sortedTables); setStartFields(sortedStartFields); setEndFields(sortedEndFields); } verifyTableQualifiers(session.getPlatform()); // A user need not set a policy on every level of an inheritance, but // historic tables can be inherited. if (getDescriptor().hasInheritance()) { ClassDescriptor parentDescriptor = getDescriptor().getInheritancePolicy().getParentDescriptor(); while ((parentDescriptor != null) && (parentDescriptor.getHistoryPolicy() == null)) { parentDescriptor = parentDescriptor.getInheritancePolicy().getParentDescriptor(); } if (parentDescriptor != null) { // Unique is required because the builder can add the same table many times. // This is done after init properties to make sure the default table is the first local one. setHistoricalTables(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getHistoricalTables(), getHistoricalTables())); setStartFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getStartFields(), getStartFields())); setEndFields(Helper.concatenateUniqueLists(parentDescriptor.getHistoryPolicy().getEndFields(), getEndFields())); } } } /** * PUBLIC: * Use to specify the names of the mirroring historical tables. * <p> * Assumes that the order in which tables are added with descriptor.addTableName() * matches the order in which mirroring historical tables are added with * descriptor.addHistoryTableName(). */ public void addHistoryTableName(String name) { HistoricalDatabaseTable table = new HistoricalDatabaseTable(""); table.setHistoricalName(name); getHistoricalTables().add(table); } /** * PUBLIC: * Use to specify the names of the mirroring historical tables. * <p> * Explicitly states that <code>sourceTableName</code> is mirrored by history table * <code>historyTableName</code>. * The order in which tables are added with descriptor.addTableName() * should still match the order in which mirroring historical tables are * added with descriptor.addMirroringHistoryTableName(). */ public void addHistoryTableName(String sourceTableName, String historyTableName) { if ((sourceTableName == null) || sourceTableName.equals("")) { addHistoryTableName(historyTableName); } HistoricalDatabaseTable table = new HistoricalDatabaseTable(sourceTableName); table.setHistoricalName(historyTableName); // Note that the equality check is only on sourceTableName, not historyTableName. int index = getHistoricalTables().indexOf(table); if (index == -1) { getHistoricalTables().add(table); } else { getHistoricalTables().set(index, table); } } /** * INTERNAL: */ public void setHistoricalTables(List<DatabaseTable> historicalTables) { this.historicalTables = historicalTables; } /** * INTERNAL: */ public void setMapping(DatabaseMapping mapping) { this.mapping = mapping; } /** * INTERNAL: */ protected void setStartFields(List<DatabaseField> startFields) { this.startFields = startFields; } /** * PUBLIC: * Sets the name of the start field. * <p> * By default all tables belonging to a descriptor have the same primary * key field names, and so the same start field names also. * <p> * However, if <code>startFieldName</code> is qualified, i.e. of the form * "EMPLOYEE_HIST.EMP_START", then this call will only set the start field * name for a single historical table. */ public void addStartFieldName(String startFieldName) { DatabaseField startField = new DatabaseField(startFieldName); startField.setType(ClassConstants.TIMESTAMP); // #440278 startField.setLength(6); if (startFields == null) { startFields = org.eclipse.persistence.internal.helper.NonSynchronizedVector.newInstance(); startFields.add(startField); return; } for (DatabaseField existing : startFields) { if (startField.getTableName().equals(existing.getTableName())) { existing.setName(startField.getName()); return; } } startFields.add(startField); } /** * ADVANCED: * Sets the type of all start fields. Not required to be set as the default * of Timestamp is assumed. */ public void setStartFieldType(Class type) { for (DatabaseField existing : startFields) { existing.setType(type); } } /** * INTERNAL: */ protected void setEndFields(List<DatabaseField> endFields) { this.endFields = endFields; } /** * PUBLIC: * @see #addStartFieldName */ public void addEndFieldName(String endFieldName) { DatabaseField endField = new DatabaseField(endFieldName); endField.setType(ClassConstants.TIMESTAMP); // #440278 endField.setLength(6); if (endFields == null) { endFields = new ArrayList(); endFields.add(endField); return; } for (DatabaseField existing : endFields) { if (endField.getTableName().equals(existing.getTableName())) { existing.setName(endField.getName()); return; } } endFields.add(endField); } /** * ADVANCED: * @see #setStartFieldType */ public void setEndFieldType(String fieldName, Class type) { for (DatabaseField existing : endFields) { existing.setType(type); } } /** * Sets if TopLink is responsible for writing history. * <p> * If history is maintained via low level database triggers or application * logic a policy is still needed for point in time querying. * <p> * If Oracle flashback is used no HistoryPolicy is needed. * <p> * Setting this to false lets you use History for many other applications. * For instance a table that tracks available flights or hotel deals may * benefit from a HistoryPolicy just to simplify temporal querying. * <p>If all hotel discounts have a start and end date, you could query on * all discounts available at a certain date. */ public void setShouldHandleWrites(boolean value) { this.shouldHandleWrites = value; } /** * Answers if TopLink is responsible for writing history. * <p> * If history is maintained via low level database triggers or application * logic a policy is still usefull for point in time querying. * <p> * If Oracle flashback is used no HistoryPolicy is needed. * @return true by default * @see #setShouldHandleWrites */ public boolean shouldHandleWrites() { return shouldHandleWrites; } /** * Sets if the Timestamp used in maintainaing history should be the * current time according to the database. * @param value if false uses localTime (default) instead */ public void setShouldUseDatabaseTime(boolean value) { usesLocalTime = !value; } /** * Answers if the Timestamp used in maintaining history should be * System.currentTimeMillis(); * @see #shouldUseDatabaseTime * @see #useLocalTime * @return true by default */ public boolean shouldUseLocalTime() { return usesLocalTime; } /** * Answers if the Timestamp used in maintaining history should be the * current time according to the database. * @see #shouldUseLocalTime * @see #useDatabaseTime * @return false by default */ public boolean shouldUseDatabaseTime() { return !usesLocalTime; } /** * Answers if the Timestamp used in maintaining history should be * System.currentTimeMillis(); * @see #useDatabaseTime * @see #shouldUseLocalTime */ public void useLocalTime() { usesLocalTime = true; } /** * Answers if the Timestamp used in maintaining history should be the * current time according to the database. * @see #useLocalTime * @see #shouldUseDatabaseTime */ public void useDatabaseTime() { usesLocalTime = false; } /** * INTERNAL: Check that the qualifiers on the historical tables are * properly set. * <p>A similar method exists on ClassDescriptor. */ protected void verifyTableQualifiers(DatasourcePlatform platform) { String tableQualifier = platform.getTableQualifier(); if (tableQualifier.length() == 0) { return; } for (DatabaseTable table : getHistoricalTables()) { // Build a scratch table to see if history table name has a qualifier. DatabaseTable scratchTable = new DatabaseTable(table.getQualifiedName()); if (scratchTable.getTableQualifier().length() == 0) { scratchTable.setTableQualifier(tableQualifier); ((HistoricalDatabaseTable)table).setHistoricalName(scratchTable.getQualifiedNameDelimited(platform)); } } } /** * INTERNAL: * Checks for the case where an object has multiple tables but only some * are part of a minimal update. */ protected boolean checkWastedVersioning(AbstractRecord modifyRow, DatabaseTable table) { for (Enumeration fieldsEnum = modifyRow.keys(); fieldsEnum.hasMoreElements();) { DatabaseField field = (DatabaseField)fieldsEnum.nextElement(); if (field.getTable().equals(table) || (!field.hasTableName())) { return true; } } return false; } /** * INTERNAL: */ public void postDelete(ModifyQuery deleteQuery) { logicalDelete(deleteQuery, false); } /** * INTERNAL: */ // Bug 319276 - pass whether shallow insert/update public void postUpdate(ObjectLevelModifyQuery writeQuery) { postUpdate(writeQuery, false); } /** * INTERNAL: */ public void postUpdate(ObjectLevelModifyQuery writeQuery, boolean isShallow) { logicalDelete(writeQuery, true, isShallow); logicalInsert(writeQuery, true); } /** * INTERNAL: */ public void postInsert(ObjectLevelModifyQuery writeQuery) { logicalInsert(writeQuery, false); } /** * INTERNAL: * Perform a logical insert into the historical schema, creating a new version * of an object. * <p>Called by postInsert() and also postUpdate() (which first does a logicalDelete * of the previous version). */ public void logicalInsert(ObjectLevelModifyQuery writeQuery, boolean isUpdate) { ClassDescriptor descriptor = getDescriptor(); AbstractRecord modifyRow = null; AbstractRecord originalModifyRow = writeQuery.getModifyRow(); Object currentTime = null; if (isUpdate) { modifyRow = descriptor.getObjectBuilder().buildRow(writeQuery.getObject(), writeQuery.getSession(), WriteType.UPDATE); // Bug 319276 // If anyone added items to the modify row, then they should also be added here. modifyRow.putAll(originalModifyRow); } else { modifyRow = originalModifyRow; // If update would have already discovered timestamp to use. currentTime = getCurrentTime(writeQuery.getSession()); } StatementQueryMechanism insertMechanism = new StatementQueryMechanism(writeQuery); for (int i = 0; i < getHistoricalTables().size(); i++) { DatabaseTable table = getHistoricalTables().get(i); if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) { continue; } if (!isUpdate) { modifyRow.add(getStart(i), currentTime); } SQLInsertStatement insertStatement = new SQLInsertStatement(); insertStatement.setTable(table); insertMechanism.getSQLStatements().add(insertStatement); } if (insertMechanism.hasMultipleStatements()) { writeQuery.setTranslationRow(modifyRow); writeQuery.setModifyRow(modifyRow); insertMechanism.insertObject(); } } /** * INTERNAL: * Performs a logical insert into the historical schema. Direct * collections and many to many mappings are maintained through the session * events. */ public void mappingLogicalInsert(DataModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) { DataModifyQuery historyQuery = new DataModifyQuery(); SQLInsertStatement historyStatement = new SQLInsertStatement(); DatabaseTable histTable = getHistoricalTables().get(0); historyStatement.setTable(histTable); AbstractRecord modifyRow = originalQuery.getModifyRow().clone(); AbstractRecord translationRow = arguments.clone(); // Start could be the version field in timestamp locking. if (!modifyRow.containsKey(getStart())) { Object time = getCurrentTime(session); modifyRow.add(getStart(), time); translationRow.add(getStart(), time); } historyQuery.setSQLStatement(historyStatement); historyQuery.setModifyRow(modifyRow); historyStatement.setModifyRow(modifyRow); session.executeQuery(historyQuery, translationRow); } /** * INTERNAL: * Performs a logical delete (update) on the historical schema. */ // Bug 319276 - pass whether shallow insert/update public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate) { logicalDelete(writeQuery, isUpdate, false); } /** * INTERNAL: * Performs a logical delete (update) on the historical schema. */ public void logicalDelete(ModifyQuery writeQuery, boolean isUpdate, boolean isShallow) { ClassDescriptor descriptor = writeQuery.getDescriptor(); AbstractRecord originalModifyRow = writeQuery.getModifyRow(); AbstractRecord modifyRow = new DatabaseRecord(); StatementQueryMechanism updateMechanism = new StatementQueryMechanism(writeQuery); Object currentTime = getCurrentTime(writeQuery.getSession()); for (int i = 0; i < getHistoricalTables().size(); i++) { DatabaseTable table = getHistoricalTables().get(i); if (isUpdate && !checkWastedVersioning(originalModifyRow, table)) { continue; } SQLUpdateStatement updateStatement = new SQLUpdateStatement(); updateStatement.setTable(table); Expression whereClause = null; if (writeQuery instanceof DeleteAllQuery) { if (writeQuery.getSelectionCriteria() != null) { whereClause = (Expression)writeQuery.getSelectionCriteria().clone(); } } else { whereClause = descriptor.getObjectBuilder().buildPrimaryKeyExpression(table); } ExpressionBuilder builder = ((whereClause == null) ? new ExpressionBuilder() : whereClause.getBuilder()); whereClause = builder.getField(getEnd(i)).isNull().and(whereClause); updateStatement.setWhereClause(whereClause); modifyRow.add(getEnd(i), currentTime); // save a little time here and add the same timestamp value for // the start field in the logicalInsert. if (isUpdate) { if (isShallow) { // Bug 319276 - increment the timestamp by 1 to avoid unique constraint violation potential java.sql.Timestamp incrementedTime = (java.sql.Timestamp) currentTime; incrementedTime.setTime(incrementedTime.getTime() + getMinimumTimeIncrement(writeQuery.getSession())); originalModifyRow.add(getStart(i), incrementedTime); } else { originalModifyRow.add(getStart(i), currentTime); } } updateMechanism.getSQLStatements().add(updateStatement); } if (updateMechanism.hasMultipleStatements()) { writeQuery.setModifyRow(modifyRow); updateMechanism.updateObject(); writeQuery.setModifyRow(originalModifyRow); } } /** * INTERNAL: * Performs a logical delete (update) on the historical schema. Direct * collections and many to many mappings are maintained through the session * events. */ public void mappingLogicalDelete(ModifyQuery originalQuery, AbstractRecord arguments, AbstractSession session) { SQLDeleteStatement originalStatement = (SQLDeleteStatement)originalQuery.getSQLStatement(); DataModifyQuery historyQuery = new DataModifyQuery(); SQLUpdateStatement historyStatement = new SQLUpdateStatement(); DatabaseTable histTable = getHistoricalTables().get(0); historyStatement.setTable(histTable); Expression whereClause = (Expression)originalStatement.getWhereClause().clone(); DatabaseField endField = getEnd(); whereClause = whereClause.getBuilder().getField(endField).isNull().and(whereClause); historyStatement.setWhereClause(whereClause); AbstractRecord modifyRow = new DatabaseRecord(); AbstractRecord translationRow = arguments.clone(); Object time = getCurrentTime(session); modifyRow.add(getEnd(), time); translationRow.add(getEnd(), time); historyStatement.setModifyRow(modifyRow); historyQuery.setSQLStatement(historyStatement); historyQuery.setModifyRow(modifyRow); session.executeQuery(historyQuery, translationRow); } }