RelationDecisionProjectionWriter.java

package com.taxonomy.relations.service;

import com.taxonomy.relations.model.RelationDecisionProjection;
import com.taxonomy.relations.repository.RelationDecisionProjectionCheckpointRepository;
import com.taxonomy.relations.repository.RelationDecisionProjectionRepository;
import com.taxonomy.relations.service.RelationDecisionProjectionService.ProjectionConflictException;
import com.taxonomy.relations.service.RelationDecisionProjectionService.ProjectionOutcome;
import com.taxonomy.relations.service.RelationDecisionProjectionService.ProjectionRequest;
import com.taxonomy.relations.service.RelationDecisionProjectionService.ProjectionResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;

/** Transactional writer for one already Git-verified incremental projection state. */
@Service
public class RelationDecisionProjectionWriter {

    private final RelationDecisionProjectionRepository projectionRepository;
    private final RelationDecisionProjectionCheckpointRepository checkpointRepository;

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

    /** Compatibility constructor for focused pre-checkpoint unit tests in this package. */
    RelationDecisionProjectionWriter(
            RelationDecisionProjectionRepository projectionRepository) {
        this.projectionRepository = Objects.requireNonNull(
                projectionRepository, "projectionRepository");
        this.checkpointRepository = null;
    }

    @Transactional
    public ProjectionResult write(ProjectionRequest request) {
        Objects.requireNonNull(request, "request");
        ProjectionRequest effectiveRequest = effectiveState(request);
        String workspaceScopeKey = RelationDecisionProjection.scopeKeyFor(
                effectiveRequest.workspaceId());

        // An incremental command proves only one relation identity. Even when it
        // is a semantic no-op, it must not leave a previous full-branch checkpoint
        // available as an assurance that every row was rebuilt at this commit.
        // Runtime failures roll this deletion back with the relation write.
        if (checkpointRepository != null) {
            checkpointRepository.deleteExact(
                    effectiveRequest.repositoryId(),
                    workspaceScopeKey,
                    effectiveRequest.branch());
        }

        RelationDecisionProjection existing = projectionRepository
                .findExactForUpdate(
                        effectiveRequest.repositoryId(),
                        workspaceScopeKey,
                        effectiveRequest.branch(),
                        effectiveRequest.sourceCode(),
                        effectiveRequest.relationType(),
                        effectiveRequest.targetCode())
                .orElse(null);

        if (existing == null) {
            RelationDecisionProjection created = new RelationDecisionProjection();
            applyIdentity(created, effectiveRequest);
            applyState(created, effectiveRequest);
            applyAuthority(created, effectiveRequest);
            projectionRepository.save(created);
            return result(ProjectionOutcome.CREATED, effectiveRequest);
        }

        if (effectiveRequest.authoritativeCommitId().equals(
                existing.getAuthoritativeCommitId())) {
            if (!sameState(existing, effectiveRequest)) {
                throw new ProjectionConflictException(
                        "Authoritative commit "
                                + effectiveRequest.authoritativeCommitId()
                                + " is already projected with different relation state");
            }
            // Multiple commands may legitimately prove the same semantic no-op
            // at one unchanged Git head. Their causation IDs identify command
            // attempts, not different authoritative relation states. A complete
            // rebuild also records its own rebuild:<commit> causation ID, so
            // requiring equality here would reject every later unchanged command.
            return result(ProjectionOutcome.REPLAYED, effectiveRequest);
        }

        applyState(existing, effectiveRequest);
        applyAuthority(existing, effectiveRequest);
        projectionRepository.save(existing);
        return result(ProjectionOutcome.UPDATED, effectiveRequest);
    }

    private static ProjectionRequest effectiveState(ProjectionRequest request) {
        boolean relationPresent = request.relationPresent()
                && RelationDecisionStatusPolicy.isRelationPresent(request.status());
        if (relationPresent == request.relationPresent()) {
            return request;
        }
        return new ProjectionRequest(
                request.repositoryId(),
                request.workspaceId(),
                request.branch(),
                request.sourceCode(),
                request.relationType(),
                request.targetCode(),
                false,
                request.status(),
                request.confidence(),
                request.provenance(),
                request.authoritativeCommitId(),
                request.causationId());
    }

    private static void applyIdentity(
            RelationDecisionProjection projection,
            ProjectionRequest request) {
        projection.setRepositoryId(request.repositoryId());
        projection.setWorkspaceId(request.workspaceId());
        projection.setBranch(request.branch());
        projection.setSourceCode(request.sourceCode());
        projection.setRelationType(request.relationType());
        projection.setTargetCode(request.targetCode());
    }

    private static void applyState(
            RelationDecisionProjection projection,
            ProjectionRequest request) {
        projection.setRelationPresent(request.relationPresent());
        projection.setStatus(request.status());
        projection.setConfidence(request.confidence());
        projection.setProvenance(request.provenance());
    }

    private static void applyAuthority(
            RelationDecisionProjection projection,
            ProjectionRequest request) {
        projection.setAuthoritativeCommitId(
                request.authoritativeCommitId());
        projection.setCausationId(request.causationId());
    }

    private static boolean sameState(
            RelationDecisionProjection projection,
            ProjectionRequest request) {
        return projection.isRelationPresent() == request.relationPresent()
                && Objects.equals(projection.getStatus(), request.status())
                && Objects.equals(
                        projection.getConfidence(), request.confidence())
                && Objects.equals(
                        projection.getProvenance(), request.provenance());
    }

    private static ProjectionResult result(
            ProjectionOutcome outcome,
            ProjectionRequest request) {
        return new ProjectionResult(
                outcome,
                request.authoritativeCommitId(),
                request.relationPresent());
    }
}