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}