diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java index cfec5759c5575de45c8ff5c387862a3a06a2a048..9f2e89c32f9aba4f2386e39d8be64f012f1c2071 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/mapper/MariaDbMapper.java @@ -618,6 +618,42 @@ public interface MariaDbMapper { return statement.toString(); } + default String tupleToRawUpdateTimestampQuery(TableDto table, TupleUpdateDto data, Timestamp tsAdd, Timestamp tsDelete) + throws TableMalformedException { + if (table.getColumns().isEmpty()) { + throw new TableMalformedException("Columns are not known"); + } + final StringBuilder statement = new StringBuilder("UPDATE `") + .append(table.getDatabase().getInternalName()) + .append("`.`") + .append(table.getInternalName()).append("_timestamps") + .append("` SET "); + + // Set the two timestamp columns + statement.append("`").append(nameToInternalName("timestamp_add")).append("` = ?, "); + statement.append("`").append(nameToInternalName("timestamp_delete")).append("` = ? "); + + // Set each primary key to its new value + for (PrimaryKeyDto pk : table.getConstraints().getPrimaryKey()) { + String pkColumn = pk.getColumn().getInternalName(); + statement.append(", `").append(pkColumn).append("` = ? "); + } + + // WHERE clause: match on database_id and the old primary key values + statement.append("WHERE "); + statement.append("`").append(nameToInternalName("database_id")).append("` = ? "); + for (PrimaryKeyDto pk : table.getConstraints().getPrimaryKey()) { + String pkColumn = pk.getColumn().getInternalName(); + statement.append("AND `").append(pkColumn).append("` = ? "); + } + statement.append(";"); + + log.trace("Mapped update query for timestamp table: {}", statement.toString()); + return statement.toString(); + } + + + default String tupleToRawCreateQuery(TableDto table, TupleDto data) throws TableMalformedException { if (table.getColumns().isEmpty()) { throw new TableMalformedException("Columns are not known"); diff --git a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java index 7b0f7a4668c974ec0b71e037e29fe365568fe1a7..aaaf84003c47d33b34ebedc4918cf6d42c1f9c55 100644 --- a/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java +++ b/dbrepo-data-service/services/src/main/java/at/tuwien/service/impl/TableServiceMariaDbImpl.java @@ -25,6 +25,7 @@ import org.springframework.stereotype.Service; import java.sql.*; import java.time.Instant; import java.util.*; +import java.util.stream.Collectors; @Log4j2 @Service @@ -368,31 +369,99 @@ public class TableServiceMariaDbImpl extends DataConnector implements TableServi } @Override - public void updateTuple(TableDto table, TupleUpdateDto data) throws SQLException, - QueryMalformedException, TableMalformedException { + public void updateTuple(TableDto table, TupleUpdateDto data) + throws SQLException, QueryMalformedException, TableMalformedException { log.trace("update tuple: {}", data); - /* prepare the statement */ final ComboPooledDataSource dataSource = getDataSource(table); final Connection connection = dataSource.getConnection(); try { + // Build and execute the main update query for the tuple. final int[] idx = new int[]{1}; - final PreparedStatement statement = connection.prepareStatement(mariaDbMapper.tupleToRawUpdateQuery(table, data)); - /* set data */ + final PreparedStatement statement = connection.prepareStatement( + mariaDbMapper.tupleToRawUpdateQuery(table, data)); + + // Bind new values (data to be updated; may include new primary key values) for (Map.Entry<String, Object> entry : data.getData().entrySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getKey(), entry.getValue()); + getColumnType(table.getColumns(), entry.getKey()), + idx[0], entry.getKey(), entry.getValue()); idx[0]++; } - /* set key(s) */ + // Bind old primary key values (for the WHERE clause) for (Map.Entry<String, Object> entry : data.getKeys().entrySet()) { mariaDbMapper.prepareStatementWithColumnTypeObject(statement, - getColumnType(table.getColumns(), entry.getKey()), idx[0], entry.getKey(), entry.getValue()); + getColumnType(table.getColumns(), entry.getKey()), + idx[0], entry.getKey(), entry.getValue()); idx[0]++; } - final long start = System.currentTimeMillis(); + long start = System.currentTimeMillis(); statement.executeUpdate(); - log.trace("executed statement in {} ms", System.currentTimeMillis() - start); + log.trace("Main update executed in {} ms", System.currentTimeMillis() - start); connection.commit(); + + // After the main update, select the system timestamps. + String selectQuery = mariaDbMapper.tupleToRawSelectSystemTimestampsQuery( + table, TupleDto.builder().data(data.getData()).build()); + try (PreparedStatement selectStmt = connection.prepareStatement(selectQuery); + ResultSet rs = selectStmt.executeQuery()) { + if (rs.next()) { + // Retrieve the new primary key values from the updated row (if needed). + Map<String, Object> pkData = new HashMap<>(); + for (PrimaryKeyDto pk : table.getConstraints().getPrimaryKey()) { + pkData.put(pk.getColumn().getInternalName(), + rs.getObject(pk.getColumn().getInternalName())); + } + Timestamp tsAdd = rs.getTimestamp("ROW_START"); + Timestamp tsDelete = rs.getTimestamp("ROW_END"); + + // Build the update query for the _timestamps table. + String updateTsQuery = mariaDbMapper.tupleToRawUpdateTimestampQuery( + table, data, tsAdd, tsDelete); + try (PreparedStatement ps = connection.prepareStatement(updateTsQuery)) { + int parameterIndex = 1; + + // SET clause: new timestamp values. + ps.setTimestamp(parameterIndex++, tsAdd); + ps.setTimestamp(parameterIndex++, tsDelete); + + // SET clause: new primary key values (assumed to be provided in data.getData()) + for (PrimaryKeyDto pk : table.getConstraints().getPrimaryKey()) { + log.info("Setting new primary key value: {}", pk.getColumn().getInternalName()); + Object newPkValue = data.getData().get(pk.getColumn().getInternalName()); + log.info("New primary key value: {}", newPkValue); + ps.setObject(parameterIndex++, newPkValue); + } + + // WHERE clause: bind the database id. + String databaseId = table.getDatabase().getInternalName(); + ps.setString(parameterIndex++, databaseId); + + // WHERE clause: bind the old primary key values. + for (PrimaryKeyDto pk : table.getConstraints().getPrimaryKey()) { + log.info("Setting new primary key value: {}", pk.getColumn().getInternalName()); + Object oldPkValue = data.getKeys().get(pk.getColumn().getInternalName()); + log.info("Old primary key value: {}", oldPkValue); + ps.setObject(parameterIndex++, oldPkValue); + } + + log.info("SQL Query: " + updateTsQuery); + log.info("Bound parameters: tsAdd={}, tsDelete={}, newPKValues={}, databaseId={}, oldPKValues={}", + tsAdd, tsDelete, + table.getConstraints().getPrimaryKey().stream() + .map(pk -> data.getData().get(pk.getColumn().getInternalName())) + .collect(Collectors.toList()), + databaseId, + table.getConstraints().getPrimaryKey().stream() + .map(pk -> data.getKeys().get(pk.getColumn().getInternalName())) + .collect(Collectors.toList())); + + int rowsUpdated = ps.executeUpdate(); + log.info("Updated {} row(s) in {}_timestamps table.", rowsUpdated, table.getInternalName()); + } + } else { + log.warn("No row found for updated PK; timestamps not updated."); + } + } } catch (SQLException e) { connection.rollback(); log.error("Failed to update tuple: {}", e.getMessage()); @@ -403,6 +472,7 @@ public class TableServiceMariaDbImpl extends DataConnector implements TableServi log.info("Updated tuple(s) from table: {}.{}", table.getDatabase(), table.getInternalName()); } + public ColumnTypeDto getColumnType(List<ColumnDto> columns, String name) throws QueryMalformedException { final Optional<ColumnDto> optional = columns.stream() .filter(c -> c.getInternalName().equals(name)).findFirst();