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