001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.nodes.TObjectName; 004import gudusoft.gsqlparser.nodes.TTable; 005import gudusoft.gsqlparser.resolver2.ColumnLevel; 006import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 007import gudusoft.gsqlparser.resolver2.matcher.DefaultNameMatcher; 008import gudusoft.gsqlparser.resolver2.model.ColumnReference; 009import gudusoft.gsqlparser.resolver2.model.ColumnSource; 010import gudusoft.gsqlparser.resolver2.model.ColumnSourceWithReferences; 011 012import java.util.*; 013 014/** 015 * Abstract base class for all namespaces. 016 * Provides common functionality for column resolution and caching. 017 * 018 * <p>Enhanced with reference traceability support: multiple syntactically different 019 * identifiers that refer to the same semantic column can be tracked and traced back.</p> 020 */ 021public abstract class AbstractNamespace implements INamespace { 022 023 /** Associated AST node */ 024 protected final Object node; 025 026 /** Whether this namespace has been validated */ 027 protected boolean validated = false; 028 029 /** Cached column sources (populated during validation) - keyed by normalized name */ 030 protected Map<String, ColumnSource> columnSources = null; 031 032 /** 033 * Enhanced column sources with reference traceability (keyed by normalized name). 034 * When enabled, this map stores ColumnSourceWithReferences that track all original 035 * references to each semantic column. 036 */ 037 protected Map<String, ColumnSourceWithReferences> columnSourcesWithRefs = null; 038 039 /** Whether reference traceability is enabled */ 040 protected boolean referenceTraceabilityEnabled = false; 041 042 /** Name matcher for column name comparisons */ 043 protected final INameMatcher nameMatcher; 044 045 /** 046 * Strategy for handling ambiguous columns. 047 * Defaults to -1, which means use global TBaseType.GUESS_COLUMN_STRATEGY. 048 * Can be overridden per-namespace instance. 049 */ 050 protected int guessColumnStrategy = -1; 051 052 protected AbstractNamespace(Object node, INameMatcher nameMatcher) { 053 this.node = node; 054 this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher(); 055 } 056 057 protected AbstractNamespace(Object node) { 058 this(node, new DefaultNameMatcher()); 059 } 060 061 /** 062 * Set the strategy for handling ambiguous columns. 063 * @param strategy One of TBaseType.GUESS_COLUMN_STRATEGY_* constants, or -1 to use global default 064 */ 065 public void setGuessColumnStrategy(int strategy) { 066 this.guessColumnStrategy = strategy; 067 } 068 069 /** 070 * Get the strategy for handling ambiguous columns. 071 * Returns the instance-level strategy if set, otherwise returns the global default. 072 * @return The strategy constant 073 */ 074 public int getGuessColumnStrategy() { 075 if (guessColumnStrategy >= 0) { 076 return guessColumnStrategy; 077 } 078 return gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY; 079 } 080 081 @Override 082 public Object getNode() { 083 return node; 084 } 085 086 @Override 087 public boolean isValidated() { 088 return validated; 089 } 090 091 @Override 092 public void validate() { 093 if (!validated) { 094 doValidate(); 095 validated = true; 096 } 097 } 098 099 /** 100 * Subclasses override this to perform actual validation logic. 101 */ 102 protected abstract void doValidate(); 103 104 /** 105 * Enable reference traceability for this namespace. 106 * When enabled, all column additions will track original references. 107 */ 108 public void enableReferenceTraceability() { 109 this.referenceTraceabilityEnabled = true; 110 if (columnSourcesWithRefs == null) { 111 columnSourcesWithRefs = new LinkedHashMap<>(); 112 } 113 } 114 115 /** 116 * Check if reference traceability is enabled. 117 * 118 * @return true if traceability is enabled 119 */ 120 public boolean isReferenceTraceabilityEnabled() { 121 return referenceTraceabilityEnabled; 122 } 123 124 /** 125 * Add a column source with reference traceability support. 126 * 127 * <p>This method normalizes the column name and either:</p> 128 * <ul> 129 * <li>Creates a new entry if this is the first reference</li> 130 * <li>Adds a reference to an existing entry if the column already exists</li> 131 * </ul> 132 * 133 * @param columnName original column name (may include quotes) 134 * @param source the column source 135 * @param objectName the original AST node for traceability (may be null) 136 */ 137 protected void addColumnSource(String columnName, ColumnSource source, TObjectName objectName) { 138 if (columnSources == null) { 139 columnSources = new LinkedHashMap<>(); 140 } 141 142 String normalizedKey = nameMatcher.normalize(columnName); 143 144 // Add to basic map if not exists 145 if (!columnSources.containsKey(normalizedKey)) { 146 columnSources.put(normalizedKey, source); 147 } 148 149 // Add to enhanced map with references if traceability is enabled 150 if (referenceTraceabilityEnabled) { 151 if (columnSourcesWithRefs == null) { 152 columnSourcesWithRefs = new LinkedHashMap<>(); 153 } 154 155 ColumnSourceWithReferences enhanced = columnSourcesWithRefs.computeIfAbsent( 156 normalizedKey, 157 k -> new ColumnSourceWithReferences(normalizedKey, source) 158 ); 159 160 if (objectName != null) { 161 enhanced.addReference(new ColumnReference(objectName)); 162 } 163 } 164 } 165 166 /** 167 * Add a column source (backward compatible - no traceability). 168 * 169 * @param columnName original column name 170 * @param source the column source 171 */ 172 protected void addColumnSource(String columnName, ColumnSource source) { 173 addColumnSource(columnName, source, null); 174 } 175 176 /** 177 * Slice S1: matcher-aware {@code containsKey} replacement for column maps 178 * that need vendor-specific identifier rules to govern dedupe / lookup. 179 * 180 * <p>Per-vendor identifier rules differ on whether {@code MyCol} and {@code mycol} 181 * are the same column (BigQuery / MySQL / SQL Server: yes for columns; 182 * Oracle / Postgres unquoted: yes via folding; quoted Oracle / Postgres: no). 183 * A raw {@code map.containsKey(name)} bypasses these rules and produces 184 * duplicate-key drift; a fixed {@code equalsIgnoreCase} loop is wrong for 185 * vendors where columns are case-sensitive (BigQuery tables, Oracle quoted). 186 * 187 * <p>This helper: 188 * <ol> 189 * <li>tries an O(1) normalized-key probe (fast path for vendors that 190 * fold unquoted identifiers to a canonical form, where the stored 191 * raw key is itself the folded form — e.g. Postgres stored 192 * {@code "mycol"} probed by query {@code "MYCOL"} that normalizes 193 * to {@code "mycol"}),</li> 194 * <li>tries an O(1) raw-key probe for the exact-match common case,</li> 195 * <li>falls back to a matcher loop that compares against {@link 196 * ColumnSource#getExposedName()} (when values are {@code ColumnSource}) 197 * so quote state is preserved on quoted-sensitive dialects.</li> 198 * </ol> 199 * 200 * <p><strong>Quote-state preservation (codex round 1 + round 2).</strong> 201 * The map values must hold the original-cased identifier (with quotes). 202 * Storage keys are also raw (= {@code exposedName}) — round 2 caught 203 * that storing under {@code nameMatcher.normalize(name)} can collide 204 * two matcher-distinct identifiers (e.g. Postgres quoted {@code "mycol"} 205 * vs unquoted {@code MYCOL} both normalize to {@code mycol}) into the 206 * same key and lose information. With raw-keyed storage, the normalize 207 * fast-probe only hits when the stored raw key already equals the 208 * normalized form, and in that case {@link 209 * gudusoft.gsqlparser.resolver2.matcher.INameMatcher#matches} agrees; 210 * the matcher loop catches the case-only-different case. 211 */ 212 protected boolean containsColumnByMatcher(Map<String, ?> map, String columnName) { 213 if (map == null || map.isEmpty() || columnName == null) { 214 return false; 215 } 216 String normalizedKey = nameMatcher.normalize(columnName); 217 if (normalizedKey != null && map.containsKey(normalizedKey)) { 218 return true; 219 } 220 if (map.containsKey(columnName)) { 221 return true; 222 } 223 for (Map.Entry<String, ?> entry : map.entrySet()) { 224 String compareName = entry.getKey(); 225 Object value = entry.getValue(); 226 if (value instanceof ColumnSource) { 227 String exposed = ((ColumnSource) value).getExposedName(); 228 if (exposed != null) { 229 compareName = exposed; 230 } 231 } 232 if (compareName != null && nameMatcher.matches(compareName, columnName)) { 233 return true; 234 } 235 } 236 return false; 237 } 238 239 /** 240 * Slice S1: matcher-aware {@code Set#contains} replacement. See 241 * {@link #containsColumnByMatcher(Map, String)} for the rationale. 242 */ 243 protected boolean containsColumnNameByMatcher(Set<String> set, String columnName) { 244 if (set == null || set.isEmpty() || columnName == null) { 245 return false; 246 } 247 if (set.contains(columnName)) { 248 return true; 249 } 250 String normalizedKey = nameMatcher.normalize(columnName); 251 if (normalizedKey != null && set.contains(normalizedKey)) { 252 return true; 253 } 254 for (String existing : set) { 255 if (nameMatcher.matches(existing, columnName)) { 256 return true; 257 } 258 } 259 return false; 260 } 261 262 @Override 263 public ColumnLevel hasColumn(String columnName) { 264 ensureValidated(); 265 266 if (columnSources == null) { 267 return ColumnLevel.NOT_EXISTS; 268 } 269 270 // Use normalized key for O(1) lookup 271 String normalizedKey = nameMatcher.normalize(columnName); 272 if (columnSources.containsKey(normalizedKey)) { 273 return ColumnLevel.EXISTS; 274 } 275 276 // Fallback to linear search for backward compatibility 277 for (String existingCol : columnSources.keySet()) { 278 if (nameMatcher.matches(existingCol, columnName)) { 279 return ColumnLevel.EXISTS; 280 } 281 } 282 283 return ColumnLevel.NOT_EXISTS; 284 } 285 286 @Override 287 public ColumnSource resolveColumn(String columnName) { 288 ensureValidated(); 289 290 if (columnSources == null) { 291 return null; 292 } 293 294 // Use normalized key for O(1) lookup 295 String normalizedKey = nameMatcher.normalize(columnName); 296 ColumnSource source = columnSources.get(normalizedKey); 297 if (source != null) { 298 return source; 299 } 300 301 // Fallback to linear search for backward compatibility 302 for (Map.Entry<String, ColumnSource> entry : columnSources.entrySet()) { 303 if (nameMatcher.matches(entry.getKey(), columnName)) { 304 return entry.getValue(); 305 } 306 } 307 308 return null; 309 } 310 311 @Override 312 public Map<String, ColumnSource> getAllColumnSources() { 313 ensureValidated(); 314 return columnSources != null 315 ? Collections.unmodifiableMap(columnSources) 316 : Collections.emptyMap(); 317 } 318 319 /** 320 * Get all column references for a specific column. 321 * 322 * <p>Requires reference traceability to be enabled.</p> 323 * 324 * @param columnName the column name (normalized or original) 325 * @return list of all references, empty if not found or traceability not enabled 326 */ 327 public List<ColumnReference> getColumnReferences(String columnName) { 328 if (columnSourcesWithRefs == null) { 329 return Collections.emptyList(); 330 } 331 332 String normalizedKey = nameMatcher.normalize(columnName); 333 ColumnSourceWithReferences enhanced = columnSourcesWithRefs.get(normalizedKey); 334 335 return enhanced != null 336 ? enhanced.getAllReferences() 337 : Collections.emptyList(); 338 } 339 340 /** 341 * Get all unique columns with their references. 342 * 343 * <p>Requires reference traceability to be enabled.</p> 344 * 345 * @return collection of enhanced column sources, empty if traceability not enabled 346 */ 347 public Collection<ColumnSourceWithReferences> getAllUniqueColumns() { 348 if (columnSourcesWithRefs == null) { 349 return Collections.emptyList(); 350 } 351 return Collections.unmodifiableCollection(columnSourcesWithRefs.values()); 352 } 353 354 /** 355 * Get enhanced column source with references for a specific column. 356 * 357 * @param columnName the column name 358 * @return enhanced column source, or null if not found 359 */ 360 public ColumnSourceWithReferences getColumnSourceWithReferences(String columnName) { 361 if (columnSourcesWithRefs == null) { 362 return null; 363 } 364 String normalizedKey = nameMatcher.normalize(columnName); 365 return columnSourcesWithRefs.get(normalizedKey); 366 } 367 368 @Override 369 public List<TTable> getAllFinalTables() { 370 TTable finalTable = getFinalTable(); 371 if (finalTable != null) { 372 return Collections.singletonList(finalTable); 373 } 374 return Collections.emptyList(); 375 } 376 377 /** 378 * Ensure this namespace is validated before accessing column info 379 */ 380 protected void ensureValidated() { 381 if (!validated) { 382 validate(); 383 } 384 } 385 386 /** 387 * Get the name matcher used by this namespace. 388 * 389 * @return the name matcher 390 */ 391 public INameMatcher getNameMatcher() { 392 return nameMatcher; 393 } 394 395 @Override 396 public String toString() { 397 return getDisplayName(); 398 } 399}