All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ehrbase.repository.AbstractVersionedObjectRepository Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * Copyright (c) 2024 vitasystems GmbH.
 *
 * This file is part of project EHRbase
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.ehrbase.repository;

import com.nedap.archie.rm.archetyped.Locatable;
import com.nedap.archie.rm.changecontrol.OriginalVersion;
import com.nedap.archie.rm.datatypes.CodePhrase;
import com.nedap.archie.rm.datavalues.DvCodedText;
import com.nedap.archie.rm.generic.AuditDetails;
import com.nedap.archie.rm.support.identification.HierObjectId;
import com.nedap.archie.rm.support.identification.ObjectRef;
import com.nedap.archie.rm.support.identification.ObjectVersionId;
import com.nedap.archie.rm.support.identification.UIDBasedId;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.ehrbase.api.exception.ObjectNotFoundException;
import org.ehrbase.api.exception.PreconditionFailedException;
import org.ehrbase.api.exception.StateConflictException;
import org.ehrbase.api.service.SystemService;
import org.ehrbase.jooq.pg.enums.ContributionChangeType;
import org.ehrbase.jooq.pg.enums.ContributionDataType;
import org.ehrbase.jooq.pg.tables.Ehr;
import org.ehrbase.jooq.pg.util.AdditionalSQLFunctions;
import org.ehrbase.openehr.dbformat.DbToRmFormat;
import org.ehrbase.openehr.dbformat.jooq.prototypes.AbstractRecordPrototype;
import org.ehrbase.openehr.dbformat.jooq.prototypes.AbstractTablePrototype;
import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectDataHistoryTablePrototype;
import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectDataTablePrototype;
import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectVersionHistoryTablePrototype;
import org.ehrbase.openehr.dbformat.jooq.prototypes.ObjectVersionTablePrototype;
import org.ehrbase.service.TimeProvider;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.InsertQuery;
import org.jooq.JSONB;
import org.jooq.Record;
import org.jooq.Record2;
import org.jooq.Record3;
import org.jooq.Result;
import org.jooq.SelectConditionStep;
import org.jooq.SelectFromStep;
import org.jooq.SelectJoinStep;
import org.jooq.SelectLimitPercentStep;
import org.jooq.SelectOnConditionStep;
import org.jooq.SelectQuery;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.UpdatableRecord;
import org.jooq.impl.DSL;

public abstract class AbstractVersionedObjectRepository<
        VR extends UpdatableRecord,
        DR extends UpdatableRecord,
        VH extends UpdatableRecord,
        DH extends UpdatableRecord,
        O extends Locatable> {

    public static final String NOT_MATCH_UID = "If-Match version_uid does not match uid";
    public static final String NOT_MATCH_SYSTEM_ID = "If-Match version_uid does not match system id";
    public static final String NOT_MATCH_LATEST_VERSION = "If-Match version_uid does not match latest version";

    protected static final ObjectVersionTablePrototype VERSION_PROTOTYPE = ObjectVersionTablePrototype.INSTANCE;
    protected static final ObjectVersionHistoryTablePrototype VERSION_HISTORY_PROTOTYPE =
            ObjectVersionHistoryTablePrototype.INSTANCE;
    protected static final ObjectDataTablePrototype DATA_PROTOTYPE = ObjectDataTablePrototype.INSTANCE;
    protected static final ObjectDataHistoryTablePrototype DATA_HISTORY_PROTOTYPE =
            ObjectDataHistoryTablePrototype.INSTANCE;
    private final AuditDetailsTargetType targetType;

    protected final class Tables {
        private final Table versionHead;
        private final Table dataHead;
        private final Table versionHistory;
        private final Table dataHistory;

        private Tables(Table versionHead, Table dataHead, Table versionHistory, Table dataHistory) {
            this.versionHead = versionHead;
            this.dataHead = dataHead;
            this.versionHistory = versionHistory;
            this.dataHistory = dataHistory;
        }

        public Table versionHead() {
            return versionHead;
        }

        public Table dataHead() {
            return dataHead;
        }

        public Table versionHistory() {
            return versionHistory;
        }

        public Table dataHistory() {
            return dataHistory;
        }

        public Table get(boolean version, boolean head) {
            if (version) {
                if (head) {
                    return versionHead;
                } else {
                    return versionHistory;
                }
            } else if (head) {
                return dataHead;
            } else {
                return dataHistory;
            }
        }
    }

    protected final Tables tables;

    protected final DSLContext context;
    protected final ContributionRepository contributionRepository;

    protected final SystemService systemService;
    protected final TimeProvider timeProvider;

    protected AbstractVersionedObjectRepository(
            AuditDetailsTargetType targetType,
            Table versionHead,
            Table dataHead,
            Table versionHistory,
            Table dataHistory,
            DSLContext context,
            ContributionRepository contributionRepository,
            SystemService systemService,
            TimeProvider timeProvider) {
        this.targetType = targetType;
        this.tables = new Tables(versionHead, dataHead, versionHistory, dataHistory);
        this.context = context;
        this.contributionRepository = contributionRepository;
        this.systemService = systemService;
        this.timeProvider = timeProvider;
    }

    public static ObjectVersionId buildObjectVersionId(
            UUID versionedObjectId, int sysVersion, SystemService systemService) {
        return new ObjectVersionId(
                versionedObjectId.toString(), systemService.getSystemId(), Integer.toString(sysVersion));
    }

    protected Optional findHead(Condition condition) {
        SelectQuery> locatableDataQuery = buildLocatableDataQuery(condition, true);
        return toLocatable(locatableDataQuery.fetchOne(), getLocatableClass());
    }

    public Optional findByVersion(Condition condition, Condition historyCondition, int version) {
        SelectQuery> headQuery = buildLocatableDataQuery(condition, true);
        headQuery.addConditions(field(VERSION_PROTOTYPE.SYS_VERSION).eq(version));

        SelectQuery> historyQuery = buildLocatableDataQuery(historyCondition, false);
        historyQuery.addConditions(field(VERSION_HISTORY_PROTOTYPE.SYS_VERSION).eq(version));

        Record3 dataRecord =
                headQuery.unionAll(historyQuery).fetchOne();
        if (dataRecord == null && !isDeleted(condition, historyCondition, version)) {
            String typeName = targetType.name();
            throw new ObjectNotFoundException(typeName, "No %s with given ID found".formatted(typeName));
        }

        return toLocatable(dataRecord, getLocatableClass());
    }

    protected  Field field(TableField, T> field) {
        if (field.getTable() instanceof AbstractTablePrototype t) {
            var targetTable =
                    switch (t) {
                        case ObjectVersionTablePrototype __ -> tables.versionHead;
                        case ObjectVersionHistoryTablePrototype __ -> tables.versionHistory;
                        case ObjectDataTablePrototype __ -> tables.dataHead;
                        case ObjectDataHistoryTablePrototype __ -> tables.dataHistory;
                    };
            return targetTable.field(field);
        } else {
            throw new IllegalArgumentException("Type of table not supported: %s".formatted(field.getTable()));
        }
    }

    protected Optional findRootRecordByVersion(Condition condition, Condition historyCondition, int version) {

        var head = tables.versionHead;
        var history = tables.versionHistory;

        Field[] historyFields = Stream.concat(
                        Arrays.stream(head.fields()),
                        Stream.of(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER, VERSION_HISTORY_PROTOTYPE.SYS_DELETED))
                .map(history::field)
                .toArray(Field[]::new);

        return versionHeadQueryExtended(context)
                .where(condition)
                .and(field(VERSION_PROTOTYPE.SYS_VERSION).eq(version))
                .unionAll(context.select(historyFields)
                        .from(history)
                        .where(historyCondition)
                        .and(history.field(VERSION_HISTORY_PROTOTYPE.SYS_VERSION)
                                .eq(version)))
                .fetchOptional()
                .map(r -> r.into(history));
    }

    public List findVersionIdsByContribution(UUID ehrId, UUID contributionId) {
        return context
                .select(field(VERSION_PROTOTYPE.VO_ID), field(VERSION_PROTOTYPE.SYS_VERSION))
                .from(tables.versionHead)
                .where(contributionCondition(ehrId, contributionId, tables.versionHead))
                .unionAll(context.select(
                                tables.versionHistory.field(VERSION_HISTORY_PROTOTYPE.VO_ID),
                                tables.versionHistory.field(VERSION_HISTORY_PROTOTYPE.SYS_VERSION))
                        .from(tables.versionHistory)
                        .where(contributionCondition(ehrId, contributionId, tables.versionHistory)))
                .orderBy(tables.versionHead.field(VERSION_PROTOTYPE.SYS_VERSION).asc())
                .stream()
                .map(r -> buildObjectVersionId(r.value1(), r.value2(), systemService))
                .toList();
    }

    protected Condition contributionCondition(UUID ehrId, UUID contributionId, Table table) {
        return table.field(VERSION_PROTOTYPE.CONTRIBUTION_ID)
                .eq(contributionId)
                .and(table.field(VERSION_PROTOTYPE.EHR_ID).eq(ehrId));
    }

    protected boolean isDeleted(Condition condition, Condition historyCondition, Integer version) {
        return findRootRecordByVersion(condition, historyCondition, version)
                .filter(r -> r.get(field(VERSION_HISTORY_PROTOTYPE.SYS_DELETED)))
                .isPresent();
    }

    protected Optional findLatestHistoryRoot(Condition condition) {
        return context.selectFrom(tables.versionHistory)
                .where(condition)
                .orderBy(field(VERSION_HISTORY_PROTOTYPE.SYS_VERSION).desc())
                .limit(1)
                .fetchOptional();
    }

    protected void delete(
            UUID ehrId,
            Condition condition,
            int version,
            @Nullable UUID contributionId,
            @Nullable UUID auditId,
            String notfoundMessage) {

        Result versionHeads = findVersionHeadRecords(condition);

        if (versionHeads.isEmpty()) {
            // not found
            throw new ObjectNotFoundException(getLocatableClass().getSimpleName(), notfoundMessage);
        }
        if (versionHeads.size() > 1) {
            throw new IllegalArgumentException("The implementation is limited to deleting one entry");
        }

        // The record is recycled fot the delete entry in the history
        VH versionHead = versionHeads.getFirst();
        VH firstRecord = versionHead.into(tables.versionHistory);

        if (firstRecord.get(VERSION_HISTORY_PROTOTYPE.SYS_VERSION) != version) {
            // concurrent modification
            throw new StateConflictException(NOT_MATCH_LATEST_VERSION);
        }

        copyHeadToHistory(versionHead, createCurrentTime(firstRecord.get(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_LOWER)));

        deleteHead(condition, version, StateConflictException::new);

        UUID finalContributionId = Optional.ofNullable(contributionId)
                .orElseGet(() -> contributionRepository.createDefault(
                        ehrId, ContributionDataType.folder, ContributionChangeType.deleted));

        UUID finalAuditId = Optional.ofNullable(auditId)
                .orElseGet(() -> contributionRepository.createDefaultAudit(ContributionChangeType.deleted, targetType));

        firstRecord.set(VERSION_HISTORY_PROTOTYPE.SYS_DELETED, true);

        firstRecord.set(VERSION_HISTORY_PROTOTYPE.SYS_VERSION, version + 1);
        firstRecord.set(VERSION_HISTORY_PROTOTYPE.AUDIT_ID, finalAuditId);
        firstRecord.set(VERSION_HISTORY_PROTOTYPE.CONTRIBUTION_ID, finalContributionId);

        firstRecord.changed(true);

        firstRecord.insert();
    }

    protected Optional> getOriginalVersion(
            Condition condition, Condition historyCondition, int version) {

        Optional root = findRootRecordByVersion(condition, historyCondition, version);

        if (root.isEmpty()) {
            return Optional.empty();
        }
        VH versionRecord = root.get();

        // create data for output, i.e. fields of the OriginalVersion
        ObjectVersionId versionId =
                buildObjectVersionId(versionRecord.get(VERSION_HISTORY_PROTOTYPE.VO_ID), version, systemService);
        DvCodedText lifecycleState = new DvCodedText(
                "complete", new CodePhrase("532")); // TODO: once lifecycle state is supported, get it here dynamically
        AuditDetails commitAudit =
                contributionRepository.findAuditDetails(versionRecord.get(VERSION_HISTORY_PROTOTYPE.AUDIT_ID));
        ObjectRef objectRef = new ObjectRef<>(
                new HierObjectId(versionRecord
                        .get(VERSION_HISTORY_PROTOTYPE.CONTRIBUTION_ID)
                        .toString()),
                "openehr",
                "contribution");

        ObjectVersionId precedingVersionId = null;
        // check if there is a preceding version and set it, if available
        if (version > 1) {
            // in the current scope version is an int and therefore: preceding = current - 1
            precedingVersionId = buildObjectVersionId(
                    versionRecord.get(VERSION_HISTORY_PROTOTYPE.VO_ID), version - 1, systemService);
        }

        Optional composition = findByVersion(condition, historyCondition, version);
        OriginalVersion originalVersion = new OriginalVersion<>(
                versionId,
                precedingVersionId,
                composition.orElse(null),
                lifecycleState,
                commitAudit,
                objectRef,
                null,
                null,
                null);

        return Optional.of(originalVersion);
    }

    protected boolean hasEhr(UUID ehrId) {
        return context.fetchExists(Ehr.EHR_, Ehr.EHR_.ID.eq(ehrId));
    }

    protected abstract Class getLocatableClass();

    public static int extractVersion(UIDBasedId uid) {
        return Integer.parseInt(((ObjectVersionId) uid).getVersionTreeId().getValue());
    }

    public static UUID extractUid(UIDBasedId uid) {

        return UUID.fromString(uid.getRoot().getValue());
    }

    public static String extractSystemId(UIDBasedId uid) {
        return ((ObjectVersionId) uid).getCreatingSystemId().getValue();
    }

    protected void commitHead(
            UUID ehrId,
            Locatable composition,
            @Nullable UUID contributionId,
            @Nullable UUID auditId,
            ContributionChangeType changeType,
            Consumer addVersionFieldsFunction,
            Consumer addDataFieldsFunction) {

        UUID finalContributionId = Optional.ofNullable(contributionId)
                .orElseGet(() ->
                        contributionRepository.createDefault(ehrId, ContributionDataType.composition, changeType));

        UUID finalAuditId = Optional.ofNullable(auditId)
                .orElseGet(() -> contributionRepository.createDefaultAudit(changeType, targetType));

        VersionDataDbRecord versionData = toRecords(ehrId, composition, finalContributionId, finalAuditId);

        // Version
        VR versionRecord = versionData.versionRecord().into(tables.versionHead);
        addVersionFieldsFunction.accept(versionRecord);
        versionRecord.store();

        // Data
        RepositoryHelper.executeBulkInsert(
                context,
                versionData
                        .dataRecords()
                        .get()
                        .map(r -> r.into(tables.dataHead))
                        .map(r -> {
                            addDataFieldsFunction.accept(r);
                            return r;
                        }),
                tables.dataHead);
    }

    protected final VersionDataDbRecord toRecords(
            UUID ehrId, Locatable versionDataObject, UUID contributionId, UUID auditId) {
        return VersionDataDbRecord.toRecords(
                ehrId, versionDataObject, contributionId, auditId, timeProvider.getNow(), context);
    }

    public void update(
            UUID ehrId,
            O versionedObject,
            Condition condition,
            Condition historyCondition,
            @Nullable UUID contributionId,
            @Nullable UUID auditId,
            Consumer addVersionFieldsFunction,
            Consumer addDataFieldsFunction,
            String notFoundErrorMessage) {

        UIDBasedId nextUid = versionedObject.getUid();

        Result versionHeads = findVersionHeadRecords(condition);
        if (versionHeads.size() > 1) {
            throw new IllegalArgumentException("%d versions were returned".formatted(versionHeads.size()));
        }

        int headVersion;
        UUID headVoId;
        OffsetDateTime now;
        VH delRecord;

        if (versionHeads.isEmpty()) {

            Optional latestHistoryRoot = findLatestHistoryRoot(historyCondition);
            if (latestHistoryRoot.isEmpty()) {

                // sanity check for existing ehr uid - this provides a more precise error
                if (!hasEhr(ehrId)) {
                    throw new ObjectNotFoundException("EHR", "EHR %s does not exist".formatted(ehrId));
                }

                // not found
                throw new ObjectNotFoundException(getLocatableClass().getSimpleName(), notFoundErrorMessage);
            }

            delRecord = latestHistoryRoot
                    .filter(r -> r.get(VERSION_HISTORY_PROTOTYPE.SYS_DELETED))
                    .orElseThrow(() -> new PreconditionFailedException(NOT_MATCH_LATEST_VERSION));

            headVersion = delRecord.get(VERSION_HISTORY_PROTOTYPE.SYS_VERSION);
            headVoId = delRecord.get(VERSION_HISTORY_PROTOTYPE.VO_ID);
            now = createCurrentTime(delRecord.get(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_LOWER));

        } else {
            delRecord = null;
            VH root = versionHeads.getFirst();
            headVersion = root.get(VERSION_HISTORY_PROTOTYPE.SYS_VERSION);
            headVoId = root.get(VERSION_HISTORY_PROTOTYPE.VO_ID);
            now = createCurrentTime(root.get(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_LOWER));
        }

        // sanity check: valid next uid in system with version
        checkIsNextHeadVoId(headVoId, headVersion, nextUid);

        if (delRecord != null) {
            // update delete record period
            delRecord.set(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER, now);
            int updateCount = context.executeUpdate(delRecord);
            if (updateCount != 1) {
                // concurrent modification
                throw new PreconditionFailedException(NOT_MATCH_LATEST_VERSION);
            }

        } else {
            copyHeadToHistory(versionHeads.getFirst(), now);
            deleteHead(condition, headVersion, PreconditionFailedException::new);
        }

        // commit new version
        commitHead(
                ehrId,
                versionedObject,
                contributionId,
                auditId,
                ContributionChangeType.modification,
                addVersionFieldsFunction,
                addDataFieldsFunction);
    }

    protected SelectQuery> buildLocatableDataQuery(Condition condition, boolean head) {
        Table versionTable = tables.get(true, head);
        Table dataTable = tables.get(false, head);

        Field voIdField = versionTable.field(VERSION_PROTOTYPE.VO_ID);
        Field sysVersionField = versionTable.field(VERSION_PROTOTYPE.SYS_VERSION);
        Field jsonbField = DSL.jsonbObjectAgg(
                        dataTable.field(DATA_PROTOTYPE.ENTITY_IDX), dataTable.field(DATA_PROTOTYPE.DATA))
                .as(DSL.name("data"));

        return fromJoinedVersionData(context.select(voIdField, sysVersionField, jsonbField), head)
                .where(condition)
                .groupBy(voIdField, sysVersionField)
                .getQuery();
    }

    protected , R extends Record> SelectOnConditionStep fromJoinedVersionData(
            S select, boolean head) {
        Table versionTable = tables.get(true, head);
        Table dataTable = tables.get(false, head);

        Condition joinCondition =
                versionDataJoinCondition(f -> versionTable.field(f).eq(dataTable.field(f)));

        if (!head) {
            joinCondition = joinCondition.and(versionTable
                    .field(VERSION_PROTOTYPE.SYS_VERSION)
                    .eq(dataTable.field(DATA_HISTORY_PROTOTYPE.SYS_VERSION)));
        }
        return select.from(versionTable).join(dataTable).on(joinCondition);
    }

    protected abstract List> getVersionDataJoinFields();

    private Condition versionDataJoinCondition(Function fieldConditionCreator) {
        var versionDataJoinFields = getVersionDataJoinFields();
        Condition joinCondition;
        if (versionDataJoinFields.size() == 1) {
            joinCondition = fieldConditionCreator.apply(versionDataJoinFields.getFirst());
        } else {
            joinCondition = DSL.and(
                    versionDataJoinFields.stream().map(fieldConditionCreator).toList());
        }
        return joinCondition;
    }

    protected Condition dataRootCondition(Table dataTable) {
        return dataTable.field(DATA_PROTOTYPE.NUM).eq(0);
    }

    protected Optional findVersionByTime(
            Condition condition, Condition historyCondition, OffsetDateTime time) {

        SelectLimitPercentStep> headQuery = context.select(
                        field(VERSION_PROTOTYPE.SYS_VERSION), field(VERSION_PROTOTYPE.VO_ID))
                .from(tables.versionHead)
                .where(field(VERSION_PROTOTYPE.SYS_PERIOD_LOWER).lessOrEqual(time))
                .and(condition)
                .limit(1);

        SelectLimitPercentStep> historyQuery = context.select(
                        field(VERSION_HISTORY_PROTOTYPE.SYS_VERSION), field(VERSION_HISTORY_PROTOTYPE.VO_ID))
                .from(tables.versionHistory)
                .where(
                        field(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_LOWER).lessOrEqual(time),
                        field(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER)
                                .greaterThan(time)
                                .or(field(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER)
                                        .isNull()))
                .and((historyCondition))
                .limit(1);

        return headQuery
                .unionAll(historyQuery)
                .limit(1)
                .fetchOptional()
                .map(r -> buildObjectVersionId(r.value2(), r.value1(), systemService));
    }

    /**
     *
     * @param jsonbRecord {vo_id, sys_version, jsonData}
     * @param locatableClass
     * @return
     * @param 
     */
    protected  Optional toLocatable(
            Record3 jsonbRecord, Class locatableClass) {
        if (jsonbRecord == null) {
            return Optional.empty();
        }
        final L composition = DbToRmFormat.reconstructRmObject(
                locatableClass, jsonbRecord.value3().data());
        composition.setUid(buildObjectVersionId(jsonbRecord.value1(), jsonbRecord.value2(), systemService));
        return Optional.of(composition);
    }

    protected void copyHeadToHistory(VH versionRecord, OffsetDateTime now) {

        // copy version to history
        versionRecord.set(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER, now);
        versionRecord.set(VERSION_HISTORY_PROTOTYPE.SYS_DELETED, false);
        versionRecord.changed(true);
        versionRecord.insert();

        Field[] headFields = Stream.concat(
                        Arrays.stream(tables.dataHead.fields()), Stream.of(field(VERSION_PROTOTYPE.SYS_VERSION)))
                .toArray(Field[]::new);
        Field[] historyFields =
                Arrays.stream(headFields).map(tables.dataHistory::field).toArray(Field[]::new);

        InsertQuery dataInsert = context.insertQuery(tables.dataHistory);

        // copy data directly: select by join fields, add sys_version
        Condition joinCondition =
                versionDataJoinCondition(f -> tables.dataHead.field(f).eq(DSL.val(versionRecord.get(f))));

        SelectConditionStep dataSelect =
                fromJoinedVersionData(context.select(headFields), true).where(joinCondition);

        dataInsert.setSelect(historyFields, dataSelect);
        dataInsert.execute();
    }

    protected void deleteHead(
            Condition versionCondition, int oldVersion, Function exceptionProvider) {

        // delete head
        int deleteCount = context.deleteFrom(tables.versionHead)
                .where(versionCondition.and(field(VERSION_PROTOTYPE.SYS_VERSION).eq(oldVersion)))
                .execute();

        if (deleteCount == 0) {
            // concurrent modification
            throw exceptionProvider.apply(NOT_MATCH_LATEST_VERSION);
        }
    }

    /**
     * version head + empty history fields:
     * SYS_PERIOD_UPPER,
     * SYS_DELETED
     *
     * @param context
     * @return
     */
    protected SelectJoinStep versionHeadQueryExtended(DSLContext context) {
        return context.select(tables.versionHead.fields())
                .select(
                        DSL.inline((Object) null).as(VERSION_HISTORY_PROTOTYPE.SYS_PERIOD_UPPER.getName()),
                        DSL.inline(false).as(VERSION_HISTORY_PROTOTYPE.SYS_DELETED.getName()))
                .from(tables.versionHead);
    }

    protected Result findVersionHeadRecords(Condition condition) {
        return versionHeadQueryExtended(context).where(condition).fetchInto(tables.versionHistory);
    }

    protected Field jsonDataField(Table table, String... path) {
        return AdditionalSQLFunctions.jsonbAttributePathText(table.field(DATA_PROTOTYPE.DATA), path);
    }

    /**
     * Determines the current time.
     *
     * @param lowerBound For proper version intervals the value is guaranteed to be at least 1 microsecond after lowerBound
     * @return
     */
    protected OffsetDateTime createCurrentTime(OffsetDateTime lowerBound) {
        OffsetDateTime now = timeProvider.getNow();
        // sysPeriodUpper must be after sysPeriodLower for proper intervals
        if (now.isAfter(lowerBound)) {
            return now;
        }
        // Add one microsecond, so the interval is valid.
        // Resolution of postgres timestamps is 1 microsecond
        // https://www.postgresql.org/docs/14/datatype-datetime.html#DATATYPE-DATETIME-TABLE
        return lowerBound.plusNanos(1_000);
    }

    protected void checkIsNextHeadVoId(UUID headVoid, int headVersion, UIDBasedId uid) {

        // uuid missmatch
        if (!Objects.equals(headVoid, extractUid(uid))) {
            throw new PreconditionFailedException(NOT_MATCH_UID);
        }
        // system id missmatch
        if (!Objects.equals(systemService.getSystemId(), extractSystemId(uid))) {
            throw new PreconditionFailedException(NOT_MATCH_SYSTEM_ID);
        }
        // versions not consecutive
        if ((headVersion + 1) != extractVersion(uid)) {
            throw new PreconditionFailedException(NOT_MATCH_LATEST_VERSION);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy