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}