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}