WorkflowSemanticIndexEntity.java

package org.hammer.audio.infrastructure.workflow.search;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.UniqueConstraint;
import java.util.List;
import org.hibernate.annotations.Nationalized;
import org.hibernate.search.engine.backend.analysis.AnalyzerNames;
import org.hibernate.search.engine.backend.types.Sortable;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.KeywordField;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath;
import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue;

/** Branch-aware, rebuildable semantic projection of one authoritative workflow commit. */
@Entity
@Indexed
@Table(
    name = "workflow_semantic_index",
    indexes = {
      @Index(
          name = "idx_workflow_semantic_repo_branch_position",
          columnList = "repository_name, branch_name, branch_position"),
      @Index(
          name = "idx_workflow_semantic_repo_workflow",
          columnList = "repository_name, workflow_id")
    },
    uniqueConstraints = {
      @UniqueConstraint(
          name = "uk_workflow_semantic_repo_branch_commit",
          columnNames = {"repository_name", "branch_name", "object_id"})
    })
public class WorkflowSemanticIndexEntity {

  static final String NODE_IDS_FIELD = "nodeIds";
  static final String NODE_TYPES_FIELD = "nodeTypes";
  static final String NODE_LABELS_FIELD = "nodeLabels";
  static final String PROPERTY_KEYS_FIELD = "propertyKeys";
  static final String PROPERTY_VALUES_FIELD = "propertyValues";
  static final String PROPERTY_PAIRS_FIELD = "propertyPairs";

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @KeywordField
  @Column(name = "repository_name", nullable = false, length = 255)
  private String repositoryName;

  @KeywordField
  @Nationalized
  @Column(name = "branch_name", nullable = false, length = 255)
  private String branchName;

  @KeywordField
  @Column(name = "object_id", nullable = false, length = 40)
  private String objectId;

  @GenericField(sortable = Sortable.YES)
  @Column(name = "branch_position", nullable = false)
  private int branchPosition;

  @KeywordField
  @Nationalized
  @Column(name = "workflow_id", nullable = false, length = 255)
  private String workflowId;

  @FullTextField(analyzer = AnalyzerNames.STANDARD)
  @Nationalized
  @Column(name = "workflow_name", nullable = false, length = 2048)
  private String workflowName;

  @Column(name = "node_ids", nullable = false, length = 65535)
  private String encodedNodeIds;

  @Column(name = "node_types", nullable = false, length = 65535)
  private String encodedNodeTypes;

  @Column(name = "node_labels", nullable = false, length = 65535)
  private String encodedNodeLabels;

  @FullTextField(analyzer = AnalyzerNames.STANDARD)
  @Nationalized
  @Column(name = "node_label_text", nullable = false, length = 65535)
  private String nodeLabelText;

  @Column(name = "property_keys", nullable = false, length = 65535)
  private String encodedPropertyKeys;

  @Column(name = "property_values", nullable = false, length = 65535)
  private String encodedPropertyValues;

  @Column(name = "property_pairs", nullable = false, length = 65535)
  private String encodedPropertyPairs;

  protected WorkflowSemanticIndexEntity() {
    // Required by Jakarta Persistence.
  }

  static WorkflowSemanticIndexEntity create(
      String repositoryName,
      String branchName,
      String objectId,
      int branchPosition,
      WorkflowSemanticProjectionValues values) {
    WorkflowSemanticIndexEntity entity = new WorkflowSemanticIndexEntity();
    entity.repositoryName = repositoryName;
    entity.branchName = branchName;
    entity.objectId = objectId;
    entity.branchPosition = branchPosition;
    entity.apply(values);
    return entity;
  }

  void apply(WorkflowSemanticProjectionValues values) {
    workflowId = values.workflowId();
    workflowName = values.workflowName();
    encodedNodeIds = WorkflowSemanticProjectionValues.encodeValues(values.nodeIds());
    encodedNodeTypes = WorkflowSemanticProjectionValues.encodeValues(values.nodeTypes());
    encodedNodeLabels = WorkflowSemanticProjectionValues.encodeValues(values.nodeLabels());
    nodeLabelText = String.join("\n", values.nodeLabels());
    encodedPropertyKeys = WorkflowSemanticProjectionValues.encodeValues(values.propertyKeys());
    encodedPropertyValues = WorkflowSemanticProjectionValues.encodeValues(values.propertyValues());
    encodedPropertyPairs = String.join("\n", values.propertyPairs());
  }

  void setBranchPosition(int branchPosition) {
    this.branchPosition = branchPosition;
  }

  String getRepositoryName() {
    return repositoryName;
  }

  String getBranchName() {
    return branchName;
  }

  String getObjectId() {
    return objectId;
  }

  int getBranchPosition() {
    return branchPosition;
  }

  String getWorkflowId() {
    return workflowId;
  }

  String getWorkflowName() {
    return workflowName;
  }

  @Transient
  @KeywordField(name = NODE_IDS_FIELD)
  @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedNodeIds")))
  public List<String> getNodeIds() {
    return WorkflowSemanticProjectionValues.decodeValues(encodedNodeIds);
  }

  @Transient
  @KeywordField(name = NODE_TYPES_FIELD)
  @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedNodeTypes")))
  public List<String> getNodeTypes() {
    return WorkflowSemanticProjectionValues.decodeValues(encodedNodeTypes);
  }

  @Transient
  @KeywordField(name = NODE_LABELS_FIELD)
  @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedNodeLabels")))
  public List<String> getNodeLabels() {
    return WorkflowSemanticProjectionValues.decodeValues(encodedNodeLabels);
  }

  @Transient
  @KeywordField(name = PROPERTY_KEYS_FIELD)
  @IndexingDependency(
      derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedPropertyKeys")))
  public List<String> getPropertyKeys() {
    return WorkflowSemanticProjectionValues.decodeValues(encodedPropertyKeys);
  }

  @Transient
  @KeywordField(name = PROPERTY_VALUES_FIELD)
  @IndexingDependency(
      derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedPropertyValues")))
  public List<String> getPropertyValues() {
    return WorkflowSemanticProjectionValues.decodeValues(encodedPropertyValues);
  }

  @Transient
  @KeywordField(name = PROPERTY_PAIRS_FIELD)
  @IndexingDependency(
      derivedFrom = @ObjectPath(@PropertyValue(propertyName = "encodedPropertyPairs")))
  public List<String> getPropertyPairs() {
    return encodedPropertyPairs == null || encodedPropertyPairs.isBlank()
        ? List.of()
        : encodedPropertyPairs.lines().filter(value -> !value.isBlank()).toList();
  }
}