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}