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}