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}