001package gudusoft.gsqlparser.sqlcmds;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.EFindSqlStateType;
005import gudusoft.gsqlparser.ESqlStatementType;
006import gudusoft.gsqlparser.TBaseType;
007import gudusoft.gsqlparser.TCustomSqlStatement;
008import gudusoft.gsqlparser.TSourceToken;
009import gudusoft.gsqlparser.stmt.TUnknownSqlStatement;
010import gudusoft.gsqlparser.stmt.oceanbase.TAlterOutlineSqlStatement;
011import gudusoft.gsqlparser.stmt.oceanbase.TAlterResourcePoolSqlStatement;
012import gudusoft.gsqlparser.stmt.oceanbase.TAlterSystemSqlStatement;
013import gudusoft.gsqlparser.stmt.oceanbase.TAlterResourceUnitSqlStatement;
014import gudusoft.gsqlparser.stmt.oceanbase.TAlterTablegroupSqlStatement;
015import gudusoft.gsqlparser.stmt.oceanbase.TAlterTenantSqlStatement;
016import gudusoft.gsqlparser.stmt.oceanbase.TCreateOutlineSqlStatement;
017import gudusoft.gsqlparser.stmt.oceanbase.TCreateResourcePoolSqlStatement;
018import gudusoft.gsqlparser.stmt.oceanbase.TCreateResourceUnitSqlStatement;
019import gudusoft.gsqlparser.stmt.oceanbase.TCreateTablegroupSqlStatement;
020import gudusoft.gsqlparser.stmt.oceanbase.TCreateTenantSqlStatement;
021import gudusoft.gsqlparser.stmt.oceanbase.TDropOutlineSqlStatement;
022import gudusoft.gsqlparser.stmt.oceanbase.TDropResourcePoolSqlStatement;
023import gudusoft.gsqlparser.stmt.oceanbase.TDropResourceUnitSqlStatement;
024import gudusoft.gsqlparser.stmt.oceanbase.TDropTablegroupSqlStatement;
025import gudusoft.gsqlparser.stmt.oceanbase.TDropTenantSqlStatement;
026import gudusoft.gsqlparser.stmt.oceanbase.TFlashbackSqlStatement;
027import gudusoft.gsqlparser.stmt.oceanbase.TPurgeSqlStatement;
028import gudusoft.gsqlparser.stmt.oceanbase.TShowTenantSqlStatement;
029import gudusoft.gsqlparser.stmt.oceanbase.TCreateDblinkSqlStatement;
030import gudusoft.gsqlparser.stmt.oceanbase.TDropDblinkSqlStatement;
031import gudusoft.gsqlparser.stmt.oceanbase.TCreateRestorePointSqlStatement;
032import gudusoft.gsqlparser.stmt.oceanbase.TDropRestorePointSqlStatement;
033import gudusoft.gsqlparser.stmt.oceanbase.TXaSqlStatement;
034
035import java.util.regex.Matcher;
036import java.util.regex.Pattern;
037
038/**
039 * OceanBase SQL command resolver — Phase 1 skeleton.
040 *
041 * <p>OceanBase user tenants in MySQL mode and the {@code sys} system tenant
042 * share the MySQL command surface, so this resolver inherits all MySQL
043 * command patterns from {@link TSqlCmdsMysql} via subclassing. The
044 * vendor field is then re-tagged to {@link EDbVendor#dbvoceanbase} so any
045 * statement constructed via the inherited resolver carries OceanBase
046 * vendor identity.
047 *
048 * <p>For Phase 1 we additionally register a small set of unmistakable
049 * OceanBase administrative DDL prefixes ({@code CREATE TENANT},
050 * {@code ALTER TENANT}, {@code DROP TENANT}, {@code ALTER SYSTEM},
051 * {@code CREATE/ALTER RESOURCE POOL}, {@code CREATE/ALTER RESOURCE UNIT})
052 * so the splitter recognizes them as statement boundaries in multi-statement
053 * admin scripts. These statements are tagged {@link ESqlStatementType#sstunknown}
054 * because Phase 1 has no AST nodes for them yet — Phase 4 will add proper
055 * AST classes and replace {@code sstunknown} with vendor-specific types
056 * such as {@code sstoceanbase_create_tenant}.
057 *
058 * <h2>Mode awareness</h2>
059 *
060 * <p>This class is constructed once per {@link gudusoft.gsqlparser.TGSqlParser}
061 * instance via {@link SqlCmdsFactory}, before the user has had a chance to
062 * call {@code setOBTenantMode}. So a single {@code TSqlCmdsOceanbase}
063 * cannot select between MySQL-family and Oracle-family command sets at
064 * construction time. Phase 1 deliberately accepts a MySQL-family command
065 * surface here because:
066 * <ul>
067 *   <li>The Oracle delegate ({@link gudusoft.gsqlparser.parser.OracleSqlParser})
068 *       used by {@code OceanBaseSqlParser} for {@code ORACLE} mode has its
069 *       own internal {@link TSqlCmdsOracle} that handles PL/SQL boundary
070 *       detection independently of this class.</li>
071 *   <li>The {@code TGSqlParser}-level {@code sqlcmds} field is consulted
072 *       primarily for validation and post-processing of MySQL-family
073 *       statements; ORACLE-mode dbvoceanbase scripts that exercise this
074 *       class are a Phase 4 concern.</li>
075 * </ul>
076 *
077 * <p>Phase 4 will revisit whether {@code TSqlCmdsOceanbase} should grow
078 * mode-awareness via a separate factory dispatch path or whether it should
079 * be split into {@code TSqlCmdsOceanbaseMysql} / {@code TSqlCmdsOceanbaseOracle}
080 * with the active instance selected from {@code EOBTenantMode}.
081 *
082 * @see TSqlCmdsMysql
083 * @see SqlCmdsFactory
084 * @see gudusoft.gsqlparser.EOBTenantMode
085 * @since 4.0.1.4
086 */
087public class TSqlCmdsOceanbase extends TSqlCmdsMysql {
088
089    public TSqlCmdsOceanbase() {
090        super();
091        // Re-tag the inherited vendor field so statements constructed via
092        // the inherited resolver carry OceanBase identity rather than MySQL.
093        // AbstractSqlCmds#vendor is protected and non-final.
094        this.vendor = EDbVendor.dbvoceanbase;
095    }
096
097    @Override
098    protected void initializeCommands() {
099        // Step 1 — inherit the MySQL command surface verbatim.
100        super.initializeCommands();
101
102        // Step 2 — register OceanBase administrative DDL prefixes for
103        // boundary detection only. These are tagged sstunknown for Phase 1;
104        // Phase 4 will replace with proper sstoceanbase_* statement types
105        // and dedicated AST classes.
106        //
107        // Per ADR-7 and AskUserQuestion #3 (D12), the scope is intentionally
108        // narrow: only unmistakable system-tenant verbs that cannot conflict
109        // with MySQL DDL of the same shape. We do NOT auto-promote the
110        // tenant mode based on these prefixes; that is the caller's
111        // responsibility via TGSqlParser.setOBTenantMode(SYSTEM).
112
113        // CREATE TENANT / ALTER TENANT / DROP TENANT
114        // Phase 4 Batch 1: boundary tags now reflect the real statement
115        // types constructed by the oceanbasemysql grammar.
116        addCmd(TBaseType.rrw_create, "tenant", " ", " ", " ", " ", " ",
117                ESqlStatementType.sstoceanbase_create_tenant);
118        addCmd(TBaseType.rrw_alter,  "tenant", " ", " ", " ", " ", " ",
119                ESqlStatementType.sstoceanbase_alter_tenant);
120        addCmd(TBaseType.rrw_drop,   "tenant", " ", " ", " ", " ", " ",
121                ESqlStatementType.sstoceanbase_drop_tenant);
122
123        // ALTER SYSTEM (Phase 4 Batch 3 + 10)
124        addCmd(TBaseType.rrw_alter, "system", " ", " ", " ", " ", " ",
125                ESqlStatementType.sstoceanbase_alter_system);
126
127        // CREATE/ALTER/DROP RESOURCE POOL
128        // Phase 4 Batch 2: boundary tags now reflect the real statement
129        // types constructed by the oceanbasemysql grammar.
130        addCmd(TBaseType.rrw_create, "resource", "pool", " ", " ", " ", " ",
131                ESqlStatementType.sstoceanbase_create_resource_pool);
132        addCmd(TBaseType.rrw_alter,  "resource", "pool", " ", " ", " ", " ",
133                ESqlStatementType.sstoceanbase_alter_resource_pool);
134        addCmd(TBaseType.rrw_drop,   "resource", "pool", " ", " ", " ", " ",
135                ESqlStatementType.sstoceanbase_drop_resource_pool);
136
137        // CREATE/ALTER/DROP RESOURCE UNIT
138        addCmd(TBaseType.rrw_create, "resource", "unit", " ", " ", " ", " ",
139                ESqlStatementType.sstoceanbase_create_resource_unit);
140        addCmd(TBaseType.rrw_alter,  "resource", "unit", " ", " ", " ", " ",
141                ESqlStatementType.sstoceanbase_alter_resource_unit);
142        addCmd(TBaseType.rrw_drop,   "resource", "unit", " ", " ", " ", " ",
143                ESqlStatementType.sstoceanbase_drop_resource_unit);
144
145        // CREATE/ALTER/DROP TABLEGROUP (Phase 4 Batch 6)
146        addCmd(TBaseType.rrw_create, "tablegroup", " ", " ", " ", " ", " ",
147                ESqlStatementType.sstoceanbase_create_tablegroup);
148        addCmd(TBaseType.rrw_alter,  "tablegroup", " ", " ", " ", " ", " ",
149                ESqlStatementType.sstoceanbase_alter_tablegroup);
150        addCmd(TBaseType.rrw_drop,   "tablegroup", " ", " ", " ", " ", " ",
151                ESqlStatementType.sstoceanbase_drop_tablegroup);
152
153        // CREATE [GLOBAL|LOCAL] INDEX (Phase 4 Batch 7)
154        // MySQL base knows CREATE INDEX but not CREATE GLOBAL/LOCAL INDEX.
155        // Map the OceanBase adjective-prefixed forms to the same
156        // sstmysqlcreateindex type so existing index tooling keeps working.
157        addCmd(TBaseType.rrw_create, "global", "index", " ", " ", " ", " ",
158                ESqlStatementType.sstmysqlcreateindex);
159        addCmd(TBaseType.rrw_create, "local",  "index", " ", " ", " ", " ",
160                ESqlStatementType.sstmysqlcreateindex);
161        addCmd(TBaseType.rrw_create, "unique", "global", "index", " ", " ", " ",
162                ESqlStatementType.sstmysqlcreateindex);
163        addCmd(TBaseType.rrw_create, "unique", "local",  "index", " ", " ", " ",
164                ESqlStatementType.sstmysqlcreateindex);
165
166        // OceanBase documented syntax gaps — system-tenant SHOW family.
167        // SHOW TENANT / SHOW TENANT LIKE 'pattern' / SHOW CREATE TENANT
168        // name / SHOW RESOURCE POOL are OB-only variants that the
169        // inherited MySQL splitter does not know about, so the splitter
170        // falls through with "Error when tokenize near SHOW" before the
171        // grammar ever gets a chance to run. Register them here under a
172        // single umbrella enum; the grammar production
173        // showOceanBaseTenantStmt builds a distinct AST that carries the
174        // variant discriminator and any captured tenant name / LIKE
175        // pattern.
176        addCmd(TBaseType.rrw_show, "tenant", " ", " ", " ", " ", " ",
177                ESqlStatementType.sstoceanbase_show_tenant);
178        addCmd(TBaseType.rrw_show, "create", "tenant", " ", " ", " ", " ",
179                ESqlStatementType.sstoceanbase_show_tenant);
180        addCmd(TBaseType.rrw_show, "resource", "pool", " ", " ", " ", " ",
181                ESqlStatementType.sstoceanbase_show_tenant);
182
183        // US-007 — SHOW extensions
184        addCmd(TBaseType.rrw_show, "tenants", " ", " ", " ", " ", " ",
185                ESqlStatementType.sstoceanbase_show_tenant);
186        addCmd(TBaseType.rrw_show, "recyclebin", " ", " ", " ", " ", " ",
187                ESqlStatementType.sstoceanbase_show_tenant);
188        addCmd(TBaseType.rrw_show, "tablegroups", " ", " ", " ", " ", " ",
189                ESqlStatementType.sstoceanbase_show_tenant);
190        addCmd(TBaseType.rrw_show, "parameters", " ", " ", " ", " ", " ",
191                ESqlStatementType.sstoceanbase_show_tenant);
192
193        // MERGE INTO (US-002) — boundary tag for MERGE statement
194        addCmd(TBaseType.rrw_merge, ESqlStatementType.sstmerge);
195
196        // FLASHBACK TABLE / FLASHBACK DATABASE / FLASHBACK TENANT (US-003, US-001 Round 3)
197        addCmd(TBaseType.rrw_flashback, "table", " ", " ", " ", " ", " ",
198                ESqlStatementType.sstoceanbase_flashback);
199        addCmd(TBaseType.rrw_flashback, "database", " ", " ", " ", " ", " ",
200                ESqlStatementType.sstoceanbase_flashback);
201        addCmd(TBaseType.rrw_flashback, "tenant", " ", " ", " ", " ", " ",
202                ESqlStatementType.sstoceanbase_flashback);
203
204        // PURGE TABLE / PURGE DATABASE / PURGE RECYCLEBIN (US-003)
205        addCmd(TBaseType.rrw_purge, "table", " ", " ", " ", " ", " ",
206                ESqlStatementType.sstoceanbase_purge);
207        addCmd(TBaseType.rrw_purge, "database", " ", " ", " ", " ", " ",
208                ESqlStatementType.sstoceanbase_purge);
209        addCmd(TBaseType.rrw_purge, "recyclebin", " ", " ", " ", " ", " ",
210                ESqlStatementType.sstoceanbase_purge);
211        // PURGE INDEX / PURGE TENANT (US-001 Round 2)
212        addCmd(TBaseType.rrw_purge, "index", " ", " ", " ", " ", " ",
213                ESqlStatementType.sstoceanbase_purge);
214        addCmd(TBaseType.rrw_purge, "tenant", " ", " ", " ", " ", " ",
215                ESqlStatementType.sstoceanbase_purge);
216
217        // CREATE TABLESPACE / DROP TABLESPACE (US-001 Round 2)
218        addCmd(TBaseType.rrw_create, "tablespace", " ", " ", " ", " ", " ",
219                ESqlStatementType.sstcreateTablespace);
220        addCmd(TBaseType.rrw_drop, "tablespace", " ", " ", " ", " ", " ",
221                ESqlStatementType.sstcreateTablespace);
222
223        // ALTER USER / CREATE ROLE / DROP ROLE (US-002 Round 2)
224        addCmd(TBaseType.rrw_alter, "user", " ", " ", " ", " ", " ",
225                ESqlStatementType.sstalteruser);
226        addCmd(TBaseType.rrw_create, "role", " ", " ", " ", " ", " ",
227                ESqlStatementType.sstcreaterole);
228        addCmd(TBaseType.rrw_drop, "role", " ", " ", " ", " ", " ",
229                ESqlStatementType.sstdroprole);
230
231        // ALTER SEQUENCE (US-009) — MySQL base has CREATE/DROP SEQUENCE
232        // but not ALTER SEQUENCE. OceanBase supports ALTER SEQUENCE for
233        // modifying sequence properties (INCREMENT BY, MAXVALUE, etc.).
234        addCmd(TBaseType.rrw_alter, "sequence", " ", " ", " ", " ", " ",
235                ESqlStatementType.sstaltersequence);
236
237        // CREATE/ALTER/DROP OUTLINE (Phase 4 Batch 8)
238        // Reuse Oracle's existing sstoracle*outline enum values per ADR-8
239        // — OUTLINE is not a MySQL construct, so the base MySQL resolver
240        // knows nothing about these types. Our issql override maps them
241        // to the OceanBase statement classes.
242        addCmd(TBaseType.rrw_create, "outline", " ", " ", " ", " ", " ",
243                ESqlStatementType.sstoraclecreateoutline);
244        addCmd(TBaseType.rrw_create, "or", "replace", "outline", " ", " ", " ",
245                ESqlStatementType.sstoraclecreateoutline);
246        addCmd(TBaseType.rrw_alter,  "outline", " ", " ", " ", " ", " ",
247                ESqlStatementType.sstoraclealteroutline);
248        addCmd(TBaseType.rrw_drop,   "outline", " ", " ", " ", " ", " ",
249                ESqlStatementType.sstoracledropoutline);
250
251        // XA transaction statements (US-004 Round 3)
252        // XA START/END/PREPARE/COMMIT/ROLLBACK/RECOVER 'xid'
253        addCmd(TBaseType.rrw_oceanbase_xa, " ", " ", " ", " ", " ", " ",
254                ESqlStatementType.sstoceanbase_xa);
255
256        // CREATE/DROP DBLINK (US-005 Round 3)
257        addCmd(TBaseType.rrw_create, "dblink", " ", " ", " ", " ", " ",
258                ESqlStatementType.sstoceanbase_create_dblink);
259        addCmd(TBaseType.rrw_drop, "dblink", " ", " ", " ", " ", " ",
260                ESqlStatementType.sstoceanbase_drop_dblink);
261
262        // CREATE/DROP RESTORE POINT (US-006 Round 3)
263        addCmd(TBaseType.rrw_create, "restore", "point", " ", " ", " ", " ",
264                ESqlStatementType.sstoceanbase_create_restore_point);
265        addCmd(TBaseType.rrw_drop, "restore", "point", " ", " ", " ", " ",
266                ESqlStatementType.sstoceanbase_drop_restore_point);
267    }
268
269    @Override
270    protected String getToken1Str(int token1) {
271        if (token1 == TBaseType.rrw_oceanbase_xa) {
272            return "XA";
273        }
274        return super.getToken1Str(token1);
275    }
276
277    // ================================================================
278    // Statement construction override — Phase 4 Batch 1
279    // ================================================================
280
281    /**
282     * {@inheritDoc}
283     *
284     * <p>The inherited {@link TSqlCmdsMysql#issql} switch statement does not
285     * know about {@code sstoceanbase_*} enum values and routes them to its
286     * {@code default} branch, which produces a {@link TUnknownSqlStatement}.
287     * This override intercepts the result and swaps in the proper OceanBase
288     * statement class when a tenant-family type is detected, preserving the
289     * splitter's tag on the returned instance.
290     */
291    @Override
292    public TCustomSqlStatement issql(TSourceToken pcst,
293                                     EFindSqlStateType pstate,
294                                     TCustomSqlStatement psqlstatement) {
295        TCustomSqlStatement ret = super.issql(pcst, pstate, psqlstatement);
296        if (ret instanceof TUnknownSqlStatement) {
297            switch (ret.sqlstatementtype) {
298                case sstoceanbase_create_tenant:
299                    return new TCreateTenantSqlStatement(vendor);
300                case sstoceanbase_alter_tenant:
301                    return new TAlterTenantSqlStatement(vendor);
302                case sstoceanbase_drop_tenant:
303                    return new TDropTenantSqlStatement(vendor);
304                case sstoceanbase_create_resource_pool:
305                    return new TCreateResourcePoolSqlStatement(vendor);
306                case sstoceanbase_alter_resource_pool:
307                    return new TAlterResourcePoolSqlStatement(vendor);
308                case sstoceanbase_drop_resource_pool:
309                    return new TDropResourcePoolSqlStatement(vendor);
310                case sstoceanbase_create_resource_unit:
311                    return new TCreateResourceUnitSqlStatement(vendor);
312                case sstoceanbase_alter_resource_unit:
313                    return new TAlterResourceUnitSqlStatement(vendor);
314                case sstoceanbase_drop_resource_unit:
315                    return new TDropResourceUnitSqlStatement(vendor);
316                case sstoceanbase_create_tablegroup:
317                    return new TCreateTablegroupSqlStatement(vendor);
318                case sstoceanbase_alter_tablegroup:
319                    return new TAlterTablegroupSqlStatement(vendor);
320                case sstoceanbase_drop_tablegroup:
321                    return new TDropTablegroupSqlStatement(vendor);
322                // Phase 4 Batch 8 — OUTLINE DDL reuses Oracle's existing
323                // enums per ADR-8
324                case sstoraclecreateoutline:
325                    return new TCreateOutlineSqlStatement(vendor);
326                case sstoraclealteroutline:
327                    return new TAlterOutlineSqlStatement(vendor);
328                case sstoracledropoutline:
329                    return new TDropOutlineSqlStatement(vendor);
330                // Phase 4 Batch 3 + 10 — ALTER SYSTEM
331                case sstoceanbase_alter_system:
332                    return new TAlterSystemSqlStatement(vendor);
333                // OceanBase documented syntax gaps — SHOW TENANT family
334                case sstoceanbase_show_tenant:
335                    return new TShowTenantSqlStatement(vendor);
336                // US-003 — FLASHBACK / PURGE
337                case sstoceanbase_flashback:
338                    return new TFlashbackSqlStatement(vendor);
339                case sstoceanbase_purge:
340                    return new TPurgeSqlStatement(vendor);
341                // US-004 Round 3 — XA transaction
342                case sstoceanbase_xa:
343                    return new TXaSqlStatement(vendor);
344                // US-005 Round 3 — DBLINK DDL
345                case sstoceanbase_create_dblink:
346                    return new TCreateDblinkSqlStatement(vendor);
347                case sstoceanbase_drop_dblink:
348                    return new TDropDblinkSqlStatement(vendor);
349                // US-006 Round 3 — RESTORE POINT
350                case sstoceanbase_create_restore_point:
351                    return new TCreateRestorePointSqlStatement(vendor);
352                case sstoceanbase_drop_restore_point:
353                    return new TDropRestorePointSqlStatement(vendor);
354                default:
355                    break;
356            }
357        }
358        return ret;
359    }
360
361    // ================================================================
362    // System-tenant prefix diagnostic (used by OceanBaseSqlParser when
363    // the active tenant mode is ORACLE)
364    // ================================================================
365
366    /**
367     * Regex anchored at a statement boundary ({@code ^} or {@code ;}) that
368     * matches only OceanBase system-tenant DDL forms that have no Oracle
369     * equivalent.
370     *
371     * <p><b>Why this list is strictly smaller than the splitter list.</b>
372     * {@code initializeCommands()} registers {@code ALTER SYSTEM} as a
373     * boundary prefix because the splitter just uses it to split raw
374     * statements and never promotes the tenant mode. But {@code ALTER
375     * SYSTEM} is also legitimate Oracle DBA syntax ({@code ALTER SYSTEM
376     * KILL SESSION}, {@code ALTER SYSTEM FLUSH SHARED_POOL}, etc.), and
377     * OceanBase Oracle-mode tenants accept a subset of those forms too.
378     * Flagging {@code ALTER SYSTEM} from ORACLE mode would produce false
379     * positives on perfectly legal scripts. Only the verbs that do not
380     * exist in Oracle at all — {@code CREATE/ALTER/DROP TENANT} and
381     * {@code CREATE/ALTER/DROP RESOURCE POOL|UNIT} — are safe to flag.
382     *
383     * <p>The regex is case-insensitive and requires a word boundary after
384     * the prefix so {@code CREATE TENANTS_VIEW} (hypothetical) would not
385     * false-match.
386     */
387    private static final Pattern SYSTEM_DDL_PREFIX_PATTERN = Pattern.compile(
388            "(?i)(?:^|;)\\s*(" +
389                    "create\\s+tenant|" +
390                    "alter\\s+tenant|" +
391                    "drop\\s+tenant|" +
392                    "(?:create|alter|drop)\\s+resource\\s+(?:pool|unit)" +
393            ")\\b"
394    );
395
396    private static final Pattern BLOCK_COMMENT_PATTERN =
397            Pattern.compile("/\\*[\\s\\S]*?\\*/");
398    private static final Pattern LINE_COMMENT_PATTERN =
399            Pattern.compile("--[^\\n]*");
400    private static final Pattern SINGLE_QUOTE_STRING_PATTERN =
401            Pattern.compile("'(?:''|[^'])*'");
402    private static final Pattern DOUBLE_QUOTE_IDENT_PATTERN =
403            Pattern.compile("\"(?:\"\"|[^\"])*\"");
404
405    /**
406     * Scan {@code sqltext} for an unmistakable OceanBase system-tenant DDL
407     * prefix at a statement boundary. Comments and string literals are
408     * masked before the scan so text inside them cannot trigger a false
409     * positive.
410     *
411     * <p>This helper exists so {@code OceanBaseSqlParser} can emit a
412     * targeted error when a user submits system-tenant DDL while the
413     * parser is in {@code EOBTenantMode.ORACLE}, matching the real
414     * OceanBase server's "wrong tenant" rejection with a clearer
415     * message than a raw Oracle yacc syntax error at the {@code TENANT}
416     * token.
417     *
418     * <p>Per ADR-7 the check is narrow by design and the caller still
419     * owns mode selection — this helper reports; it never promotes.
420     *
421     * @param sqltext the raw SQL script to scan; may be null
422     * @return a canonical uppercase prefix name (for example
423     *         {@code "CREATE TENANT"}) when a system-only prefix is
424     *         found, or {@code null} when the script contains no such
425     *         prefix (or the input is null / empty)
426     * @since 4.0.1.4
427     */
428    public static String detectSystemPrefixConflict(String sqltext) {
429        if (sqltext == null || sqltext.isEmpty()) {
430            return null;
431        }
432        // Mask comments and string / double-quoted-identifier literals.
433        // Positions are preserved (masks are runs of spaces) so that any
434        // future line/column reporting remains accurate; newlines inside
435        // masked runs are retained so line counting still works.
436        String cleaned = maskRuns(sqltext, BLOCK_COMMENT_PATTERN);
437        cleaned = maskRuns(cleaned, LINE_COMMENT_PATTERN);
438        cleaned = maskRuns(cleaned, SINGLE_QUOTE_STRING_PATTERN);
439        cleaned = maskRuns(cleaned, DOUBLE_QUOTE_IDENT_PATTERN);
440
441        Matcher m = SYSTEM_DDL_PREFIX_PATTERN.matcher(cleaned);
442        if (m.find()) {
443            // Collapse internal whitespace and uppercase for a canonical
444            // prefix name in the error message (for example "create\ttenant"
445            // becomes "CREATE TENANT").
446            return m.group(1).replaceAll("\\s+", " ").toUpperCase();
447        }
448        return null;
449    }
450
451    /**
452     * Replace every match of {@code pattern} in {@code s} with a run of
453     * ASCII spaces of the same length. Newlines and carriage returns
454     * inside the match are preserved so line/column positions computed
455     * from the masked string stay consistent with the original text.
456     */
457    private static String maskRuns(String s, Pattern pattern) {
458        Matcher m = pattern.matcher(s);
459        if (!m.find()) {
460            return s;
461        }
462        StringBuilder sb = new StringBuilder(s);
463        do {
464            for (int i = m.start(); i < m.end(); i++) {
465                char c = sb.charAt(i);
466                if (c != '\n' && c != '\r') {
467                    sb.setCharAt(i, ' ');
468                }
469            }
470        } while (m.find());
471        return sb.toString();
472    }
473}