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}