001package gudusoft.gsqlparser.stmt; 002 003 004import gudusoft.gsqlparser.*; 005import gudusoft.gsqlparser.compiler.TFrame; 006import gudusoft.gsqlparser.compiler.TSymbolTableManager; 007import gudusoft.gsqlparser.compiler.TVariable; 008import gudusoft.gsqlparser.nodes.*; 009import gudusoft.gsqlparser.nodes.mssql.TProcedureOption; 010 011/** 012 * Create procedure statement. 013 * 存储过程如果是 $$ body $$ 形式的,有两种解析方法, 014 * 方法1,把 $$ body $$ 中的 body 在 lexical 阶段即分解为独立的 token, 整个 create procedure statement 在 parser 中一次性完成解析 015 * 采用这个方法的数据库: greenplum, postgres, redshift, snowflake 016 * 017 * GSP Java version 2.8.9.3(2024-5-27) [Snowflake] support string literal delimiter $ or ' around procedure definition. 018 * '' 中的存储过程代码被 {@link TGSqlParser.expandDollarString()} 方法处理,处理后的代码被解析。 019 * 020 * 2024/6/22, snowflake create procedure 的 body 如果是采用 $$ body $$, 在 lexer 中分解为独立的 token 021 * 022 * 方法2,把 $$ body $$ 作为一个整体字符串看待,需要进行二次解析, 都是调用 postgres parser 进行的分析。采用这种方法是,body中用到的 select 023 * 等语句用的是 postgres的语法分析,而不是原数据库 select 语句的语法分析,这可能导致不正确的解析结果。 024 * 这种方法为历史遗留问题,需要都改为方法1。目前采用这种方法的数据库有: 025 * netezza, 026 * vertica 027 * 028 * Supported databases 029 * <ul> 030 * <li>Postgres</li> 031 *<li>redshift</li> 032 * </ul> 033 */ 034public class TCreateProcedureStmt extends TRoutine{ 035 036 private TObjectName functionName = null; 037 private TConstant objfile; 038 private TConstant linkSymbol; 039 private TConstant procedureLanguage; 040 private TTableElementList returnTableDefinitions = null; 041 042 /** 043 * Returns the column definitions from the RETURNS TABLE(...) clause. 044 * For example, for {@code RETURNS TABLE(id INT, name VARCHAR)}, this returns 045 * a list containing column definitions for "id INT" and "name VARCHAR". 046 * Returns null if the procedure does not use RETURNS TABLE or has an empty TABLE(). 047 * <p>Currently supported for Snowflake.</p> 048 * @return the column definitions list, or null 049 */ 050 public TTableElementList getReturnTableDefinitions() { 051 return returnTableDefinitions; 052 } 053 054 public TConstant getProcedureLanguage() { 055 return procedureLanguage; 056 } 057 058 public TConstant getLinkSymbol() { 059 return linkSymbol; 060 } 061 public TConstant getObjfile() { 062 return objfile; 063 } 064 065 private TPTNodeList <TProcedureOption> procedureOptions; 066 private boolean isForReplication; 067 068 public void setProcedureOptions(TPTNodeList<TProcedureOption> procedureOptions) { 069 this.procedureOptions = procedureOptions; 070 } 071 072 public TPTNodeList<TProcedureOption> getProcedureOptions() { 073 074 return procedureOptions; 075 } 076 077 public void setForReplication(boolean isForReplication) { 078 this.isForReplication = isForReplication; 079 } 080 081 public boolean isForReplication() { 082 return isForReplication; 083 } 084 085 public TCreateProcedureStmt (EDbVendor dbvendor){ 086 super(dbvendor); 087 sqlstatementtype = ESqlStatementType.sstcreateprocedure ; 088 } 089 090 @Override 091 public TObjectName getStoredProcedureName(){ 092 return procedureName; 093 } 094 095 /** 096 * The name that you give to the procedure that you are declaring or defining. 097 * @return 098 */ 099 public TObjectName getProcedureName() { 100 return procedureName; 101 } 102 103 private TObjectName procedureName = null; 104 105 private TObjectName externalName = null; 106 107 /** 108 * SQL Server CLR procedure body: the <code>assembly.class.method</code> name 109 * that follows <code>AS EXTERNAL NAME</code>. Null for every procedure that 110 * has a SQL body. 111 * 112 * @return the external method name, or null 113 * @since 4.1.9 114 */ 115 public TObjectName getExternalName() { 116 return externalName; 117 } 118 119 120 public int doParseStatement(TCustomSqlStatement psql) { 121 if (rootNode == null) return -1; 122 // rootNode may be a stale node left over from a previous statement parsed by 123 // the shared vendor parser when this statement failed checksyntax (yyparse does 124 // not reset rootNode on failure). The Sybase re-parse path in 125 // TCustomSqlStatement.parsestatement() calls doParseStatement() whenever 126 // rootNode != null, so guard the cast to avoid a ClassCastException (Mantis 4453). 127 // Mirrors the same guard already used by the Informix/Teradata create-procedure stmts. 128 if (!(rootNode instanceof TCreateProcedureSqlNode)) return -1; 129 TCreateProcedureSqlNode createProcedureNode = (TCreateProcedureSqlNode)rootNode; 130 131 super.doParseStatement(psql); 132 TFrame currentFrame = new TFrame(this.stmtScope); 133 currentFrame.pushMeToStack(getFrameStack()); 134 135 procedureName = createProcedureNode.getProcedureName(); 136 137 if (getSqlEnv() != null) { 138 getSqlEnv().addProcedure(procedureName.toString(),true); 139 140 // move to TDatabaseObjectResolver 141 142// if (getSqlEnv().getDefaultCatalogName() != null){ 143// if (procedureName.getDatabaseToken() == null){ 144// procedureName.setDatabaseToken(new TSourceToken(getSqlEnv().getDefaultCatalogName())); 145// } 146// } 147 } 148 149 setRoutineName(procedureName); 150 151 procedureLanguage = createProcedureNode.getProcedureLanguage(); 152 if (procedureLanguage != null){ 153 if (getRoutineLanguage() == null){ 154 // not already set during TGsqlParser.dopostgresqlgetrawsqlstatements() 155 // then we set it here 156 setRoutineLanguage(procedureLanguage.toString()); 157 } 158 setRoutineLanguageInConstant(procedureLanguage); 159 }else { 160 161 if (dbvendor == EDbVendor.dbvvertica){ 162 // vertica, set default to plvsql 163 TConstant c = new TConstant(); 164 c.init(ELiteralType.etString, new TSourceToken("plvsql")); 165 procedureLanguage = c; 166 setRoutineLanguageInConstant(procedureLanguage); 167 setRoutineLanguage("plvsql"); 168 } 169 170 } 171 this.setLabelName(createProcedureNode.getLabelName()); 172 objfile = createProcedureNode.getObjfile(); 173 linkSymbol = createProcedureNode.getLinkSymbol(); 174 this.setReturnDataType(createProcedureNode.getReturnDataType()); 175 // Extract RETURNS TABLE column definitions (Snowflake) 176 if (createProcedureNode.getReturnTable() != null) { 177 TDummy dmy = createProcedureNode.getReturnTable(); 178 if (dmy.list1 instanceof TTableElementList) { 179 this.returnTableDefinitions = (TTableElementList) dmy.list1; 180 this.returnTableDefinitions.doParse(this, ESqlClause.unknown); 181 } 182 } 183 if (createProcedureNode.getFunctionBody() != null){ 184 setRoutineBodyInConstant(createProcedureNode.getFunctionBody()); 185 setRoutineBody(createProcedureNode.getFunctionBody().toString()); 186 } 187 188 this.setParameterDeclarations( createProcedureNode.getParameters()); 189 // sql server 190 procedureOptions = createProcedureNode.getProcedureOptions(); 191 isForReplication = createProcedureNode.isForReplication(); 192 this.setReturnDataType(createProcedureNode.getReturnDataType()); 193 // The CLR method name is not a database object, so it is exposed through 194 // getExternalName() but deliberately kept out of acceptChildren(). 195 if (createProcedureNode.getExternalName() != null){ 196 this.externalName = createProcedureNode.getExternalName(); 197 } 198 // end sql server 199 200 // push parameterDeclarations into symbolTable 201 if (this.getParameterDeclarations() != null){ 202 for(int i=0;i< this.getParameterDeclarations().size();i++){ 203 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjParameter,this, this.getParameterDeclarations().getParameterDeclarationItem(i))); 204 TParameterDeclaration parameterDeclaration = this.getParameterDeclarations().getParameterDeclarationItem(i); 205 if (parameterDeclaration.getParameterName() != null){ 206 TVariable variable = new TVariable(parameterDeclaration.getParameterName(),parameterDeclaration,procedureName); 207 208 TVariable variableOfThisParam = TSymbolTableManager.searchSymbolVariable(this.getFrameStack(),procedureName+".$"+i); 209 if (variableOfThisParam != null){ 210 variable.setVariableStr(variableOfThisParam.getVariableStr()); 211 } 212 213 // variable.variableStr = TBaseType.getStringInsideLiteral(getSqlEnv().getProcedureParameterValue(procedureName.toString(), i)); 214 this.stmtScope.addSymbol(variable); 215 } 216 } 217 } 218 219 220 switch (dbvendor){ 221 case dbvdb2: 222 TCompoundSqlNode compoundSqlNode = createProcedureNode.getCompoundSqls(); 223 if (compoundSqlNode != null){ 224 if (compoundSqlNode.getDeclareStmts() != null){ 225 compoundSqlNode.getDeclareStmts().doParse(this,ESqlClause.unknown); 226 227 // push variable declare into symbolTable, and add to declareStatements 228 for(int i=0;i<compoundSqlNode.getDeclareStmts().size();i++){ 229 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,compoundSqlNode.getDeclareStmts().getStatementSqlNode(i).getStmt() )); 230 this.getDeclareStatements().add(compoundSqlNode.getDeclareStmts().getStatementSqlNode(i).getStmt()); 231 } 232 } 233 234 if (compoundSqlNode.getStmts() != null){ 235 compoundSqlNode.getStmts().doParse(this,ESqlClause.unknown); 236 237 for(int i= 0; i<compoundSqlNode.getStmts().size();i++){ 238 this.getBodyStatements().add(compoundSqlNode.getStmts().getStatementSqlNode(i).getStmt()); 239 } 240 } 241 242 if (compoundSqlNode.getDeclareStmts() != null){ 243 // pop variable declare from symbolTable 244 for(int i=0;i<compoundSqlNode.getDeclareStmts().size();i++){ 245 this.getTopStatement().getSymbolTable().pop(); 246 } 247 } 248 } 249 break; 250 case dbvsnowflake: 251 if (createProcedureNode.getBlcok() != null){ 252 createProcedureNode.getBlcok().doParse(this,ESqlClause.unknown); 253 this.blockBody = createProcedureNode.getBlcok(); 254 }else{ 255 // javascript code body 256 } 257 258 break; 259 case dbvnetezza: 260 if (createProcedureNode.getStmts() != null){ 261 createProcedureNode.getStmts().doParse(this, ESqlClause.unknown); 262 263 TCommonBlock commonBlock = (TCommonBlock)createProcedureNode.getStmts().getStatementSqlNode(0).getStmt(); 264 this.setLabelName(commonBlock.getLabelName()); 265 this.blockBody = commonBlock.getBlockBody(); 266 this.blockBody.setParent(this); 267 268// for(int i=0;i<createProcedureNode.getStmts().size();i++){ 269// this.getBodyStatements().add(createProcedureNode.getStmts().getStatementSqlNode(i).getStmt()); 270// } 271 }else if ((createProcedureNode.getFunctionBody() != null)&&(getProcedureLanguage() != null)&&(getProcedureLanguage().toString() !=null)){ 272 // use constant in procedure body 273 processProcedureDefinition(EDbVendor.dbvnetezza,psql,createProcedureNode); 274 } 275 break; 276 default: 277 if (createProcedureNode.getDeclareStmts() != null){ 278 createProcedureNode.getDeclareStmts().doParse(this, ESqlClause.unknown); 279 280 // push variable declare into symbolTable , and add to declareStatements 281 for(int i=0;i<createProcedureNode.getDeclareStmts().size();i++){ 282 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,createProcedureNode.getDeclareStmts().getStatementSqlNode(i).getStmt() )); 283 this.getDeclareStatements().add(createProcedureNode.getDeclareStmts().getStatementSqlNode(i).getStmt()); 284 } 285 286 } 287 288 if (createProcedureNode.getStmt() != null){ 289 createProcedureNode.getStmt().doParse(this, ESqlClause.unknown); 290 this.getBodyStatements().add(createProcedureNode.getStmt().getStmt()); 291 if (getRoutineBody() == null){ 292 setRoutineBody(createProcedureNode.getStmt().toString()); 293 } 294 } 295 else if (createProcedureNode.getBlcok() != null){ 296 createProcedureNode.getBlcok().doParse(this,ESqlClause.unknown); 297 this.blockBody = createProcedureNode.getBlcok(); 298 if (getRoutineBody() == null){ 299 setRoutineBody(createProcedureNode.getBlcok().toString()); 300 } 301 }else if (createProcedureNode.getStmts() != null){ //teradata,sql server 302 createProcedureNode.getStmts().doParse(this, ESqlClause.unknown); 303 304 // Teradata: validate semicolons between statements in procedure body 305 if (this.dbvendor == EDbVendor.dbvteradata) { 306 validateSemicolonsBetweenStatements(this, createProcedureNode.getStmts()); 307 } 308 309 for(int i=0;i<createProcedureNode.getStmts().size();i++){ 310 this.getBodyStatements().add(createProcedureNode.getStmts().getStatementSqlNode(i).getStmt()); 311 } 312 if (getRoutineBody() == null){ 313 String stmtsStr = createProcedureNode.getStmts().toString(); 314 if (stmtsStr == null && this.getBodyStatements().size() > 0){ 315 // Fallback: construct routineBody from body statements text 316 StringBuilder sb = new StringBuilder(); 317 for(int i=0;i<this.getBodyStatements().size();i++){ 318 if (i > 0) sb.append("\n"); 319 sb.append(this.getBodyStatements().get(i).toString()); 320 } 321 stmtsStr = sb.toString(); 322 } 323 if (stmtsStr != null){ 324 setRoutineBody(stmtsStr); 325 } 326 } 327 } 328 329 if (createProcedureNode.getExceptionClause() != null){ 330 createProcedureNode.getExceptionClause().doParse(this,ESqlClause.unknown); 331 this.setExceptionClause(createProcedureNode.getExceptionClause()); 332 } 333 334 if (createProcedureNode.getDeclareStmts() != null){ 335 336 // pop variable declare from symbolTable 337 for(int i=0;i<createProcedureNode.getDeclareStmts().size();i++){ 338 this.getTopStatement().getSymbolTable().pop(); 339 } 340 341 } 342 343 break; 344 345 } 346 347 // pop parameterDeclarations from symbolTable 348 if (this.getParameterDeclarations() != null){ 349 for(int i=0;i< this.getParameterDeclarations().size();i++){ 350 this.getTopStatement().getSymbolTable().pop(); 351 } 352 } 353 354 switch (this.dbvendor){ 355 case dbvpostgresql: 356 case dbvredshift: 357 if (createProcedureNode.getFunctionBody() != null){ 358 // function body only inside '' will be processed here 359 processProcedureDefinition(EDbVendor.dbvpostgresql,psql,createProcedureNode); 360 }else{ 361 // procedure body only inside $$ already processed in the above default section 362 } 363 364 break; 365 366 case dbvvertica: 367 processProcedureDefinition(EDbVendor.dbvpostgresql,psql,createProcedureNode); 368 break; 369 case dbvsnowflake: 370 processProcedureDefinition(this.dbvendor,psql,createProcedureNode); 371 break; 372 } 373 374 375 this.setEndlabelName(createProcedureNode.getEndlabelName()); 376 377 currentFrame.popMeFromStack(getFrameStack()); 378 379 return 0; 380 } 381 382 private void doParseFunctionBody(EDbVendor dbVendor,String inputSQL,boolean isSQLBlock,int bodyDeltaLine){ 383 TGSqlParser newParser = new TGSqlParser(dbVendor); 384 newParser.sqltext = inputSQL; 385 newParser.setSinglePLBlock(isSQLBlock); 386 newParser.setFrameStack(getFrameStack()); 387 int iRet = newParser.parse(); 388 TReparseCoordinateShifter.shift(newParser, bodyDeltaLine, 0); 389 if ((iRet == 0)&&(newParser.getSqlstatements().size() >0)){ 390 if (isSQLBlock){ 391 TCommonBlock commonBlock = (TCommonBlock)newParser.getSqlstatements().get(0); 392 this.setLabelName(commonBlock.getLabelName()); 393 this.blockBody = commonBlock.getBlockBody(); 394 this.blockBody.setParent(this); 395// if (this.blockBody.getDeclareStatements().size()>0){ 396// //this.getDeclareStatements().clear(); 397// for(int i=0;i<this.blockBody.getDeclareStatements().size();i++){ 398// this.getDeclareStatements().add(this.blockBody.getDeclareStatements().get(i)); 399// } 400// } 401 402// for(int i=0;i<commonBlock.getDeclareStatements().size();i++){ 403// this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,commonBlock.getDeclareStatements().get(i))); 404// this.getDeclareStatements().add(commonBlock.getDeclareStatements().get(i)); 405// } 406// for(int i=0;i<commonBlock.getBodyStatements().size();i++){ 407// commonBlock.getBodyStatements().get(i).setAlreadyAddToParent(false); 408// commonBlock.getBodyStatements().get(i).setParentStmt(this); 409// 410// this.getBodyStatements().add(commonBlock.getBodyStatements().get(i)); 411// } 412// if (commonBlock.getExceptionClause() != null){ 413// this.setExceptionClause(commonBlock.getExceptionClause()); 414// } 415// for(int i=0;i<commonBlock.getDeclareStatements().size();i++){ 416// this.getTopStatement().getSymbolTable().pop(); 417// } 418 }else{ 419 this.getBodyStatements().add(newParser.getSqlstatements().get(0)); 420 } 421 }else { 422 for(int j=0;j<newParser.getErrorCount();j++){ 423 this.parseerrormessagehandle(newParser.getSyntaxErrors().get(j)); 424 } 425 } 426 } 427 428 private void processProcedureDefinition(EDbVendor pDbVendor, TCustomSqlStatement psql, TCreateProcedureSqlNode createProcedureNode){ 429 boolean isSQLBlock = true; 430 String inputSQL = ""; 431 // Line offset applied to the re-parsed body after parsing, replacing the 432 // blank-line padding that stringBlock() capped at 1000. The column 433 // padding is kept (stringBlock first arg 0); only the line number needs 434 // shifting. A marker line ("plpgsql_function_delimiter\n") occupies 435 // parser line 1, so the body starts on parser line 2 -> delta (lineNo-2); 436 // without a marker the body starts on parser line 1 -> delta (lineNo-1). 437 int bodyDeltaLine = 0; 438 439 if ((createProcedureNode.getFunctionBody() != null)&&(getProcedureLanguage() == null || getProcedureLanguage().toString() !=null)){ 440 String langStr = (getProcedureLanguage() != null) ? getProcedureLanguage().toString() : "sql"; 441 if (langStr.equalsIgnoreCase("sql") 442 ||(langStr.equalsIgnoreCase("plpgsql")) 443 ||(langStr.equalsIgnoreCase("plvsql")) //vertica 444 ||(langStr.equalsIgnoreCase("nzplsql"))){ 445 446 String bodyStr = createProcedureNode.getFunctionBody().getStartToken().getQuotedString();//.trim(); 447 // CREATE OR REPLACE FUNCTION testspg__getString (varchar) RETURNS varchar as ' DECLARE inString alias for $1; begin return ''bob''; end; ' LANGUAGE plpgsql 448 // escaped quotes in string literals 449 // mantisbt/view.php?id=1331 450 if (createProcedureNode.getFunctionBody().getStartToken().toString().startsWith("'")){ 451 bodyStr = bodyStr.replaceAll("''","'"); 452 } 453 454 455 //System.out.println(bodyStr); 456 int testLen = 9; 457 // if (bodyStr.length() < testLen) testLen = bodyStr.length(); 458 String prefixStr = bodyStr.trim(); 459 if (prefixStr.length() < testLen) testLen = prefixStr.length(); 460 461 // String prefixStr = bodyStr.trim().substring(0,testLen).toLowerCase(); 462 prefixStr = prefixStr.substring(0,testLen).toLowerCase(); 463 464 if ((prefixStr.startsWith("declare"))||(prefixStr.startsWith("begin"))||(prefixStr.startsWith("<<"))){ 465 //bodyStr.replaceAll("''","'"); 466 //System.out.println(bodyStr); 467 468 // newParser.sqltext = "plpgsql_function_delimiter \n"+bodyStr; 469 bodyDeltaLine = (int)createProcedureNode.getFunctionBody().getStartToken().lineNo - 2; 470 inputSQL = "plpgsql_function_delimiter\n" 471 + TBaseType.stringBlock(0,(int)createProcedureNode.getFunctionBody().getStartToken().columnNo) 472 +bodyStr; 473 }else{ 474 //newParser.sqltext = bodyStr; 475 476 bodyDeltaLine = (int)createProcedureNode.getFunctionBody().getStartToken().lineNo - 1; 477 inputSQL = 478 TBaseType.stringBlock(0,(int)createProcedureNode.getFunctionBody().getStartToken().columnNo) 479 + bodyStr; 480 481 isSQLBlock = false; 482 } 483 484 485 } 486 }else if (createProcedureNode.getFunctionBodyTokenList() != null){ 487 //netezza, function body inside BEGIN_PROC...END_PROC 488 for(int i=1;i<createProcedureNode.getFunctionBodyTokenList().size()-1;i++){ 489 inputSQL = inputSQL+createProcedureNode.getFunctionBodyTokenList().get(i).toString(); 490 } 491 492 // Use a newline after the marker so the column padding lands the 493 // body at the right column on its own parser line (line 2); the line 494 // offset (lineNo - 2) is applied after parsing instead of via capped 495 // blank-line padding. 496 bodyDeltaLine = (int)createProcedureNode.getFunctionBodyTokenList().get(0).lineNo - 2; 497 inputSQL = "plpgsql_function_delimiter\n" 498 + TBaseType.stringBlock(0,(int)createProcedureNode.getFunctionBodyTokenList().get(0).columnNo) 499 +inputSQL; 500 isSQLBlock = true; 501 } 502 503 if (inputSQL.length() > 0){ 504 doParseFunctionBody(pDbVendor, inputSQL,isSQLBlock, bodyDeltaLine); 505 } 506 } 507 508 509 510 public void accept(TParseTreeVisitor v){ 511 v.preVisit(this); 512 v.postVisit(this); 513 } 514 515 public void acceptChildren(TParseTreeVisitor v){ 516 v.preVisit(this); 517 if (this.getProcedureName() != null) this.getProcedureName().acceptChildren(v); 518 if (getParameterDeclarations() != null) getParameterDeclarations().acceptChildren(v); 519 if (getBodyStatements().size() > 0) getBodyStatements().acceptChildren(v); 520 v.postVisit(this); 521 } 522 523 public void setProcedureName(TObjectName procedureName) { 524 this.procedureName = procedureName; 525 } 526 527 /** 528 * Validates that each statement in a Teradata procedure body (except the last) is terminated with a semicolon. 529 * Labels are exempt since they prefix the next statement without requiring a semicolon. 530 */ 531 public static void validateSemicolonsBetweenStatements(TCustomSqlStatement psql, TStatementListSqlNode stmtList) { 532 for (int i = 0; i < stmtList.size() - 1; i++) { 533 TStatementSqlNode stmtNode = stmtList.getStatementSqlNode(i); 534 TCustomSqlStatement innerStmt = stmtNode.getStmt(); 535 if (innerStmt == null) continue; 536 537 // Labels don't require semicolons (they prefix the next statement) 538 if (innerStmt.sqlstatementtype == ESqlStatementType.sstmssqllabel) { 539 continue; 540 } 541 542 // DECLARE statements (conditions, handlers, variables) don't require semicolons 543 // in Teradata procedure declaration sections 544 if (innerStmt.sqlstatementtype == ESqlStatementType.sstmssqldeclare 545 || innerStmt.sqlstatementtype == ESqlStatementType.sstplsql_vardecl) { 546 continue; 547 } 548 549 // Check for semicolon: first try the wrapper node's end token (set by grammar 550 // when semicolon is present), then fall back to the inner statement's end token 551 TSourceToken endToken = stmtNode.getEndToken(); 552 if (endToken == null) { 553 endToken = innerStmt.getEndToken(); 554 } 555 556 if (endToken == null || endToken.tokentype != ETokenType.ttsemicolon) { 557 // Missing semicolon - find the next statement's start token for error location 558 TStatementSqlNode nextStmtNode = stmtList.getStatementSqlNode(i + 1); 559 TCustomSqlStatement nextInnerStmt = nextStmtNode.getStmt(); 560 TSourceToken nextStart = nextStmtNode.getStartToken(); 561 if (nextStart == null && nextInnerStmt != null) { 562 nextStart = nextInnerStmt.getStartToken(); 563 } 564 if (nextStart != null) { 565 psql.getAncestorStmt().parseerrormessagehandle( 566 new TSyntaxError(nextStart, 567 "missing semicolon before", 568 EErrorType.spfatalerror, 569 10102, 570 psql.getAncestorStmt()) 571 ); 572 } 573 } 574 } 575 } 576}