001package gudusoft.gsqlparser.sqlenv;
002
003import java.text.Collator;
004import java.util.*;
005
006/**
007 * 分桶索引(SQL Server COLLATION_BASED 专用)
008 *
009 * <p>用于优化 SQL Server 的 schema 对象查找,将 O(N) 全表扫描优化为 O(1) + O(bucket_size)。
010 *
011 * <p><strong>核心思想:</strong>
012 * <ul>
013 * <li>桶键 = the collation key of the name under the bucket's own collator, so a bucket is
014 *     exactly one class of names the collator compares equal
015 * <li>相同桶键的对象放在同一桶内
016 * <li>查找时先定位桶(O(1)),再用 Collator 比较桶内对象(O(bucket_size))
017 * </ul>
018 *
019 * <p><strong>性能特性:</strong>
020 * <ul>
021 * <li>10,000 对象场景:从 ~20μs(全表扫描)优化到 ~800ns(分桶)
022 * <li>性能提升:~25x
023 * <li>内存开销:每个对象额外 ~8 bytes(HashMap overhead)
024 * </ul>
025 *
026 * <p>使用示例:
027 * <pre>
028 * CollatorProvider provider = new CollatorProvider();
029 * BucketedIndex index = new BucketedIndex(provider, "SQL_Latin1_General_CP1_CI_AS");
030 *
031 * // 添加对象
032 * index.put("MyTable", tableObject);
033 * index.put("MYTABLE", tableObject2);  // 同一桶(CI: one collation key)
034 *
035 * // 查找对象(使用 Collator 比较)
036 * TSQLSchemaObject found = index.get("myTable");  // CI: 能找到
037 * </pre>
038 *
039 * @since 3.1.0.9
040 */
041public class BucketedIndex {
042
043    // 桶索引:bucketKey → 候选对象列表
044    private final Map<String, List<TSQLSchemaObject>> buckets = new HashMap<>();
045
046    private final CollatorProvider collatorProvider;
047    private final String collationName;
048    private final ESQLDataObjectType objectType;
049
050    /**
051     * 构造分桶索引
052     *
053     * @param collatorProvider Collator 提供者(ThreadLocal 缓存)
054     * @param collationName SQL Server collation 名称
055     * @param objectType 对象类型(用于日志)
056     */
057    public BucketedIndex(CollatorProvider collatorProvider, String collationName, ESQLDataObjectType objectType) {
058        this.collatorProvider = collatorProvider;
059        this.collationName = collationName;
060        this.objectType = objectType;
061    }
062
063    /**
064     * 添加对象到桶
065     *
066     * <p>语义:与 {@link java.util.HashMap#put} 一致——若桶内已存在与 {@code name} 在
067     * collation 下相等的条目,则替换之;否则追加。这样 BucketedIndex 与 schemaObjectMap
068     * (legacy)/ tables(hierarchical)三处索引在重复写入时一致采用 overwrite 语义,
069     * 避免 {@link #get} 在 case-different 重复 put 后返回较早条目而 legacy/hierarchical
070     * 返回较晚条目的不对称(slice S3 修复)。
071     *
072     * <p><strong>已知遗留问题(slice S3 范围外,待 S4 处理):</strong>
073     * {@link #computeBucketKey} 与 collator 比较都基于 raw {@code name},不剥离 vendor
074     * delimiter({@code []} / {@code ""} / {@code ``})。因此 SQL Server 下以加引号
075     * 与不加引号写入的等价标识符(如 {@code "[FOO]"} 与 {@code "foo"})会落入不同的桶,
076     * 而 legacy {@code SQLUtil.getIdentifierNormalName} 会把两者归一化为同一 key
077     * ({@code "FOO"})。此差异在 SQL Server + bucketed flag 启用 + 直接构造重复 table
078     * 的窄场景下出现;正常使用 {@code createTable} 不会触发,因为它在构造前会先
079     * dedupe。后续修复方向:在 {@code computeBucketKey} 与 collator 比较时先剥离
080     * vendor delimiter(需要在 {@link BucketedIndex} 引入 vendor / delimiter
081     * stripper 上下文)。
082     *
083     * @param name 对象名称
084     * @param object schema 对象
085     */
086    public void put(String name, TSQLSchemaObject object) {
087        if (name == null || object == null) {
088            return;
089        }
090
091        String bucketKey = computeBucketKey(name);
092
093        // buckets 与桶内 ArrayList 均非线程安全;共享 TSQLEnv 下注册与查找并发进行
094        synchronized (buckets) {
095            List<TSQLSchemaObject> bucket = buckets.computeIfAbsent(bucketKey,
096                k -> new ArrayList<>());
097
098            Collator collator = collatorProvider.getCollator(collationName);
099            for (int i = 0; i < bucket.size(); i++) {
100                if (collator.compare(name, bucket.get(i).name) == 0) {
101                    bucket.set(i, object);
102                    return;
103                }
104            }
105            bucket.add(object);
106        }
107    }
108
109    /**
110     * 查找对象(先定位桶,再 Collator 比较)
111     *
112     * @param name 对象名称
113     * @return 找到的对象,未找到时返回 null
114     */
115    public TSQLSchemaObject get(String name) {
116        if (name == null) {
117            return null;
118        }
119
120        String bucketKey = computeBucketKey(name);
121
122        synchronized (buckets) {
123            List<TSQLSchemaObject> candidates = buckets.get(bucketKey);
124            if (candidates == null || candidates.isEmpty()) {
125                return null;  // 桶不存在
126            }
127
128            // 在候选列表中使用 Collator 比较(通常 < 20 个对象)
129            Collator collator = collatorProvider.getCollator(collationName);
130            for (TSQLSchemaObject candidate : candidates) {
131                if (collator.compare(name, candidate.name) == 0) {
132                    return candidate;  // 找到匹配
133                }
134            }
135
136            return null;  // 未找到
137        }
138    }
139
140    /**
141     * 移除对象
142     *
143     * @param name 对象名称
144     * @param object 要移除的对象
145     * @return true 如果成功移除
146     */
147    public boolean remove(String name, TSQLSchemaObject object) {
148        if (name == null || object == null) {
149            return false;
150        }
151
152        String bucketKey = computeBucketKey(name);
153
154        synchronized (buckets) {
155            List<TSQLSchemaObject> bucket = buckets.get(bucketKey);
156            if (bucket != null) {
157                boolean removed = bucket.remove(object);
158                if (bucket.isEmpty()) {
159                    buckets.remove(bucketKey);  // 清理空桶
160                }
161                return removed;
162            }
163
164            return false;
165        }
166    }
167
168    /**
169     * 获取所有对象(用于遍历)
170     *
171     * @return 所有对象的列表
172     */
173    public List<TSQLSchemaObject> getAllObjects() {
174        List<TSQLSchemaObject> result = new ArrayList<>();
175        synchronized (buckets) {
176            for (List<TSQLSchemaObject> bucket : buckets.values()) {
177                result.addAll(bucket);
178            }
179        }
180        return result;
181    }
182
183    /**
184     * 获取桶数量(用于调试)
185     *
186     * @return 桶的数量
187     */
188    public int getBucketCount() {
189        synchronized (buckets) {
190            return buckets.size();
191        }
192    }
193
194    /**
195     * 获取对象数量
196     *
197     * @return 对象总数
198     */
199    public int getObjectCount() {
200        int count = 0;
201        synchronized (buckets) {
202            for (List<TSQLSchemaObject> bucket : buckets.values()) {
203                count += bucket.size();
204            }
205        }
206        return count;
207    }
208
209    /**
210     * 计算桶键:the collation key of the name under the SAME collator the bucket compares with.
211     *
212     * <p>Two names fall in one bucket exactly when the collator compares them equal (Java
213     * {@link java.text.CollationKey} equality is {@code compare == 0}), so a bucket is one
214     * equivalence class and a lookup can never miss an equal name in another bucket. The
215     * earlier key (ASCII lower case + length) assumed only ASCII letters vary between equal
216     * names: under {@code _AI} {@code café} and {@code cafe} were equal but bucketed apart, and
217     * under {@code _CI} so were {@code Ä} and {@code ä}, and {@code ß} and {@code ss} (different
218     * lengths).
219     *
220     * @param name 对象名称
221     * @return 桶键
222     */
223    private String computeBucketKey(String name) {
224        byte[] key = collatorProvider.getCollator(collationName).getCollationKey(name).toByteArray();
225        return new String(key, java.nio.charset.StandardCharsets.ISO_8859_1);
226    }
227
228    /**
229     * 获取桶统计信息(用于性能分析)
230     *
231     * @return 桶统计信息字符串
232     */
233    public String getBucketStats() {
234        synchronized (buckets) {
235            if (buckets.isEmpty()) {
236                return "Empty index";
237            }
238
239            int totalObjects = 0;
240            int maxBucketSize = 0;
241            int minBucketSize = Integer.MAX_VALUE;
242
243            for (List<TSQLSchemaObject> bucket : buckets.values()) {
244                int size = bucket.size();
245                totalObjects += size;
246                maxBucketSize = Math.max(maxBucketSize, size);
247                minBucketSize = Math.min(minBucketSize, size);
248            }
249
250            double avgBucketSize = (double) totalObjects / buckets.size();
251
252            return String.format(
253                "Buckets: %d, Objects: %d, Avg/Min/Max per bucket: %.2f/%d/%d",
254                buckets.size(), totalObjects, avgBucketSize, minBucketSize, maxBucketSize
255            );
256        }
257    }
258}