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 /** 400 * Scan {@code sqltext} for an unmistakable OceanBase system-tenant DDL 401 * prefix at a statement boundary. Comments and string literals are 402 * masked before the scan so text inside them cannot trigger a false 403 * positive. 404 * 405 * <p>This helper exists so {@code OceanBaseSqlParser} can emit a 406 * targeted error when a user submits system-tenant DDL while the 407 * parser is in {@code EOBTenantMode.ORACLE}, matching the real 408 * OceanBase server's "wrong tenant" rejection with a clearer 409 * message than a raw Oracle yacc syntax error at the {@code TENANT} 410 * token. 411 * 412 * <p>Per ADR-7 the check is narrow by design and the caller still 413 * owns mode selection — this helper reports; it never promotes. 414 * 415 * @param sqltext the raw SQL script to scan; may be null 416 * @return a canonical uppercase prefix name (for example 417 * {@code "CREATE TENANT"}) when a system-only prefix is 418 * found, or {@code null} when the script contains no such 419 * prefix (or the input is null / empty) 420 * @since 4.0.1.4 421 */ 422 public static String detectSystemPrefixConflict(String sqltext) { 423 if (sqltext == null || sqltext.isEmpty()) { 424 return null; 425 } 426 // Mask comments and string / double-quoted-identifier literals. 427 // Positions are preserved (masks are runs of spaces) so that any 428 // future line/column reporting remains accurate; newlines inside 429 // masked runs are retained so line counting still works. 430 String cleaned = maskCommentsAndLiterals(sqltext); 431 432 Matcher m = SYSTEM_DDL_PREFIX_PATTERN.matcher(cleaned); 433 if (m.find()) { 434 // Collapse internal whitespace and uppercase for a canonical 435 // prefix name in the error message (for example "create\ttenant" 436 // becomes "CREATE TENANT"). 437 return m.group(1).replaceAll("\\s+", " ").toUpperCase(java.util.Locale.ROOT); 438 } 439 return null; 440 } 441 442 /** 443 * {@code sqltext} with its comments and quoted runs masked, in four passes over the text, each on the result of 444 * the one before: block comments ({@code /* ... *}{@code /}), line comments ({@code --} to the end of the line), 445 * single-quoted strings, then double-quoted identifiers. The order is part of the result: a {@code --} inside a 446 * string is masked as a line comment before strings are looked at. 447 * 448 * <p>Each pass finds what the regex it replaces found ({@code /\\*[\\s\\S]*?\\*}{@code /}, 449 * {@code --[^\\n]*}, {@code '(?:''|[^'])*'}, {@code "(?:""|[^"])*"}), left to right, in one linear scan. The 450 * regexes retried from every position of a long unterminated run, which made the scan of a whole script 451 * quadratic. 452 */ 453 static String maskCommentsAndLiterals(String sqltext) { 454 StringBuilder sb = new StringBuilder(sqltext); 455 maskBlockComments(sb); 456 maskLineComments(sb); 457 maskQuoted(sb, '\''); 458 maskQuoted(sb, '"'); 459 return sb.toString(); 460 } 461 462 /** Each {@code /*} up to the first {@code *}{@code /} that starts after it; an unclosed one masks nothing. */ 463 private static void maskBlockComments(StringBuilder sb) { 464 int from = 0; 465 while (true) { 466 int start = sb.indexOf("/*", from); 467 if (start < 0) { 468 return; 469 } 470 int close = sb.indexOf("*/", start + 2); 471 if (close < 0) { 472 return; // no later "/*" can close either 473 } 474 mask(sb, start, close + 2); 475 from = close + 2; 476 } 477 } 478 479 /** Each {@code --} up to, not including, the next line feed. */ 480 private static void maskLineComments(StringBuilder sb) { 481 int from = 0; 482 while (true) { 483 int start = sb.indexOf("--", from); 484 if (start < 0) { 485 return; 486 } 487 int end = sb.indexOf("\n", start + 2); 488 if (end < 0) { 489 end = sb.length(); 490 } 491 mask(sb, start, end); 492 from = end; 493 } 494 } 495 496 /** 497 * Each run from a {@code quote} to the next quote that is not doubled. An unclosed run ends at the first quote 498 * of its last doubled pair, where the regex's backtracking ended it, and a run with no pair masks nothing. 499 */ 500 private static void maskQuoted(StringBuilder sb, char quote) { 501 int length = sb.length(); 502 int start = indexOf(sb, quote, 0); 503 while (start >= 0) { 504 int lastPair = -1; 505 int end = -1; 506 int j = start + 1; 507 while (j < length) { 508 if (sb.charAt(j) == quote) { 509 if (j + 1 < length && sb.charAt(j + 1) == quote) { 510 lastPair = j; 511 j += 2; 512 continue; 513 } 514 end = j + 1; 515 break; 516 } 517 j++; 518 } 519 if (end < 0 && lastPair >= 0) { 520 end = lastPair + 1; 521 } 522 if (end < 0) { 523 start = indexOf(sb, quote, start + 1); 524 continue; 525 } 526 mask(sb, start, end); 527 start = indexOf(sb, quote, end); 528 } 529 } 530 531 private static int indexOf(StringBuilder sb, char c, int from) { 532 for (int i = from; i < sb.length(); i++) { 533 if (sb.charAt(i) == c) { 534 return i; 535 } 536 } 537 return -1; 538 } 539 540 /** 541 * Replace {@code [start, end)} with ASCII spaces, keeping newlines and carriage returns so line/column positions 542 * computed from the masked string stay consistent with the original text. 543 */ 544 private static void mask(StringBuilder sb, int start, int end) { 545 for (int i = start; i < end; i++) { 546 char c = sb.charAt(i); 547 if (c != '\n' && c != '\r') { 548 sb.setCharAt(i, ' '); 549 } 550 } 551 } 552}