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}