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 // The MySQL base now registers these heads too (with sstmysql* types), 219 // so override the inherited entries in place instead of appending 220 // duplicates that lose the first-registered tie. 221 overrideCmd(TBaseType.rrw_create, "tablespace", "", 222 ESqlStatementType.sstcreateTablespace); 223 overrideCmd(TBaseType.rrw_drop, "tablespace", "", 224 ESqlStatementType.sstcreateTablespace); 225 226 // ALTER USER / CREATE ROLE / DROP ROLE (US-002 Round 2) 227 overrideCmd(TBaseType.rrw_alter, "user", "", 228 ESqlStatementType.sstalteruser); 229 addCmd(TBaseType.rrw_create, "role", " ", " ", " ", " ", " ", 230 ESqlStatementType.sstcreaterole); 231 addCmd(TBaseType.rrw_drop, "role", " ", " ", " ", " ", " ", 232 ESqlStatementType.sstdroprole); 233 234 // ALTER SEQUENCE (US-009) — MySQL base has CREATE/DROP SEQUENCE 235 // but not ALTER SEQUENCE. OceanBase supports ALTER SEQUENCE for 236 // modifying sequence properties (INCREMENT BY, MAXVALUE, etc.). 237 addCmd(TBaseType.rrw_alter, "sequence", " ", " ", " ", " ", " ", 238 ESqlStatementType.sstaltersequence); 239 240 // CREATE/ALTER/DROP OUTLINE (Phase 4 Batch 8) 241 // Reuse Oracle's existing sstoracle*outline enum values per ADR-8 242 // — OUTLINE is not a MySQL construct, so the base MySQL resolver 243 // knows nothing about these types. Our issql override maps them 244 // to the OceanBase statement classes. 245 addCmd(TBaseType.rrw_create, "outline", " ", " ", " ", " ", " ", 246 ESqlStatementType.sstoraclecreateoutline); 247 addCmd(TBaseType.rrw_create, "or", "replace", "outline", " ", " ", " ", 248 ESqlStatementType.sstoraclecreateoutline); 249 addCmd(TBaseType.rrw_alter, "outline", " ", " ", " ", " ", " ", 250 ESqlStatementType.sstoraclealteroutline); 251 addCmd(TBaseType.rrw_drop, "outline", " ", " ", " ", " ", " ", 252 ESqlStatementType.sstoracledropoutline); 253 254 // XA transaction statements (US-004 Round 3) 255 // XA START/END/PREPARE/COMMIT/ROLLBACK/RECOVER 'xid' 256 addCmd(TBaseType.rrw_oceanbase_xa, " ", " ", " ", " ", " ", " ", 257 ESqlStatementType.sstoceanbase_xa); 258 259 // CREATE/DROP DBLINK (US-005 Round 3) 260 addCmd(TBaseType.rrw_create, "dblink", " ", " ", " ", " ", " ", 261 ESqlStatementType.sstoceanbase_create_dblink); 262 addCmd(TBaseType.rrw_drop, "dblink", " ", " ", " ", " ", " ", 263 ESqlStatementType.sstoceanbase_drop_dblink); 264 265 // CREATE/DROP RESTORE POINT (US-006 Round 3) 266 addCmd(TBaseType.rrw_create, "restore", "point", " ", " ", " ", " ", 267 ESqlStatementType.sstoceanbase_create_restore_point); 268 addCmd(TBaseType.rrw_drop, "restore", "point", " ", " ", " ", " ", 269 ESqlStatementType.sstoceanbase_drop_restore_point); 270 } 271 272 @Override 273 protected String getToken1Str(int token1) { 274 if (token1 == TBaseType.rrw_oceanbase_xa) { 275 return "XA"; 276 } 277 return super.getToken1Str(token1); 278 } 279 280 // ================================================================ 281 // Statement construction override — Phase 4 Batch 1 282 // ================================================================ 283 284 /** 285 * {@inheritDoc} 286 * 287 * <p>The inherited {@link TSqlCmdsMysql#issql} switch statement does not 288 * know about {@code sstoceanbase_*} enum values and routes them to its 289 * {@code default} branch, which produces a {@link TUnknownSqlStatement}. 290 * This override intercepts the result and swaps in the proper OceanBase 291 * statement class when a tenant-family type is detected, preserving the 292 * splitter's tag on the returned instance. 293 */ 294 @Override 295 public TCustomSqlStatement issql(TSourceToken pcst, 296 EFindSqlStateType pstate, 297 TCustomSqlStatement psqlstatement) { 298 TCustomSqlStatement ret = super.issql(pcst, pstate, psqlstatement); 299 if (ret instanceof TUnknownSqlStatement) { 300 switch (ret.sqlstatementtype) { 301 case sstoceanbase_create_tenant: 302 return new TCreateTenantSqlStatement(vendor); 303 case sstoceanbase_alter_tenant: 304 return new TAlterTenantSqlStatement(vendor); 305 case sstoceanbase_drop_tenant: 306 return new TDropTenantSqlStatement(vendor); 307 case sstoceanbase_create_resource_pool: 308 return new TCreateResourcePoolSqlStatement(vendor); 309 case sstoceanbase_alter_resource_pool: 310 return new TAlterResourcePoolSqlStatement(vendor); 311 case sstoceanbase_drop_resource_pool: 312 return new TDropResourcePoolSqlStatement(vendor); 313 case sstoceanbase_create_resource_unit: 314 return new TCreateResourceUnitSqlStatement(vendor); 315 case sstoceanbase_alter_resource_unit: 316 return new TAlterResourceUnitSqlStatement(vendor); 317 case sstoceanbase_drop_resource_unit: 318 return new TDropResourceUnitSqlStatement(vendor); 319 case sstoceanbase_create_tablegroup: 320 return new TCreateTablegroupSqlStatement(vendor); 321 case sstoceanbase_alter_tablegroup: 322 return new TAlterTablegroupSqlStatement(vendor); 323 case sstoceanbase_drop_tablegroup: 324 return new TDropTablegroupSqlStatement(vendor); 325 // Phase 4 Batch 8 — OUTLINE DDL reuses Oracle's existing 326 // enums per ADR-8 327 case sstoraclecreateoutline: 328 return new TCreateOutlineSqlStatement(vendor); 329 case sstoraclealteroutline: 330 return new TAlterOutlineSqlStatement(vendor); 331 case sstoracledropoutline: 332 return new TDropOutlineSqlStatement(vendor); 333 // Phase 4 Batch 3 + 10 — ALTER SYSTEM 334 case sstoceanbase_alter_system: 335 return new TAlterSystemSqlStatement(vendor); 336 // OceanBase documented syntax gaps — SHOW TENANT family 337 case sstoceanbase_show_tenant: 338 return new TShowTenantSqlStatement(vendor); 339 // US-003 — FLASHBACK / PURGE 340 case sstoceanbase_flashback: 341 return new TFlashbackSqlStatement(vendor); 342 case sstoceanbase_purge: 343 return new TPurgeSqlStatement(vendor); 344 // US-004 Round 3 — XA transaction 345 case sstoceanbase_xa: 346 return new TXaSqlStatement(vendor); 347 // US-005 Round 3 — DBLINK DDL 348 case sstoceanbase_create_dblink: 349 return new TCreateDblinkSqlStatement(vendor); 350 case sstoceanbase_drop_dblink: 351 return new TDropDblinkSqlStatement(vendor); 352 // US-006 Round 3 — RESTORE POINT 353 case sstoceanbase_create_restore_point: 354 return new TCreateRestorePointSqlStatement(vendor); 355 case sstoceanbase_drop_restore_point: 356 return new TDropRestorePointSqlStatement(vendor); 357 default: 358 break; 359 } 360 } 361 return ret; 362 } 363 364 // ================================================================ 365 // System-tenant prefix diagnostic (used by OceanBaseSqlParser when 366 // the active tenant mode is ORACLE) 367 // ================================================================ 368 369 /** 370 * Regex anchored at a statement boundary ({@code ^} or {@code ;}) that 371 * matches only OceanBase system-tenant DDL forms that have no Oracle 372 * equivalent. 373 * 374 * <p><b>Why this list is strictly smaller than the splitter list.</b> 375 * {@code initializeCommands()} registers {@code ALTER SYSTEM} as a 376 * boundary prefix because the splitter just uses it to split raw 377 * statements and never promotes the tenant mode. But {@code ALTER 378 * SYSTEM} is also legitimate Oracle DBA syntax ({@code ALTER SYSTEM 379 * KILL SESSION}, {@code ALTER SYSTEM FLUSH SHARED_POOL}, etc.), and 380 * OceanBase Oracle-mode tenants accept a subset of those forms too. 381 * Flagging {@code ALTER SYSTEM} from ORACLE mode would produce false 382 * positives on perfectly legal scripts. Only the verbs that do not 383 * exist in Oracle at all — {@code CREATE/ALTER/DROP TENANT} and 384 * {@code CREATE/ALTER/DROP RESOURCE POOL|UNIT} — are safe to flag. 385 * 386 * <p>The regex is case-insensitive and requires a word boundary after 387 * the prefix so {@code CREATE TENANTS_VIEW} (hypothetical) would not 388 * false-match. 389 */ 390 private static final Pattern SYSTEM_DDL_PREFIX_PATTERN = Pattern.compile( 391 "(?i)(?:^|;)\\s*(" + 392 "create\\s+tenant|" + 393 "alter\\s+tenant|" + 394 "drop\\s+tenant|" + 395 "(?:create|alter|drop)\\s+resource\\s+(?:pool|unit)" + 396 ")\\b" 397 ); 398 399 private static final Pattern BLOCK_COMMENT_PATTERN = 400 Pattern.compile("/\\*[\\s\\S]*?\\*/"); 401 private static final Pattern LINE_COMMENT_PATTERN = 402 Pattern.compile("--[^\\n]*"); 403 private static final Pattern SINGLE_QUOTE_STRING_PATTERN = 404 Pattern.compile("'(?:''|[^'])*'"); 405 private static final Pattern DOUBLE_QUOTE_IDENT_PATTERN = 406 Pattern.compile("\"(?:\"\"|[^\"])*\""); 407 408 /** 409 * Scan {@code sqltext} for an unmistakable OceanBase system-tenant DDL 410 * prefix at a statement boundary. Comments and string literals are 411 * masked before the scan so text inside them cannot trigger a false 412 * positive. 413 * 414 * <p>This helper exists so {@code OceanBaseSqlParser} can emit a 415 * targeted error when a user submits system-tenant DDL while the 416 * parser is in {@code EOBTenantMode.ORACLE}, matching the real 417 * OceanBase server's "wrong tenant" rejection with a clearer 418 * message than a raw Oracle yacc syntax error at the {@code TENANT} 419 * token. 420 * 421 * <p>Per ADR-7 the check is narrow by design and the caller still 422 * owns mode selection — this helper reports; it never promotes. 423 * 424 * @param sqltext the raw SQL script to scan; may be null 425 * @return a canonical uppercase prefix name (for example 426 * {@code "CREATE TENANT"}) when a system-only prefix is 427 * found, or {@code null} when the script contains no such 428 * prefix (or the input is null / empty) 429 * @since 4.0.1.4 430 */ 431 public static String detectSystemPrefixConflict(String sqltext) { 432 if (sqltext == null || sqltext.isEmpty()) { 433 return null; 434 } 435 // Mask comments and string / double-quoted-identifier literals. 436 // Positions are preserved (masks are runs of spaces) so that any 437 // future line/column reporting remains accurate; newlines inside 438 // masked runs are retained so line counting still works. 439 String cleaned = maskRuns(sqltext, BLOCK_COMMENT_PATTERN); 440 cleaned = maskRuns(cleaned, LINE_COMMENT_PATTERN); 441 cleaned = maskRuns(cleaned, SINGLE_QUOTE_STRING_PATTERN); 442 cleaned = maskRuns(cleaned, DOUBLE_QUOTE_IDENT_PATTERN); 443 444 Matcher m = SYSTEM_DDL_PREFIX_PATTERN.matcher(cleaned); 445 if (m.find()) { 446 // Collapse internal whitespace and uppercase for a canonical 447 // prefix name in the error message (for example "create\ttenant" 448 // becomes "CREATE TENANT"). 449 return m.group(1).replaceAll("\\s+", " ").toUpperCase(); 450 } 451 return null; 452 } 453 454 /** 455 * Replace every match of {@code pattern} in {@code s} with a run of 456 * ASCII spaces of the same length. Newlines and carriage returns 457 * inside the match are preserved so line/column positions computed 458 * from the masked string stay consistent with the original text. 459 */ 460 private static String maskRuns(String s, Pattern pattern) { 461 Matcher m = pattern.matcher(s); 462 if (!m.find()) { 463 return s; 464 } 465 StringBuilder sb = new StringBuilder(s); 466 do { 467 for (int i = m.start(); i < m.end(); i++) { 468 char c = sb.charAt(i); 469 if (c != '\n' && c != '\r') { 470 sb.setCharAt(i, ' '); 471 } 472 } 473 } while (m.find()); 474 return sb.toString(); 475 } 476}