001package gudusoft.gsqlparser.lineage2.contract; 002 003/** 004 * FINGERPRINT-V1 — query fingerprint participating in variant/occurrence identity, 005 * per lineage2-contract.md §3 [D-10] (normative source: 006 * CONTRACT_lineage_role_and_hash_identity.md rev 3, pinned commit b1d5d893). 007 * 008 * <p>This algorithm is promised stable across engine versions; any change requires a 009 * new name (FINGERPRINT-V2) and a contract major bump. Byte-identity against 010 * {@code gudu-sqlflow/tools/lineage-contract-fixtures/hash_identity_fixtures.json} 011 * is asserted by {@code Lineage2ContractConformanceTest}. 012 * 013 * <p>Algorithm (frozen): 014 * <ol> 015 * <li>input = exact statement text sliced from the artifact original (UTF-8);</li> 016 * <li>strip comments: {@code --} to end-of-line, {@code /* *}{@code /} with T-SQL 017 * nesting; comment markers inside {@code '...'} string literals and 018 * {@code [...]} / {@code "..."} quoted identifiers are not comment starts;</li> 019 * <li>string literals and quoted identifiers preserved verbatim (including case); 020 * all other text lowercased;</li> 021 * <li>whitespace runs collapse to one space; trim;</li> 022 * <li>SHA-256 over UTF-8, 64 lowercase hex chars.</li> 023 * </ol> 024 * 025 * <p>Internal API: not part of the public gsqlparser surface. 026 */ 027public final class FingerprintV1 { 028 029 private FingerprintV1() { 030 } 031 032 /** FINGERPRINT-V1 of the statement text: sha256 hex of {@link #normalize(String)}. */ 033 public static String fingerprint(String statementText) { 034 return GuidEncoder.sha256Hex(normalize(statementText)); 035 } 036 037 /** 038 * The normalization family shared with transformation normalizedExpression 039 * (contract §4.2): comment strip + lowercase outside quotes + whitespace collapse. 040 * Null input normalizes to the empty string. 041 */ 042 public static String normalize(String text) { 043 if (text == null) { 044 return ""; 045 } 046 StringBuilder out = new StringBuilder(text.length()); 047 int i = 0; 048 int n = text.length(); 049 while (i < n) { 050 char c = text.charAt(i); 051 if (c == '\'') { 052 // string literal; '' is an escaped quote, content preserved verbatim 053 i = appendQuoted(text, i, '\'', out); 054 } else if (c == '[') { 055 // T-SQL bracket identifier; ]] is an escaped close bracket 056 i = appendQuoted(text, i, ']', out); 057 } else if (c == '"') { 058 // quoted identifier; "" is an escaped quote 059 i = appendQuoted(text, i, '"', out); 060 } else if (c == '-' && i + 1 < n && text.charAt(i + 1) == '-') { 061 int j = text.indexOf('\n', i); 062 i = (j < 0) ? n : j; 063 } else if (c == '/' && i + 1 < n && text.charAt(i + 1) == '*') { 064 int depth = 1; 065 int j = i + 2; 066 while (j < n && depth > 0) { 067 if (text.charAt(j) == '/' && j + 1 < n && text.charAt(j + 1) == '*') { 068 depth++; 069 j += 2; 070 } else if (text.charAt(j) == '*' && j + 1 < n && text.charAt(j + 1) == '/') { 071 depth--; 072 j += 2; 073 } else { 074 j++; 075 } 076 } 077 i = j; 078 } else { 079 out.append(Character.toLowerCase(c)); 080 i++; 081 } 082 } 083 return out.toString().replaceAll("\\s+", " ").trim(); 084 } 085 086 /** 087 * Appends the quoted region starting at {@code start} (whose opening delimiter is 088 * {@code text.charAt(start)}) verbatim, treating a doubled {@code closer} as an 089 * escape. Returns the index just past the closing delimiter (or end of text when 090 * unterminated). 091 */ 092 private static int appendQuoted(String text, int start, char closer, StringBuilder out) { 093 int n = text.length(); 094 int j = start + 1; 095 while (j < n) { 096 if (text.charAt(j) == closer && j + 1 < n && text.charAt(j + 1) == closer) { 097 j += 2; 098 continue; 099 } 100 if (text.charAt(j) == closer) { 101 break; 102 } 103 j++; 104 } 105 int end = Math.min(j + 1, n); 106 out.append(text, start, end); 107 return end; 108 } 109}