All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mengweijin.flyway.database.dm.DmParser Maven / Gradle / Ivy

Go to download

Flyway、Liquibase 扩展支持达梦(DM)数据库、南大通用(GBase 8s)数据库。

There is a newer version: 2.0.2
Show newest version
package com.github.mengweijin.flyway.database.dm;

import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.internal.parser.Parser;
import org.flywaydb.core.internal.parser.ParserContext;
import org.flywaydb.core.internal.parser.ParsingContext;
import org.flywaydb.core.internal.parser.PeekingReader;
import org.flywaydb.core.internal.parser.Recorder;
import org.flywaydb.core.internal.parser.StatementType;
import org.flywaydb.core.internal.parser.Token;
import org.flywaydb.core.internal.parser.TokenType;
import org.flywaydb.core.internal.sqlscript.Delimiter;
import org.flywaydb.core.internal.sqlscript.ParsedSqlStatement;
import org.flywaydb.core.internal.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @author mengweijin
 */
public class DmParser extends Parser {

    /**
     * Delimiter of PL/SQL blocks and statements.
     */
    private static final Delimiter PLSQL_DELIMITER = new Delimiter("/", true);


    //                                                 accessible   by    (        keywordoptionalidentifier                      )
    private static final String ACCESSIBLE_BY_REGEX = "ACCESSIBLE\\sBY\\s\\(?(((FUNCTION|PROCEDURE|PACKAGE|TRIGGER|TYPE)\\s)?[^\\s]\\s?+)*\\)?";

    private static final Pattern PLSQL_TYPE_BODY_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sTYPE\\sBODY\\s([^\\s]*\\s)?(IS|AS)");

    private static final Pattern PLSQL_PACKAGE_BODY_REGEX = Pattern.compile(
            "^CREATE(\\s*OR\\s*REPLACE)?(\\s*(NON)?EDITIONABLE)?\\s*PACKAGE\\s*BODY\\s*([^\\s]*\\s)?(IS|AS)");
    private static final StatementType PLSQL_PACKAGE_BODY_STATEMENT = new StatementType();

    private static final Pattern PLSQL_PACKAGE_DEFINITION_REGEX = Pattern.compile(
            "^CREATE(\\s*OR\\s*REPLACE)?(\\s*(NON)?EDITIONABLE)?\\s*PACKAGE\\s([^\\s*]*\\s*)?(AUTHID\\s*[^\\s*]*\\s*|" + ACCESSIBLE_BY_REGEX + ")*(IS|AS)");

    private static final Pattern PLSQL_VIEW_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?((\\sNO)?\\sFORCE)?(\\s(NON)?EDITIONABLE)?\\sVIEW\\s([^\\s]*\\s)?AS\\sWITH\\s(PROCEDURE|FUNCTION)");
    private static final StatementType PLSQL_VIEW_STATEMENT = new StatementType();

    private static final Pattern PLSQL_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\s(FUNCTION(\\s\\S*)|PROCEDURE|TYPE|TRIGGER)");
    private static final Pattern DECLARE_BEGIN_REGEX = Pattern.compile("^DECLARE|BEGIN|WITH");
    private static final StatementType PLSQL_STATEMENT = new StatementType();

    private static final Pattern JAVA_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\sAND\\s(RESOLVE|COMPILE))?(\\sNOFORCE)?\\sJAVA\\s(SOURCE|RESOURCE|CLASS)");
    private static final StatementType PLSQL_JAVA_STATEMENT = new StatementType();

    private static final Pattern PLSQL_PACKAGE_BODY_WRAPPED_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE\\sBODY(\\s\\S*)?\\sWRAPPED(\\s\\S*)*");
    private static final Pattern PLSQL_PACKAGE_DEFINITION_WRAPPED_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\sPACKAGE(\\s\\S*)?\\sWRAPPED(\\s\\S*)*");
    private static final Pattern PLSQL_WRAPPED_REGEX = Pattern.compile(
            "^CREATE(\\sOR\\sREPLACE)?(\\s(NON)?EDITIONABLE)?\\s(FUNCTION|PROCEDURE|TYPE)(\\s\\S*)?\\sWRAPPED(\\s\\S*)*");

    private static final StatementType PLSQL_WRAPPED_STATEMENT = new StatementType();
    private int initialWrappedBlockDepth = -1;

    private static Pattern toRegex(String... commands) {
        return Pattern.compile(toRegexPattern(commands));
    }

    private static String toRegexPattern(String... commands) {
        return "^(" + StringUtils.arrayToDelimitedString("|", commands) + ")";
    }


    public DmParser(Configuration configuration


            , ParsingContext parsingContext
    ) {
        super(configuration, parsingContext, 3);


    }


    @Override
    protected ParsedSqlStatement createStatement(PeekingReader reader, Recorder recorder,
                                                 int statementPos, int statementLine, int statementCol,
                                                 int nonCommentPartPos, int nonCommentPartLine, int nonCommentPartCol,
                                                 StatementType statementType, boolean canExecuteInTransaction,
                                                 Delimiter delimiter, String sql, boolean batchable
    ) throws IOException {
        if (PLSQL_VIEW_STATEMENT == statementType) {
            sql = sql.trim();

            // Strip extra semicolon to avoid issues with WITH statements containing PL/SQL
            if (sql.endsWith(";")) {
                sql = sql.substring(0, sql.length() - 1);
            }
        }

        return super.createStatement(reader, recorder, statementPos, statementLine, statementCol,
                nonCommentPartPos, nonCommentPartLine, nonCommentPartCol,
                statementType, canExecuteInTransaction, delimiter, sql


                , batchable
        );
    }

    @Override
    protected StatementType detectStatementType(String simplifiedStatement, ParserContext context, PeekingReader reader) {
        if (PLSQL_PACKAGE_BODY_WRAPPED_REGEX.matcher(simplifiedStatement).matches()
                || PLSQL_PACKAGE_DEFINITION_WRAPPED_REGEX.matcher(simplifiedStatement).matches()
                || PLSQL_WRAPPED_REGEX.matcher(simplifiedStatement).matches()) {
            if (initialWrappedBlockDepth == -1) {
                initialWrappedBlockDepth = context.getBlockDepth();
            }
            return PLSQL_WRAPPED_STATEMENT;
        }

        if (PLSQL_PACKAGE_BODY_REGEX.matcher(simplifiedStatement).matches()) {
            return PLSQL_PACKAGE_BODY_STATEMENT;
        }

        if (PLSQL_REGEX.matcher(simplifiedStatement).matches()
                || PLSQL_PACKAGE_DEFINITION_REGEX.matcher(simplifiedStatement).matches()
                || DECLARE_BEGIN_REGEX.matcher(simplifiedStatement).matches()) {
            try {
                String wrappedKeyword = " WRAPPED";
                if (!reader.peek(wrappedKeyword.length()).equalsIgnoreCase(wrappedKeyword)) {
                    return PLSQL_STATEMENT;
                }
            } catch (IOException e) {
                return PLSQL_STATEMENT;
            }
        }

        if (JAVA_REGEX.matcher(simplifiedStatement).matches()) {
            return PLSQL_JAVA_STATEMENT;
        }

        if (PLSQL_VIEW_REGEX.matcher(simplifiedStatement).matches()) {
            return PLSQL_VIEW_STATEMENT;
        }


        return super.detectStatementType(simplifiedStatement, context, reader);
    }

    @Override
    protected boolean shouldDiscard(Token token, boolean nonCommentPartSeen) {
        // Discard dangling PL/SQL '/' delimiters
        return ("/".equals(token.getText()) && !nonCommentPartSeen) || super.shouldDiscard(token, nonCommentPartSeen);
    }

    @Override
    protected void adjustDelimiter(ParserContext context, StatementType statementType) {
        if (statementType == PLSQL_STATEMENT || statementType == PLSQL_VIEW_STATEMENT || statementType == PLSQL_JAVA_STATEMENT
                || statementType == PLSQL_PACKAGE_BODY_STATEMENT) {
            context.setDelimiter(PLSQL_DELIMITER);


        } else {
            context.setDelimiter(Delimiter.SEMICOLON);
        }
    }


    @Override
    protected boolean shouldAdjustBlockDepth(ParserContext context, List tokens, Token token) {
        // Package bodies can have an unbalanced BEGIN without END in the initialisation section.
        TokenType tokenType = token.getType();
        if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) {
            return true;
        }

        // Handle wrapped SQL on these token types to ensure it gets treated as one block
        if (context.getStatementType() == PLSQL_WRAPPED_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType)) {
            return true;
        }

        // In Oracle, symbols { } affect the block depth in embedded Java code
        if (token.getType() == TokenType.SYMBOL && context.getStatementType() == PLSQL_JAVA_STATEMENT) {
            return true;
        }


        return super.shouldAdjustBlockDepth(context, tokens, token);
    }

    // These words increase the block depth - unless preceded by END (in which case the END will decrease the block depth)
    private static final List CONTROL_FLOW_KEYWORDS = Arrays.asList("IF", "LOOP", "CASE");


    private static final String EXISTS = "EXISTS";

    private static final String CREATE_OR_DROP_IF_OR_NOT = "(CREATE|DROP)\\s(TABLE|PROCEDURE|FUNCTION)\\sIF(\\sNOT)?";

    /**
     * 调整块深度
* * @param context 参数 * @param tokens 参数 * @param keyword 参数 * @param reader 参数 * @author 作者: Jack Chu E-mail: [email protected] * @since 创建时间:2023/12/13 20:52 */ private void adjustBlockDepthDm(ParserContext context, List tokens, Token keyword, PeekingReader reader) { String keywordText = keyword.getText(); if (EXISTS.equals(keywordText) && context.getBlockDepth() > 0) { String sql = tokens.stream().map(Token::getText).collect(Collectors.joining(" ")); if (sql.matches(CREATE_OR_DROP_IF_OR_NOT)) { context.decreaseBlockDepth(); } } } @Override protected void adjustBlockDepth(ParserContext context, List tokens, Token keyword, PeekingReader reader) { this.adjustBlockDepthDm(context, tokens, keyword, reader); TokenType tokenType = keyword.getType(); String keywordText = keyword.getText(); int parensDepth = keyword.getParensDepth(); if (lastTokenIs(tokens, parensDepth, "GOTO")) { return; } if (context.getStatementType() == PLSQL_WRAPPED_STATEMENT) { // ensure wrapped SQL has an increased block depth so it gets treated as one statement if (context.getBlockDepth() == initialWrappedBlockDepth) { context.increaseBlockDepth("WRAPPED"); } // decrease block depth at the end to step out of a wrapped SQL block if ((TokenType.EOF == tokenType || (TokenType.DELIMITER == tokenType && "/".equals(keywordText))) && context.getBlockDepth() > 0) { context.decreaseBlockDepth(); } // return early as we don't need to parse the contents of wrapped SQL - it's all one statement anyways return; } else { // decrease block depth when wrapped SQL ends to step out of wrapped SQL block if (context.getBlockDepth() > initialWrappedBlockDepth && context.getBlockInitiator().equals("WRAPPED")) { initialWrappedBlockDepth = -1; context.decreaseBlockDepth(); } } // In embedded Java code we judge the end of a class definition by the depth of braces. // We ignore normal SQL keywords as Java code can contain arbitrary identifiers. if (context.getStatementType() == PLSQL_JAVA_STATEMENT) { if ("{".equals(keywordText)) { context.increaseBlockDepth("PLSQL_JAVA_STATEMENT"); } else if ("}".equals(keywordText)) { context.decreaseBlockDepth(); } return; } if ("BEGIN".equals(keywordText) || (CONTROL_FLOW_KEYWORDS.contains(keywordText) && !precedingEndAttachesToThisKeyword(tokens, parensDepth, context, keyword)) || ("TRIGGER".equals(keywordText) && lastTokenIs(tokens, parensDepth, "COMPOUND")) || (context.getBlockDepth() == 0 && ( doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_BODY_REGEX) || doTokensMatchPattern(tokens, keyword, PLSQL_PACKAGE_DEFINITION_REGEX) || doTokensMatchPattern(tokens, keyword, PLSQL_TYPE_BODY_REGEX))) ) { context.increaseBlockDepth(keywordText); } else if ("END".equals(keywordText)) { context.decreaseBlockDepth(); } // Package bodies can have an unbalanced BEGIN without END in the initialisation section. This allows us // to exit the package even though we are still at block depth 1 due to the BEGIN. if (context.getStatementType() == PLSQL_PACKAGE_BODY_STATEMENT && (TokenType.EOF == tokenType || TokenType.DELIMITER == tokenType) && context.getBlockDepth() == 1) { context.decreaseBlockDepth(); } } private boolean precedingEndAttachesToThisKeyword(List tokens, int parensDepth, ParserContext context, Token keyword) { // Normally IF, LOOP and CASE all pair up with END IF, END LOOP, END CASE // However, CASE ... END is valid in expressions, so in code such as // FOR i IN 1 .. CASE WHEN foo THEN 5 ELSE 6 END // LOOP // ... // END LOOP // the first END does *not* attach to the subsequent LOOP. The same is possible with $IF ... $END constructions return lastTokenIs(tokens, parensDepth, "END") && lastTokenIsOnLine(tokens, parensDepth, keyword.getLine()) && keyword.getText().equals(context.getLastClosedBlockInitiator()); } @Override protected boolean doTokensMatchPattern(List previousTokens, Token current, Pattern regex) { if (regex == PLSQL_PACKAGE_DEFINITION_REGEX && previousTokens.stream().anyMatch(t -> t.getType() == TokenType.KEYWORD && t.getText().equalsIgnoreCase("ACCESSIBLE"))) { ArrayList tokenStrings = new ArrayList<>(); tokenStrings.add(current.getText()); for (int i = previousTokens.size() - 1; i >= 0; i--) { Token prevToken = previousTokens.get(i); if (prevToken.getType() == TokenType.KEYWORD) { tokenStrings.add(prevToken.getText()); } } StringBuilder builder = new StringBuilder(); for (int i = tokenStrings.size() - 1; i >= 0; i--) { builder.append(tokenStrings.get(i)); if (i != 0) { builder.append(" "); } } return regex.matcher(builder.toString()).matches() || super.doTokensMatchPattern(previousTokens, current, regex); } return super.doTokensMatchPattern(previousTokens, current, regex); } @Override protected boolean isDelimiter(String peek, ParserContext context, int col, int colIgnoringWhitespace) { Delimiter delimiter = context.getDelimiter(); if (peek.startsWith(delimiter.getEscape() + delimiter.getDelimiter())) { return true; } if (delimiter.shouldBeAloneOnLine()) { // Only consider alone-on-line delimiters (such as "/" for PL/SQL) if // it's the first character on the line return colIgnoringWhitespace == 1 && peek.trim().equals(delimiter.getDelimiter()); } else { if (colIgnoringWhitespace == 1 && "/".equals(peek.trim())) { return true; } } return super.isDelimiter(peek, context, col, colIgnoringWhitespace); } @Override protected Token handleMultilineComment(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { reader.swallow("/*".length()); String text = reader.readUntilExcluding("*/"); reader.swallow("*/".length()); return new Token(TokenType.COMMENT, pos, line, col, text, text, context.getParensDepth()); } @Override protected Token handleDelimiter(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { if (reader.peek('/')) { reader.swallow(1); return new Token(TokenType.DELIMITER, pos, line, col, "/", "/", context.getParensDepth()); } return super.handleDelimiter(reader, context, pos, line, col); } @Override protected boolean isAlternativeStringLiteral(String peek) { if (peek.length() < 3) { return false; } // Oracle's quoted-literal syntax is introduced by q (case-insensitive) followed by a literal surrounded by // any of !!, [], {}, (), <> provided the selected pair do not appear in the literal string; the others may do. char firstChar = peek.charAt(0); return (firstChar == 'q' || firstChar == 'Q') && peek.charAt(1) == '\''; } @Override protected Token handleAlternativeStringLiteral(PeekingReader reader, ParserContext context, int pos, int line, int col) throws IOException { reader.swallow(2); String closeQuote = computeAlternativeCloseQuote((char) reader.read()); reader.swallowUntilExcluding(closeQuote); reader.swallow(closeQuote.length()); return new Token(TokenType.STRING, pos, line, col, null, null, context.getParensDepth()); } private String computeAlternativeCloseQuote(char specialChar) { switch (specialChar) { case '!': return "!'"; case '[': return "]'"; case '(': return ")'"; case '{': return "}'"; case '<': return ">'"; default: return specialChar + "'"; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy