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}