RelationBranchProjectionRebuildWriter.java

package com.taxonomy.relations.service;

import com.taxonomy.relations.model.RelationDecisionProjection;
import com.taxonomy.relations.model.RelationDecisionProjectionCheckpoint;
import com.taxonomy.relations.repository.RelationDecisionProjectionCheckpointRepository;
import com.taxonomy.relations.repository.RelationDecisionProjectionRepository;
import com.taxonomy.relations.service.RelationBranchProjectionRebuildService.RebuildResult;
import com.taxonomy.relations.service.RelationBranchProjectionRebuildService.RelationSnapshot;
import com.taxonomy.workspace.service.RepositoryContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/** Transactional replacement boundary for one fully validated branch projection. */
@Service
public class RelationBranchProjectionRebuildWriter {

    private final RelationDecisionProjectionRepository projectionRepository;
    private final RelationDecisionProjectionCheckpointRepository checkpointRepository;

    public RelationBranchProjectionRebuildWriter(
            RelationDecisionProjectionRepository projectionRepository,
            RelationDecisionProjectionCheckpointRepository checkpointRepository) {
        this.projectionRepository = Objects.requireNonNull(
                projectionRepository, "projectionRepository");
        this.checkpointRepository = Objects.requireNonNull(
                checkpointRepository, "checkpointRepository");
    }

    /**
     * Replaces the exact repository/workspace/branch rows and checkpoint in one
     * transaction. Failure leaves the previously complete projection untouched.
     * Rejected decisions stay in authoritative TaxDSL but are omitted from the
     * complete active-relation projection.
     */
    @Transactional
    public RebuildResult replace(
            RepositoryContext context,
            String authoritativeCommitId,
            List<RelationSnapshot> relations) {
        Objects.requireNonNull(context, "context");
        Objects.requireNonNull(authoritativeCommitId, "authoritativeCommitId");
        List<RelationSnapshot> snapshots = List.copyOf(
                Objects.requireNonNull(relations, "relations"));
        String workspaceScopeKey = RelationDecisionProjection.scopeKeyFor(
                context.workspaceId());

        RelationDecisionProjectionCheckpoint checkpoint = checkpointRepository
                .findExactForUpdate(
                        context.repositoryId(),
                        workspaceScopeKey,
                        context.branch())
                .orElseGet(RelationDecisionProjectionCheckpoint::new);

        projectionRepository.deleteExactBranch(
                context.repositoryId(),
                workspaceScopeKey,
                context.branch());
        projectionRepository.flush();

        List<RelationDecisionProjection> replacements = snapshots.stream()
                .filter(snapshot -> RelationDecisionStatusPolicy
                        .isRelationPresent(snapshot.status()))
                .sorted(Comparator
                        .comparing(RelationSnapshot::sourceCode)
                        .thenComparing(snapshot -> snapshot.relationType().name())
                        .thenComparing(RelationSnapshot::targetCode))
                .map(snapshot -> projection(
                        context,
                        authoritativeCommitId,
                        snapshot))
                .toList();
        projectionRepository.saveAll(replacements);
        projectionRepository.flush();

        checkpoint.setRepositoryId(context.repositoryId());
        checkpoint.setWorkspaceId(context.workspaceId());
        checkpoint.setBranch(context.branch());
        checkpoint.setAuthoritativeCommitId(authoritativeCommitId);
        checkpoint.setRelationCount(replacements.size());
        checkpointRepository.saveAndFlush(checkpoint);

        return new RebuildResult(
                context.repositoryId(),
                context.workspaceId(),
                context.branch(),
                authoritativeCommitId,
                replacements.size());
    }

    private static RelationDecisionProjection projection(
            RepositoryContext context,
            String authoritativeCommitId,
            RelationSnapshot snapshot) {
        RelationDecisionProjection projection = new RelationDecisionProjection();
        projection.setRepositoryId(context.repositoryId());
        projection.setWorkspaceId(context.workspaceId());
        projection.setBranch(context.branch());
        projection.setSourceCode(snapshot.sourceCode());
        projection.setRelationType(snapshot.relationType());
        projection.setTargetCode(snapshot.targetCode());
        projection.setRelationPresent(true);
        projection.setStatus(snapshot.status());
        projection.setConfidence(snapshot.confidence());
        projection.setProvenance(snapshot.provenance());
        projection.setAuthoritativeCommitId(authoritativeCommitId);
        projection.setCausationId("rebuild:" + authoritativeCommitId);
        return projection;
    }
}