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 // buckets 与桶内 ArrayList 均非线程安全;共享 TSQLEnv 下注册与查找并发进行 093 synchronized (buckets) { 094 List<TSQLSchemaObject> bucket = buckets.computeIfAbsent(bucketKey, 095 k -> new ArrayList<>()); 096 097 Collator collator = collatorProvider.getCollator(collationName); 098 for (int i = 0; i < bucket.size(); i++) { 099 if (collator.compare(name, bucket.get(i).name) == 0) { 100 bucket.set(i, object); 101 return; 102 } 103 } 104 bucket.add(object); 105 } 106 } 107 108 /** 109 * 查找对象(先定位桶,再 Collator 比较) 110 * 111 * @param name 对象名称 112 * @return 找到的对象,未找到时返回 null 113 */ 114 public TSQLSchemaObject get(String name) { 115 if (name == null) { 116 return null; 117 } 118 119 String bucketKey = computeBucketKey(name); 120 121 synchronized (buckets) { 122 List<TSQLSchemaObject> candidates = buckets.get(bucketKey); 123 if (candidates == null || candidates.isEmpty()) { 124 return null; // 桶不存在 125 } 126 127 // 在候选列表中使用 Collator 比较(通常 < 20 个对象) 128 Collator collator = collatorProvider.getCollator(collationName); 129 for (TSQLSchemaObject candidate : candidates) { 130 if (collator.compare(name, candidate.name) == 0) { 131 return candidate; // 找到匹配 132 } 133 } 134 135 return null; // 未找到 136 } 137 } 138 139 /** 140 * 移除对象 141 * 142 * @param name 对象名称 143 * @param object 要移除的对象 144 * @return true 如果成功移除 145 */ 146 public boolean remove(String name, TSQLSchemaObject object) { 147 if (name == null || object == null) { 148 return false; 149 } 150 151 String bucketKey = computeBucketKey(name); 152 153 synchronized (buckets) { 154 List<TSQLSchemaObject> bucket = buckets.get(bucketKey); 155 if (bucket != null) { 156 boolean removed = bucket.remove(object); 157 if (bucket.isEmpty()) { 158 buckets.remove(bucketKey); // 清理空桶 159 } 160 return removed; 161 } 162 163 return false; 164 } 165 } 166 167 /** 168 * 获取所有对象(用于遍历) 169 * 170 * @return 所有对象的列表 171 */ 172 public List<TSQLSchemaObject> getAllObjects() { 173 List<TSQLSchemaObject> result = new ArrayList<>(); 174 synchronized (buckets) { 175 for (List<TSQLSchemaObject> bucket : buckets.values()) { 176 result.addAll(bucket); 177 } 178 } 179 return result; 180 } 181 182 /** 183 * 获取桶数量(用于调试) 184 * 185 * @return 桶的数量 186 */ 187 public int getBucketCount() { 188 synchronized (buckets) { 189 return buckets.size(); 190 } 191 } 192 193 /** 194 * 获取对象数量 195 * 196 * @return 对象总数 197 */ 198 public int getObjectCount() { 199 int count = 0; 200 synchronized (buckets) { 201 for (List<TSQLSchemaObject> bucket : buckets.values()) { 202 count += bucket.size(); 203 } 204 } 205 return count; 206 } 207 208 /** 209 * 计算桶键:ASCII lowercase + 长度 210 * 211 * <p>桶键计算规则: 212 * <ol> 213 * <li>将 ASCII 字母转换为小写(A-Z → a-z) 214 * <li>保留非 ASCII 字符(如中文) 215 * <li>附加长度信息(避免不同长度的字符串冲突) 216 * </ol> 217 * 218 * <p>例如: 219 * <ul> 220 * <li>"MyTable" → "mytable_7" 221 * <li>"MYTABLE" → "mytable_7"(同一桶) 222 * <li>"myTable" → "mytable_7"(同一桶) 223 * <li>"MyTab" → "mytab_5"(不同桶,长度不同) 224 * <li>"员工Table" → "员工table_8"(保留中文,T→t) 225 * </ul> 226 * 227 * @param name 对象名称 228 * @return 桶键 229 */ 230 private String computeBucketKey(String name) { 231 if (name == null || name.isEmpty()) { 232 return "_0"; 233 } 234 235 StringBuilder sb = new StringBuilder(name.length() + 5); 236 237 for (int i = 0; i < name.length(); i++) { 238 char c = name.charAt(i); 239 240 // 仅将 ASCII 大写字母转换为小写(A-Z → a-z) 241 if (c >= 'A' && c <= 'Z') { 242 sb.append((char) (c + 32)); // A(65) + 32 = a(97) 243 } else { 244 sb.append(c); // 保留其他字符(小写字母、数字、Unicode) 245 } 246 } 247 248 // 附加长度信息(避免 "ab" 和 "abc" 的桶键冲突) 249 sb.append('_').append(name.length()); 250 251 return sb.toString(); 252 } 253 254 /** 255 * 获取桶统计信息(用于性能分析) 256 * 257 * @return 桶统计信息字符串 258 */ 259 public String getBucketStats() { 260 synchronized (buckets) { 261 if (buckets.isEmpty()) { 262 return "Empty index"; 263 } 264 265 int totalObjects = 0; 266 int maxBucketSize = 0; 267 int minBucketSize = Integer.MAX_VALUE; 268 269 for (List<TSQLSchemaObject> bucket : buckets.values()) { 270 int size = bucket.size(); 271 totalObjects += size; 272 maxBucketSize = Math.max(maxBucketSize, size); 273 minBucketSize = Math.min(minBucketSize, size); 274 } 275 276 double avgBucketSize = (double) totalObjects / buckets.size(); 277 278 return String.format( 279 "Buckets: %d, Objects: %d, Avg/Min/Max per bucket: %.2f/%d/%d", 280 buckets.size(), totalObjects, avgBucketSize, minBucketSize, maxBucketSize 281 ); 282 } 283 } 284}