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}