001package gudusoft.gsqlparser.lineage2.contract;
002
003import java.nio.charset.StandardCharsets;
004import java.security.MessageDigest;
005import java.security.NoSuchAlgorithmException;
006import java.util.Locale;
007
008/**
009 * Frozen identity-layer hash computation per lineage2-contract.md §3 (normative source:
010 * CONTRACT_lineage_role_and_hash_identity.md rev 3, pinned gudu-sqlflow commit
011 * b1d5d893149510dc63de400bc2a6aa5d47d35a4a).
012 *
013 * <p>All hashes: SHA-256 over UTF-8 of pipe-joined normalized fields; output is 64
014 * lowercase hex chars. Normalization rules (rev 3): trim every field; null and empty
015 * both contribute an empty string ("empty→empty, not NULL"); field positions are
016 * preserved when values are empty; names are lowercased, roles/record types uppercased;
017 * {@code |} is the frozen P0 separator.
018 *
019 * <p>Byte-identity against
020 * {@code gudu-sqlflow/tools/lineage-contract-fixtures/hash_identity_fixtures.json}
021 * is asserted by {@code Lineage2ContractConformanceTest} on every build.
022 *
023 * <p>Internal API: not part of the public gsqlparser surface.
024 */
025public final class GuidEncoder {
026
027    private GuidEncoder() {
028    }
029
030    /**
031     * Canonical edge identity (contract §3.4): workspace + source endpoint + target
032     * endpoint + role. Excludes record_type, procedure context, statement index and
033     * transformation text by design.
034     */
035    public static String factGuid(String workspaceId,
036                                  String sourceDatabase, String sourceSchema,
037                                  String sourceObjectName, String sourceColumnName,
038                                  String targetDatabase, String targetSchema,
039                                  String targetObjectName, String targetColumnName,
040                                  String lineageRole) {
041        return sha256Hex(joinPipe(
042                lowerTrim(workspaceId),
043                lowerTrim(sourceDatabase), lowerTrim(sourceSchema),
044                lowerTrim(sourceObjectName), lowerTrim(sourceColumnName),
045                lowerTrim(targetDatabase), lowerTrim(targetSchema),
046                lowerTrim(targetObjectName), lowerTrim(targetColumnName),
047                upperTrim(lineageRole)));
048    }
049
050    /**
051     * transformation_hash / predicate_hash / expression_hash component: sha256 of the
052     * trimmed text, or the empty string (not a hash) when the text is null/empty.
053     */
054    public static String textHash(String text) {
055        String t = trim(text);
056        return t.isEmpty() ? "" : sha256Hex(t);
057    }
058
059    /** Semantic/effect signature under one fact (contract §4.2). */
060    public static String variantGuid(String workspaceId, String factGuid, String lineageRole,
061                                     String transformationHash, String predicateHash,
062                                     String queryFingerprint) {
063        return sha256Hex(joinPipe(
064                lowerTrim(workspaceId), trim(factGuid), upperTrim(lineageRole),
065                trim(transformationHash), trim(predicateHash), trim(queryFingerprint)));
066    }
067
068    /** Evidence instance of a variant (contract §5.2). */
069    public static String occurrenceGuid(String workspaceId, String factGuid, String variantGuid,
070                                        String recordType, String procedureName, String queryId,
071                                        String queryFingerprint, String inputUnitId,
072                                        int statementIndex) {
073        return sha256Hex(joinPipe(
074                lowerTrim(workspaceId), trim(factGuid), trim(variantGuid),
075                upperTrim(recordType), lowerTrim(procedureName), trim(queryId),
076                trim(queryFingerprint), trim(inputUnitId),
077                Integer.toString(statementIndex)));
078    }
079
080    /** Mutable-content hash for INCOMING occurrences (contract §7.2). */
081    public static String incomingDeltaHash(String predicateText, String transformationText,
082                                           String queryFingerprint, String recordStatus) {
083        return sha256Hex(joinPipe(
084                trim(predicateText), trim(transformationText),
085                queryFingerprint == null ? "" : queryFingerprint,
086                upperTrim(recordStatus)));
087    }
088
089    /** Mutable-content hash for OUTGOING occurrences (contract §7.3). */
090    public static String outgoingDeltaHash(String queryFingerprint, Long executionCount,
091                                           String firstExecutionAtIso, String lastExecutionAtIso,
092                                           String recordStatus) {
093        return sha256Hex(joinPipe(
094                queryFingerprint == null ? "" : queryFingerprint,
095                executionCount == null ? "" : Long.toString(executionCount),
096                trim(firstExecutionAtIso), trim(lastExecutionAtIso),
097                upperTrim(recordStatus)));
098    }
099
100    /** Statement-local transform graph node identity (contract §6.2). */
101    public static String transformNodeGuid(String workspaceId, String occurrenceGuid,
102                                           int statementIndex, String localScopeId,
103                                           String localNodeId, String nodeKind) {
104        return sha256Hex(joinPipe(
105                lowerTrim(workspaceId), occurrenceGuid, Integer.toString(statementIndex),
106                lowerTrim(localScopeId), lowerTrim(localNodeId), upperTrim(nodeKind)));
107    }
108
109    /** Statement-local transform graph step identity (contract §6.3). */
110    public static String transformStepGuid(String workspaceId, String occurrenceGuid,
111                                           int statementIndex, String localScopeId,
112                                           int stepIndex, String operationType,
113                                           String targetNodeGuid, String expressionHash) {
114        return sha256Hex(joinPipe(
115                lowerTrim(workspaceId), occurrenceGuid, Integer.toString(statementIndex),
116                lowerTrim(localScopeId), Integer.toString(stepIndex), upperTrim(operationType),
117                targetNodeGuid, trim(expressionHash)));
118    }
119
120    /** SHA-256 of the UTF-8 bytes, 64 lowercase hex chars. */
121    public static String sha256Hex(String text) {
122        try {
123            MessageDigest md = MessageDigest.getInstance("SHA-256");
124            byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8));
125            StringBuilder sb = new StringBuilder(digest.length * 2);
126            for (byte b : digest) {
127                sb.append(Character.forDigit((b >> 4) & 0xF, 16));
128                sb.append(Character.forDigit(b & 0xF, 16));
129            }
130            return sb.toString();
131        } catch (NoSuchAlgorithmException e) {
132            throw new IllegalStateException("SHA-256 must be available", e);
133        }
134    }
135
136    private static String joinPipe(String... fields) {
137        StringBuilder sb = new StringBuilder();
138        for (int i = 0; i < fields.length; i++) {
139            if (i > 0) {
140                sb.append('|');
141            }
142            sb.append(fields[i]);
143        }
144        return sb.toString();
145    }
146
147    private static String trim(String s) {
148        return s == null ? "" : s.trim();
149    }
150
151    private static String lowerTrim(String s) {
152        return trim(s).toLowerCase(Locale.ROOT);
153    }
154
155    private static String upperTrim(String s) {
156        return trim(s).toUpperCase(Locale.ROOT);
157    }
158}