Browse Source

为支持动态表引入 sql table name parser class

hubin 6 năm trước cách đây
mục cha
commit
12874cd837

+ 254 - 0
mybatis-plus-core/src/main/java/com/baomidou/mybatisplus/core/toolkit/TableNameParser.java

@@ -0,0 +1,254 @@
+package com.baomidou.mybatisplus.core.toolkit;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * SQL 表名解析
+ * https://github.com/mnadeem/sql-table-name-parser
+ * Ultra light, Ultra fast parser to extract table name out SQLs, supports oracle dialect SQLs as well.
+ * USE: new TableNameParser(sql).tables()
+ *
+ * @author Nadeem Mohammad
+ * @since 2019-04-22
+ */
+public final class TableNameParser {
+
+    private static final int NO_INDEX = -1;
+    private static final String SPACE = " ";
+    private static final String REGEX_SPACE = "\\s+";
+
+    private static final String TOKEN_ORACLE_HINT_START = "/*+";
+    private static final String TOKEN_ORACLE_HINT_END = "*/";
+    private static final String TOKEN_SINGLE_LINE_COMMENT = "--";
+    private static String TOKEN_NEWLINE = "\\r\\n|\\r|\\n|\\n\\r";
+    private static final String TOKEN_SEMI_COLON = ";";
+    private static final String TOKEN_PARAN_START = "(";
+    private static final String TOKEN_COMMA = ",";
+    private static final String TOKEN_SET = "set";
+    private static final String TOKEN_OF = "of";
+    private static final String TOKEN_DUAL = "dual";
+    private static final String TOKEN_DELETE = "delete";
+    private static final String TOKEN_CREATE = "create";
+    private static final String TOKEN_INDEX = "index";
+    private static final String TOKEN_ASTERICK = "*";
+
+    private static final String KEYWORD_JOIN = "join";
+    private static final String KEYWORD_INTO = "into";
+    private static final String KEYWORD_TABLE = "table";
+    private static final String KEYWORD_FROM = "from";
+    private static final String KEYWORD_USING = "using";
+    private static final String KEYWORD_UPDATE = "update";
+
+    private static final List<String> concerned = Arrays.asList(KEYWORD_TABLE, KEYWORD_INTO, KEYWORD_JOIN, KEYWORD_USING, KEYWORD_UPDATE);
+    private static final List<String> ignored = Arrays.asList(TOKEN_PARAN_START, TOKEN_SET, TOKEN_OF, TOKEN_DUAL);
+
+    private Map<String, String> tables = new HashMap<>();
+
+    /**
+     * Extracts table names out of SQL
+     * @param sql
+     */
+    public TableNameParser(final String sql) {
+        String noComments = removeComments(sql);
+        String normalized = normalized(noComments);
+        String cleansed = clean(normalized);
+        String[] tokens = cleansed.split(REGEX_SPACE);
+        int index = 0;
+
+        String firstToken = tokens[index];
+        if (isOracleSpecialDelete(firstToken, tokens, index)) {
+            handleSpecialOracleSpecialDelete(firstToken, tokens, index);
+        } else if (isCreateIndex(firstToken, tokens, index)) {
+            handleCreateIndex(firstToken, tokens, index);
+        } else {
+            while (moreTokens(tokens, index)) {
+                String currentToken = tokens[index++];
+
+                if (isFromToken(currentToken)) {
+                    processFromToken(tokens, index);
+                } else if (shouldProcess(currentToken)) {
+                    String nextToken = tokens[index++];
+                    considerInclusion(nextToken);
+
+                    if (moreTokens(tokens, index)) {
+                        nextToken = tokens[index++];
+                    }
+                }
+            }
+        }
+    }
+
+    private String removeComments(final String sql) {
+        StringBuilder sb = new StringBuilder(sql);
+        int nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT);
+        while (nextCommentPosition > -1) {
+            int end = indexOfRegex(TOKEN_NEWLINE, sb.substring(nextCommentPosition));
+            if (end == -1) {
+                return sb.substring(0, nextCommentPosition);
+            } else {
+                sb.replace(nextCommentPosition, end + nextCommentPosition, "");
+            }
+            nextCommentPosition = sb.indexOf(TOKEN_SINGLE_LINE_COMMENT);
+        }
+        return sb.toString();
+    }
+
+    private int indexOfRegex(String regex, String string) {
+        Pattern pattern = Pattern.compile(regex);
+        Matcher matcher = pattern.matcher(string);
+        return matcher.find() ? matcher.start() : -1;
+    }
+
+    private String normalized(final String sql) {
+        String normalized = sql.trim().replaceAll(TOKEN_NEWLINE, SPACE).replaceAll(TOKEN_COMMA, " , ")
+            .replaceAll("\\(", " ( ").replaceAll("\\)", " ) ");
+        if (normalized.endsWith(TOKEN_SEMI_COLON)) {
+            normalized = normalized.substring(0, normalized.length() - 1);
+        }
+        return normalized;
+    }
+
+    private String clean(final String normalized) {
+        int start = normalized.indexOf(TOKEN_ORACLE_HINT_START);
+        int end;
+        if (start != NO_INDEX) {
+            end = normalized.indexOf(TOKEN_ORACLE_HINT_END);
+            if (end != NO_INDEX) {
+                String firstHalf = normalized.substring(0, start);
+                String secondHalf = normalized.substring(end + 2, normalized.length());
+                return firstHalf.trim() + SPACE + secondHalf.trim();
+            }
+        }
+        return normalized;
+    }
+
+    private boolean isOracleSpecialDelete(final String currentToken, final String[] tokens, int index) {
+        index++;// Point to next token
+        if (TOKEN_DELETE.equals(currentToken)) {
+            if (moreTokens(tokens, index)) {
+                String nextToken = tokens[index++];
+                if (!KEYWORD_FROM.equals(nextToken) && !TOKEN_ASTERICK.equals(nextToken)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void handleSpecialOracleSpecialDelete(final String currentToken, final String[] tokens, int index) {
+        String tableName = tokens[index + 1];
+        considerInclusion(tableName);
+    }
+
+    private boolean isCreateIndex(String currentToken, String[] tokens, int index) {
+        index++; // Point to next token
+        if (TOKEN_CREATE.equals(currentToken.toLowerCase()) && hasIthToken(tokens, index, 3)) {
+            String nextToken = tokens[index++];
+            if (TOKEN_INDEX.equals(nextToken.toLowerCase())) {
+                return true;
+            }
+
+        }
+        return false;
+    }
+
+    private void handleCreateIndex(String currentToken, String[] tokens, int index) {
+        String tableName = tokens[index + 4];
+        considerInclusion(tableName);
+    }
+
+    private boolean hasIthToken(String[] tokens, int currentIndex, int tokenNumber) {
+        if (moreTokens(tokens, currentIndex) && tokens.length > currentIndex + tokenNumber) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean shouldProcess(final String currentToken) {
+        return concerned.contains(currentToken.toLowerCase());
+    }
+
+    private boolean isFromToken(final String currentToken) {
+        return KEYWORD_FROM.equals(currentToken.toLowerCase());
+    }
+
+    private void processFromToken(final String[] tokens, int index) {
+        String currentToken = tokens[index++];
+        considerInclusion(currentToken);
+
+        String nextToken = null;
+        if (moreTokens(tokens, index)) {
+            nextToken = tokens[index++];
+        }
+
+        if (shouldProcessMultipleTables(nextToken)) {
+            processNonAliasedMultiTables(tokens, index, nextToken);
+        } else {
+            processAliasedMultiTables(tokens, index, currentToken);
+        }
+    }
+
+    private void processNonAliasedMultiTables(final String[] tokens, int index, String nextToken) {
+        while (nextToken.equals(TOKEN_COMMA)) {
+            String currentToken = tokens[index++];
+            considerInclusion(currentToken);
+            if (moreTokens(tokens, index)) {
+                nextToken = tokens[index++];
+            } else {
+                break;
+            }
+        }
+    }
+
+    private void processAliasedMultiTables(final String[] tokens, int index, String currentToken) {
+        String nextNextToken = null;
+        if (moreTokens(tokens, index)) {
+            nextNextToken = tokens[index++];
+        }
+
+        if (shouldProcessMultipleTables(nextNextToken)) {
+            while (moreTokens(tokens, index) && nextNextToken.equals(TOKEN_COMMA)) {
+                if (moreTokens(tokens, index)) {
+                    currentToken = tokens[index++];
+                }
+                if (moreTokens(tokens, index)) {
+                    index++;
+                }
+                if (moreTokens(tokens, index)) {
+                    nextNextToken = tokens[index++];
+                }
+                considerInclusion(currentToken);
+            }
+        }
+    }
+
+    private boolean shouldProcessMultipleTables(final String nextToken) {
+        return nextToken != null && nextToken.equals(TOKEN_COMMA);
+    }
+
+    private boolean moreTokens(final String[] tokens, int index) {
+        return index < tokens.length;
+    }
+
+    private void considerInclusion(final String token) {
+        if (!ignored.contains(token.toLowerCase()) && !this.tables.containsKey(token.toLowerCase())) {
+            this.tables.put(token.toLowerCase(), token);
+        }
+    }
+
+    /**
+     * parser tables
+     *
+     * @return table names extracted out of sql
+     */
+    public Collection<String> tables() {
+        return new HashSet<>(this.tables.values());
+    }
+}

+ 467 - 0
mybatis-plus-core/src/test/java/com/baomidou/mybatisplus/core/toolkit/TableNameParserTest.java

@@ -0,0 +1,467 @@
+package com.baomidou.mybatisplus.core.toolkit;
+
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class TableNameParserTest {
+
+    private static final String SQL_SELECT_SUB_QUERY = "SELECT /*+ materialize*/ strategy_id"
+        + "FROM"
+        + " ( SELECT  strat.cf_strategy_id "
+        + "   FROM strategy strt,"
+        + "        doc_sect_ver prodGrp"
+        + "  WHERE  strat.src_id               = prodGrp.struct_doc_sect_id"
+        + "           AND strat.module_type   IN ('sdfdsf','assdf')"
+        + ")";
+
+
+    private static final String SQL_SELECT_THREE_JOIN_WITH_ALIASE = "select c.name, s.name, s.id, r.result"
+        + " from colleges c "
+        + " join students s"
+        + "   on c.id = s.college_id"
+        + " join results r"
+        + "   on s.id = r.student_id"
+        + "where c.id = 3"
+        + "  and r.dt =  to_date('22-09-2005','dd-mm-yyyy')";
+
+    private static final String SQL_COMPLEX_ONE = "INSERT INTO static_product"
+        + "  ("
+        + "   DISCOUNT_ID,"
+        + "    CATEGORY_ID,"
+        + "    PRODUCT_ID"
+        + "   )"
+        + "  ( SELECT DISTINCT ALLNDC11.BUNDLE_DISCOUNT_ID,"
+        + "     ALLNDC11.PRODUCT_ID,"
+        + "     ALLNDC11.NDC11"
+        + "  FROM ITEM ITEM"
+        + " INNER JOIN"
+        + "   (SELECT NODE.SOURCE_ID NDC11,"
+        + "    PR.PRODUCT_ID,"
+        + "     BD1.BUNDLE_DISCOUNT_ID"
+        + "    FROM DR_BUNDLE B,"
+        + "      DR_BUNDLE_DISCOUNT BD1,"
+        + "        DR_BD_PRODUCT PR,"
+        + "       map_edge_ver node"
+        + "     WHERE B.DATE_ACTIVATED BETWEEN NODE.EFF_START_DATE AND NODE.EFF_END_DATE"
+        + "     AND B.DATE_ACTIVATED BETWEEN NODE.VER_START_DATE AND NODE.VER_END_DATE"
+        + "      AND B.BUNDLE_ID             =BD1.BUNDLE_ID"
+        + "    AND B.BUNDLE_STATUS         =3"
+        + "    AND PR.BUNDLE_DISCOUNT_ID   =BD1.BUNDLE_DISCOUNT_ID"
+        + "     AND BD1.IS_DYNAMIC_CATEGORY!= 1"
+        + "     AND NODE.EDGE_TYPE          = 1"
+        + "      START WITH"
+        + "      ("
+        + "        NODE.DEST_ID              = PR.PRODUCT_ID"
+        + "      AND B.BUNDLE_ID             =BD1.BUNDLE_ID"
+        + "      AND B.BUNDLE_STATUS         =3"
+        + "      AND PR.BUNDLE_DISCOUNT_ID   =BD1.BUNDLE_DISCOUNT_ID"
+        + "     AND BD1.IS_DYNAMIC_CATEGORY!= 1"
+        + "      AND NODE.EDGE_TYPE          = 1"
+        + "      AND B.DATE_ACTIVATED BETWEEN NODE.EFF_START_DATE AND NODE.EFF_END_DATE"
+        + "      AND B.DATE_ACTIVATED BETWEEN NODE.VER_START_DATE AND NODE.VER_END_DATE"
+        + "     )"
+        + "       CONNECT BY ( PRIOR NODE.SOURCE_ID=NODE.DEST_ID"
+        + "    AND PRIOR NODE.EDGE_TYPE           = 1"
+        + "    AND PRIOR B.DATE_ACTIVATED BETWEEN NODE.EFF_START_DATE AND NODE.EFF_END_DATE"
+        + "   AND PRIOR B.DATE_ACTIVATED BETWEEN NODE.VER_START_DATE AND NODE.VER_END_DATE"
+        + "    AND prior bd1.bundle_discount_id= bd1.bundle_discount_id)"
+        + "    ) ALLNDC11"
+        + "  ON (ALLNDC11.NDC11 = ITEM.CAT_MAP_ID)"
+        + "  UNION"
+        + "   ( SELECT BD1.BUNDLE_DISCOUNT_ID,"
+        + "      PR.PRODUCT_ID,"
+        + "     ITEM.CAT_MAP_ID"
+        + "    FROM DR_BUNDLE B,"
+        + "      DR_BUNDLE_DISCOUNT BD1,"
+        + "     DR_BD_PRODUCT PR,"
+        + "    ITEM ITEM"
+        + "    WHERE B.BUNDLE_ID           =BD1.BUNDLE_ID"
+        + "   AND B.BUNDLE_STATUS         =3"
+        + "   AND PR.BUNDLE_DISCOUNT_ID   =BD1.BUNDLE_DISCOUNT_ID"
+        + "    AND BD1.IS_DYNAMIC_CATEGORY!= 1"
+        + "   AND item.cat_map_id         =pr.product_id"
+        + "    )";
+
+    private static final String SQL_MERGE_COMPLEX = "MERGE INTO  cf_procedure proc USING"
+        + " ("
+        + " WITH NON_STRATEGY_DETAILS AS"
+        + "   ("
+        + "   SELECT /*+ materialize*/ cf_strategy_id"
+        + "    FROM"
+        + "     ( SELECT  strat.cf_strategy_id"
+        + "        FROM cf_strategy strat,"
+        + "             struct_doc_Sect_ver prodGrp"
+        + "        WHERE  strat.src_id               = prodGrp.struct_doc_sect_id"
+        + "                 AND strat.src_mgr_id     = prodGrp.mgr_id"
+        + "                 AND strat.src_ver_num    = prodGrp.ver_num"
+        + "                 AND strat.module_type   IN ('COMPL','PRCMSTR')"
+        + "   )  ),"
+        + "   NON_STRATEGY_COMPS AS"
+        + "   ("
+        + "   SELECT /*+ materialize*/ cf_component_id"
+        + "   FROM"
+        + "   ("
+        + "     SELECT comp.cf_component_id AS cf_component_id"
+        + "     FROM   cf_component comp,"
+        + "            tier_basis_ver tb"
+        + "     WHERE  comp.bucket_src_id   = tb.tier_basis_id"
+        + "             AND comp.bucket_src_mgr_id  = tb.mgr_id"
+        + "             AND comp.bucket_src_ver_num = tb.ver_num"
+        + "             AND comp.module_type       IN ('COMPL','PRCMSTR')"
+        + "   )"
+        + "   ) ,"
+        + " NON_STRAT_PERIODS AS ("
+        + "   SELECT /*+ materialize*/ cf_period_id"
+        + "   FROM"
+        + "         cf_period per,"
+        + "         struct_doc_sect_ver prodGrp"
+        + "   WHERE  per.src_id            = prodGrp.struct_doc_sect_id"
+        + "         AND per.src_mgr_id     = prodGrp.mgr_id"
+        + "         AND per.src_ver_num    = prodGrp.ver_num"
+        + "         AND per.module_type    IN ('COMPL','PRCMSTR')"
+        + "         AND per.pmt_status NOT IN ('TERM','REV')"
+
+        + "    SELECT DISTINCT cf_procedure_id"
+        + "   FROM"
+        + "     (SELECT /*+ LEADING(comp,proc)*/"
+        + "           proc.cf_procedure_id AS cf_procedure_id"
+        + "     FROM  non_strategy_comps comp,"
+        + "           cf_procedure proc"
+        + "     WHERE proc.variable_name          ='CALCULATION_LEVEL_RESULT'"
+        + "           AND comp.cf_component_id    = proc.cf_component_id"
+        + "    UNION ALL"
+        + "     SELECT  /*+ LEADING(strat,proc)*/"
+        + "           proc.cf_procedure_id AS cf_procedure_id"
+        + "     FROM  cf_procedure proc,"
+        + "           non_strategy_details strat"
+        + "     WHERE proc.variable_name       ='CALCULATION_LEVEL_RESULT'"
+        + "           AND strat.cf_strategy_id = proc.cf_strategy_id"
+        + "     UNION ALL"
+        + "     SELECT  /*+ LEADING(strat,proc)*/"
+        + "          proc.cf_procedure_id AS cf_procedure_id"
+        + "     FROM cf_procedure proc,"
+        + "          non_strat_periods periods"
+        + "     WHERE proc.variable_name       ='CALCULATION_LEVEL_RESULT'"
+        + "           AND periods.CF_PERIOD_ID = proc.period_id"
+        + "     )"
+        + "      )TMP ON (proc.cf_procedure_id = tmp.cf_procedure_id)"
+        + " WHEN MATCHED THEN"
+        + "   UPDATE SET proc.variable_name = 'TierResultSSName';";
+
+    private static final String SQL_MERGE_COMPLEX_TWO = " MERGE INTO cf_procedure_ver procVer USING"
+        + "   (SELECT cf_procedure_id"
+        + "    FROM cf_procedure proc"
+        + "    WHERE proc.variable_name                  = 'TierResultSSName'"
+        + "   ) proc_main ON (proc_main.cf_procedure_id = procVer.cf_procedure_id )"
+        + " WHEN MATCHED THEN"
+        + "   UPDATE SET procVer.variable_name = 'TierResultSSName'"
+        + "   WHERE procVer.variable_name <> 'TierResultSSName';";
+
+    @Test
+    public void testSelectOneTable() {
+        String sql = "SELECT name, age FROM table1 group by xyx";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1"));
+    }
+
+    @Test
+    public void testSelectTwoTables() {
+        String sql = "SELECT name, age FROM table1,table2";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testSelectThreeTables() {
+        String sql = "SELECT name, age FROM table1,table2,table3 group by xyx";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2","table3"));
+    }
+
+    @Test
+    public void testSelectOneTableWithAliase() {
+        String sql = "SELECT name, age FROM table1 t1 whatever group by xyx";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1"));
+    }
+
+    @Test
+    public void testSelectTwoTablesWithAliase() {
+        String sql = "SELECT name, age FROM table1 t1,table2 t2 whatever group by xyx";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testSelectTwoTablesWithAliaseAndNoCondition() {
+        String sql = "select xx from table1 a,table2 b";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testSelectThreeTablesWithAliase() {
+        String sql = "SELECT name, age FROM table1 t1,table2 t2, table3 t3 whatever group by xyx";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2", "table3"));
+    }
+
+
+    @Test
+    public void testSelectWithSubQuery() {
+        assertThat(new TableNameParser(SQL_SELECT_SUB_QUERY).tables()).isEqualTo(asSet("strategy", "doc_sect_ver"));
+    }
+
+    @Test
+    public void testSelectWithOneJoin() {
+        String sql = "SELECT coluname(s) FROM table1 join table2 ON table1.coluname=table2.coluname";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testSelectOneJoinWithAliase() {
+        String sql = "SELECT coluname(s) FROM table1 t1 join table2 t2 ON t1.coluname=t2.coluname";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testSelectOneLeftJoin() {
+        String sql = "SELECT coluname(s) FROM table1 left outer join table2 ON table1.coluname=table2.coluname";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+
+    @Test
+    public void testShouldIgnoreDual() {
+        String sql = "select * from dual";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+
+    @Test
+    public void testSelectTwoJoinWithAliase() {
+        assertThat(new TableNameParser(SQL_SELECT_THREE_JOIN_WITH_ALIASE).tables()).isEqualTo(asSet("colleges", "students", "results"));
+    }
+
+
+    @Test
+    public void testInsertWithValues() {
+        String sql = "INSERT INTO table_name VALUES (value1,value2,value3,...)";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table_name"));
+    }
+
+    @Test
+    public void testInsertComplex() {
+        assertThat(new TableNameParser(SQL_COMPLEX_ONE).tables()).isEqualTo(asSet("static_product", "DR_BD_PRODUCT", "DR_BUNDLE", "map_edge_ver", "ITEM", "DR_BUNDLE_DISCOUNT"));
+    }
+
+    @Test
+    public void testInsertWithSelect() {
+        String sql = "INSERT INTO Customers (CustomerName, Country) SELECT SupplierName, Country FROM Suppliers;";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("Customers", "Suppliers"));
+    }
+
+    @Test
+    public void testDelete() {
+        String sql = "DELETE FROM validation_task WHERE task_name = 'ValidateSoldToCustId' AND conf_id IN (SELECT conf_id FROM validation_conf WHERE conf_name IN ('SaleValidation'))";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("validation_task", "validation_conf"));
+    }
+
+    @Test
+    public void testOracleSpecialDelete() {
+        String sql = "delete table1 where column_name=xyz";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1"));
+    }
+
+    @Test
+    public void testAlter() {
+        String sql = "ALTER TABLE Persons ADD UNIQUE (P_Id)";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("Persons"));
+    }
+
+    @Test
+    public void testAlter2() {
+        String sql = "ALTER TABLE table_name MODIFY coluname datatype";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table_name"));
+    }
+
+    @Test
+    public void testDrop() {
+        String sql = "DROP table tname;\n\r";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("tname"));
+    }
+
+    @Test
+    public void testDropFunction() {
+        String sql = "DROP FUNCTION functionName;";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+    @Test
+    public void testDropProcedure() {
+        String sql = "drop procedure procedureName";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+    @Test
+    public void testDropView() {
+        String sql = "DROP VIEW viewName";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+    @Test
+    public void testDropIndex() {
+        String sql = "DROP INDEX indexName";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+    @Test
+    public void testUnionAll() {
+        String sql = "SELECT coluname(s) FROM table1 UNION ALL SELECT coluname(s) FROM table2;";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testMerge() {
+        String sql = "MERGE INTO employees e  USING hr_records h  ON (e.id = h.emp_id) WHEN MATCHED THEN  UPDATE SET e.address = h.address  WHEN NOT MATCHED THEN    INSERT (id, address) VALUES (h.emp_id, h.address);";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("employees", "hr_records"));
+    }
+
+    @Test
+    public void testMergeUsingQuery() {
+        String sql = "MERGE INTO employees e USING (SELECT * FROM hr_records WHERE start_date > ADD_MONTHS(SYSDATE, -1)) h  ON (e.id = h.emp_id)  WHEN MATCHED THEN  UPDATE SET e.address = h.address WHEN NOT MATCHED THEN INSERT (id, address) VALUES (h.emp_id, h.address)";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("employees", "hr_records"));
+    }
+
+    @Test
+    public void testMergeComplexQuery() {
+        assertThat(new TableNameParser(SQL_MERGE_COMPLEX).tables()).isEqualTo(asSet("non_strategy_comps","cf_procedure", "struct_doc_Sect_ver", "cf_period", "cf_component", "cf_strategy", "tier_basis_ver", "non_strategy_details", "cf_procedure", "non_strat_periods"));
+    }
+
+    @Test
+    public void testMergeComplexQuery2() {
+        assertThat(new TableNameParser(SQL_MERGE_COMPLEX_TWO).tables()).isEqualTo(asSet("cf_procedure_ver", "cf_procedure"));
+    }
+
+    @Test
+    public void testCreateTable() {
+        String sql = "CREATE TABLE Persons(PersonID int,LastName varchar(255),FirstName varchar(255),Address varchar(255),City varchar(255));";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("Persons"));
+    }
+
+    @Test
+    public void testCreateGlobalTable() {
+        String sql = "CREATE GLOBAL TEMPORARY TABLE excl_cust (gen_name VARCHAR2(100),run_date TIMESTAMP(3), item_root_uuid  VARCHAR2(22), owner_member_id  NUMBER(20)) ON COMMIT DELETE ROWS";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("excl_cust"));
+    }
+
+    @Test
+    public void testCreateIndex() {
+        String sql = "CREATE INDEX temp_name_idx ON table1(name) NOLOGGING PARALLEL (DEGREE 8);";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1"));
+    }
+
+    @Test
+    public void testCreateView() {
+        String sql = "CREATE VIEW dept AS SELECT * FROM dept;";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("dept"));
+    }
+
+    @Test
+    public void testCreateView2() {
+        String sql = "CREATE VIEW division1_staff AS SELECT ename, empno, job, dname FROM emp, dept WHERE emp.deptno IN (10, 30) AND emp.deptno = dept.deptno;";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("dept","emp"));
+    }
+
+    @Test
+    public void testCreateType() {
+        String sql = "CREATE OR REPLACE TYPE TYPE_NAME IS TABLE OF VARCHAR2(100)";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet());
+    }
+
+    @Test
+    public void testUpdateTable() {
+        String sql = "UPDATE tableName SET column1 = expression1, column2 = expression2";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("tableName"));
+    }
+
+    @Test
+    public void testUpdateTableSubQuery() {
+        String sql = "UPDATE table1 SET table1.value = (SELECT table2.CODE FROM table2 WHERE table1.value = table2.DESC) WHERE table1.UPDATETYPE='blah' AND EXISTS (SELECT table2.CODE  FROM table2    WHERE table1.value = table2.DESC);";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testUpdateTableSubQuery2() {
+        String sql = "UPDATE (SELECT table1.value as OLD, table2.CODE as NEW FROM table1 INNER JOIN table2 ON table1.value = table2.DESC  WHERE table1.UPDATETYPE='blah' ) t SET t.OLD = t.NEW";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("table1", "table2"));
+    }
+
+    @Test
+    public void testUpdateTableSubQueryWithOracleHint() {
+        String sql = "update /*+ PARALLEL OPT_PARAM('parallel_min_percent','0') */ eligible ec set ec.END_DATE = ec.END_DATE + INTERVAL '0 0:0:0.999' DAY TO SECOND";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("eligible"));
+    }
+
+    @Test
+    public void testTruncateTable() {
+        String sql = "truncate table eligible_item";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("eligible_item"));
+    }
+
+    @Test
+    public void testSqlWithComment() {
+        String sql = "select * from foo -- this is a comment";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo"));
+    }
+
+    @Test
+    public void testSqlWithCommentContainingKeyword() {
+        String sql = "select * from foo -- what happens if I say update in a comment";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo"));
+    }
+
+    @Test
+    public void testSqlWithCommentEndingWithKeyword() {
+        String sql = "select * from foo -- what happens if I end a comment with an update";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo"));
+    }
+
+    @Test
+    public void testSqlWithCommentInTheMiddle() {
+        String sql = "select * -- I like stars \n from foo";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo"));
+    }
+
+    @Test
+    public void testSqlWithCommentInTheMiddleAndEnd() {
+        String sql = "select * -- I like stars \n from foo -- comment ending with update";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo"));
+    }
+
+    @Test
+    public void testSqlWithMultipleCommentsInTheMiddle() {
+        String sql = "select * -- I like stars \n from foo f -- I like foo \n join bar b -- I also like bar \n on f.id = b.id";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo","bar"));
+    }
+
+    @Test
+    public void testSqlWithMultipleCommentsAndNewlines() {
+        String sql = "select * -- I like stars \n from foo f -- I like foo \n\n join bar b -- I also like bar \n on f.id = b.id";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo","bar"));
+    }
+
+    @Test
+    public void testSqlWithMultipleCommentsInTheMiddleAndEnd() {
+        String sql = "select * -- I like stars \n from foo f -- I like foo \n join bar b -- I also like bar \n on f.id = b.id -- comment ending with update";
+        assertThat(new TableNameParser(sql).tables()).isEqualTo(asSet("foo","bar"));
+    }
+
+    private static Collection<String> asSet(String... a) {
+        Set<String> result = new HashSet<String>();
+        for (String item : a) {
+            result.add(item);
+        }
+        return result;
+    }
+
+}