001package gudusoft.gsqlparser.ir.builder.mssql; 002 003import java.util.ArrayList; 004import java.util.List; 005 006/** 007 * Normalizes SQL Server multi-part names. 008 * <p> 009 * Handles: {@code [brackets]}, {@code "double quotes"}, case normalization, 010 * 1-4 part names ({@code server.database.schema.object}), and 011 * missing schema defaulting to {@code dbo}. 012 */ 013public final class MssqlNameNormalizer { 014 015 private MssqlNameNormalizer() {} 016 017 /** 018 * Normalizes a raw SQL Server object name. 019 * 020 * @param rawName the raw name (e.g., {@code [dbo].[proc_name]}) 021 * @return normalized name with parts split, quotes stripped, upper-cased for matching 022 */ 023 public static NormalizedName normalize(String rawName) { 024 if (rawName == null || rawName.trim().isEmpty()) { 025 return new NormalizedName(null, null, null, "", "", ""); 026 } 027 List<String> rawParts = splitParts(rawName.trim()); 028 List<String> stripped = new ArrayList<String>(); 029 for (String part : rawParts) { 030 stripped.add(stripQuotes(part)); 031 } 032 033 String server = null; 034 String database = null; 035 String schema = null; 036 String object; 037 038 switch (stripped.size()) { 039 case 1: 040 object = stripped.get(0); 041 break; 042 case 2: 043 schema = stripped.get(0); 044 object = stripped.get(1); 045 break; 046 case 3: 047 database = stripped.get(0); 048 schema = stripped.get(1); 049 // Handle db..object (empty schema → default dbo) 050 if (schema.isEmpty()) { 051 schema = "dbo"; 052 } 053 object = stripped.get(2); 054 break; 055 case 4: 056 server = stripped.get(0); 057 database = stripped.get(1); 058 schema = stripped.get(2); 059 if (schema.isEmpty()) { 060 schema = "dbo"; 061 } 062 object = stripped.get(3); 063 break; 064 default: 065 object = rawName.trim(); 066 break; 067 } 068 069 String displayText = rawName.trim(); 070 071 // Build match key: [schema.]object (uppercase) 072 StringBuilder matchKeyBuilder = new StringBuilder(); 073 if (schema != null && !schema.isEmpty()) { 074 matchKeyBuilder.append(schema.toUpperCase()).append('.'); 075 } 076 matchKeyBuilder.append(object.toUpperCase()); 077 String matchKey = matchKeyBuilder.toString(); 078 079 return new NormalizedName(server, database, schema, object, displayText, matchKey); 080 } 081 082 /** 083 * Splits a multi-part name by dots, respecting bracket and quote delimiters. 084 */ 085 public static List<String> splitParts(String rawName) { 086 List<String> parts = new ArrayList<String>(); 087 StringBuilder current = new StringBuilder(); 088 boolean inBracket = false; 089 boolean inQuote = false; 090 091 for (int i = 0; i < rawName.length(); i++) { 092 char c = rawName.charAt(i); 093 if (c == '[' && !inQuote) { 094 inBracket = true; 095 current.append(c); 096 } else if (c == ']' && inBracket) { 097 inBracket = false; 098 current.append(c); 099 } else if (c == '"' && !inBracket) { 100 inQuote = !inQuote; 101 current.append(c); 102 } else if (c == '.' && !inBracket && !inQuote) { 103 parts.add(current.toString()); 104 current.setLength(0); 105 } else { 106 current.append(c); 107 } 108 } 109 parts.add(current.toString()); 110 return parts; 111 } 112 113 /** 114 * Strips surrounding {@code []} and {@code ""} quote characters from a name part. 115 */ 116 public static String stripQuotes(String part) { 117 if (part == null) { 118 return ""; 119 } 120 String s = part.trim(); 121 if (s.startsWith("[") && s.endsWith("]") && s.length() >= 2) { 122 return s.substring(1, s.length() - 1); 123 } 124 if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) { 125 return s.substring(1, s.length() - 1); 126 } 127 return s; 128 } 129 130 /** 131 * Normalized SQL Server name with server/database/schema/object components. 132 */ 133 public static class NormalizedName { 134 private final String server; 135 private final String database; 136 private final String schema; 137 private final String object; 138 private final String displayText; 139 private final String matchKey; 140 141 public NormalizedName(String server, String database, String schema, 142 String object, String displayText, String matchKey) { 143 this.server = server; 144 this.database = database; 145 this.schema = schema; 146 this.object = object; 147 this.displayText = displayText; 148 this.matchKey = matchKey; 149 } 150 151 public String getServer() { return server; } 152 public String getDatabase() { return database; } 153 public String getSchema() { return schema; } 154 public String getObject() { return object; } 155 public String getDisplayText() { return displayText; } 156 public String getMatchKey() { return matchKey; } 157 158 /** 159 * Returns the number of name segments (1 = object only, 2 = schema.object, etc.). 160 */ 161 public int getPartCount() { 162 int count = 1; 163 if (schema != null && !schema.isEmpty()) count++; 164 if (database != null && !database.isEmpty()) count++; 165 if (server != null && !server.isEmpty()) count++; 166 return count; 167 } 168 169 @Override 170 public String toString() { 171 return "NormalizedName{" + matchKey + ", parts=" + getPartCount() + "}"; 172 } 173 } 174}