001package gudusoft.gsqlparser.lineage.v2.identity; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Collections; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009import java.util.regex.Matcher; 010import java.util.regex.Pattern; 011 012/** 013 * The parser-free rules of the path profile {@code sqllr-path-1} ({@code lineage-spec/algorithms/path-v1.md}, 014 * Section 4): the grammar, the registered step codes, which step may follow which, and the steps whose index 015 * is always 0. Which steps a statement emits needs the parse tree and belongs to the producer. 016 * 017 * <p>The reference is {@code path_v1_violations} of {@code sqllr_ids.py}.</p> 018 * 019 * @since 4.2.17 020 */ 021public final class PathV1 { 022 023 private static final Pattern STEP = Pattern.compile("([A-Z][A-Z0-9_]*)\\[(0|[1-9][0-9]*)\\]"); 024 025 private static List<String> list(String... values) { 026 return Collections.unmodifiableList(Arrays.asList(values)); 027 } 028 029 /** The steps that may start a path. */ 030 public static final List<String> ROOTS = list("SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE_TABLE", 031 "CREATE_VIEW", "COPY", "CALL", "EXECUTE", "CREATE_ROUTINE", "BLOCK", "ASSIGNMENT"); 032 /** A query block's clauses, and the rows of a VALUES list that is the query expression (plan 09 slice 11). */ 033 private static final List<String> BLOCK = list("WITH", "SET_OP", "SELECT_LIST", "FROM", "WHERE", "GROUP_BY", 034 "HAVING", "QUALIFY", "ORDER_BY", "LIMIT", "WINDOW", "VALUES"); 035 private static final List<String> EXPRESSION = list("FUNCTION_ARG", "CASE_WHEN", "CASE_THEN", "CASE_ELSE", 036 "SUBQUERY", "WINDOW"); 037 private static final List<String> EXPRESSION_STEPS = list("PIVOT", "SELECT_LIST", "WHERE", "GROUP_BY", "HAVING", "QUALIFY", 038 "ORDER_BY", "LIMIT", "ON", "SET", "VALUES", "PARTITION_BY", "FUNCTION_ARG", "CASE_WHEN", "CASE_THEN", 039 "CASE_ELSE", "RETURN", "ASSIGN", "CONDITION"); 040 private static final List<String> ALWAYS_ZERO = list("SELECT", "WITH", "SET_OP", "SELECT_LIST", "FROM", "WHERE", 041 "GROUP_BY", "HAVING", "QUALIFY", "ORDER_BY", "LIMIT", "TARGET", "COLUMN_LIST", "SET", "ON", "USING", 042 "PARTITION_BY", "CASE_ELSE", "RETURNING"); 043 private static final Map<String, List<String>> TABLE = new HashMap<String, List<String>>(); 044 045 static { 046 TABLE.put("SET_OP", list("BRANCH", "ORDER_BY", "LIMIT")); 047 TABLE.put("WITH", list("CTE")); 048 TABLE.put("FROM", list("JOIN", "SUBQUERY", "PIVOT", "LATERAL")); 049 TABLE.put("JOIN", list("ON", "USING", "SUBQUERY", "PIVOT", "LATERAL")); 050 TABLE.put("TARGET", list("JOIN", "SUBQUERY", "LATERAL")); 051 // a lateral item: a LATERAL derived table's or an APPLY's query, or a call's operands (plan 09 slice 11) 052 TABLE.put("LATERAL", list("SUBQUERY", "FUNCTION_ARG")); 053 TABLE.put("WINDOW", list("PARTITION_BY", "ORDER_BY")); 054 TABLE.put("INSERT", list("WITH", "TARGET", "COLUMN_LIST", "SELECT", "VALUES", "EXECUTE", "RETURNING")); 055 TABLE.put("CREATE_TABLE", list("WITH", "TARGET", "COLUMN_LIST", "SELECT", "SOURCE")); 056 TABLE.put("CREATE_VIEW", list("WITH", "TARGET", "COLUMN_LIST", "SELECT")); 057 TABLE.put("UPDATE", list("WITH", "TARGET", "SET", "FROM", "WHERE", "ORDER_BY", "LIMIT", "RETURNING")); 058 TABLE.put("DELETE", list("WITH", "TARGET", "FROM", "USING", "WHERE", "ORDER_BY", "LIMIT", "RETURNING")); 059 // the rows the statement returns (doc 09, Section 10, question 6): its items, and SQL Server OUTPUT INTO's table 060 // and column list 061 List<String> returning = new ArrayList<String>(Arrays.asList("TARGET", "COLUMN_LIST")); 062 returning.addAll(EXPRESSION); 063 TABLE.put("RETURNING", Collections.unmodifiableList(returning)); 064 TABLE.put("MERGE", list("WITH", "TARGET", "USING", "ON", "WHEN")); 065 List<String> when = new ArrayList<String>(Arrays.asList("SET", "COLUMN_LIST", "VALUES")); 066 when.addAll(EXPRESSION); 067 TABLE.put("WHEN", Collections.unmodifiableList(when)); 068 TABLE.put("CREATE_ROUTINE", list("RETURN", "ASSIGN", "CONDITION", "CURSOR", "FETCH")); 069 TABLE.put("BLOCK", list("ASSIGN", "CONDITION", "CURSOR", "FETCH")); 070 TABLE.put("ASSIGNMENT", list("ASSIGN")); 071 // a call's arguments (plan 09 slice 12): CALL, EXECUTE, and the EXECUTE of INSERT ... EXEC 072 TABLE.put("CALL", list("FUNCTION_ARG")); 073 TABLE.put("EXECUTE", list("FUNCTION_ARG")); 074 } 075 076 private PathV1() { 077 } 078 079 /** The step codes PATH-V1 admits directly below {@code code}, whose own parent step is {@code parent}. */ 080 public static List<String> children(String code, String parent) { 081 if ("SELECT".equals(code) || "SUBQUERY".equals(code) || "BRANCH".equals(code) || "CURSOR".equals(code)) { 082 return BLOCK; 083 } 084 if ("CTE".equals(code)) { 085 List<String> out = new ArrayList<String>(BLOCK); 086 out.add("COLUMN_LIST"); 087 // a data-changing statement as the CTE's body (PostgreSQL WITH d AS (DELETE ...), Section 3.2, item 6), 088 // a MERGE included (PostgreSQL 17, plan 09 slice 13i) 089 out.add("INSERT"); 090 out.add("UPDATE"); 091 out.add("DELETE"); 092 out.add("MERGE"); 093 return out; 094 } 095 if ("USING".equals(code)) { 096 return "JOIN".equals(parent) ? Collections.<String>emptyList() : list("JOIN", "SUBQUERY", "LATERAL"); 097 } 098 if ("TARGET".equals(code) && "RETURNING".equals(parent)) { 099 return Collections.emptyList(); // the table of OUTPUT ... INTO 100 } 101 if (TABLE.containsKey(code)) { 102 return TABLE.get(code); 103 } 104 if (EXPRESSION_STEPS.contains(code)) { 105 return EXPRESSION; 106 } 107 return Collections.emptyList(); // COLUMN_LIST, SOURCE, COPY, FETCH: nothing below them in PATH-V1 108 } 109 110 /** 111 * The rules of PATH-V1 that a path text breaks: the grammar, the registered step codes, the nesting and the 112 * fixed indexes. Empty when the path follows them. 113 */ 114 public static List<String> violations(Object value) { 115 List<String> problems = new ArrayList<String>(); 116 if (!(value instanceof String)) { 117 problems.add(value + " is not a " + IdentityV1.PATH_PROFILE_V1 + " path"); 118 return problems; 119 } 120 String[] parts = ((String) value).split("/", -1); 121 List<String> codes = new ArrayList<String>(); 122 List<Long> indexes = new ArrayList<Long>(); 123 for (String part : parts) { 124 Matcher m = STEP.matcher(part); 125 if (!m.matches()) { 126 problems.add(value + " is not a " + IdentityV1.PATH_PROFILE_V1 + " path"); 127 return problems; 128 } 129 codes.add(m.group(1)); 130 String digits = m.group(2); 131 indexes.add(digits.length() > 18 ? Long.MAX_VALUE : Long.valueOf(digits)); 132 } 133 for (String code : codes) { 134 if (!IdentityV1.PATH_STEP_CODES.contains(code)) { 135 problems.add("step " + code + " is not a registered step code"); 136 } 137 } 138 if (!problems.isEmpty()) { 139 return problems; 140 } 141 if (!ROOTS.contains(codes.get(0))) { 142 problems.add(codes.get(0) + " cannot start a path"); 143 } else if (indexes.get(0) != 0) { 144 problems.add("the statement step " + codes.get(0) + " has index " + indexes.get(0) + ", not 0"); 145 } 146 for (int i = 1; i < codes.size(); i++) { 147 String code = codes.get(i); 148 String parent = codes.get(i - 1); 149 String grand = i >= 2 ? codes.get(i - 2) : null; 150 if (!children(parent, grand).contains(code)) { 151 problems.add(code + " cannot follow " + parent); 152 break; 153 } 154 boolean zero = ALWAYS_ZERO.contains(code) || ("VALUES".equals(code) && "WHEN".equals(parent)) 155 || ("WINDOW".equals(code) && EXPRESSION_STEPS.contains(parent)) 156 || ("SUBQUERY".equals(code) && "LATERAL".equals(parent)) 157 || ("EXECUTE".equals(code) && "INSERT".equals(parent)) 158 || (("INSERT".equals(code) || "UPDATE".equals(code) || "DELETE".equals(code) 159 || "MERGE".equals(code)) && "CTE".equals(parent)); 160 if (zero && indexes.get(i) != 0) { 161 problems.add(code + " below " + parent + " has index " + indexes.get(i) + ", not 0"); 162 } 163 } 164 return problems; 165 } 166}