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 106 107 public int doParseStatement(TCustomSqlStatement psql) { 108 if (rootNode == null) return -1; 109 TCreateProcedureSqlNode createProcedureNode = (TCreateProcedureSqlNode)rootNode; 110 111 super.doParseStatement(psql); 112 TFrame currentFrame = new TFrame(this.stmtScope); 113 currentFrame.pushMeToStack(getFrameStack()); 114 115 procedureName = createProcedureNode.getProcedureName(); 116 117 if (getSqlEnv() != null) { 118 getSqlEnv().addProcedure(procedureName.toString(),true); 119 120 // move to TDatabaseObjectResolver 121 122// if (getSqlEnv().getDefaultCatalogName() != null){ 123// if (procedureName.getDatabaseToken() == null){ 124// procedureName.setDatabaseToken(new TSourceToken(getSqlEnv().getDefaultCatalogName())); 125// } 126// } 127 } 128 129 setRoutineName(procedureName); 130 131 procedureLanguage = createProcedureNode.getProcedureLanguage(); 132 if (procedureLanguage != null){ 133 if (getRoutineLanguage() == null){ 134 // not already set during TGsqlParser.dopostgresqlgetrawsqlstatements() 135 // then we set it here 136 setRoutineLanguage(procedureLanguage.toString()); 137 } 138 setRoutineLanguageInConstant(procedureLanguage); 139 }else { 140 141 if (dbvendor == EDbVendor.dbvvertica){ 142 // vertica, set default to plvsql 143 TConstant c = new TConstant(); 144 c.init(ELiteralType.etString, new TSourceToken("plvsql")); 145 procedureLanguage = c; 146 setRoutineLanguageInConstant(procedureLanguage); 147 setRoutineLanguage("plvsql"); 148 } 149 150 } 151 this.setLabelName(createProcedureNode.getLabelName()); 152 objfile = createProcedureNode.getObjfile(); 153 linkSymbol = createProcedureNode.getLinkSymbol(); 154 this.setReturnDataType(createProcedureNode.getReturnDataType()); 155 // Extract RETURNS TABLE column definitions (Snowflake) 156 if (createProcedureNode.getReturnTable() != null) { 157 TDummy dmy = createProcedureNode.getReturnTable(); 158 if (dmy.list1 instanceof TTableElementList) { 159 this.returnTableDefinitions = (TTableElementList) dmy.list1; 160 this.returnTableDefinitions.doParse(this, ESqlClause.unknown); 161 } 162 } 163 if (createProcedureNode.getFunctionBody() != null){ 164 setRoutineBodyInConstant(createProcedureNode.getFunctionBody()); 165 setRoutineBody(createProcedureNode.getFunctionBody().toString()); 166 } 167 168 this.setParameterDeclarations( createProcedureNode.getParameters()); 169 // sql server 170 procedureOptions = createProcedureNode.getProcedureOptions(); 171 isForReplication = createProcedureNode.isForReplication(); 172 this.setReturnDataType(createProcedureNode.getReturnDataType()); 173 // end sql server 174 175 // push parameterDeclarations into symbolTable 176 if (this.getParameterDeclarations() != null){ 177 for(int i=0;i< this.getParameterDeclarations().size();i++){ 178 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjParameter,this, this.getParameterDeclarations().getParameterDeclarationItem(i))); 179 TParameterDeclaration parameterDeclaration = this.getParameterDeclarations().getParameterDeclarationItem(i); 180 if (parameterDeclaration.getParameterName() != null){ 181 TVariable variable = new TVariable(parameterDeclaration.getParameterName(),parameterDeclaration,procedureName); 182 183 TVariable variableOfThisParam = TSymbolTableManager.searchSymbolVariable(this.getFrameStack(),procedureName+".$"+i); 184 if (variableOfThisParam != null){ 185 variable.setVariableStr(variableOfThisParam.getVariableStr()); 186 } 187 188 // variable.variableStr = TBaseType.getStringInsideLiteral(getSqlEnv().getProcedureParameterValue(procedureName.toString(), i)); 189 this.stmtScope.addSymbol(variable); 190 } 191 } 192 } 193 194 195 switch (dbvendor){ 196 case dbvdb2: 197 TCompoundSqlNode compoundSqlNode = createProcedureNode.getCompoundSqls(); 198 if (compoundSqlNode != null){ 199 if (compoundSqlNode.getDeclareStmts() != null){ 200 compoundSqlNode.getDeclareStmts().doParse(this,ESqlClause.unknown); 201 202 // push variable declare into symbolTable, and add to declareStatements 203 for(int i=0;i<compoundSqlNode.getDeclareStmts().size();i++){ 204 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,compoundSqlNode.getDeclareStmts().getStatementSqlNode(i).getStmt() )); 205 this.getDeclareStatements().add(compoundSqlNode.getDeclareStmts().getStatementSqlNode(i).getStmt()); 206 } 207 } 208 209 if (compoundSqlNode.getStmts() != null){ 210 compoundSqlNode.getStmts().doParse(this,ESqlClause.unknown); 211 212 for(int i= 0; i<compoundSqlNode.getStmts().size();i++){ 213 this.getBodyStatements().add(compoundSqlNode.getStmts().getStatementSqlNode(i).getStmt()); 214 } 215 } 216 217 if (compoundSqlNode.getDeclareStmts() != null){ 218 // pop variable declare from symbolTable 219 for(int i=0;i<compoundSqlNode.getDeclareStmts().size();i++){ 220 this.getTopStatement().getSymbolTable().pop(); 221 } 222 } 223 } 224 break; 225 case dbvsnowflake: 226 if (createProcedureNode.getBlcok() != null){ 227 createProcedureNode.getBlcok().doParse(this,ESqlClause.unknown); 228 this.blockBody = createProcedureNode.getBlcok(); 229 }else{ 230 // javascript code body 231 } 232 233 break; 234 case dbvnetezza: 235 if (createProcedureNode.getStmts() != null){ 236 createProcedureNode.getStmts().doParse(this, ESqlClause.unknown); 237 238 TCommonBlock commonBlock = (TCommonBlock)createProcedureNode.getStmts().getStatementSqlNode(0).getStmt(); 239 this.setLabelName(commonBlock.getLabelName()); 240 this.blockBody = commonBlock.getBlockBody(); 241 this.blockBody.setParent(this); 242 243// for(int i=0;i<createProcedureNode.getStmts().size();i++){ 244// this.getBodyStatements().add(createProcedureNode.getStmts().getStatementSqlNode(i).getStmt()); 245// } 246 }else if ((createProcedureNode.getFunctionBody() != null)&&(getProcedureLanguage() != null)&&(getProcedureLanguage().toString() !=null)){ 247 // use constant in procedure body 248 processProcedureDefinition(EDbVendor.dbvnetezza,psql,createProcedureNode); 249 } 250 break; 251 default: 252 if (createProcedureNode.getDeclareStmts() != null){ 253 createProcedureNode.getDeclareStmts().doParse(this, ESqlClause.unknown); 254 255 // push variable declare into symbolTable , and add to declareStatements 256 for(int i=0;i<createProcedureNode.getDeclareStmts().size();i++){ 257 this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,createProcedureNode.getDeclareStmts().getStatementSqlNode(i).getStmt() )); 258 this.getDeclareStatements().add(createProcedureNode.getDeclareStmts().getStatementSqlNode(i).getStmt()); 259 } 260 261 } 262 263 if (createProcedureNode.getStmt() != null){ 264 createProcedureNode.getStmt().doParse(this, ESqlClause.unknown); 265 this.getBodyStatements().add(createProcedureNode.getStmt().getStmt()); 266 if (getRoutineBody() == null){ 267 setRoutineBody(createProcedureNode.getStmt().toString()); 268 } 269 } 270 else if (createProcedureNode.getBlcok() != null){ 271 createProcedureNode.getBlcok().doParse(this,ESqlClause.unknown); 272 this.blockBody = createProcedureNode.getBlcok(); 273 if (getRoutineBody() == null){ 274 setRoutineBody(createProcedureNode.getBlcok().toString()); 275 } 276 }else if (createProcedureNode.getStmts() != null){ //teradata,sql server 277 createProcedureNode.getStmts().doParse(this, ESqlClause.unknown); 278 279 // Teradata: validate semicolons between statements in procedure body 280 if (this.dbvendor == EDbVendor.dbvteradata) { 281 validateSemicolonsBetweenStatements(this, createProcedureNode.getStmts()); 282 } 283 284 for(int i=0;i<createProcedureNode.getStmts().size();i++){ 285 this.getBodyStatements().add(createProcedureNode.getStmts().getStatementSqlNode(i).getStmt()); 286 } 287 if (getRoutineBody() == null){ 288 String stmtsStr = createProcedureNode.getStmts().toString(); 289 if (stmtsStr == null && this.getBodyStatements().size() > 0){ 290 // Fallback: construct routineBody from body statements text 291 StringBuilder sb = new StringBuilder(); 292 for(int i=0;i<this.getBodyStatements().size();i++){ 293 if (i > 0) sb.append("\n"); 294 sb.append(this.getBodyStatements().get(i).toString()); 295 } 296 stmtsStr = sb.toString(); 297 } 298 if (stmtsStr != null){ 299 setRoutineBody(stmtsStr); 300 } 301 } 302 } 303 304 if (createProcedureNode.getExceptionClause() != null){ 305 createProcedureNode.getExceptionClause().doParse(this,ESqlClause.unknown); 306 this.setExceptionClause(createProcedureNode.getExceptionClause()); 307 } 308 309 if (createProcedureNode.getDeclareStmts() != null){ 310 311 // pop variable declare from symbolTable 312 for(int i=0;i<createProcedureNode.getDeclareStmts().size();i++){ 313 this.getTopStatement().getSymbolTable().pop(); 314 } 315 316 } 317 318 break; 319 320 } 321 322 // pop parameterDeclarations from symbolTable 323 if (this.getParameterDeclarations() != null){ 324 for(int i=0;i< this.getParameterDeclarations().size();i++){ 325 this.getTopStatement().getSymbolTable().pop(); 326 } 327 } 328 329 switch (this.dbvendor){ 330 case dbvpostgresql: 331 case dbvredshift: 332 if (createProcedureNode.getFunctionBody() != null){ 333 // function body only inside '' will be processed here 334 processProcedureDefinition(EDbVendor.dbvpostgresql,psql,createProcedureNode); 335 }else{ 336 // procedure body only inside $$ already processed in the above default section 337 } 338 339 break; 340 341 case dbvvertica: 342 processProcedureDefinition(EDbVendor.dbvpostgresql,psql,createProcedureNode); 343 break; 344 case dbvsnowflake: 345 processProcedureDefinition(this.dbvendor,psql,createProcedureNode); 346 break; 347 } 348 349 350 this.setEndlabelName(createProcedureNode.getEndlabelName()); 351 352 currentFrame.popMeFromStack(getFrameStack()); 353 354 return 0; 355 } 356 357 private void doParseFunctionBody(EDbVendor dbVendor,String inputSQL,boolean isSQLBlock){ 358 TGSqlParser newParser = new TGSqlParser(dbVendor); 359 newParser.sqltext = inputSQL; 360 newParser.setSinglePLBlock(isSQLBlock); 361 newParser.setFrameStack(getFrameStack()); 362 int iRet = newParser.parse(); 363 if ((iRet == 0)&&(newParser.getSqlstatements().size() >0)){ 364 if (isSQLBlock){ 365 TCommonBlock commonBlock = (TCommonBlock)newParser.getSqlstatements().get(0); 366 this.setLabelName(commonBlock.getLabelName()); 367 this.blockBody = commonBlock.getBlockBody(); 368 this.blockBody.setParent(this); 369// if (this.blockBody.getDeclareStatements().size()>0){ 370// //this.getDeclareStatements().clear(); 371// for(int i=0;i<this.blockBody.getDeclareStatements().size();i++){ 372// this.getDeclareStatements().add(this.blockBody.getDeclareStatements().get(i)); 373// } 374// } 375 376// for(int i=0;i<commonBlock.getDeclareStatements().size();i++){ 377// this.getTopStatement().getSymbolTable().push( new TSymbolTableItem(TObjectName.ttobjVariable,this,commonBlock.getDeclareStatements().get(i))); 378// this.getDeclareStatements().add(commonBlock.getDeclareStatements().get(i)); 379// } 380// for(int i=0;i<commonBlock.getBodyStatements().size();i++){ 381// commonBlock.getBodyStatements().get(i).setAlreadyAddToParent(false); 382// commonBlock.getBodyStatements().get(i).setParentStmt(this); 383// 384// this.getBodyStatements().add(commonBlock.getBodyStatements().get(i)); 385// } 386// if (commonBlock.getExceptionClause() != null){ 387// this.setExceptionClause(commonBlock.getExceptionClause()); 388// } 389// for(int i=0;i<commonBlock.getDeclareStatements().size();i++){ 390// this.getTopStatement().getSymbolTable().pop(); 391// } 392 }else{ 393 this.getBodyStatements().add(newParser.getSqlstatements().get(0)); 394 } 395 }else { 396 for(int j=0;j<newParser.getErrorCount();j++){ 397 this.parseerrormessagehandle(newParser.getSyntaxErrors().get(j)); 398 } 399 } 400 } 401 402 private void processProcedureDefinition(EDbVendor pDbVendor, TCustomSqlStatement psql, TCreateProcedureSqlNode createProcedureNode){ 403 boolean isSQLBlock = true; 404 String inputSQL = ""; 405 406 if ((createProcedureNode.getFunctionBody() != null)&&(getProcedureLanguage() == null || getProcedureLanguage().toString() !=null)){ 407 String langStr = (getProcedureLanguage() != null) ? getProcedureLanguage().toString() : "sql"; 408 if (langStr.equalsIgnoreCase("sql") 409 ||(langStr.equalsIgnoreCase("plpgsql")) 410 ||(langStr.equalsIgnoreCase("plvsql")) //vertica 411 ||(langStr.equalsIgnoreCase("nzplsql"))){ 412 413 String bodyStr = createProcedureNode.getFunctionBody().getStartToken().getQuotedString();//.trim(); 414 // CREATE OR REPLACE FUNCTION testspg__getString (varchar) RETURNS varchar as ' DECLARE inString alias for $1; begin return ''bob''; end; ' LANGUAGE plpgsql 415 // escaped quotes in string literals 416 // mantisbt/view.php?id=1331 417 if (createProcedureNode.getFunctionBody().getStartToken().toString().startsWith("'")){ 418 bodyStr = bodyStr.replaceAll("''","'"); 419 } 420 421 422 //System.out.println(bodyStr); 423 int testLen = 9; 424 // if (bodyStr.length() < testLen) testLen = bodyStr.length(); 425 String prefixStr = bodyStr.trim(); 426 if (prefixStr.length() < testLen) testLen = prefixStr.length(); 427 428 // String prefixStr = bodyStr.trim().substring(0,testLen).toLowerCase(); 429 prefixStr = prefixStr.substring(0,testLen).toLowerCase(); 430 431 if ((prefixStr.startsWith("declare"))||(prefixStr.startsWith("begin"))||(prefixStr.startsWith("<<"))){ 432 //bodyStr.replaceAll("''","'"); 433 //System.out.println(bodyStr); 434 435 // newParser.sqltext = "plpgsql_function_delimiter \n"+bodyStr; 436 inputSQL = "plpgsql_function_delimiter\n" 437 + TBaseType.stringBlock((int)createProcedureNode.getFunctionBody().getStartToken().lineNo - 2,(int)createProcedureNode.getFunctionBody().getStartToken().columnNo) 438 +bodyStr; 439 }else{ 440 //newParser.sqltext = bodyStr; 441 442 inputSQL = 443 TBaseType.stringBlock((int)createProcedureNode.getFunctionBody().getStartToken().lineNo - 1,(int)createProcedureNode.getFunctionBody().getStartToken().columnNo) 444 + bodyStr; 445 446 isSQLBlock = false; 447 } 448 449 450 } 451 }else if (createProcedureNode.getFunctionBodyTokenList() != null){ 452 //netezza, function body inside BEGIN_PROC...END_PROC 453 for(int i=1;i<createProcedureNode.getFunctionBodyTokenList().size()-1;i++){ 454 inputSQL = inputSQL+createProcedureNode.getFunctionBodyTokenList().get(i).toString(); 455 } 456 457 inputSQL = "plpgsql_function_delimiter " 458 + TBaseType.stringBlock((int)createProcedureNode.getFunctionBodyTokenList().get(0).lineNo - 1,(int)createProcedureNode.getFunctionBodyTokenList().get(0).columnNo) 459 +inputSQL; 460 isSQLBlock = true; 461 } 462 463 if (inputSQL.length() > 0){ 464 doParseFunctionBody(pDbVendor, inputSQL,isSQLBlock); 465 } 466 } 467 468 469 470 public void accept(TParseTreeVisitor v){ 471 v.preVisit(this); 472 v.postVisit(this); 473 } 474 475 public void acceptChildren(TParseTreeVisitor v){ 476 v.preVisit(this); 477 this.getProcedureName().acceptChildren(v); 478 if (getParameterDeclarations() != null) getParameterDeclarations().acceptChildren(v); 479 if (getBodyStatements().size() > 0) getBodyStatements().acceptChildren(v); 480 v.postVisit(this); 481 } 482 483 public void setProcedureName(TObjectName procedureName) { 484 this.procedureName = procedureName; 485 } 486 487 /** 488 * Validates that each statement in a Teradata procedure body (except the last) is terminated with a semicolon. 489 * Labels are exempt since they prefix the next statement without requiring a semicolon. 490 */ 491 public static void validateSemicolonsBetweenStatements(TCustomSqlStatement psql, TStatementListSqlNode stmtList) { 492 for (int i = 0; i < stmtList.size() - 1; i++) { 493 TStatementSqlNode stmtNode = stmtList.getStatementSqlNode(i); 494 TCustomSqlStatement innerStmt = stmtNode.getStmt(); 495 if (innerStmt == null) continue; 496 497 // Labels don't require semicolons (they prefix the next statement) 498 if (innerStmt.sqlstatementtype == ESqlStatementType.sstmssqllabel) { 499 continue; 500 } 501 502 // DECLARE statements (conditions, handlers, variables) don't require semicolons 503 // in Teradata procedure declaration sections 504 if (innerStmt.sqlstatementtype == ESqlStatementType.sstmssqldeclare 505 || innerStmt.sqlstatementtype == ESqlStatementType.sstplsql_vardecl) { 506 continue; 507 } 508 509 // Check for semicolon: first try the wrapper node's end token (set by grammar 510 // when semicolon is present), then fall back to the inner statement's end token 511 TSourceToken endToken = stmtNode.getEndToken(); 512 if (endToken == null) { 513 endToken = innerStmt.getEndToken(); 514 } 515 516 if (endToken == null || endToken.tokentype != ETokenType.ttsemicolon) { 517 // Missing semicolon - find the next statement's start token for error location 518 TStatementSqlNode nextStmtNode = stmtList.getStatementSqlNode(i + 1); 519 TCustomSqlStatement nextInnerStmt = nextStmtNode.getStmt(); 520 TSourceToken nextStart = nextStmtNode.getStartToken(); 521 if (nextStart == null && nextInnerStmt != null) { 522 nextStart = nextInnerStmt.getStartToken(); 523 } 524 if (nextStart != null) { 525 psql.getAncestorStmt().parseerrormessagehandle( 526 new TSyntaxError(nextStart, 527 "missing semicolon before", 528 EErrorType.spfatalerror, 529 10102, 530 psql.getAncestorStmt()) 531 ); 532 } 533 } 534 } 535 } 536}