001package gudusoft.gsqlparser.lineage.v2.validation;
002
003import gudusoft.gsqlparser.lineage.v2.Finding;
004
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.List;
008import java.util.Map;
009import java.util.TreeSet;
010
011/**
012 * The staged validator of SQL-LR (specification Section 14.2): {@code SCHEMA}, {@code SEMANTIC},
013 * {@code REFERENCE}, {@code IDENTITY}, in the order of the registry group {@code validationStages}. It stops at
014 * the first stage that reports a finding, as the reference runner does ({@code conformance/run.py},
015 * {@code validate_bytes}).
016 *
017 * <p>A validator is immutable and may be shared between threads.</p>
018 *
019 * @since 4.2.17
020 */
021public final class LineageValidator {
022
023    /** The stages in order. */
024    public static final List<String> STAGES = Collections.unmodifiableList(
025            java.util.Arrays.asList("SCHEMA", "SEMANTIC", "REFERENCE", "IDENTITY"));
026
027    /** The outcome of a validation. */
028    public static final class Result {
029        private final String stage;
030        private final List<Finding> findings;
031        private final List<String> notes;
032
033        Result(String stage, List<Finding> findings, List<String> notes) {
034            this.stage = stage;
035            this.findings = Collections.unmodifiableList(findings);
036            this.notes = Collections.unmodifiableList(notes);
037        }
038
039        /** The first stage that reported a finding, or null when the document passes every stage. */
040        public String getStage() {
041            return stage;
042        }
043
044        /** True when the document passes every stage. */
045        public boolean isValid() {
046            return stage == null;
047        }
048
049        /** The findings of the failing stage. */
050        public List<Finding> getFindings() {
051            return findings;
052        }
053
054        /** The distinct codes of the findings, sorted. */
055        public List<String> getCodes() {
056            TreeSet<String> codes = new TreeSet<String>();
057            for (Finding finding : findings) {
058                codes.add(finding.getCode());
059            }
060            return new ArrayList<String>(codes);
061        }
062
063        /** What the validator could not check. A document can pass and still break these. */
064        public List<String> getNotes() {
065            return notes;
066        }
067    }
068
069    private final SchemaStage schema;
070    private final SemanticStage semantic;
071
072    public LineageValidator(SchemaStage schema, SemanticStage semantic) {
073        this.schema = schema;
074        this.semantic = semantic;
075    }
076
077    /**
078     * A validator with the shipped schema and registry, the given source text provider, and the FOLD-V1
079     * {@code PROJECTOR} checks of {@link gudusoft.gsqlparser.lineage.v2.fold.Projector}.
080     */
081    public static LineageValidator standard(SourceTextProvider sources) {
082        return standard(sources, gudusoft.gsqlparser.lineage.v2.fold.Projector.CHECK);
083    }
084
085    /**
086     * A validator with the shipped schema and registry, the given source text provider, and the given FOLD-V1
087     * checks (null to skip them; a document with an ExplainGraph then gets a note saying so).
088     */
089    public static LineageValidator standard(SourceTextProvider sources, SemanticStage.FoldCheck fold) {
090        return new LineageValidator(SchemaStage.standard(), new SemanticStage(sources, fold));
091    }
092
093    /**
094     * Validates the bytes of a lineage document.
095     *
096     * @throws gudusoft.gsqlparser.lineage.v2.fold.FoldLimitException when folding a statement needs more work than
097     *         the FOLD-V1 kernel allows; the document was then not validated
098     */
099    public Result validate(byte[] data) {
100        List<String> notes = new ArrayList<String>();
101        SchemaStage.Result first = schema.run(data);
102        if (!first.getFindings().isEmpty()) {
103            return new Result("SCHEMA", new ArrayList<Finding>(first.getFindings()), notes);
104        }
105        Map<?, ?> doc = (Map<?, ?>) first.getDocument();
106        List<Finding> findings = semantic.run(doc, first.getDeferred(), notes);
107        if (!findings.isEmpty()) {
108            return new Result("SEMANTIC", findings, notes);
109        }
110        findings = ReferenceStage.run(doc);
111        if (!findings.isEmpty()) {
112            return new Result("REFERENCE", findings, notes);
113        }
114        findings = IdentityStage.run(doc, notes);
115        if (!findings.isEmpty()) {
116            return new Result("IDENTITY", findings, notes);
117        }
118        return new Result(null, new ArrayList<Finding>(), notes);
119    }
120}