private int isPLBlockEnd(Token[] tokens, int pos) {
        if (pos >= tokens.length || subPLStack.empty()) {
            return -1;
        }

        boolean isEnd;
        int posShift = 0;
        /**
         * <pre>
         * `subPLStack` in this function must not be empty because this fuction must be called when state in PL_STMT
         * `subPLStack` must have been pushed at least once before enter PL_STMT
         * </pre>
         */
        SubPLLevel peekLevel = subPLStack.peek();
        switch (peekLevel.startSymbol) {
            case CREATE_TYPE:
                isEnd = matchDelimiterTokens(tokens, pos);
                break;
            case BEGIN:
            case CREATE_TYPE_BODY:
            case TRIGGER:
            case CREATE_TRIGGER:
                isEnd = tokens[pos].getType() == this.tokenDefinition.END();
                break;
            case FUNCTION:
            case PROCEDURE:
                // member or static function && procedure declare in type which does not contain IS or AS
                // should end with `)` or `,`
                if (peekLevel.matchMemberOrStatic && !peekLevel.matchIsOrAs) {
                    isEnd = tokens[pos].getText().equals(")") || tokens[pos].getText().equals(",");
                    break;
                }
            case PACKAGE:
            case PACKAGE_BODY:
            case CREATE_FUNCTION:
            case CREATE_PROCEDURE:
            case CREATE_PACKAGE:
            case CREATE_PACKAGE_BODY:
                boolean matchDelimiter = matchDelimiterTokens(tokens, pos);
                if (matchDelimiter) {
                    // in only two cases, delimiter can be the end of pl create sentence
                    // 1. `IS` or `AS` is not matched
                    // 2. `IS` or `AS` is matched but `EXTERNAL` or `LANGUAGE` is also matched
                    isEnd = !peekLevel.matchIsOrAs || peekLevel.matchExternalOrLanguage;
                } else {
                    isEnd = tokens[pos].getType() == this.tokenDefinition.END();
                }
                break;
            case FOR:
            case LOOP:
            case WHILE:
                isEnd = matchPLBlockEnd(tokens, pos, this.tokenDefinition.LOOP());
                posShift = isEnd ? 2 : 0;
                break;
            case IF:
                isEnd = matchPLBlockEnd(tokens, pos, this.tokenDefinition.IF());
                posShift = isEnd ? 2 : 0;
                break;
            case CASE:
                isEnd = matchPLBlockEnd(tokens, pos, this.tokenDefinition.CASE());
                posShift = isEnd ? 2 : 0;
                break;
            case UNKNOWN:
            default:
                throw new RuntimeException(
                        String.format("Unsupported pl start symbol: %s", peekLevel.startSymbol));
        }
        if (isEnd) {
            return posShift;
        }
        return -1;
    }
<pre>
`subPLStack` in this function must not be empty because this fuction must be called when state in PL_STMT
`subPLStack` must have been pushed at least once before enter PL_STMT
</pre>
chat2db-server/chat2db-spi/src/main/java/ai/chat2db/spi/util/SqlSplitter.java