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.matcher.VendorNameMatcher; 009import gudusoft.gsqlparser.resolver2.model.ColumnReference; 010import gudusoft.gsqlparser.resolver2.model.ColumnSource; 011import gudusoft.gsqlparser.resolver2.model.ColumnSourceWithReferences; 012import gudusoft.gsqlparser.sqlenv.CanonKey; 013import gudusoft.gsqlparser.sqlenv.IdentifierService; 014 015import java.util.*; 016 017/** 018 * Abstract base class for all namespaces. 019 * Provides common functionality for column resolution and caching. 020 * 021 * <p>Enhanced with reference traceability support: multiple syntactically different 022 * identifiers that refer to the same semantic column can be tracked and traced back.</p> 023 */ 024public abstract class AbstractNamespace implements INamespace { 025 026 /** Associated AST node */ 027 protected final Object node; 028 029 /** Whether this namespace has been validated */ 030 protected boolean validated = false; 031 032 /** Cached column sources (populated during validation) - keyed by normalized name */ 033 protected Map<String, ColumnSource> columnSources = null; 034 035 /** 036 * Enhanced column sources with reference traceability (keyed by normalized name). 037 * When enabled, this map stores ColumnSourceWithReferences that track all original 038 * references to each semantic column. 039 */ 040 protected Map<String, ColumnSourceWithReferences> columnSourcesWithRefs = null; 041 042 /** Whether reference traceability is enabled */ 043 protected boolean referenceTraceabilityEnabled = false; 044 045 /** Name matcher for column name comparisons */ 046 protected final INameMatcher nameMatcher; 047 048 /** 049 * Canonical-key index over {@link #columnSources} (Mantis 4684). 050 * 051 * <p>The matcher fallback loops in {@link #hasColumn}/{@link #resolveColumn} 052 * are O(map size) with a full identifier comparison per entry — on SQL Server 053 * each comparison is a {@link java.text.RuleBasedCollator} run. Inferred 054 * columns ({@code TableNamespace.resolveColumn}) grow the map by one per 055 * unresolved reference, so a wide select list makes resolution O(N²) in 056 * collator comparisons (a 2000-column select took ~24s).</p> 057 * 058 * <p>This index maps {@link CanonKey} → first map key carrying that key. 059 * Since P0d.1, {@code IdentifierService.areEqual} IS canonical-key equality 060 * (collation-based dialects use {@link java.text.CollationKey} bytes from the 061 * SAME cached collator), so an index probe is exactly equivalent to the 062 * matcher loop; {@code putIfAbsent} in insertion order preserves the loop's 063 * first-match-wins choice. The index is synced lazily: {@code columnSources} 064 * is append-only in every namespace (no removals exist), so a sync only 065 * indexes the entries added since the last one. Any name outside the 066 * canonical-key domain (multi-segment, malformed quoting) permanently 067 * disables the index for this namespace and the loop takes over.</p> 068 */ 069 private Map<CanonKey, String> canonKeyIndex = null; 070 /** Identity of the map instance the index was built over (subclasses reassign {@link #columnSources}). */ 071 private Map<String, ColumnSource> canonIndexedMap = null; 072 /** How many insertion-order entries of {@link #canonIndexedMap} are indexed. */ 073 private int canonIndexedCount = 0; 074 /** Set when a name refuses canonical-key construction; falls back to the matcher loop. */ 075 private boolean canonIndexUnusable = false; 076 077 /** 078 * Strategy for handling ambiguous columns. 079 * Defaults to -1, which means use global TBaseType.GUESS_COLUMN_STRATEGY. 080 * Can be overridden per-namespace instance. 081 */ 082 protected int guessColumnStrategy = -1; 083 084 protected AbstractNamespace(Object node, INameMatcher nameMatcher) { 085 this.node = node; 086 this.nameMatcher = nameMatcher != null ? nameMatcher : new DefaultNameMatcher(); 087 } 088 089 protected AbstractNamespace(Object node) { 090 this(node, new DefaultNameMatcher()); 091 } 092 093 /** 094 * Set the strategy for handling ambiguous columns. 095 * @param strategy One of TBaseType.GUESS_COLUMN_STRATEGY_* constants, or -1 to use global default 096 */ 097 public void setGuessColumnStrategy(int strategy) { 098 this.guessColumnStrategy = strategy; 099 } 100 101 /** 102 * Get the strategy for handling ambiguous columns. 103 * Returns the instance-level strategy if set, otherwise returns the global default. 104 * @return The strategy constant 105 */ 106 public int getGuessColumnStrategy() { 107 if (guessColumnStrategy >= 0) { 108 return guessColumnStrategy; 109 } 110 return gudusoft.gsqlparser.TBaseType.GUESS_COLUMN_STRATEGY; 111 } 112 113 @Override 114 public Object getNode() { 115 return node; 116 } 117 118 @Override 119 public boolean isValidated() { 120 return validated; 121 } 122 123 @Override 124 public void validate() { 125 if (!validated) { 126 doValidate(); 127 validated = true; 128 } 129 } 130 131 /** 132 * Subclasses override this to perform actual validation logic. 133 */ 134 protected abstract void doValidate(); 135 136 /** 137 * Enable reference traceability for this namespace. 138 * When enabled, all column additions will track original references. 139 */ 140 public void enableReferenceTraceability() { 141 this.referenceTraceabilityEnabled = true; 142 if (columnSourcesWithRefs == null) { 143 columnSourcesWithRefs = new LinkedHashMap<>(); 144 } 145 } 146 147 /** 148 * Check if reference traceability is enabled. 149 * 150 * @return true if traceability is enabled 151 */ 152 public boolean isReferenceTraceabilityEnabled() { 153 return referenceTraceabilityEnabled; 154 } 155 156 /** 157 * Add a column source with reference traceability support. 158 * 159 * <p>This method normalizes the column name and either:</p> 160 * <ul> 161 * <li>Creates a new entry if this is the first reference</li> 162 * <li>Adds a reference to an existing entry if the column already exists</li> 163 * </ul> 164 * 165 * @param columnName original column name (may include quotes) 166 * @param source the column source 167 * @param objectName the original AST node for traceability (may be null) 168 */ 169 protected void addColumnSource(String columnName, ColumnSource source, TObjectName objectName) { 170 if (columnSources == null) { 171 columnSources = new LinkedHashMap<>(); 172 } 173 174 String normalizedKey = nameMatcher.normalize(columnName); 175 176 // Add to basic map if not exists 177 if (!columnSources.containsKey(normalizedKey)) { 178 columnSources.put(normalizedKey, source); 179 noteColumnSourceAdded(normalizedKey); 180 } 181 182 // Add to enhanced map with references if traceability is enabled 183 if (referenceTraceabilityEnabled) { 184 if (columnSourcesWithRefs == null) { 185 columnSourcesWithRefs = new LinkedHashMap<>(); 186 } 187 188 ColumnSourceWithReferences enhanced = columnSourcesWithRefs.computeIfAbsent( 189 normalizedKey, 190 k -> new ColumnSourceWithReferences(normalizedKey, source) 191 ); 192 193 if (objectName != null) { 194 enhanced.addReference(new ColumnReference(objectName)); 195 } 196 } 197 } 198 199 /** 200 * Add a column source (backward compatible - no traceability). 201 * 202 * @param columnName original column name 203 * @param source the column source 204 */ 205 protected void addColumnSource(String columnName, ColumnSource source) { 206 addColumnSource(columnName, source, null); 207 } 208 209 /** 210 * Slice S1: matcher-aware {@code containsKey} replacement for column maps 211 * that need vendor-specific identifier rules to govern dedupe / lookup. 212 * 213 * <p>Per-vendor identifier rules differ on whether {@code MyCol} and {@code mycol} 214 * are the same column (BigQuery / MySQL / SQL Server: yes for columns; 215 * Oracle / Postgres unquoted: yes via folding; quoted Oracle / Postgres: no). 216 * A raw {@code map.containsKey(name)} bypasses these rules and produces 217 * duplicate-key drift; a fixed {@code equalsIgnoreCase} loop is wrong for 218 * vendors where columns are case-sensitive (BigQuery tables, Oracle quoted). 219 * 220 * <p>This helper: 221 * <ol> 222 * <li>tries an O(1) normalized-key probe (fast path for vendors that 223 * fold unquoted identifiers to a canonical form, where the stored 224 * raw key is itself the folded form — e.g. Postgres stored 225 * {@code "mycol"} probed by query {@code "MYCOL"} that normalizes 226 * to {@code "mycol"}),</li> 227 * <li>tries an O(1) raw-key probe for the exact-match common case,</li> 228 * <li>falls back to a matcher loop that compares against {@link 229 * ColumnSource#getExposedName()} (when values are {@code ColumnSource}) 230 * so quote state is preserved on quoted-sensitive dialects.</li> 231 * </ol> 232 * 233 * <p><strong>Quote-state preservation (codex round 1 + round 2).</strong> 234 * The map values must hold the original-cased identifier (with quotes). 235 * Storage keys are also raw (= {@code exposedName}) — round 2 caught 236 * that storing under {@code nameMatcher.normalize(name)} can collide 237 * two matcher-distinct identifiers (e.g. Postgres quoted {@code "mycol"} 238 * vs unquoted {@code MYCOL} both normalize to {@code mycol}) into the 239 * same key and lose information. With raw-keyed storage, the normalize 240 * fast-probe only hits when the stored raw key already equals the 241 * normalized form, and in that case {@link 242 * gudusoft.gsqlparser.resolver2.matcher.INameMatcher#matches} agrees; 243 * the matcher loop catches the case-only-different case. 244 */ 245 protected boolean containsColumnByMatcher(Map<String, ?> map, String columnName) { 246 if (map == null || map.isEmpty() || columnName == null) { 247 return false; 248 } 249 String normalizedKey = nameMatcher.normalize(columnName); 250 if (normalizedKey != null && map.containsKey(normalizedKey)) { 251 return true; 252 } 253 if (map.containsKey(columnName)) { 254 return true; 255 } 256 for (Map.Entry<String, ?> entry : map.entrySet()) { 257 String compareName = entry.getKey(); 258 Object value = entry.getValue(); 259 if (value instanceof ColumnSource) { 260 String exposed = ((ColumnSource) value).getExposedName(); 261 if (exposed != null) { 262 compareName = exposed; 263 } 264 } 265 if (compareName != null && nameMatcher.matches(compareName, columnName)) { 266 return true; 267 } 268 } 269 return false; 270 } 271 272 /** 273 * Slice S1: matcher-aware {@code Set#contains} replacement. See 274 * {@link #containsColumnByMatcher(Map, String)} for the rationale. 275 */ 276 protected boolean containsColumnNameByMatcher(Set<String> set, String columnName) { 277 if (set == null || set.isEmpty() || columnName == null) { 278 return false; 279 } 280 if (set.contains(columnName)) { 281 return true; 282 } 283 String normalizedKey = nameMatcher.normalize(columnName); 284 if (normalizedKey != null && set.contains(normalizedKey)) { 285 return true; 286 } 287 for (String existing : set) { 288 if (nameMatcher.matches(existing, columnName)) { 289 return true; 290 } 291 } 292 return false; 293 } 294 295 @Override 296 public ColumnLevel hasColumn(String columnName) { 297 ensureValidated(); 298 299 if (columnSources == null) { 300 return ColumnLevel.NOT_EXISTS; 301 } 302 303 // Use normalized key for O(1) lookup 304 String normalizedKey = nameMatcher.normalize(columnName); 305 if (columnSources.containsKey(normalizedKey)) { 306 return ColumnLevel.EXISTS; 307 } 308 309 // Matcher-equality fallback (canonical-key indexed — see canonKeyIndex) 310 if (findColumnKeyByMatcher(columnName) != null) { 311 return ColumnLevel.EXISTS; 312 } 313 314 return ColumnLevel.NOT_EXISTS; 315 } 316 317 @Override 318 public ColumnSource resolveColumn(String columnName) { 319 ensureValidated(); 320 321 if (columnSources == null) { 322 return null; 323 } 324 325 // Use normalized key for O(1) lookup 326 String normalizedKey = nameMatcher.normalize(columnName); 327 ColumnSource source = columnSources.get(normalizedKey); 328 if (source != null) { 329 return source; 330 } 331 332 // Matcher-equality fallback (canonical-key indexed — see canonKeyIndex) 333 String matchedKey = findColumnKeyByMatcher(columnName); 334 return matchedKey != null ? columnSources.get(matchedKey) : null; 335 } 336 337 /** 338 * Find the first insertion-order key of {@link #columnSources} that the 339 * {@link #nameMatcher} considers equal to {@code columnName}, or null when 340 * no key matches. 341 * 342 * <p>Equivalent to the historical 343 * {@code for (key : columnSources.keySet()) if (nameMatcher.matches(key, columnName))} 344 * loop, but near-O(1) via the canonical-key index when the matcher is a 345 * {@link VendorNameMatcher} (Mantis 4684 — the loop made wide select lists 346 * quadratic with a collator comparison per entry on SQL Server).</p> 347 */ 348 protected String findColumnKeyByMatcher(String columnName) { 349 if (columnSources == null || columnSources.isEmpty() || columnName == null) { 350 return null; 351 } 352 // Exact class only: a VendorNameMatcher SUBCLASS may override matches(), 353 // and the index would bypass the override (Codex round-1 P2). 354 if (!canonIndexUnusable && nameMatcher.getClass() == VendorNameMatcher.class) { 355 try { 356 syncCanonKeyIndex(); 357 return canonKeyIndex.get(canonKeyOf(columnName)); 358 } catch (RuntimeException e) { 359 // Name outside the canonical-key domain (multi-segment or 360 // malformed quoting): give up on the index for this namespace. 361 canonIndexUnusable = true; 362 canonKeyIndex = null; 363 canonIndexedMap = null; 364 } 365 } 366 for (String existingCol : columnSources.keySet()) { 367 if (nameMatcher.matches(existingCol, columnName)) { 368 return existingCol; 369 } 370 } 371 return null; 372 } 373 374 /** Canonical key of one name under the matcher's vendor + object type. */ 375 private CanonKey canonKeyOf(String name) { 376 VendorNameMatcher vm = (VendorNameMatcher) nameMatcher; 377 return IdentifierService.canonKeyStatic(vm.getVendor(), vm.getDefaultObjectType(), name); 378 } 379 380 /** 381 * Incremental index maintenance for the append-one case (Codex round-1 P1): 382 * without this, every {@link #syncCanonKeyIndex} after a single-key insert 383 * re-walks the already-indexed prefix just to skip it — O(N²) iterator 384 * steps on the wide-select workload even with O(N) canonical keys. Call 385 * after putting {@code key} into {@link #columnSources}. A batch of puts 386 * without notes is still safe: the size check fails and the next sync 387 * catches up with the (now rare) prefix walk. 388 */ 389 protected void noteColumnSourceAdded(String key) { 390 if (canonIndexUnusable || canonKeyIndex == null || canonIndexedMap != columnSources) { 391 return; // index not built yet or map replaced; next sync handles it 392 } 393 if (columnSources.size() != canonIndexedCount + 1) { 394 return; // overwrite (size unchanged) or un-noted batch; sync catches up 395 } 396 try { 397 canonKeyIndex.putIfAbsent(canonKeyOf(key), key); 398 canonIndexedCount++; 399 } catch (RuntimeException e) { 400 canonIndexUnusable = true; 401 canonKeyIndex = null; 402 canonIndexedMap = null; 403 } 404 } 405 406 /** 407 * Bring {@link #canonKeyIndex} up to date with {@link #columnSources}. 408 * The map is append-only between syncs, so only the entries beyond 409 * {@link #canonIndexedCount} need indexing; a reassigned map instance or a 410 * shrunken size (defensive — no remover exists today) rebuilds from scratch. 411 */ 412 private void syncCanonKeyIndex() { 413 if (canonKeyIndex == null || canonIndexedMap != columnSources 414 || columnSources.size() < canonIndexedCount) { 415 canonKeyIndex = new HashMap<>(); 416 canonIndexedMap = columnSources; 417 canonIndexedCount = 0; 418 } 419 int size = columnSources.size(); 420 if (canonIndexedCount == size) { 421 return; 422 } 423 int i = 0; 424 for (String key : columnSources.keySet()) { 425 if (i++ < canonIndexedCount) { 426 continue; 427 } 428 // First entry with a given canonical key wins, matching the scan. 429 canonKeyIndex.putIfAbsent(canonKeyOf(key), key); 430 } 431 canonIndexedCount = size; 432 } 433 434 @Override 435 public Map<String, ColumnSource> getAllColumnSources() { 436 ensureValidated(); 437 return columnSources != null 438 ? Collections.unmodifiableMap(columnSources) 439 : Collections.emptyMap(); 440 } 441 442 /** 443 * Get all column references for a specific column. 444 * 445 * <p>Requires reference traceability to be enabled.</p> 446 * 447 * @param columnName the column name (normalized or original) 448 * @return list of all references, empty if not found or traceability not enabled 449 */ 450 public List<ColumnReference> getColumnReferences(String columnName) { 451 if (columnSourcesWithRefs == null) { 452 return Collections.emptyList(); 453 } 454 455 String normalizedKey = nameMatcher.normalize(columnName); 456 ColumnSourceWithReferences enhanced = columnSourcesWithRefs.get(normalizedKey); 457 458 return enhanced != null 459 ? enhanced.getAllReferences() 460 : Collections.emptyList(); 461 } 462 463 /** 464 * Get all unique columns with their references. 465 * 466 * <p>Requires reference traceability to be enabled.</p> 467 * 468 * @return collection of enhanced column sources, empty if traceability not enabled 469 */ 470 public Collection<ColumnSourceWithReferences> getAllUniqueColumns() { 471 if (columnSourcesWithRefs == null) { 472 return Collections.emptyList(); 473 } 474 return Collections.unmodifiableCollection(columnSourcesWithRefs.values()); 475 } 476 477 /** 478 * Get enhanced column source with references for a specific column. 479 * 480 * @param columnName the column name 481 * @return enhanced column source, or null if not found 482 */ 483 public ColumnSourceWithReferences getColumnSourceWithReferences(String columnName) { 484 if (columnSourcesWithRefs == null) { 485 return null; 486 } 487 String normalizedKey = nameMatcher.normalize(columnName); 488 return columnSourcesWithRefs.get(normalizedKey); 489 } 490 491 @Override 492 public List<TTable> getAllFinalTables() { 493 TTable finalTable = getFinalTable(); 494 if (finalTable != null) { 495 return Collections.singletonList(finalTable); 496 } 497 return Collections.emptyList(); 498 } 499 500 /** 501 * Ensure this namespace is validated before accessing column info 502 */ 503 protected void ensureValidated() { 504 if (!validated) { 505 validate(); 506 } 507 } 508 509 /** 510 * Get the name matcher used by this namespace. 511 * 512 * @return the name matcher 513 */ 514 public INameMatcher getNameMatcher() { 515 return nameMatcher; 516 } 517 518 @Override 519 public String toString() { 520 return getDisplayName(); 521 } 522}