001package gudusoft.gsqlparser.slpc.validation; 002 003import gudusoft.gsqlparser.slpc.identity.SlpcIdentity; 004import gudusoft.gsqlparser.slpc.model.SlpcDocument; 005import gudusoft.gsqlparser.slpc.model.SlpcObject; 006 007import java.math.BigInteger; 008import java.nio.charset.StandardCharsets; 009import java.security.MessageDigest; 010import java.security.NoSuchAlgorithmException; 011import java.util.ArrayDeque; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.Comparator; 016import java.util.HashMap; 017import java.util.HashSet; 018import java.util.LinkedHashMap; 019import java.util.LinkedHashSet; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.regex.Pattern; 024 025/** Executable cross-record invariants for SLPC v0.2. */ 026public final class SlpcSemanticValidator { 027 028 public static final List<String> CAPABILITIES; 029 private static final Set<String> CORE_DIAGNOSTICS; 030 private static final Pattern SECRET = Pattern.compile( 031 "(?i)(?:aws[_-]?(?:secret|access)[_-]?key|secret[_-]?key|access[_-]?token|password)\\s*[:=]\\s*(?!<SLPC-REDACTED)[^\\s,;)]+"); 032 033 static { 034 ArrayList<String> values = new ArrayList<String>(Arrays.asList( 035 "FACT_VALUE", "FACT_ROW_FILTER", "FACT_ROW_JOIN", "FACT_GROUP_BY", "FACT_WINDOW", 036 "DATASET_SORT", "PHYSICAL_HOP_COLLAPSE", "DYNAMIC_SQL", "PROCEDURAL_COMPOSITION", 037 "SOURCE_LOCATION", "TRANSFORM_EVIDENCE", "EXACT_IDENTIFIER_IDENTITY", 038 "AUTHORITATIVE_CATALOG_IDENTITY", "SCOPED_OBJECTS", "OBJECT_RELATIONSHIP", 039 "FOREIGN_KEY_RELATIONSHIP", "JOIN_SEMANTICS", "ROW_IMPACT_DETAIL", "STATEMENT_OPERATION", 040 "INTERMEDIATE_GRAPH", "INTERACTIVE_EXPLAIN", "LITERAL_PROVENANCE", "SECRET_REDACTION", 041 "STORAGE_ASSET_IDENTITY", "ROUTINE_CALL", "ROUTINE_PARAMETER_FLOW", 042 "ROUTINE_OVERLOAD_IDENTITY", "STREAM_DEFINITION", "OBJECT_RENAME_SWAP_CLONE", 043 "COPY_CLASSIFICATION")); 044 Collections.sort(values); 045 CAPABILITIES = Collections.unmodifiableList(values); 046 CORE_DIAGNOSTICS = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( 047 "SLPC_ADAPTER_CAPABILITY_GAP", "SLPC_IDENTITY_APPROXIMATE", "SLPC_IDENTITY_UNAVAILABLE", 048 "SLPC_MALFORMED_IDENTIFIER", "SLPC_ENDPOINT_AMBIGUOUS", "SLPC_ENDPOINT_UNRESOLVED", 049 "SLPC_ENDPOINT_NOT_PUBLISHABLE", "SLPC_UNBOUND_RESULT_SET", "SLPC_SCOPE_UNAVAILABLE", 050 "SLPC_PSEUDO_COLUMN_REJECTED", "SLPC_INTERMEDIATE_HOP_COLLAPSED", 051 "SLPC_INTERMEDIATE_HOP_UNCOLLAPSIBLE", "SLPC_INVALID_AXIS_ROLE_OPERATION", "SLPC_ROLE_UNKNOWN", 052 "SLPC_OPERATION_UNSUPPORTED", "SLPC_RELATIONSHIP_UNMAPPED", "SLPC_DYNAMIC_LOW_CONFIDENCE", 053 "SLPC_INVALID_REFERENCE", "SLPC_ID_COLLISION", "SLPC_EXTENSION_UNSUPPORTED", 054 "SLPC_OBJECT_RELATIONSHIP_UNPUBLISHABLE", "SLPC_LEGACY_FK_DATAFLOW_RECLASSIFIED", 055 "SLPC_COMPOSITE_FK_PAIRING_UNPROVEN", "SLPC_COMPOSITE_FK_PAIRING_PROVEN", 056 "SLPC_ROUTINE_BINDING_UNPROVEN", 057 "SLPC_JOIN_SEMANTICS_PARTIAL", "SLPC_DERIVED_FIELD_EXPANSION", "SLPC_LITERAL_TRUNCATED", 058 "SLPC_SECRET_REDACTED", "SLPC_COPY_CLASSIFICATION_CONFLICT", "SLPC_STORAGE_IDENTITY_UNAVAILABLE", 059 "SLPC_NON_FACT_TRAVERSAL_UNSUPPORTED", "SLPC_STATEMENT_OPERATION_MISMATCH", 060 "SLPC_OBJECT_REBOUND_RISK", "SLPC_INTERMEDIATE_GRAPH_INCOMPLETE"))); 061 } 062 063 public List<SlpcValidationError> validate(SlpcDocument document) { 064 try { 065 validateOrThrow(document.root()); 066 return Collections.emptyList(); 067 } catch (Failure failure) { 068 return Collections.singletonList(new SlpcValidationError( 069 SlpcValidationError.Stage.SEMANTIC, failure.path, failure.getMessage())); 070 } catch (IllegalArgumentException failure) { 071 return Collections.singletonList(new SlpcValidationError( 072 SlpcValidationError.Stage.SEMANTIC, "$", failure.getMessage())); 073 } 074 } 075 076 public List<SlpcValidationError> validateProjectionReport(SlpcDocument document, SlpcObject report) { 077 try { 078 validateProjectionReportOrThrow(document.root(), report); 079 return Collections.emptyList(); 080 } catch (Failure failure) { 081 return Collections.singletonList(new SlpcValidationError( 082 SlpcValidationError.Stage.SEMANTIC, failure.path, failure.getMessage())); 083 } catch (IllegalArgumentException failure) { 084 return Collections.singletonList(new SlpcValidationError( 085 SlpcValidationError.Stage.SEMANTIC, "$", failure.getMessage())); 086 } 087 } 088 089 public void validateProjectionReportOrThrow(SlpcObject document, SlpcObject report) { 090 String[] collections = {"facts", "factCandidates", "datasetOperationInfluences", "datasetOperationCandidates", 091 "objectRelationships", "objectRelationshipCandidates", "statementOperations", "joinSemantics", "intermediateGraphs"}; 092 String[] kinds = {"FACT", "FACT_CANDIDATE", "DATASET_OPERATION", "DATASET_OPERATION_CANDIDATE", 093 "OBJECT_RELATIONSHIP", "OBJECT_RELATIONSHIP_CANDIDATE", "STATEMENT_OPERATION", "JOIN_SEMANTICS", "INTERMEDIATE_GRAPH"}; 094 String[] ids = {"factId", "candidateId", "influenceId", "candidateId", "relationshipId", "candidateId", 095 "statementOperationId", "joinSemanticsId", "graphRef"}; 096 SlpcObject inputCounts = object(report, "inputCounts"); 097 Set<String> expectedOwners = new HashSet<String>(); int total = 0; 098 for (int index = 0; index < collections.length; index++) { 099 List<Object> records = array(document, collections[index]); int count = records.size(); total += count; 100 equal(integer(inputCounts, collections[index]), (long) count, "projection report inputCounts." + collections[index]); 101 for (Object value : records) expectedOwners.add(kinds[index] + "\u0000" + string(asObject(value), ids[index])); 102 } 103 equal(integer(inputCounts, "total"), (long) total, "projection report inputCounts.total"); 104 List<Object> records = array(report, "records"); 105 equal(records.size(), total, "projection report records length"); 106 Set<String> observedOwners = new HashSet<String>(); List<String> order = new ArrayList<String>(); 107 Map<String, Integer> outcomes = new HashMap<String, Integer>(), diagnostics = new HashMap<String, Integer>(); 108 for (Object value : records) { 109 SlpcObject record = asObject(value), owner = object(record, "ownerRef"); 110 String ownerKey = string(owner, "kind") + "\u0000" + string(owner, "id"); 111 if (!observedOwners.add(ownerKey)) fail("$.records", "projection report has duplicate owner: " + ownerKey.replace('\u0000', ':')); 112 order.add(ownerKey); String outcome = string(record, "outcome"); 113 outcomes.put(outcome, outcomes.containsKey(outcome) ? outcomes.get(outcome) + 1 : 1); 114 List<String> extensionTypes = strings(array(record, "extensionTypes")); 115 List<String> diagnosticCodes = strings(array(record, "diagnosticCodes")); 116 equal(extensionTypes, sorted(extensionTypes), "projection report extensionTypes order"); 117 equal(diagnosticCodes, sorted(diagnosticCodes), "projection report diagnosticCodes order"); 118 if (ownerKey.startsWith("FACT_CANDIDATE\u0000") || ownerKey.startsWith("DATASET_OPERATION_CANDIDATE\u0000") 119 || ownerKey.startsWith("OBJECT_RELATIONSHIP_CANDIDATE\u0000")) { 120 if (!"INELIGIBLE".equals(outcome) || integer(record, "standardOutputCount") != 0) 121 fail("$.records", "projection candidate must be INELIGIBLE with standardOutputCount 0: " + ownerKey.replace('\u0000', ':')); 122 } 123 for (String code : diagnosticCodes) diagnostics.put(code, diagnostics.containsKey(code) ? diagnostics.get(code) + 1 : 1); 124 } 125 equal(observedOwners, expectedOwners, "projection report owner coverage"); 126 equal(order, sorted(order), "projection report record order"); 127 SlpcObject outcomeCounts = object(report, "outcomeCounts"); long outcomeTotal = 0; 128 for (String code : Arrays.asList("STANDARD", "EXTENSION_ONLY", "CONDITIONAL", "INELIGIBLE", "UNSUPPORTED")) { 129 long actual = integer(outcomeCounts, code); int expected = outcomes.containsKey(code) ? outcomes.get(code) : 0; 130 equal(actual, (long) expected, "projection report outcomeCounts." + code); outcomeTotal += actual; 131 } 132 equal(outcomeTotal, (long) total, "projection report outcome total"); 133 List<Object> diagnosticCounts = array(report, "diagnosticCounts"); List<String> diagnosticOrder = new ArrayList<String>(); 134 Set<String> seenCodes = new HashSet<String>(); 135 for (Object value : diagnosticCounts) { 136 SlpcObject count = asObject(value); String code = string(count, "code"); 137 if (!seenCodes.add(code)) fail("$.diagnosticCounts", "duplicate projection diagnostic count: " + code); 138 diagnosticOrder.add(code); equal(integer(count, "count"), (long) (diagnostics.containsKey(code) ? diagnostics.get(code) : 0), 139 "projection report diagnosticCounts." + code); 140 } 141 equal(diagnosticOrder, sorted(diagnosticOrder), "projection report diagnosticCounts order"); 142 equal(seenCodes, diagnostics.keySet(), "projection report diagnostic code coverage"); 143 } 144 145 public void validateOrThrow(SlpcObject document) { 146 String version = string(document, "contractVersion"); 147 if (!version.startsWith("0.2.")) fail("$", "semantic validator only accepts contractVersion 0.2.x"); 148 149 SlpcObject producer = object(document, "producer"); 150 List<String> capabilityCodes = fields(array(producer, "capabilities"), "code"); 151 equal(capabilityCodes, CAPABILITIES, "capability registry/order"); 152 Map<String, String> capabilityStatus = new HashMap<String, String>(); 153 for (Object value : array(producer, "capabilities")) { 154 SlpcObject capability = asObject(value); 155 capabilityStatus.put(string(capability, "code"), string(capability, "status")); 156 } 157 if (capabilityStatus.containsValue("PARTIAL") && !hasDiagnosticCode(document, "SLPC_ADAPTER_CAPABILITY_GAP")) { 158 fail("$.producer.capabilities", "PARTIAL capability requires SLPC_ADAPTER_CAPABILITY_GAP diagnostic"); 159 } 160 if (!"UNSUPPORTED".equals(capabilityStatus.get("INTERACTIVE_EXPLAIN")) 161 && "UNSUPPORTED".equals(capabilityStatus.get("INTERMEDIATE_GRAPH"))) { 162 fail("$.producer.capabilities", "INTERACTIVE_EXPLAIN requires INTERMEDIATE_GRAPH capability"); 163 } 164 validateProfileLocations(document); 165 166 List<Object> artifactsList = array(document, "artifacts"); 167 List<Object> statementsList = array(document, "statements"); 168 List<Object> endpointsList = array(document, "endpoints"); 169 Map<String, SlpcObject> artifacts = uniqueIndex(artifactsList, "artifactId", "artifact"); 170 Map<String, SlpcObject> statements = uniqueIndex(statementsList, "statementId", "statement"); 171 Map<String, SlpcObject> endpoints = uniqueIndex(endpointsList, "endpointRef", "endpoint"); 172 equal(fields(artifactsList, "artifactId"), sorted(artifacts.keySet()), "artifact order"); 173 equal(fields(statementsList, "statementId"), sorted(statements.keySet()), "statement order"); 174 for (Object value : artifactsList) { 175 SlpcObject item = asObject(value); 176 equal(string(item, "artifactId"), SlpcIdentity.artifactId(item), "artifactId"); 177 } 178 for (Object value : statementsList) { 179 SlpcObject item = asObject(value); 180 requireRef(artifacts, string(item, "artifactId"), "statement references missing artifact"); 181 equal(string(item, "statementId"), SlpcIdentity.statementId(item), "statementId"); 182 } 183 184 List<String> endpointSort = new ArrayList<String>(); 185 for (Object value : endpointsList) { 186 SlpcObject endpoint = asObject(value); 187 String canonical = endpoint.string("canonicalEndpointId"); 188 if (canonical != null) { 189 equal(canonical, SlpcIdentity.endpointId(endpoint), string(endpoint, "endpointRef") + " canonicalEndpointId"); 190 endpointSort.add("0\u0000" + canonical); 191 } else { 192 endpointSort.add("1\u0000" + SlpcIdentity.candidateEndpointKey(endpoint)); 193 } 194 validateUriDigest(endpoint); 195 } 196 validateRoutineParameterProfiles(endpointsList); 197 equal(endpointSort, sorted(endpointSort), "endpoint order"); 198 localIds(fields(endpointsList, "endpointRef"), "ep-", "endpoint"); 199 200 List<Object> facts = array(document, "facts"); 201 List<Object> factCandidates = array(document, "factCandidates"); 202 List<Object> influences = array(document, "datasetOperationInfluences"); 203 List<Object> operationCandidates = array(document, "datasetOperationCandidates"); 204 List<Object> relationships = array(document, "objectRelationships"); 205 List<Object> relationshipCandidates = array(document, "objectRelationshipCandidates"); 206 Map<String, SlpcObject> factById = uniqueIndex(facts, "factId", "factId"); 207 Map<String, SlpcObject> factCandidateById = uniqueIndex(factCandidates, "candidateId", "fact candidateId"); 208 Map<String, SlpcObject> influenceById = uniqueIndex(influences, "influenceId", "influenceId"); 209 Map<String, SlpcObject> operationCandidateById = uniqueIndex(operationCandidates, "candidateId", "operation candidateId"); 210 Map<String, SlpcObject> relationshipById = uniqueIndex(relationships, "relationshipId", "object relationshipId"); 211 Map<String, SlpcObject> relationshipCandidateById = uniqueIndex(relationshipCandidates, "candidateId", "object relationship candidateId"); 212 unique(fields(factCandidates, "candidateFingerprint"), "fact candidate fingerprint"); 213 unique(fields(operationCandidates, "candidateFingerprint"), "operation candidate fingerprint"); 214 unique(fields(relationshipCandidates, "candidateFingerprint"), "object relationship candidate fingerprint"); 215 216 for (Object value : facts) { 217 SlpcObject item = asObject(value); validateFactEndpointLevels(item, endpoints); 218 equal(string(item, "factId"), SlpcIdentity.factId(item, endpoints), "factId"); 219 } 220 for (Object value : factCandidates) { 221 SlpcObject item = asObject(value); validateFactEndpointLevels(item, endpoints); 222 equal(string(item, "candidateFingerprint"), SlpcIdentity.factCandidateFingerprint(item, endpoints), "fact candidate fingerprint"); 223 } 224 for (Object value : influences) { 225 SlpcObject item = asObject(value); equal(string(item, "influenceId"), SlpcIdentity.datasetOperationInfluenceId(item, endpoints), "dataset influenceId"); 226 } 227 for (Object value : operationCandidates) { 228 SlpcObject item = asObject(value); equal(string(item, "candidateFingerprint"), SlpcIdentity.datasetOperationCandidateFingerprint(item, endpoints), "dataset operation candidate fingerprint"); 229 } 230 equal(fields(facts, "factId"), sorted(factById.keySet()), "fact order"); 231 equal(fields(influences, "influenceId"), sorted(influenceById.keySet()), "influence order"); 232 equal(fields(factCandidates, "candidateFingerprint"), sorted(fields(factCandidates, "candidateFingerprint")), "fact candidate order"); 233 localIds(fields(factCandidates, "candidateId"), "fact-cand-", "fact candidate"); 234 equal(fields(operationCandidates, "candidateFingerprint"), sorted(fields(operationCandidates, "candidateFingerprint")), "operation candidate order"); 235 localIds(fields(operationCandidates, "candidateId"), "op-cand-", "operation candidate"); 236 237 for (Object value : relationships) { 238 SlpcObject item = asObject(value); validateRelationshipRoles(item); 239 equal(string(item, "relationshipId"), SlpcIdentity.objectRelationshipId(item, endpoints), "object relationshipId"); 240 } 241 for (Object value : relationshipCandidates) { 242 SlpcObject item = asObject(value); validateRelationshipRoles(item); 243 equal(string(item, "candidateFingerprint"), SlpcIdentity.objectRelationshipCandidateFingerprint(item, endpoints), "object relationship candidate fingerprint"); 244 } 245 equal(fields(relationships, "relationshipId"), sorted(relationshipById.keySet()), "object relationship order"); 246 equal(fields(relationshipCandidates, "candidateFingerprint"), sorted(fields(relationshipCandidates, "candidateFingerprint")), "object relationship candidate order"); 247 localIds(fields(relationshipCandidates, "candidateId"), "objrel-cand-", "object relationship candidate"); 248 249 List<Object> operations = array(document, "statementOperations"); 250 Map<String, SlpcObject> operationById = uniqueIndex(operations, "statementOperationId", "statement operationId"); 251 validateGroupedOrdinals(operations, "statementId", "operationOrdinal", statements, "statement operation references missing statement", " operation"); 252 for (Object value : operations) { 253 SlpcObject item = asObject(value); validateOperationRoles(item, endpoints); 254 equal(string(item, "statementOperationId"), SlpcIdentity.statementOperationId(item, endpoints), "statement operationId"); 255 } 256 equal(fields(operations, "statementOperationId"), sorted(operationById.keySet()), "statement operation order"); 257 258 List<Object> joins = array(document, "joinSemantics"); 259 Map<String, SlpcObject> joinById = uniqueIndex(joins, "joinSemanticsId", "joinSemanticsId"); 260 validateGroupedOrdinals(joins, "statementId", "joinOrdinal", statements, "join semantics references missing statement", " join"); 261 Set<String> factCandidateFingerprints = new HashSet<String>(fields(factCandidates, "candidateFingerprint")); 262 for (Object value : joins) { 263 SlpcObject item = asObject(value); 264 contiguous(integers(array(item, "keyPairs"), "ordinal"), string(item, "joinSemanticsId") + " key-pair"); 265 equal(string(item, "joinSemanticsId"), SlpcIdentity.joinSemanticsId(item, endpoints), "joinSemanticsId"); 266 refsExist(strings(array(item, "refinesFactIds")), factById.keySet(), "join semantics references missing fact"); 267 refsExist(strings(array(item, "refinesFactCandidateFingerprints")), factCandidateFingerprints, "join semantics references missing fact candidate fingerprint"); 268 } 269 equal(fields(joins, "joinSemanticsId"), sorted(joinById.keySet()), "join semantics order"); 270 271 List<Object> graphs = array(document, "intermediateGraphs"); 272 Map<String, SlpcObject> graphByRef = uniqueIndex(graphs, "graphRef", "intermediate graphRef"); 273 unique(fields(graphs, "graphFingerprint"), "intermediate graph fingerprint"); 274 validateGroupedOrdinals(graphs, "statementId", "graphOrdinal", statements, "intermediate graph references missing statement", " graph"); 275 List<String> graphSort = new ArrayList<String>(); 276 for (Object value : graphs) { 277 SlpcObject graph = asObject(value); 278 validateGraph(graph, endpoints, document, capabilityStatus); 279 graphSort.add(string(graph, "statementId") + "\u0000" + string(graph, "graphFingerprint")); 280 } 281 equal(graphSort, sorted(graphSort), "intermediate graph order"); 282 localIds(fields(graphs, "graphRef"), "graph-", "intermediate graph"); 283 284 Map<String, String> ownerKeys = new HashMap<String, String>(); 285 addOwnerKeys(ownerKeys, facts, "FACT", "factId", "factId"); 286 addOwnerKeys(ownerKeys, factCandidates, "FACT_CANDIDATE", "candidateId", "candidateFingerprint"); 287 addOwnerKeys(ownerKeys, influences, "DATASET_OPERATION", "influenceId", "influenceId"); 288 addOwnerKeys(ownerKeys, operationCandidates, "DATASET_OPERATION_CANDIDATE", "candidateId", "candidateFingerprint"); 289 addOwnerKeys(ownerKeys, relationships, "OBJECT_RELATIONSHIP", "relationshipId", "relationshipId"); 290 addOwnerKeys(ownerKeys, relationshipCandidates, "OBJECT_RELATIONSHIP_CANDIDATE", "candidateId", "candidateFingerprint"); 291 292 List<Object> occurrences = array(document, "occurrences"); 293 Map<String, List<SlpcObject>> occurrencesByOwner = new HashMap<String, List<SlpcObject>>(); 294 List<String> occurrenceIds = new ArrayList<String>(); 295 for (Object value : occurrences) { 296 SlpcObject occurrence = asObject(value); 297 SlpcObject owner = object(occurrence, "ownerRef"); 298 String ownerKey = string(owner, "kind") + "\u0000" + string(owner, "id"); 299 if (!ownerKeys.containsKey(ownerKey)) fail("$.occurrences", "occurrence references missing owner: " + ownerKey.replace('\u0000', ':')); 300 requireRef(artifacts, string(occurrence, "artifactId"), "occurrence references missing artifact/statement"); 301 if (occurrence.string("statementId") != null) requireRef(statements, occurrence.string("statementId"), "occurrence references missing artifact/statement"); 302 equal(string(occurrence, "occurrenceId"), SlpcIdentity.occurrenceId(occurrence, ownerKeys), "occurrenceId"); 303 validateSupportPaths(occurrence, graphByRef); 304 validateLiterals(occurrence, artifacts); 305 if (!occurrencesByOwner.containsKey(ownerKey)) occurrencesByOwner.put(ownerKey, new ArrayList<SlpcObject>()); 306 occurrencesByOwner.get(ownerKey).add(occurrence); 307 occurrenceIds.add(string(occurrence, "occurrenceId")); 308 } 309 equal(occurrenceIds, sorted(occurrenceIds), "occurrence order"); 310 unique(occurrenceIds, "occurrenceId"); 311 validateQuality(facts, "FACT", "factId", false, occurrencesByOwner); 312 validateQuality(factCandidates, "FACT_CANDIDATE", "candidateId", true, occurrencesByOwner); 313 validateQuality(influences, "DATASET_OPERATION", "influenceId", false, occurrencesByOwner); 314 validateQuality(operationCandidates, "DATASET_OPERATION_CANDIDATE", "candidateId", true, occurrencesByOwner); 315 validateQuality(relationships, "OBJECT_RELATIONSHIP", "relationshipId", false, occurrencesByOwner); 316 validateQuality(relationshipCandidates, "OBJECT_RELATIONSHIP_CANDIDATE", "candidateId", true, occurrencesByOwner); 317 318 validateRenameSwap(operations, facts); 319 validateCreateStream(operations, relationships); 320 validateDiagnostics(document, artifacts.keySet(), statements.keySet(), endpoints.keySet(), factById.keySet(), 321 factCandidateById.keySet(), influenceById.keySet(), operationCandidateById.keySet(), relationshipById.keySet(), 322 relationshipCandidateById.keySet(), operationById.keySet(), joinById.keySet(), graphByRef, occurrenceIds); 323 validateSecretHygiene(document); 324 } 325 326 private void validateGraph(SlpcObject graph, Map<String, SlpcObject> endpoints, SlpcObject document, 327 Map<String, String> capabilityStatus) { 328 List<Object> nodeList = array(graph, "nodes"), edgeList = array(graph, "edges"), pathList = array(graph, "paths"); 329 Map<String, SlpcObject> nodes = uniqueIndex(nodeList, "nodeRef", string(graph, "graphRef") + " nodeRef"); 330 Map<String, SlpcObject> edges = uniqueIndex(edgeList, "edgeRef", string(graph, "graphRef") + " edgeRef"); 331 unique(fields(nodeList, "nodeFingerprint"), string(graph, "graphRef") + " node fingerprint"); 332 unique(fields(edgeList, "edgeFingerprint"), string(graph, "graphRef") + " edge fingerprint"); 333 unique(fields(pathList, "pathRef"), string(graph, "graphRef") + " pathRef"); 334 unique(fields(pathList, "pathFingerprint"), string(graph, "graphRef") + " path fingerprint"); 335 contiguous(sortedIntegers(nodeList, "nodeOrdinal"), string(graph, "graphRef") + " node"); 336 for (Object value : nodeList) { 337 SlpcObject node = asObject(value); String parentRef = nullableString(node, "parentNodeRef"); 338 if (parentRef != null) { 339 SlpcObject parent = nodes.get(parentRef); 340 if (parent == null) fail("$.intermediateGraphs", "intermediate node references missing parent: " + parentRef); 341 if ("BOUNDARY_ENDPOINT".equals(string(parent, "nodeKind"))) fail("$.intermediateGraphs", "BOUNDARY_ENDPOINT cannot contain intermediate nodes"); 342 if (integer(parent, "nodeOrdinal") >= integer(node, "nodeOrdinal")) fail("$.intermediateGraphs", "intermediate containment must be acyclic and parent ordinal must precede child"); 343 } 344 equal(string(node, "nodeFingerprint"), SlpcIdentity.intermediateNodeFingerprint(node, graph, endpoints), "intermediate node fingerprint"); 345 } 346 equal(fields(nodeList, "nodeFingerprint"), sorted(fields(nodeList, "nodeFingerprint")), string(graph, "graphRef") + " node order"); 347 localIds(fields(nodeList, "nodeRef"), "ign-", string(graph, "graphRef") + " node"); 348 for (Object value : edgeList) { 349 SlpcObject edge = asObject(value); 350 validateAnchor(object(edge, "source"), nodes, endpoints, "edge"); 351 validateAnchor(object(edge, "target"), nodes, endpoints, "edge"); 352 equal(string(edge, "edgeFingerprint"), SlpcIdentity.intermediateEdgeFingerprint(edge, endpoints, nodes), "intermediate edge fingerprint"); 353 } 354 equal(fields(edgeList, "edgeFingerprint"), sorted(fields(edgeList, "edgeFingerprint")), string(graph, "graphRef") + " edge order"); 355 localIds(fields(edgeList, "edgeRef"), "ige-", string(graph, "graphRef") + " edge"); 356 for (Object value : pathList) { 357 SlpcObject path = asObject(value); 358 refsExist(strings(array(path, "edgeRefs")), edges.keySet(), "path references missing graph edge"); 359 equal(string(path, "pathFingerprint"), SlpcIdentity.intermediatePathFingerprint(path, edges), "intermediate path fingerprint"); 360 } 361 equal(fields(pathList, "pathFingerprint"), sorted(fields(pathList, "pathFingerprint")), string(graph, "graphRef") + " path order"); 362 localIds(fields(pathList, "pathRef"), "igp-", string(graph, "graphRef") + " path"); 363 equal(string(graph, "graphFingerprint"), SlpcIdentity.intermediateGraphFingerprint(graph), "intermediate graph fingerprint"); 364 365 List<SlpcObject> profiles = extensions(graph, "gsp.interactive-explain.v1"); 366 if (profiles.size() > 1) fail("$.intermediateGraphs", string(graph, "graphRef") + " has duplicate interactive explain profiles"); 367 if (!profiles.isEmpty()) { 368 if ("UNSUPPORTED".equals(capabilityStatus.get("INTERACTIVE_EXPLAIN"))) fail("$.intermediateGraphs", "interactive explain profile requires declared capability"); 369 List<Object> steps = array(object(profiles.get(0), "payload"), "steps"); 370 contiguous(integers(steps, "stepOrdinal"), string(graph, "graphRef") + " interactive step"); 371 localIds(fields(steps, "stepRef"), "igs-", string(graph, "graphRef") + " interactive step"); 372 unique(fields(steps, "stepFingerprint"), string(graph, "graphRef") + " interactive step fingerprint"); 373 Set<String> usedEdges = new HashSet<String>(); 374 for (Object value : steps) { 375 SlpcObject step = asObject(value); List<Object> bindings = array(step, "inputBindings"); 376 contiguous(integers(bindings, "ordinal"), string(step, "stepRef") + " input binding"); 377 for (Object bindingValue : bindings) { 378 SlpcObject binding = asObject(bindingValue); String edgeRef = string(binding, "edgeRef"); 379 SlpcObject edge = edges.get(edgeRef); 380 if (edge == null) fail("$.intermediateGraphs", "interactive step references missing graph edge: " + edgeRef); 381 if (!usedEdges.add(edgeRef)) fail("$.intermediateGraphs", "interactive edge is bound to multiple steps: " + edgeRef); 382 if (!object(edge, "target").equals(object(step, "resultAnchor"))) fail("$.intermediateGraphs", "interactive input edge target does not match resultAnchor: " + edgeRef); 383 } 384 validateAnchor(object(step, "resultAnchor"), nodes, endpoints, "interactive step"); 385 equal(string(step, "stepFingerprint"), SlpcIdentity.interactiveStepFingerprint(step, graph, endpoints, nodes, edges), "interactive step fingerprint"); 386 } 387 } 388 validateGraphCompleteness(graph, array(document, "diagnostics")); 389 } 390 391 private void validateGraphCompleteness(SlpcObject graph, List<Object> diagnostics) { 392 List<SlpcObject> incomplete = new ArrayList<SlpcObject>(), bare = new ArrayList<SlpcObject>(); 393 String graphRef = string(graph, "graphRef"); 394 for (Object value : diagnostics) { 395 SlpcObject diagnostic = asObject(value); 396 String ref = nullableString(diagnostic, "ref"); 397 if ("SLPC_INTERMEDIATE_GRAPH_INCOMPLETE".equals(string(diagnostic, "code")) 398 && "INTERMEDIATE_GRAPH".equals(string(diagnostic, "scope")) && ref != null 399 && (ref.equals(graphRef) || ref.startsWith(graphRef + "/"))) { 400 incomplete.add(diagnostic); if (ref.equals(graphRef)) bare.add(diagnostic); 401 } 402 } 403 String completeness = string(graph, "analysisCompleteness"); 404 if ("COMPLETE".equals(completeness) && !incomplete.isEmpty()) fail("$.intermediateGraphs", "COMPLETE graph cannot carry an incomplete diagnostic"); 405 if (("PARTIAL".equals(completeness) || "UNKNOWN".equals(completeness)) && bare.isEmpty()) { 406 fail("$.intermediateGraphs", "PARTIAL/UNKNOWN graph requires a bare graph incomplete diagnostic"); 407 } 408 for (SlpcObject diagnostic : bare) { 409 if (!"WARN".equals(string(diagnostic, "severity")) && !"ERROR".equals(string(diagnostic, "severity"))) 410 fail("$.diagnostics", "incomplete graph diagnostic severity must be WARN or ERROR"); 411 } 412 } 413 414 private void validateLiterals(SlpcObject occurrence, Map<String, SlpcObject> artifacts) { 415 List<Object> literals = optionalArray(occurrence, "literalInputs"); 416 contiguous(integers(literals, "inputOrdinal"), string(occurrence, "occurrenceId") + " literal input"); 417 String dialect = string(artifacts.get(string(occurrence, "artifactId")), "sqlDialect"); 418 for (Object value : literals) { 419 SlpcObject literal = asObject(value); SlpcObject fingerprint = object(literal, "fingerprint"); 420 String algorithm = string(fingerprint, "algorithm"), expected; 421 if ("SLPC-REDACTED-LITERAL-V1".equals(algorithm)) { 422 long ordinal = integer(literal, "redactionOrdinal"); 423 expected = sha256("SLPC-REDACTED-LITERAL-V1", SlpcIdentity.string(dialect), 424 SlpcIdentity.string(string(literal, "literalKind")), SlpcIdentity.integer(ordinal)); 425 equal(string(fingerprint, "value"), expected, "redacted literal fingerprint"); 426 if (literal.contains("text")) equal(literal.string("text"), "<SLPC-REDACTED:" + string(literal, "literalKind") + ":" + ordinal + ">", "redacted literal marker"); 427 } else if ("SLPC-LITERAL-TEXT-V1".equals(algorithm) && literal.contains("text")) { 428 expected = sha256("SLPC-LITERAL-TEXT-V1", SlpcIdentity.string(dialect), 429 SlpcIdentity.string(string(literal, "literalKind")), SlpcIdentity.string(literal.string("text"))); 430 equal(string(fingerprint, "value"), expected, "literal fingerprint"); 431 } 432 } 433 } 434 435 private void validateSupportPaths(SlpcObject occurrence, Map<String, SlpcObject> graphs) { 436 for (Object value : optionalArray(occurrence, "supportPathRefs")) { 437 SlpcObject support = asObject(value); SlpcObject graph = graphs.get(string(support, "graphRef")); 438 if (graph == null || !fields(array(graph, "paths"), "pathRef").contains(string(support, "pathRef"))) 439 fail("$.occurrences", "occurrence has dangling support path: " + support.asMap()); 440 } 441 } 442 443 private void validateQuality(List<Object> records, String ownerKind, String idField, boolean alwaysIneligible, 444 Map<String, List<SlpcObject>> occurrencesByOwner) { 445 for (Object value : records) { 446 SlpcObject record = asObject(value); String owner = ownerKind + "\u0000" + string(record, idField); 447 List<SlpcObject> evidence = occurrencesByOwner.get(owner); 448 if (evidence == null || evidence.isEmpty()) fail("$", "owner has no occurrence: " + owner.replace('\u0000', ':')); 449 boolean ineligible = alwaysIneligible || ("UNKNOWN".equals(record.string("role")) 450 && "NONE".equals(record.string("operationKind")) && optionalArray(record, "extensions").isEmpty()); 451 equal(object(record, "qualitySummary"), aggregateQuality(evidence, ineligible), owner.replace('\u0000', ':') + " qualitySummary"); 452 } 453 } 454 455 private SlpcObject aggregateQuality(List<SlpcObject> occurrences, boolean ineligible) { 456 String resolution = worst(occurrences, "resolutionGrade", Arrays.asList("NOT_APPLICABLE", "RESOLVED", "GUESSED", "PARTIAL", "UNRESOLVED", "AMBIGUOUS", "PARSER_UNSUPPORTED")); 457 String evaluation = worst(occurrences, "evaluationModel", Arrays.asList("EXACT", "OVER_APPROXIMATE", "HEURISTIC", "UNKNOWN")); 458 String proof = worst(occurrences, "semanticProof", Arrays.asList("PROVEN", "INFERRED", "ASSUMED", "UNKNOWN")); 459 boolean dynamic = false; long confidence = 1000000; 460 Set<String> ids = new HashSet<String>(); 461 for (SlpcObject occurrence : occurrences) { 462 SlpcObject quality = object(occurrence, "quality"); 463 dynamic |= Boolean.TRUE.equals(quality.get("dynamic")); 464 confidence = Math.min(confidence, integer(quality, "confidenceMicros")); ids.add(string(occurrence, "occurrenceId")); 465 } 466 String eligibility = ineligible ? "INELIGIBLE" : 467 (("RESOLVED".equals(resolution) || "NOT_APPLICABLE".equals(resolution)) && "EXACT".equals(evaluation) 468 && "PROVEN".equals(proof) && !dynamic && confidence == 1000000 ? "ELIGIBLE" : "CONDITIONAL"); 469 return SlpcObject.builder().put("projectionEligibility", eligibility).put("resolutionGrade", resolution) 470 .put("evaluationModel", evaluation).put("semanticProof", proof).put("dynamic", dynamic) 471 .put("confidenceMicros", confidence).put("evidenceCount", ids.size()).build(); 472 } 473 474 private String worst(List<SlpcObject> occurrences, String field, List<String> rank) { 475 String worst = rank.get(0); 476 for (SlpcObject occurrence : occurrences) { 477 String value = string(object(occurrence, "quality"), field); 478 if (rank.indexOf(value) > rank.indexOf(worst)) worst = value; 479 } 480 return worst; 481 } 482 483 private void validateRelationshipRoles(SlpcObject record) { 484 List<String> actual = fields(array(record, "participants"), "role"); 485 List<String> expected = relationshipRoles(string(record, "relationshipKind")); 486 equal(actual, expected, (record.string("relationshipId") == null ? record.string("candidateId") : record.string("relationshipId")) + " participant roles"); 487 List<Object> details = record.array("keyFields"); if (details == null || details.isEmpty()) details = optionalArray(record, "keyPairs"); 488 if (!details.isEmpty()) contiguous(integers(details, "ordinal"), "relationship detail"); 489 } 490 491 private void validateOperationRoles(SlpcObject record, Map<String, SlpcObject> endpoints) { 492 List<RoleRule> rules = operationRoles(string(record, "operationKind")); 493 Map<String, List<SlpcObject>> byRole = new LinkedHashMap<String, List<SlpcObject>>(); 494 for (Object value : array(record, "participants")) { 495 SlpcObject participant = asObject(value); String role = string(participant, "role"); 496 if (!byRole.containsKey(role)) byRole.put(role, new ArrayList<SlpcObject>()); 497 byRole.get(role).add(participant); 498 } 499 Set<String> known = new HashSet<String>(); for (RoleRule rule : rules) known.add(rule.role); 500 Set<String> unknown = new HashSet<String>(byRole.keySet()); unknown.removeAll(known); 501 if (!unknown.isEmpty()) fail("$.statementOperations", string(record, "statementOperationId") + " has invalid participant roles: " + unknown); 502 List<Object> expected = new ArrayList<Object>(); 503 for (RoleRule rule : rules) { 504 List<SlpcObject> values = byRole.containsKey(rule.role) ? byRole.get(rule.role) : new ArrayList<SlpcObject>(); 505 if (rule.required && values.size() != 1) fail("$.statementOperations", string(record, "statementOperationId") + " requires exactly one " + rule.role); 506 if (!rule.setLike && values.size() > 1) fail("$.statementOperations", string(record, "statementOperationId") + " repeats singleton role " + rule.role); 507 if (rule.setLike) Collections.sort(values, endpointParticipantComparator(endpoints)); 508 expected.addAll(values); 509 } 510 equal(array(record, "participants"), expected, string(record, "statementOperationId") + " participant order"); 511 } 512 513 private void validateRenameSwap(List<Object> operations, List<Object> facts) { 514 for (Object value : operations) { 515 SlpcObject operation = asObject(value); String kind = string(operation, "operationKind"); 516 if (!"RENAME".equals(kind) && !"SWAP".equals(kind)) continue; 517 Set<String> refs = new HashSet<String>(fields(array(operation, "participants"), "endpointRef")); 518 for (Object factValue : facts) { 519 SlpcObject fact = asObject(factValue); 520 if ("VALUE".equals(string(fact, "axis")) && "DIRECT".equals(string(fact, "role")) 521 && refs.contains(fact.string("sourceEndpointRef")) && refs.contains(string(fact, "targetEndpointRef"))) 522 fail("$.facts", kind + " must not create a default VALUE/DIRECT fact"); 523 } 524 } 525 } 526 527 private void validateCreateStream(List<Object> operations, List<Object> relationships) { 528 Set<String> index = new HashSet<String>(); 529 for (Object value : relationships) { 530 SlpcObject relation = asObject(value); StringBuilder key = new StringBuilder(string(relation, "relationshipKind")); 531 for (Object p : array(relation, "participants")) { SlpcObject participant = asObject(p); key.append('|').append(string(participant, "role")).append('=').append(string(participant, "endpointRef")); } 532 index.add(key.toString()); 533 } 534 for (Object value : operations) { 535 SlpcObject operation = asObject(value); if (!"CREATE_STREAM".equals(string(operation, "operationKind"))) continue; 536 Map<String, String> roles = new HashMap<String, String>(); 537 for (Object p : array(operation, "participants")) { SlpcObject participant = asObject(p); roles.put(string(participant, "role"), string(participant, "endpointRef")); } 538 String expected = "STREAMS_FROM|STREAM=" + roles.get("CREATED_STREAM") + "|BASE_RELATION=" + roles.get("BASE_RELATION"); 539 if (!index.contains(expected)) fail("$.statementOperations", "CREATE_STREAM requires a STREAMS_FROM object relationship"); 540 } 541 } 542 543 private void validateDiagnostics(SlpcObject document, Set<String> artifacts, Set<String> statements, Set<String> endpoints, 544 Set<String> facts, Set<String> factCandidates, Set<String> influences, Set<String> operationCandidates, 545 Set<String> relationships, Set<String> relationshipCandidates, Set<String> operations, Set<String> joins, 546 Map<String, SlpcObject> graphs, List<String> occurrences) { 547 Set<String> factLike = union(facts, factCandidates, influences, operationCandidates); 548 Set<String> relationshipLike = union(relationships, relationshipCandidates); 549 Set<String> intermediate = new HashSet<String>(graphs.keySet()); 550 for (SlpcObject graph : graphs.values()) { 551 String prefix = string(graph, "graphRef") + "/"; 552 for (String ref : fields(array(graph, "nodes"), "nodeRef")) intermediate.add(prefix + ref); 553 for (String ref : fields(array(graph, "edges"), "edgeRef")) intermediate.add(prefix + ref); 554 for (String ref : fields(array(graph, "paths"), "pathRef")) intermediate.add(prefix + ref); 555 for (SlpcObject extension : extensions(graph, "gsp.interactive-explain.v1")) 556 for (String ref : fields(array(object(extension, "payload"), "steps"), "stepRef")) intermediate.add(prefix + ref); 557 } 558 Map<String, Set<String>> targets = new HashMap<String, Set<String>>(); 559 targets.put("ARTIFACT", artifacts); targets.put("STATEMENT", statements); targets.put("ENDPOINT", endpoints); 560 targets.put("FACT", factLike); targets.put("OCCURRENCE", new HashSet<String>(occurrences)); 561 targets.put("OBJECT_RELATIONSHIP", relationshipLike); targets.put("STATEMENT_OPERATION", operations); 562 targets.put("JOIN_SEMANTICS", joins); targets.put("INTERMEDIATE_GRAPH", intermediate); 563 Set<String> extensionTargets = union(factLike, relationshipLike, operations, joins, graphs.keySet(), new HashSet<String>(occurrences), endpoints); 564 for (Object value : array(document, "diagnostics")) { 565 SlpcObject diagnostic = asObject(value); String scope = string(diagnostic, "scope"), ref = nullableString(diagnostic, "ref"); 566 String code = string(diagnostic, "code"); 567 if (code.startsWith("SLPC_") && !CORE_DIAGNOSTICS.contains(code)) 568 fail("$.diagnostics", "unregistered core diagnostic code: " + code); 569 if ("DOCUMENT".equals(scope) && ref != null) fail("$.diagnostics", "DOCUMENT diagnostic ref must be null"); 570 if (targets.containsKey(scope) && !targets.get(scope).contains(ref)) fail("$.diagnostics", "diagnostic references missing " + scope + " owner: " + ref); 571 if ("EXTENSION".equals(scope) && ref != null && !extensionTargets.contains(ref)) fail("$.diagnostics", "diagnostic references missing EXTENSION owner: " + ref); 572 } 573 } 574 575 private void validateSecretHygiene(SlpcObject document) { 576 ArrayDeque<PathValue> stack = new ArrayDeque<PathValue>(); stack.push(new PathValue("$", document)); 577 while (!stack.isEmpty()) { 578 PathValue current = stack.pop(); Object value = current.value; 579 if (value instanceof String && SECRET.matcher((String) value).find()) fail(current.path, "secret-bearing text is not redacted at " + current.path); 580 if (value instanceof SlpcObject) for (String name : ((SlpcObject) value).names()) stack.push(new PathValue(current.path + "." + name, ((SlpcObject) value).get(name))); 581 else if (value instanceof List<?>) for (int index = 0; index < ((List<?>) value).size(); index++) stack.push(new PathValue(current.path + "[" + index + "]", ((List<?>) value).get(index))); 582 } 583 } 584 585 private boolean hasDiagnosticCode(SlpcObject document, String expected) { 586 for (Object value : array(document, "diagnostics")) { 587 if (expected.equals(asObject(value).string("code"))) return true; 588 } 589 return false; 590 } 591 592 private void validateFactEndpointLevels(SlpcObject fact, Map<String, SlpcObject> endpoints) { 593 if (!"ROW".equals(string(fact, "axis"))) return; 594 String targetRef = string(fact, "targetEndpointRef"); 595 SlpcObject target = endpoints.get(targetRef); 596 if (target == null) fail("$.facts", "ROW fact references missing target endpoint: " + targetRef); 597 if (!"RELATION".equals(string(target, "endpointLevel"))) 598 fail("$.facts", "ROW fact target must be a holder RELATION endpoint: " + targetRef); 599 } 600 601 private void validateProfileLocations(SlpcObject document) { 602 ArrayDeque<PathValue> stack = new ArrayDeque<PathValue>(); stack.push(new PathValue("$", document)); 603 Pattern allowed = Pattern.compile("\\$\\.intermediateGraphs\\[[0-9]+\\]\\.extensions\\[[0-9]+\\]"); 604 while (!stack.isEmpty()) { 605 PathValue current = stack.pop(); Object value = current.value; 606 if (value instanceof SlpcObject) { 607 SlpcObject object = (SlpcObject) value; 608 if ("gsp.interactive-explain.v1".equals(object.string("extensionType")) && object.contains("schemaVersion") 609 && object.contains("criticality") && object.contains("payload") && !allowed.matcher(current.path).matches()) 610 fail(current.path, "interactive explain profile must be graph-local: " + current.path); 611 for (String name : object.names()) stack.push(new PathValue(current.path + "." + name, object.get(name))); 612 } else if (value instanceof List<?>) for (int index = 0; index < ((List<?>) value).size(); index++) stack.push(new PathValue(current.path + "[" + index + "]", ((List<?>) value).get(index))); 613 } 614 } 615 616 private void validateRoutineParameterProfiles(List<Object> endpoints) { 617 for (Object value : endpoints) { 618 SlpcObject endpoint = asObject(value); int profiles = 0; 619 for (Object extensionValue : array(endpoint, "extensions")) { 620 SlpcObject extension = asObject(extensionValue); 621 if ("gsp.routine-parameter.v1".equals(extension.string("extensionType"))) profiles++; 622 } 623 boolean parameter = "PARAMETER".equals(endpoint.string("objectKind")); 624 if (parameter && profiles != 1) fail("$.endpoints", 625 "PARAMETER endpoint requires exactly one gsp.routine-parameter.v1 profile"); 626 if (!parameter && profiles != 0) fail("$.endpoints", 627 "gsp.routine-parameter.v1 is allowed only on PARAMETER endpoints"); 628 } 629 } 630 631 private void validateUriDigest(SlpcObject endpoint) { 632 SlpcObject identity = object(endpoint, "identity"); if (!"URI_EXACT".equals(string(identity, "kind"))) return; 633 for (SlpcObject extension : extensions(endpoint, "gsp.storage-asset.v1")) { 634 String locator = object(extension, "payload").string("secretFreeNormalizedLocator"); 635 if (locator != null) { 636 String expected = sha256("SLPC-URI-LOCATOR-DIGEST-V1", 637 SlpcIdentity.string(string(identity, "normalizationPolicyId")), SlpcIdentity.string(string(identity, "scheme")), SlpcIdentity.string(locator)); 638 equal(string(identity, "locatorDigest"), expected, string(endpoint, "endpointRef") + " locatorDigest"); 639 } 640 } 641 } 642 643 private static String sha256(String domain, byte[]... payload) { 644 try { 645 MessageDigest digest = MessageDigest.getInstance("SHA-256"); digest.update(domain.getBytes(StandardCharsets.US_ASCII)); digest.update((byte) 0); 646 for (byte[] value : payload) digest.update(value); StringBuilder hex = new StringBuilder(); 647 for (byte value : digest.digest()) hex.append(String.format("%02x", value & 255)); return hex.toString(); 648 } catch (NoSuchAlgorithmException impossible) { throw new IllegalStateException(impossible); } 649 } 650 651 private void validateGroupedOrdinals(List<Object> records, String groupField, String ordinalField, 652 Map<String, SlpcObject> validGroups, String missingMessage, String suffix) { 653 Map<String, List<Long>> groups = new HashMap<String, List<Long>>(); 654 for (Object value : records) { SlpcObject record = asObject(value); String group = string(record, groupField); 655 requireRef(validGroups, group, missingMessage); if (!groups.containsKey(group)) groups.put(group, new ArrayList<Long>()); groups.get(group).add(integer(record, ordinalField)); } 656 for (Map.Entry<String, List<Long>> entry : groups.entrySet()) { Collections.sort(entry.getValue()); contiguous(entry.getValue(), entry.getKey() + suffix); } 657 } 658 659 private void validateAnchor(SlpcObject anchor, Map<String, SlpcObject> nodes, Map<String, SlpcObject> endpoints, String kind) { 660 if ("NODE".equals(string(anchor, "kind")) && !nodes.containsKey(string(anchor, "nodeRef"))) fail("$.intermediateGraphs", kind + " references missing graph node: " + anchor.string("nodeRef")); 661 if ("ENDPOINT".equals(string(anchor, "kind")) && !endpoints.containsKey(string(anchor, "endpointRef"))) fail("$.intermediateGraphs", kind + " references missing endpoint: " + anchor.string("endpointRef")); 662 } 663 664 private static void addOwnerKeys(Map<String, String> target, List<Object> records, String kind, String idField, String semanticField) { 665 for (Object value : records) { SlpcObject record = asObject(value); target.put(kind + "\u0000" + string(record, idField), string(record, semanticField)); } 666 } 667 668 private static List<SlpcObject> extensions(SlpcObject owner, String type) { 669 ArrayList<SlpcObject> result = new ArrayList<SlpcObject>(); 670 for (Object value : optionalArray(owner, "extensions")) { SlpcObject extension = asObject(value); if (type.equals(extension.string("extensionType"))) result.add(extension); } 671 return result; 672 } 673 674 private static Comparator<SlpcObject> endpointParticipantComparator(final Map<String, SlpcObject> endpoints) { 675 return new Comparator<SlpcObject>() { @Override public int compare(SlpcObject left, SlpcObject right) { 676 String a = SlpcIdentity.candidateEndpointKey(endpoints.get(string(left, "endpointRef"))); 677 String b = SlpcIdentity.candidateEndpointKey(endpoints.get(string(right, "endpointRef"))); 678 return utf8Compare(a, b); 679 }}; 680 } 681 682 private static int utf8Compare(String left, String right) { 683 byte[] a = left.getBytes(StandardCharsets.UTF_8), b = right.getBytes(StandardCharsets.UTF_8); int common = Math.min(a.length, b.length); 684 for (int index = 0; index < common; index++) { int difference = (a[index] & 255) - (b[index] & 255); if (difference != 0) return difference; } 685 return a.length - b.length; 686 } 687 688 private static Map<String, SlpcObject> uniqueIndex(List<Object> records, String field, String label) { 689 LinkedHashMap<String, SlpcObject> result = new LinkedHashMap<String, SlpcObject>(); 690 for (Object value : records) { SlpcObject record = asObject(value); String key = string(record, field); if (result.put(key, record) != null) fail("$", "duplicate " + label); } 691 return result; 692 } 693 694 private static List<String> relationshipRoles(String kind) { 695 if ("PRIMARY_KEY".equals(kind) || "UNIQUE_KEY".equals(kind)) return Arrays.asList("KEY_RELATION"); 696 if ("FOREIGN_KEY_REFERENCE".equals(kind)) return Arrays.asList("FOREIGN_KEY_RELATION", "REFERENCED_RELATION"); 697 if ("CALLS".equals(kind)) return Arrays.asList("CALLER_ROUTINE", "CALLEE_ROUTINE"); 698 if ("STREAMS_FROM".equals(kind)) return Arrays.asList("STREAM", "BASE_RELATION"); 699 if ("SYNONYM_OF".equals(kind)) return Arrays.asList("SYNONYM", "BASE_OBJECT"); 700 if ("READS_FROM".equals(kind)) return Arrays.asList("CONSUMER_RELATION", "STORAGE_ASSET"); 701 if ("WRITES_TO".equals(kind)) return Arrays.asList("PRODUCER_RELATION", "STORAGE_ASSET"); 702 fail("$", "unsupported relationship kind: " + kind); return Collections.emptyList(); 703 } 704 705 private static List<RoleRule> operationRoles(String kind) { 706 if ("QUERY".equals(kind)) return rules("READ_RELATION", true, false, "OUTPUT_RELATION", true, false); 707 if ("INSERT".equals(kind) || "UPDATE".equals(kind) || "DELETE".equals(kind)) return rules("WRITE_TARGET", false, true, "READ_SOURCE", true, false); 708 if ("MERGE".equals(kind)) return rules("MERGE_TARGET", false, true, "MERGE_SOURCE", true, false); 709 if ("CREATE_TABLE".equals(kind)) return rules("CREATED_OBJECT", false, true); 710 if ("CREATE_TABLE_AS".equals(kind) || "CREATE_VIEW".equals(kind) || "CREATE_MATERIALIZED_VIEW".equals(kind)) return rules("CREATED_OBJECT", false, true, "READ_SOURCE", true, false); 711 if ("CREATE_EXTERNAL_TABLE".equals(kind) || "CREATE_STAGE".equals(kind)) return rules("CREATED_OBJECT", false, true, "STORAGE_SOURCE", true, false); 712 if ("CREATE_STREAM".equals(kind)) return rules("CREATED_STREAM", false, true, "BASE_RELATION", false, true); 713 if ("CREATE_ROUTINE".equals(kind)) return rules("CREATED_ROUTINE", false, true); 714 if ("CREATE_SYNONYM".equals(kind)) return rules("SYNONYM", false, true, "BASE_OBJECT", false, true); 715 if ("CREATE_INDEX".equals(kind)) return rules("CREATED_INDEX", false, true, "INDEXED_RELATION", false, true); 716 if ("RENAME".equals(kind)) return rules("BEFORE_OBJECT", false, true, "AFTER_OBJECT", false, true); 717 if ("SWAP".equals(kind)) return rules("SWAP_LEFT_OBJECT", false, true, "SWAP_RIGHT_OBJECT", false, true); 718 if ("CLONE".equals(kind)) return rules("CLONE_SOURCE", false, true, "CLONE_TARGET", false, true); 719 if ("LOAD".equals(kind)) return rules("STORAGE_SOURCE", true, false, "WRITE_TARGET", false, true); 720 if ("UNLOAD".equals(kind) || "INSERT_OVERWRITE_DIRECTORY".equals(kind)) return rules("READ_SOURCE", true, false, "STORAGE_TARGET", false, true); 721 if ("CALL_ROUTINE".equals(kind)) return rules("CALLEE_ROUTINE", false, true, "ARGUMENT_SOURCE", true, false); 722 if ("ALTER".equals(kind) || "DROP".equals(kind) || "TRUNCATE".equals(kind)) return rules("AFFECTED_OBJECT", false, true); 723 if ("OTHER".equals(kind)) return rules("AFFECTED_OBJECT", true, false); 724 fail("$", "unsupported operation kind: " + kind); return Collections.emptyList(); 725 } 726 727 private static List<RoleRule> rules(Object... values) { 728 ArrayList<RoleRule> result = new ArrayList<RoleRule>(); 729 for (int index = 0; index < values.length; index += 3) result.add(new RoleRule((String) values[index], (Boolean) values[index + 1], (Boolean) values[index + 2])); 730 return result; 731 } 732 733 @SafeVarargs private static Set<String> union(Set<String>... values) { 734 HashSet<String> result = new HashSet<String>(); for (Set<String> value : values) result.addAll(value); return result; 735 } 736 737 private static List<String> sorted(Iterable<String> values) { ArrayList<String> result = new ArrayList<String>(); for (String value : values) result.add(value); Collections.sort(result); return result; } 738 private static List<Long> sortedIntegers(List<Object> values, String field) { ArrayList<Long> result = new ArrayList<Long>(integers(values, field)); Collections.sort(result); return result; } 739 private static List<String> fields(List<Object> values, String field) { ArrayList<String> result = new ArrayList<String>(); for (Object value : values) result.add(string(asObject(value), field)); return result; } 740 private static List<String> strings(List<Object> values) { ArrayList<String> result = new ArrayList<String>(); for (Object value : values) result.add((String) value); return result; } 741 private static List<Long> integers(List<Object> values, String field) { ArrayList<Long> result = new ArrayList<Long>(); for (Object value : values) result.add(integer(asObject(value), field)); return result; } 742 private static List<Object> optionalArray(SlpcObject object, String field) { List<Object> result = object.array(field); return result == null ? Collections.<Object>emptyList() : result; } 743 private static List<Object> array(SlpcObject object, String field) { List<Object> result = object.array(field); if (result == null) fail("$", "missing array field: " + field); return result; } 744 private static SlpcObject object(SlpcObject object, String field) { SlpcObject result = object.object(field); if (result == null) fail("$", "missing object field: " + field); return result; } 745 private static String string(SlpcObject object, String field) { String result = object == null ? null : object.string(field); if (result == null) fail("$", "missing string field: " + field); return result; } 746 private static String nullableString(SlpcObject object, String field) { if (!object.contains(field)) fail("$", "missing nullable field: " + field); Object result = object.get(field); if (result != null && !(result instanceof String)) fail("$", "field is not string/null: " + field); return (String) result; } 747 private static long integer(SlpcObject object, String field) { BigInteger result = object.integer(field); if (result == null) fail("$", "missing integer field: " + field); return result.longValueExact(); } 748 private static SlpcObject asObject(Object value) { if (!(value instanceof SlpcObject)) fail("$", "expected object"); return (SlpcObject) value; } 749 private static void requireRef(Map<String, SlpcObject> values, String ref, String message) { if (!values.containsKey(ref)) fail("$", message + ": " + ref); } 750 private static void refsExist(List<String> refs, Set<String> values, String message) { for (String ref : refs) if (!values.contains(ref)) fail("$", message + ": " + ref); } 751 private static void unique(List<String> values, String label) { if (new HashSet<String>(values).size() != values.size()) fail("$", "duplicate " + label); } 752 private static void contiguous(List<Long> values, String label) { for (int index = 0; index < values.size(); index++) if (values.get(index) != index + 1L) fail("$", label + " ordinals: expected contiguous values starting at 1, got " + values); } 753 private static void localIds(List<String> values, String prefix, String label) { for (int index = 0; index < values.size(); index++) { String expected = String.format("%s%06d", prefix, index + 1); if (!expected.equals(values.get(index))) fail("$", label + " local IDs: expected " + expected + ", got " + values.get(index)); } } 754 private static void equal(Object actual, Object expected, String message) { if (actual == null ? expected != null : !actual.equals(expected)) fail("$", message + ": expected " + expected + ", got " + actual); } 755 private static void fail(String path, String message) { throw new Failure(path, message); } 756 757 private static final class RoleRule { private final String role; private final boolean setLike; private final boolean required; private RoleRule(String role, boolean setLike, boolean required) { this.role = role; this.setLike = setLike; this.required = required; } } 758 private static final class PathValue { private final String path; private final Object value; private PathValue(String path, Object value) { this.path = path; this.value = value; } } 759 private static final class Failure extends IllegalArgumentException { private static final long serialVersionUID = 1L; private final String path; private Failure(String path, String message) { super(message); this.path = path; } } 760}