001package gudusoft.gsqlparser.ir.builder.postgresql;
002
003import gudusoft.gsqlparser.ir.bound.*;
004import gudusoft.gsqlparser.ir.builder.common.AbstractRoutineRefResolver;
005import gudusoft.gsqlparser.ir.common.Evidence;
006import gudusoft.gsqlparser.ir.common.EvidenceKind;
007
008import java.util.*;
009
010/**
011 * PostgreSQL routine reference resolver with search_path support.
012 * <p>
013 * Resolution tiers:
014 * <ol>
015 *   <li>Exact schema.name match</li>
016 *   <li>Walk search_path for unqualified names</li>
017 *   <li>Same-schema preference for intra-file calls</li>
018 *   <li>pg_catalog always implicit last in search_path</li>
019 *   <li>Ambiguity when multiple search-path matches with equal strength</li>
020 * </ol>
021 */
022public final class PostgresqlRoutineRefResolver extends AbstractRoutineRefResolver {
023
024    private static final String[] KIND_CODES = {"F", "P", "T", "A", "NP", "NF"};
025
026    private final List<String> searchPath;
027
028    public PostgresqlRoutineRefResolver(List<String> searchPath) {
029        this.searchPath = searchPath != null ? searchPath
030                : Arrays.asList("public");
031    }
032
033    @Override
034    protected BoundRoutineRef tryResolve(
035            BoundRoutineRef ref,
036            BoundProgram program,
037            Map<String, BoundRoutineSymbol> upperIndex,
038            Map<String, List<BoundRoutineSymbol>> nameIndex) {
039
040        List<String> parts = ref.getNameParts();
041        if (parts.isEmpty()) return null;
042
043        Integer argCountProp = ref.getProperty("argCount");
044        int argCount = argCountProp != null ? argCountProp : ref.getArguments().size();
045        String simpleName = parts.get(parts.size() - 1).toUpperCase();
046
047        // Tier 1: Qualified schema.name match
048        if (parts.size() >= 2) {
049            String schemaName = parts.get(parts.size() - 2).toUpperCase();
050            BoundRoutineSymbol match = tryKindVariants(upperIndex,
051                    schemaName + "." + simpleName, argCount, KIND_CODES);
052            if (match != null) {
053                return ref.withResolvedRoutine(match, EBindingStatus.EXACT, null,
054                        new Evidence(EvidenceKind.STATIC_RESOLVED,
055                                "Tier1: qualified schema.name match"));
056            }
057        }
058
059        // Tier 2: Walk search_path for unqualified names
060        if (parts.size() == 1) {
061            List<BoundRoutineSymbol> pathMatches = new ArrayList<BoundRoutineSymbol>();
062
063            for (String schema : searchPath) {
064                BoundRoutineSymbol match = tryKindVariants(upperIndex,
065                        schema.toUpperCase() + "." + simpleName, argCount, KIND_CODES);
066                if (match != null) {
067                    pathMatches.add(match);
068                }
069            }
070
071            // Also try without schema (for routines declared without schema)
072            BoundRoutineSymbol noSchemaMatch = tryKindVariants(upperIndex,
073                    simpleName, argCount, KIND_CODES);
074            if (noSchemaMatch != null) {
075                pathMatches.add(noSchemaMatch);
076            }
077
078            if (pathMatches.size() == 1) {
079                return ref.withResolvedRoutine(pathMatches.get(0), EBindingStatus.EXACT, null,
080                        new Evidence(EvidenceKind.STATIC_RESOLVED,
081                                "Tier2: search_path resolution"));
082            }
083            if (pathMatches.size() > 1) {
084                return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS,
085                        Collections.unmodifiableList(pathMatches),
086                        new Evidence(EvidenceKind.AMBIGUOUS_NAME,
087                                "Tier2: " + pathMatches.size() + " candidates in search_path"));
088            }
089        }
090
091        // Tier 3: Name + argCount match (any schema)
092        List<BoundRoutineSymbol> exactMatches = findByNameAndArgCount(nameIndex, simpleName, argCount);
093        if (exactMatches.size() == 1) {
094            return ref.withResolvedRoutine(exactMatches.get(0), EBindingStatus.EXACT, null,
095                    new Evidence(EvidenceKind.STATIC_RESOLVED,
096                            "Tier3: name+argCount single match"));
097        }
098        if (exactMatches.size() > 1) {
099            return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS,
100                    Collections.unmodifiableList(exactMatches),
101                    new Evidence(EvidenceKind.AMBIGUOUS_NAME,
102                            "Tier3: " + exactMatches.size() + " candidates"));
103        }
104
105        // Tier 3b: Default parameter tolerance
106        BoundRoutineSymbol bestDefault = findBestDefaultParamMatch(nameIndex, simpleName, argCount);
107        if (bestDefault != null) {
108            return ref.withResolvedRoutine(bestDefault, EBindingStatus.EXACT, null,
109                    new Evidence(EvidenceKind.STATIC_RESOLVED,
110                            "Tier3b: name match with default params"));
111        }
112
113        return null;
114    }
115
116    @Override
117    protected void classifyExternalIfKnown(BoundRoutineRef ref) {
118        String name = ref.getOriginalText();
119        if (!PostgresqlExternalDepClassifier.isExternal(name)) return;
120        ref.setProperty("externalDependency", true);
121        ref.setProperty("externalType", PostgresqlExternalDepClassifier.classify(name));
122        ref.setProperty("externalName", name);
123    }
124}