Skip to content

Commit

Permalink
Merge pull request #100 from qbicsoftware/development
Browse files Browse the repository at this point in the history
Add migration endpoint to migrate data (#99)
  • Loading branch information
KochTobi authored Aug 22, 2022
2 parents ed0b42b + a1485e6 commit 2d1ca80
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package life.qbic.controller;

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import java.time.Instant;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import life.qbic.auth.Authentication;
import life.qbic.db.tools.Migrator;

/**
* Utility tool to migrate data.
*
* @since 2.0.0
*/
@Requires(beans = Authentication.class)
@Secured(SecurityRule.IS_AUTHENTICATED)
@Controller("/v2/migrate")
public class DatabaseMigrationController {

private final Migrator migrator;

@Inject
public DatabaseMigrationController(Migrator migrator) {
this.migrator = migrator;
}

@Get(produces = MediaType.APPLICATION_JSON)
@RolesAllowed("WRITER")
void migrateAllData() {
migrator.migrateFromVersionOneToVersionTwo();
}

@Get(uri = "/{utcDateTime}", produces = MediaType.APPLICATION_JSON)
@RolesAllowed("WRITER")
void migrateAllData(@PathVariable String utcDateTime) {
Instant earliestTime = Instant.parse(utcDateTime);
migrator.migrateFromVersionOneToVersionTwo(earliestTime);
}
}
80 changes: 80 additions & 0 deletions src/main/groovy/life/qbic/db/tools/Migrator.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package life.qbic.db.tools

import groovy.sql.Sql
import life.qbic.api.rest.v2.samples.SampleStatusDto
import life.qbic.api.rest.v2.samples.SamplesControllerV2
import life.qbic.api.rest.v2.samples.StatusChangeRequest

import javax.inject.Inject
import javax.inject.Singleton
import javax.sql.DataSource
import java.sql.Timestamp
import java.time.Instant

import static java.util.Objects.requireNonNull

@Singleton
class Migrator {

private final DbInteractor dbInteractor
private final SamplesControllerV2 controllerV2

@Inject
Migrator(DbInteractor dbInteractor, SamplesControllerV2 controllerV2) {
this.dbInteractor = dbInteractor
this.controllerV2 = controllerV2
}

/**
* migrates data from the old data schema to the schema of v2. Starts at the earliestModification.
* @param earliestModification
*/
void migrateFromVersionOneToVersionTwo(Instant earliestModification) {
dbInteractor.statusChangeRequests(earliestModification).forEach(it -> controllerV2.moveSampleToStatus(it.sampleCode, new StatusChangeRequest(it.statusDto, it.arrivalTime.toString())))
}

/**
* migrates data from the old data schema to the schema of v2.
*/
void migrateFromVersionOneToVersionTwo() {
dbInteractor.statusChangeRequests(new Date(0).toInstant()).forEach(it -> controllerV2.moveSampleToStatus(it.sampleCode, new StatusChangeRequest(it.statusDto, it.arrivalTime.toString())))
}

static class StatusChange {
final String sampleCode
final SampleStatusDto statusDto
final Instant arrivalTime

StatusChange(String sampleCode, SampleStatusDto statusDto, Instant arrivalTime) {
this.sampleCode = sampleCode
this.statusDto = statusDto
this.arrivalTime = arrivalTime
}
}

@Singleton
static class DbInteractor {

private final DataSource dataSource

@Inject
DbInteractor(DataSource dataSource) {
this.dataSource = dataSource
}

List<StatusChange> statusChangeRequests(Instant earliestModification) {
def connection = requireNonNull(dataSource.getConnection())
String query = "SELECT sample_id, sample_status, arrival_time FROM samples_locations WHERE arrival_time >= :earliestModification ORDER BY arrival_time"
try (Sql sql = new Sql(connection)) {
def rows = sql.rows(query, [earliestModification: Timestamp.from(earliestModification)])
return rows.stream()
.map(it -> new StatusChange(
it.get("sample_id") as String,
SampleStatusDto.valueOf(it.get("sample_status") as String),
(it.get("arrival_time") as Timestamp).toInstant()))
.sorted((it1, it2) -> it1.arrivalTime.compareTo(it2.arrivalTime))
.collect()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public HttpResponse<?> moveSampleToStatus(@PathVariable String sampleCode,
"Provided sample status not recognized: "
+ requestedStatus, ErrorCode.BAD_SAMPLE_STATUS, ErrorParameters.create().with("sampleStatus", requestedStatus));
}
log.info(String.format("Sample %s is in status %s valid since %s", sampleCode, statusChangeRequest.status(), statusChangeRequest.validSince()));
log.info(String.format("Processed request for sample %s to status %s at %s.", sampleCode, statusChangeRequest.status(), statusChangeRequest.validSince()));
return HttpResponse.ok();
}

Expand Down
27 changes: 19 additions & 8 deletions src/main/java/life/qbic/domain/sample/Sample.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ public static Sample fromEvents(Collection<SampleEvent> events) {
throw new IllegalArgumentException(
"Sample creation from events not possible without provided events.");
}
Optional<SampleCode> containedSampleCode = events.stream().findAny().map(SampleEvent::sampleCode);
Optional<SampleCode> containedSampleCode = events.stream()
.findAny()
.map(SampleEvent::sampleCode);
SampleCode sampleCode = containedSampleCode.orElseThrow(() ->
new UnrecoverableException("Could not identify sample code from events: " + events));
if (events.stream().anyMatch(it -> !it.sampleCode().equals(sampleCode))) {
Expand Down Expand Up @@ -104,20 +106,26 @@ public <T extends SampleEvent> void handle(T event) {
if (events.contains(event)) {
return;
}
if (!occurredAfterCurrentState(event)) {
throw new UnrecoverableException(
String.format("The sample (%s) was modified after %s", sampleCode, event.occurredOn()));
}
validateEventTimeOrThrowException(event);
apply(event);
events.add(event);
}

private boolean occurredAfterCurrentState(SampleEvent event) {
private void validateEventTimeOrThrowException(SampleEvent event) {
if (events.isEmpty()) {
return true;
return;
}
SampleEvent lastEvent = events.get(events.size() - 1);
return event.occurredOn().isAfter(lastEvent.occurredOn());
if (!event.occurredOn().isAfter(lastEvent.occurredOn())) {
throw new UnrecoverableException(
String.format(
"The sample (%s) was last modified at %s by %s. Modification with %s event at %s not possible.",
sampleCode,
lastEvent.occurredOn(),
lastEvent.getClass().getSimpleName(),
event.getClass().getSimpleName(),
event.occurredOn()));
}
}

public <T extends SampleEvent> void apply(T event) {
Expand All @@ -140,6 +148,7 @@ public <T extends SampleEvent> void apply(T event) {

/**
* A history of events leading to the current state.
*
* @return all events leading to the current state of the sample
* @since 2.0.0
*/
Expand Down Expand Up @@ -187,6 +196,7 @@ public static class CurrentState {

/**
* The status the sample is in.
*
* @return the current status
*/
public Status status() {
Expand All @@ -195,6 +205,7 @@ public Status status() {

/**
* The instant from which the current state is valid from.
*
* @return the instant of this state
*/
public Instant statusValidSince() {
Expand Down

0 comments on commit 2d1ca80

Please sign in to comment.