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>桶键 = ASCII lowercase + 长度(例如:"MyTable" → "mytable_7")
014 * <li>相同桶键的对象放在同一桶内(通常 < 20 个对象)
015 * <li>查找时先定位桶(O(1)),再用 Collator 比较桶内对象(O(bucket_size))
016 * </ul>
017 *
018 * <p><strong>性能特性:</strong>
019 * <ul>
020 * <li>10,000 对象场景:从 ~20μs(全表扫描)优化到 ~800ns(分桶)
021 * <li>性能提升:~25x
022 * <li>内存开销:每个对象额外 ~8 bytes(HashMap overhead)
023 * </ul>
024 *
025 * <p>使用示例:
026 * <pre>
027 * CollatorProvider provider = new CollatorProvider();
028 * BucketedIndex index = new BucketedIndex(provider, "SQL_Latin1_General_CP1_CI_AS");
029 *
030 * // 添加对象
031 * index.put("MyTable", tableObject);
032 * index.put("MYTABLE", tableObject2);  // 同一桶(mytable_7)
033 *
034 * // 查找对象(使用 Collator 比较)
035 * TSQLSchemaObject found = index.get("myTable");  // CI: 能找到
036 * </pre>
037 *
038 * @since 3.1.0.9
039 */
040public class BucketedIndex {
041
042    // 桶索引:bucketKey → 候选对象列表
043    private final Map<String, List<TSQLSchemaObject>> buckets = new HashMap<>();
044
045    private final CollatorProvider collatorProvider;
046    private final String collationName;
047    private final ESQLDataObjectType objectType;
048
049    /**
050     * 构造分桶索引
051     *
052     * @param collatorProvider Collator 提供者(ThreadLocal 缓存)
053     * @param collationName SQL Server collation 名称
054     * @param objectType 对象类型(用于日志)
055     */
056    public BucketedIndex(CollatorProvider collatorProvider, String collationName, ESQLDataObjectType objectType) {
057        this.collatorProvider = collatorProvider;
058        this.collationName = collationName;
059        this.objectType = objectType;
060    }
061
062    /**
063     * 添加对象到桶
064     *
065     * <p>语义:与 {@link java.util.HashMap#put} 一致——若桶内已存在与 {@code name} 在
066     * collation 下相等的条目,则替换之;否则追加。这样 BucketedIndex 与 schemaObjectMap
067     * (legacy)/ tables(hierarchical)三处索引在重复写入时一致采用 overwrite 语义,
068     * 避免 {@link #get} 在 case-different 重复 put 后返回较早条目而 legacy/hierarchical
069     * 返回较晚条目的不对称(slice S3 修复)。
070     *
071     * <p><strong>已知遗留问题(slice S3 范围外,待 S4 处理):</strong>
072     * {@link #computeBucketKey} 与 collator 比较都基于 raw {@code name},不剥离 vendor
073     * delimiter({@code []} / {@code ""} / {@code ``})。因此 SQL Server 下以加引号
074     * 与不加引号写入的等价标识符(如 {@code "[FOO]"} 与 {@code "foo"})会落入不同的桶,
075     * 而 legacy {@code SQLUtil.getIdentifierNormalName} 会把两者归一化为同一 key
076     * ({@code "FOO"})。此差异在 SQL Server + bucketed flag 启用 + 直接构造重复 table
077     * 的窄场景下出现;正常使用 {@code createTable} 不会触发,因为它在构造前会先
078     * dedupe。后续修复方向:在 {@code computeBucketKey} 与 collator 比较时先剥离
079     * vendor delimiter(需要在 {@link BucketedIndex} 引入 vendor / delimiter
080     * stripper 上下文)。
081     *
082     * @param name 对象名称
083     * @param object schema 对象
084     */
085    public void put(String name, TSQLSchemaObject object) {
086        if (name == null || object == null) {
087            return;
088        }
089
090        String bucketKey = computeBucketKey(name);
091
092        List<TSQLSchemaObject> bucket = buckets.computeIfAbsent(bucketKey,
093            k -> new ArrayList<>());
094
095        Collator collator = collatorProvider.getCollator(collationName);
096        for (int i = 0; i < bucket.size(); i++) {
097            if (collator.compare(name, bucket.get(i).name) == 0) {
098                bucket.set(i, object);
099                return;
100            }
101        }
102        bucket.add(object);
103    }
104
105    /**
106     * 查找对象(先定位桶,再 Collator 比较)
107     *
108     * @param name 对象名称
109     * @return 找到的对象,未找到时返回 null
110     */
111    public TSQLSchemaObject get(String name) {
112        if (name == null) {
113            return null;
114        }
115
116        String bucketKey = computeBucketKey(name);
117
118        List<TSQLSchemaObject> candidates = buckets.get(bucketKey);
119        if (candidates == null || candidates.isEmpty()) {
120            return null;  // 桶不存在
121        }
122
123        // 在候选列表中使用 Collator 比较(通常 < 20 个对象)
124        Collator collator = collatorProvider.getCollator(collationName);
125        for (TSQLSchemaObject candidate : candidates) {
126            if (collator.compare(name, candidate.name) == 0) {
127                return candidate;  // 找到匹配
128            }
129        }
130
131        return null;  // 未找到
132    }
133
134    /**
135     * 移除对象
136     *
137     * @param name 对象名称
138     * @param object 要移除的对象
139     * @return true 如果成功移除
140     */
141    public boolean remove(String name, TSQLSchemaObject object) {
142        if (name == null || object == null) {
143            return false;
144        }
145
146        String bucketKey = computeBucketKey(name);
147
148        List<TSQLSchemaObject> bucket = buckets.get(bucketKey);
149        if (bucket != null) {
150            boolean removed = bucket.remove(object);
151            if (bucket.isEmpty()) {
152                buckets.remove(bucketKey);  // 清理空桶
153            }
154            return removed;
155        }
156
157        return false;
158    }
159
160    /**
161     * 获取所有对象(用于遍历)
162     *
163     * @return 所有对象的列表
164     */
165    public List<TSQLSchemaObject> getAllObjects() {
166        List<TSQLSchemaObject> result = new ArrayList<>();
167        for (List<TSQLSchemaObject> bucket : buckets.values()) {
168            result.addAll(bucket);
169        }
170        return result;
171    }
172
173    /**
174     * 获取桶数量(用于调试)
175     *
176     * @return 桶的数量
177     */
178    public int getBucketCount() {
179        return buckets.size();
180    }
181
182    /**
183     * 获取对象数量
184     *
185     * @return 对象总数
186     */
187    public int getObjectCount() {
188        int count = 0;
189        for (List<TSQLSchemaObject> bucket : buckets.values()) {
190            count += bucket.size();
191        }
192        return count;
193    }
194
195    /**
196     * 计算桶键:ASCII lowercase + 长度
197     *
198     * <p>桶键计算规则:
199     * <ol>
200     * <li>将 ASCII 字母转换为小写(A-Z → a-z)
201     * <li>保留非 ASCII 字符(如中文)
202     * <li>附加长度信息(避免不同长度的字符串冲突)
203     * </ol>
204     *
205     * <p>例如:
206     * <ul>
207     * <li>"MyTable" → "mytable_7"
208     * <li>"MYTABLE" → "mytable_7"(同一桶)
209     * <li>"myTable" → "mytable_7"(同一桶)
210     * <li>"MyTab" → "mytab_5"(不同桶,长度不同)
211     * <li>"员工Table" → "员工table_8"(保留中文,T→t)
212     * </ul>
213     *
214     * @param name 对象名称
215     * @return 桶键
216     */
217    private String computeBucketKey(String name) {
218        if (name == null || name.isEmpty()) {
219            return "_0";
220        }
221
222        StringBuilder sb = new StringBuilder(name.length() + 5);
223
224        for (int i = 0; i < name.length(); i++) {
225            char c = name.charAt(i);
226
227            // 仅将 ASCII 大写字母转换为小写(A-Z → a-z)
228            if (c >= 'A' && c <= 'Z') {
229                sb.append((char) (c + 32));  // A(65) + 32 = a(97)
230            } else {
231                sb.append(c);  // 保留其他字符(小写字母、数字、Unicode)
232            }
233        }
234
235        // 附加长度信息(避免 "ab" 和 "abc" 的桶键冲突)
236        sb.append('_').append(name.length());
237
238        return sb.toString();
239    }
240
241    /**
242     * 获取桶统计信息(用于性能分析)
243     *
244     * @return 桶统计信息字符串
245     */
246    public String getBucketStats() {
247        if (buckets.isEmpty()) {
248            return "Empty index";
249        }
250
251        int totalObjects = 0;
252        int maxBucketSize = 0;
253        int minBucketSize = Integer.MAX_VALUE;
254
255        for (List<TSQLSchemaObject> bucket : buckets.values()) {
256            int size = bucket.size();
257            totalObjects += size;
258            maxBucketSize = Math.max(maxBucketSize, size);
259            minBucketSize = Math.min(minBucketSize, size);
260        }
261
262        double avgBucketSize = (double) totalObjects / buckets.size();
263
264        return String.format(
265            "Buckets: %d, Objects: %d, Avg/Min/Max per bucket: %.2f/%d/%d",
266            buckets.size(), totalObjects, avgBucketSize, minBucketSize, maxBucketSize
267        );
268    }
269}