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}