001package gudusoft.gsqlparser.ir.builder.mssql; 002 003import gudusoft.gsqlparser.ir.bound.*; 004import gudusoft.gsqlparser.ir.common.*; 005 006import java.util.*; 007 008/** 009 * Resolves T-SQL {@link BoundRoutineRef} instances against the full 010 * {@link BoundProgram#getRoutineIndex()}. 011 * <p> 012 * Uses a four-tier candidate scoring strategy: 013 * <ol> 014 * <li>Tier 1: Qualified name exact match ({@code schema.name/KIND(argCount)})</li> 015 * <li>Tier 2: Unqualified name + default schema ({@code dbo}) inference</li> 016 * <li>Tier 3: Name + argCount → single candidate (with default param tolerance)</li> 017 * <li>Tier 4: Multiple candidates → AMBIGUOUS</li> 018 * </ol> 019 * <p> 020 * Refs marked with {@code externalDependency=true} are skipped. 021 * <p> 022 * This resolver is designed to run AFTER merging partial BoundPrograms 023 * from multiple scripts, so that the routineIndex is complete. 024 */ 025public final class MssqlRoutineRefResolver { 026 027 private MssqlRoutineRefResolver() {} 028 029 /** KIND codes to try in order. */ 030 private static final String[] KIND_CODES = {"P", "SF", "IF", "TF", "F", "T", "TR", "A"}; 031 032 /** 033 * Resolves all routine references in the program. 034 * Modifies the program's routineRefs list in place (via replaceRoutineRefs). 035 */ 036 public static void resolve(BoundProgram program) { 037 if (program == null) return; 038 039 // Build case-insensitive lookup: uppercase routineId → symbol 040 Map<String, BoundRoutineSymbol> upperIndex = new LinkedHashMap<String, BoundRoutineSymbol>(); 041 for (Map.Entry<String, BoundRoutineSymbol> entry : program.getRoutineIndex().entrySet()) { 042 upperIndex.put(entry.getKey().toUpperCase(), entry.getValue()); 043 } 044 045 // Build name-based index: uppercase name → list of symbols 046 Map<String, List<BoundRoutineSymbol>> nameIndex = 047 new LinkedHashMap<String, List<BoundRoutineSymbol>>(); 048 for (BoundRoutineSymbol sym : program.getRoutineIndex().values()) { 049 String nameKey = sym.getRoutineName().toUpperCase(); 050 List<BoundRoutineSymbol> list = nameIndex.get(nameKey); 051 if (list == null) { 052 list = new ArrayList<BoundRoutineSymbol>(); 053 nameIndex.put(nameKey, list); 054 } 055 list.add(sym); 056 } 057 058 List<BoundRoutineRef> resolved = new ArrayList<BoundRoutineRef>(); 059 060 for (BoundRoutineRef ref : program.getAllRoutineRefs()) { 061 // Skip already resolved or external deps 062 if (ref.getBindingStatus() == EBindingStatus.EXACT) { 063 resolved.add(ref); 064 continue; 065 } 066 if (Boolean.TRUE.equals(ref.getProperty("externalDependency"))) { 067 resolved.add(ref); 068 continue; 069 } 070 071 BoundRoutineRef resolvedRef = resolveRef(ref, upperIndex, nameIndex); 072 resolved.add(resolvedRef); 073 } 074 075 program.replaceRoutineRefs(resolved); 076 } 077 078 private static BoundRoutineRef resolveRef( 079 BoundRoutineRef ref, 080 Map<String, BoundRoutineSymbol> upperIndex, 081 Map<String, List<BoundRoutineSymbol>> nameIndex) { 082 083 String rawText = ref.getOriginalText(); 084 MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawText); 085 String objectName = normalized.getObject().toUpperCase(); 086 String schemaName = normalized.getSchema(); 087 Integer argCount = ref.getProperty("argCount"); 088 int args = argCount != null ? argCount : 0; 089 090 // Tier 1: Qualified exact match (schema.name/KIND(argCount)) 091 if (schemaName != null && !schemaName.isEmpty()) { 092 String schemaUpper = schemaName.toUpperCase(); 093 for (String kind : KIND_CODES) { 094 String candidateId = (schemaUpper + "." + objectName + "/" + kind + "(" + args + ")").toUpperCase(); 095 BoundRoutineSymbol found = upperIndex.get(candidateId); 096 if (found != null) { 097 return ref.withResolvedRoutine(found, EBindingStatus.EXACT, null, 098 new Evidence(EvidenceKind.STATIC_RESOLVED, 099 "Tier1: qualified match " + found.getRoutineId())); 100 } 101 } 102 // Try with default param tolerance 103 for (String kind : KIND_CODES) { 104 for (Map.Entry<String, BoundRoutineSymbol> entry : upperIndex.entrySet()) { 105 BoundRoutineSymbol sym = entry.getValue(); 106 if (sym.getRoutineName().toUpperCase().equals(objectName) 107 && sym.getPackageName() != null 108 && sym.getPackageName().toUpperCase().equals(schemaUpper.toUpperCase()) 109 && sym.getParameters().size() >= args) { 110 return ref.withResolvedRoutine(sym, EBindingStatus.EXACT, null, 111 new Evidence(EvidenceKind.STATIC_RESOLVED, 112 "Tier1: qualified match with default params " + sym.getRoutineId())); 113 } 114 } 115 } 116 } 117 118 // Tier 2: Unqualified name + default schema 'dbo' 119 if (schemaName == null || schemaName.isEmpty()) { 120 for (String kind : KIND_CODES) { 121 String candidateId = ("DBO." + objectName + "/" + kind + "(" + args + ")").toUpperCase(); 122 BoundRoutineSymbol found = upperIndex.get(candidateId); 123 if (found != null) { 124 return ref.withResolvedRoutine(found, EBindingStatus.EXACT, null, 125 new Evidence(EvidenceKind.STATIC_RESOLVED, 126 "Tier2: unqualified + default schema dbo")); 127 } 128 } 129 } 130 131 // Tier 3: Name + argCount across all schemas 132 List<BoundRoutineSymbol> candidates = nameIndex.get(objectName); 133 if (candidates != null && !candidates.isEmpty()) { 134 // Exact arg count match 135 List<BoundRoutineSymbol> exactArgMatch = new ArrayList<BoundRoutineSymbol>(); 136 for (BoundRoutineSymbol sym : candidates) { 137 if (sym.getParameters().size() == args) { 138 exactArgMatch.add(sym); 139 } 140 } 141 if (exactArgMatch.size() == 1) { 142 return ref.withResolvedRoutine(exactArgMatch.get(0), EBindingStatus.EXACT, null, 143 new Evidence(EvidenceKind.STATIC_RESOLVED, 144 "Tier3: name+argCount single match")); 145 } 146 147 // Tier 3b: Default parameter tolerance (paramCount > argCount) 148 if (exactArgMatch.isEmpty()) { 149 List<BoundRoutineSymbol> tolerantMatch = new ArrayList<BoundRoutineSymbol>(); 150 for (BoundRoutineSymbol sym : candidates) { 151 if (sym.getParameters().size() >= args) { 152 tolerantMatch.add(sym); 153 } 154 } 155 if (tolerantMatch.size() == 1) { 156 return ref.withResolvedRoutine(tolerantMatch.get(0), EBindingStatus.EXACT, null, 157 new Evidence(EvidenceKind.STATIC_RESOLVED, 158 "Tier3b: name match with default params")); 159 } 160 } 161 162 // Tier 4: Multiple candidates → AMBIGUOUS 163 if (exactArgMatch.size() > 1) { 164 StringBuilder sb = new StringBuilder("Tier4: ambiguous, "); 165 sb.append(exactArgMatch.size()).append(" candidates: ["); 166 for (int i = 0; i < exactArgMatch.size(); i++) { 167 if (i > 0) sb.append(", "); 168 sb.append(exactArgMatch.get(i).getRoutineId()); 169 } 170 sb.append("]"); 171 return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS, exactArgMatch, 172 new Evidence(EvidenceKind.AMBIGUOUS_NAME, sb.toString())); 173 } 174 175 // Single candidate by name alone (any arg count) 176 if (candidates.size() == 1) { 177 return ref.withResolvedRoutine(candidates.get(0), EBindingStatus.EXACT, null, 178 new Evidence(EvidenceKind.HEURISTIC_MATCH, 179 "Tier3: single name match (argCount relaxed)")); 180 } 181 182 // Multiple by name → ambiguous 183 if (candidates.size() > 1) { 184 StringBuilder sb = new StringBuilder("Tier4: ambiguous by name, "); 185 sb.append(candidates.size()).append(" candidates"); 186 return ref.withResolvedRoutine(null, EBindingStatus.AMBIGUOUS, candidates, 187 new Evidence(EvidenceKind.AMBIGUOUS_NAME, sb.toString())); 188 } 189 } 190 191 // No match → UNRESOLVED_SOFT 192 return ref; 193 } 194}