001package gudusoft.gsqlparser.catalog.runtime; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.catalog.diagnostic.CatalogDiagnostic; 005 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.LinkedHashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Optional; 012 013/** 014 * In-memory {@link CatalogSnapshot} backed by {@code Map<String, CatalogEntry>}s keyed via 015 * {@link CatalogIdentifierPolicy#keyForMap(CatalogQualifiedName)}. 016 * 017 * <p>Plan §6 / §7.2. The fast path is the kind-segregated keyForMap lookup; on a miss for 018 * COLLATION_BASED dialects (MSSQL with case-insensitive collation, MySQL 019 * {@code lower_case_table_names=2}) the snapshot falls back to a linear scan via 020 * {@link CatalogIdentifierPolicy#areEqual} — same contract as 021 * {@code IdentifierService.canUseCompositeKey}.</p> 022 * 023 * <p>Construction goes through {@link #builder()}; the snapshot is immutable once built. 024 * Children are precomputed at build time so {@link #children(CatalogObjectId, CatalogObjectKind)} 025 * does not walk every entry.</p> 026 */ 027public final class InMemoryCatalogSnapshot implements CatalogSnapshot { 028 029 private final EDbVendor vendor; 030 /** Outer key = kind; inner key = {@code keyForMap(name)} → entry. */ 031 private final Map<CatalogObjectKind, Map<String, CatalogEntry>> byKey; 032 /** Per-kind list, used for the slow {@code areEqual} fallback. */ 033 private final Map<CatalogObjectKind, List<CatalogEntry>> byKind; 034 private final Map<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> childrenByParent; 035 private final List<CatalogDiagnostic> diagnostics; 036 private final long materializedAtMillis; 037 /** 038 * Whether the snapshot's vendor uses {@code IdentifierService.areEqual} semantics 039 * that {@code keyForMap} cannot fully capture. {@code false} for vendors where the 040 * composite key is a complete equality key — in that case a key miss really means 041 * "no entry" and the linear fallback can be skipped, keeping {@link #find} O(1) 042 * even on 100k-table catalogs. 043 */ 044 private final boolean needsAreEqualFallback; 045 046 private InMemoryCatalogSnapshot(Builder b) { 047 if (b.vendor == null) { 048 throw new IllegalArgumentException("InMemoryCatalogSnapshot.vendor is required"); 049 } 050 this.vendor = b.vendor; 051 this.byKey = freezeNested(b.byKey); 052 this.byKind = freezeListMap(b.byKind); 053 this.childrenByParent = freezeChildren(b.childrenByParent); 054 this.diagnostics = Collections.unmodifiableList( 055 new ArrayList<CatalogDiagnostic>(b.diagnostics)); 056 this.materializedAtMillis = b.materializedAtMillis != 0 057 ? b.materializedAtMillis : System.currentTimeMillis(); 058 this.needsAreEqualFallback = NeedsAreEqualFallback.forVendor(vendor); 059 } 060 061 public static Builder builder() { 062 return new Builder(); 063 } 064 065 @Override 066 public EDbVendor vendor() { 067 return vendor; 068 } 069 070 @Override 071 public Optional<CatalogEntry> find(CatalogQualifiedName name, CatalogObjectKind kind) { 072 if (name == null || kind == null) { 073 return Optional.empty(); 074 } 075 Map<String, CatalogEntry> bucket = byKey.get(kind); 076 if (bucket != null) { 077 CatalogEntry direct = bucket.get(CatalogIdentifierPolicy.keyForMap(name)); 078 if (direct != null) { 079 return Optional.of(direct); 080 } 081 } 082 // Slow fallback only for dialects whose normalize cannot be summarized by a 083 // composite key (COLLATION_BASED). For Oracle / PostgreSQL / MySQL etc., the 084 // keyForMap miss is a real miss — skipping the linear scan keeps find() O(1) 085 // on 100k-entry snapshots. 086 if (needsAreEqualFallback) { 087 List<CatalogEntry> all = byKind.get(kind); 088 if (all != null) { 089 for (CatalogEntry entry : all) { 090 if (CatalogIdentifierPolicy.areEqual(entry.name(), name)) { 091 return Optional.of(entry); 092 } 093 } 094 } 095 } 096 return Optional.empty(); 097 } 098 099 @Override 100 public List<CatalogEntry> children(CatalogObjectId parent, CatalogObjectKind kind) { 101 if (parent == null || kind == null) { 102 return Collections.emptyList(); 103 } 104 Map<CatalogObjectKind, List<CatalogEntry>> kindMap = childrenByParent.get(parent); 105 if (kindMap == null) { 106 return Collections.emptyList(); 107 } 108 List<CatalogEntry> entries = kindMap.get(kind); 109 return entries == null ? Collections.<CatalogEntry>emptyList() : entries; 110 } 111 112 @Override 113 public List<CatalogDiagnostic> diagnostics() { 114 return diagnostics; 115 } 116 117 @Override 118 public long materializedAtMillis() { 119 return materializedAtMillis; 120 } 121 122 /** Total entry count across all kinds. Useful for logging / smoke tests. */ 123 public int size() { 124 int n = 0; 125 for (List<CatalogEntry> list : byKind.values()) n += list.size(); 126 return n; 127 } 128 129 private static Map<CatalogObjectKind, Map<String, CatalogEntry>> freezeNested( 130 Map<CatalogObjectKind, Map<String, CatalogEntry>> in) { 131 Map<CatalogObjectKind, Map<String, CatalogEntry>> out = 132 new LinkedHashMap<CatalogObjectKind, Map<String, CatalogEntry>>(in.size()); 133 for (Map.Entry<CatalogObjectKind, Map<String, CatalogEntry>> e : in.entrySet()) { 134 out.put(e.getKey(), Collections.unmodifiableMap( 135 new LinkedHashMap<String, CatalogEntry>(e.getValue()))); 136 } 137 return Collections.unmodifiableMap(out); 138 } 139 140 private static Map<CatalogObjectKind, List<CatalogEntry>> freezeListMap( 141 Map<CatalogObjectKind, List<CatalogEntry>> in) { 142 Map<CatalogObjectKind, List<CatalogEntry>> out = 143 new LinkedHashMap<CatalogObjectKind, List<CatalogEntry>>(in.size()); 144 for (Map.Entry<CatalogObjectKind, List<CatalogEntry>> e : in.entrySet()) { 145 out.put(e.getKey(), Collections.unmodifiableList( 146 new ArrayList<CatalogEntry>(e.getValue()))); 147 } 148 return Collections.unmodifiableMap(out); 149 } 150 151 private static Map<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> freezeChildren( 152 Map<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> in) { 153 Map<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> out = 154 new LinkedHashMap<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>>(in.size()); 155 for (Map.Entry<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> e : in.entrySet()) { 156 out.put(e.getKey(), freezeListMap(e.getValue())); 157 } 158 return Collections.unmodifiableMap(out); 159 } 160 161 public static final class Builder { 162 163 private EDbVendor vendor; 164 private final Map<CatalogObjectKind, Map<String, CatalogEntry>> byKey = 165 new LinkedHashMap<CatalogObjectKind, Map<String, CatalogEntry>>(); 166 private final Map<CatalogObjectKind, List<CatalogEntry>> byKind = 167 new LinkedHashMap<CatalogObjectKind, List<CatalogEntry>>(); 168 private final Map<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>> childrenByParent = 169 new LinkedHashMap<CatalogObjectId, Map<CatalogObjectKind, List<CatalogEntry>>>(); 170 private final List<CatalogDiagnostic> diagnostics = new ArrayList<CatalogDiagnostic>(); 171 private long materializedAtMillis; 172 173 private Builder() { 174 } 175 176 public Builder vendor(EDbVendor v) { 177 this.vendor = v; 178 return this; 179 } 180 181 /** 182 * Add an entry with optional parent. {@code parent} may be {@code null} for top-level 183 * objects (catalogs). Children are folded into the snapshot's {@code childrenByParent} 184 * index immediately so the resulting snapshot can answer {@code children(...)} in 185 * constant time per parent/kind. 186 */ 187 public Builder put(CatalogEntry entry, CatalogObjectId parent) { 188 if (entry == null) { 189 throw new IllegalArgumentException("InMemoryCatalogSnapshot entry may not be null"); 190 } 191 CatalogObjectKind kind = entry.kind(); 192 String key = CatalogIdentifierPolicy.keyForMap(entry.name()); 193 byKey.computeIfAbsent(kind, k -> new LinkedHashMap<String, CatalogEntry>()).put(key, entry); 194 byKind.computeIfAbsent(kind, k -> new ArrayList<CatalogEntry>()).add(entry); 195 if (parent != null) { 196 childrenByParent 197 .computeIfAbsent(parent, k -> new LinkedHashMap<CatalogObjectKind, List<CatalogEntry>>()) 198 .computeIfAbsent(kind, k -> new ArrayList<CatalogEntry>()) 199 .add(entry); 200 } 201 return this; 202 } 203 204 public Builder put(CatalogEntry entry) { 205 return put(entry, null); 206 } 207 208 public Builder addDiagnostic(CatalogDiagnostic d) { 209 if (d != null) this.diagnostics.add(d); 210 return this; 211 } 212 213 public Builder materializedAtMillis(long v) { 214 this.materializedAtMillis = v; 215 return this; 216 } 217 218 public InMemoryCatalogSnapshot build() { 219 return new InMemoryCatalogSnapshot(this); 220 } 221 } 222}