001package gudusoft.gsqlparser.catalog.runtime; 002 003import java.util.ArrayList; 004import java.util.LinkedHashMap; 005import java.util.List; 006import java.util.Map; 007import java.util.Optional; 008 009/** 010 * Default {@link CatalogOverlay} implementation. Holds DDL discovered mid-batch, temp 011 * tables, and user-driven additions. Lookups follow the same precedence the resolver 012 * uses (overlay first, snapshot second) and use the same {@link CatalogIdentifierPolicy} 013 * key + areEqual fallback as {@link InMemoryCatalogSnapshot}. 014 * 015 * <p>Plan §7.2 / §10.5. Per-parse-batch lifecycle managed by the caller via {@link #clear()}; 016 * {@code provider.refresh(...)} never disturbs overlay state.</p> 017 * 018 * <p>This implementation is not thread-safe; concurrent parsers should use one overlay 019 * per analysis run (the standard pattern).</p> 020 */ 021public final class InMemoryCatalogOverlay implements CatalogOverlay { 022 023 private final Map<CatalogObjectKind, Map<String, CatalogEntry>> byKey = 024 new LinkedHashMap<CatalogObjectKind, Map<String, CatalogEntry>>(); 025 private final Map<CatalogObjectKind, List<CatalogEntry>> byKind = 026 new LinkedHashMap<CatalogObjectKind, List<CatalogEntry>>(); 027 028 public InMemoryCatalogOverlay() { 029 } 030 031 @Override 032 public void put(CatalogEntry entry) { 033 if (entry == null) { 034 throw new IllegalArgumentException("CatalogOverlay.put: entry may not be null"); 035 } 036 CatalogObjectKind kind = entry.kind(); 037 String newKey = CatalogIdentifierPolicy.keyForMap(entry.name()); 038 Map<String, CatalogEntry> bucket = 039 byKey.computeIfAbsent(kind, k -> new LinkedHashMap<String, CatalogEntry>()); 040 // For COLLATION_BASED dialects (e.g., MSSQL with case-insensitive collation), 041 // two areEqual names can have different keyForMap forms. Walk the list-shaped 042 // mirror first to find any prior entry for the same logical name, drop its 043 // (possibly different) keyForMap, and then write the new key. This keeps 044 // find()'s fast path consistent with the slow fallback under reinsert. 045 List<CatalogEntry> list = byKind.computeIfAbsent(kind, k -> new ArrayList<CatalogEntry>()); 046 for (int i = 0; i < list.size(); i++) { 047 CatalogEntry existing = list.get(i); 048 if (CatalogIdentifierPolicy.areEqual(existing.name(), entry.name())) { 049 String existingKey = CatalogIdentifierPolicy.keyForMap(existing.name()); 050 if (!existingKey.equals(newKey)) { 051 bucket.remove(existingKey); 052 } 053 bucket.put(newKey, entry); 054 list.set(i, entry); 055 return; 056 } 057 } 058 bucket.put(newKey, entry); 059 list.add(entry); 060 } 061 062 @Override 063 public Optional<CatalogEntry> find(CatalogQualifiedName name, CatalogObjectKind kind) { 064 if (name == null || kind == null) { 065 return Optional.empty(); 066 } 067 Map<String, CatalogEntry> bucket = byKey.get(kind); 068 if (bucket != null) { 069 CatalogEntry direct = bucket.get(CatalogIdentifierPolicy.keyForMap(name)); 070 if (direct != null) { 071 return Optional.of(direct); 072 } 073 } 074 // Linear fallback only matters for dialects whose normalize/compare semantics 075 // can't be expressed by a single composite key (MSSQL/Azure with case-insensitive 076 // collation, MySQL lower_case_table_names=2). For every other vendor a key miss 077 // really means "no entry" — skip the scan to keep overlay lookups O(1). 078 if (NeedsAreEqualFallback.forVendor(name.vendor())) { 079 List<CatalogEntry> list = byKind.get(kind); 080 if (list != null) { 081 for (CatalogEntry entry : list) { 082 if (CatalogIdentifierPolicy.areEqual(entry.name(), name)) { 083 return Optional.of(entry); 084 } 085 } 086 } 087 } 088 return Optional.empty(); 089 } 090 091 @Override 092 public void clear() { 093 byKey.clear(); 094 byKind.clear(); 095 } 096 097 /** Total entry count. */ 098 public int size() { 099 int n = 0; 100 for (List<CatalogEntry> list : byKind.values()) n += list.size(); 101 return n; 102 } 103 104 /** Whether the overlay holds any entries. */ 105 public boolean isEmpty() { 106 return size() == 0; 107 } 108}