001package gudusoft.gsqlparser.ir.semantic.validation.oracle; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.EExpressionType; 005import gudusoft.gsqlparser.ir.semantic.Diagnostic; 006import gudusoft.gsqlparser.ir.semantic.RecoveryMetadata; 007import gudusoft.gsqlparser.ir.semantic.RelatedLocation; 008import gudusoft.gsqlparser.ir.semantic.RelationSource; 009import gudusoft.gsqlparser.ir.semantic.SourceSpan; 010import gudusoft.gsqlparser.ir.semantic.binding.NameBindingProvider; 011import gudusoft.gsqlparser.nodes.TExpression; 012import gudusoft.gsqlparser.stmt.TSelectSqlStatement; 013 014import java.util.ArrayList; 015import java.util.Collections; 016import java.util.Comparator; 017import java.util.IdentityHashMap; 018import java.util.LinkedHashMap; 019import java.util.LinkedHashSet; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023 024/** 025 * Oracle source-dialect Sema for legacy (+) join intent. 026 * 027 * <p>The validator is invariant-driven: the extractor records markers, 028 * relation instances and boolean ownership; this class checks local, 029 * query-block and relation-pair invariants. ORA identifiers exist only in the 030 * diagnostic catalog and never participate in the checks below. 031 */ 032public final class OracleLegacyOuterJoinValidator { 033 034 private OracleLegacyOuterJoinValidator() {} 035 036 public static OracleLegacyOuterJoinValidationResult validate( 037 TSelectSqlStatement select, 038 List<RelationSource> relations, 039 NameBindingProvider provider) { 040 return validate(select, relations, provider, 041 OracleSemanticProfile.ORACLE_MODERN); 042 } 043 044 public static OracleLegacyOuterJoinValidationResult validate( 045 TSelectSqlStatement select, 046 List<RelationSource> relations, 047 NameBindingProvider provider, 048 OracleSemanticProfile profile) { 049 if (select == null || select.dbvendor != EDbVendor.dbvoracle) { 050 return OracleLegacyOuterJoinValidationResult.empty(); 051 } 052 OracleLegacyOuterJoinFacts facts = 053 new OracleLegacyOuterJoinFactExtractor().extract( 054 select, relations, provider); 055 if (facts.getAtoms().isEmpty()) { 056 return OracleLegacyOuterJoinValidationResult.empty(); 057 } 058 059 ValidationState state = new ValidationState(profile, select); 060 checkPredicateLocal(facts, state); 061 checkBooleanContext(facts, state); 062 resolveNullGeneratedFilters(facts, state); 063 reportUnsupportedLowering(facts, state); 064 checkReciprocalDirections(facts, state); 065 state.sortDiagnosticsAndRecovery(); 066 return new OracleLegacyOuterJoinValidationResult( 067 state.diagnostics, state.recoveryEntries, state.intents); 068 } 069 070 private static void checkPredicateLocal( 071 OracleLegacyOuterJoinFacts facts, 072 ValidationState state) { 073 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 074 List<OracleLegacyOuterJoinFacts.MarkerFact> markers = 075 sortedMarkers(atom.getMarkers()); 076 Set<Integer> markedRelations = new LinkedHashSet<Integer>(); 077 for (OracleLegacyOuterJoinFacts.MarkerFact marker : markers) { 078 if (marker.getRelationInstanceId() != null) { 079 markedRelations.add(marker.getRelationInstanceId()); 080 } 081 } 082 if (markedRelations.size() > 1) { 083 OracleLegacyOuterJoinFacts.MarkerFact primary = 084 markers.get(markers.size() - 1); 085 List<RelatedLocation> related = new ArrayList<RelatedLocation>(); 086 for (int i = 0; i < markers.size() - 1; i++) { 087 addRelated(related, "first-marker", 088 "another (+) marker in this predicate is here", 089 markers.get(i).getSpan()); 090 } 091 state.report( 092 OracleOuterJoinViolation.Kind.MULTIPLE_NULL_GENERATED_RELATIONS, 093 "GSP found " + markerDisplayList(markers) 094 + " marked as null-generated in the same predicate.", 095 firstNonNull(primary.getSpan(), atom.getSpan()), 096 related, 097 atom.getExpression()); 098 state.poison(markers); 099 state.intents.put(atom.getExpression(), 100 PredicateJoinIntent.invalid( 101 atom.getReferencedRelationInstanceIds())); 102 continue; 103 } 104 105 Integer nullGenerated = markedRelations.isEmpty() 106 ? null : markedRelations.iterator().next(); 107 Integer preserved = soleOtherRelation( 108 atom.getReferencedRelationInstanceIds(), nullGenerated); 109 boolean simpleComparison = atom.getExpression().getExpressionType() 110 == EExpressionType.simple_comparison_t; 111 if (simpleComparison && nullGenerated != null && preserved != null) { 112 state.intents.put(atom.getExpression(), PredicateJoinIntent.valid( 113 preserved.intValue(), nullGenerated.intValue(), 114 atom.getReferencedRelationInstanceIds())); 115 continue; 116 } 117 if (simpleComparison && nullGenerated != null 118 && atom.getReferencedRelationInstanceIds().size() == 1) { 119 state.intents.put(atom.getExpression(), 120 PredicateJoinIntent.validNullGeneratedFilter( 121 nullGenerated.intValue(), 122 atom.getReferencedRelationInstanceIds())); 123 continue; 124 } 125 state.intents.put(atom.getExpression(), 126 PredicateJoinIntent.unknown( 127 atom.getReferencedRelationInstanceIds())); 128 } 129 } 130 131 /** 132 * A marked constant/function filter is valid only when another predicate 133 * establishes at least one preserved partner for its null-generated side. 134 */ 135 private static void resolveNullGeneratedFilters( 136 OracleLegacyOuterJoinFacts facts, 137 ValidationState state) { 138 Map<Integer, Set<Integer>> preservedByNull = 139 new LinkedHashMap<Integer, Set<Integer>>(); 140 for (PredicateJoinIntent intent : state.intents.values()) { 141 if (intent.getValidity() != PredicateJoinIntent.Validity.VALID 142 || intent.isNullGeneratedFilter() 143 || intent.getPreservedRelationInstanceId() == null) { 144 continue; 145 } 146 Integer nullId = intent.getNullGeneratedRelationInstanceId(); 147 Set<Integer> preserved = preservedByNull.get(nullId); 148 if (preserved == null) { 149 preserved = new LinkedHashSet<Integer>(); 150 preservedByNull.put(nullId, preserved); 151 } 152 preserved.add(intent.getPreservedRelationInstanceId()); 153 } 154 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 155 PredicateJoinIntent intent = state.intents.get(atom.getExpression()); 156 if (intent == null || !intent.isNullGeneratedFilter()) continue; 157 Set<Integer> preserved = preservedByNull.get( 158 intent.getNullGeneratedRelationInstanceId()); 159 if (preserved == null || preserved.isEmpty()) { 160 state.intents.put(atom.getExpression(), 161 PredicateJoinIntent.unknown( 162 atom.getReferencedRelationInstanceIds())); 163 } 164 } 165 } 166 167 /** Report UNKNOWN lowering only after dialect-local rules had priority. */ 168 private static void reportUnsupportedLowering( 169 OracleLegacyOuterJoinFacts facts, 170 ValidationState state) { 171 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 172 PredicateJoinIntent intent = state.intents.get(atom.getExpression()); 173 if (intent == null 174 || intent.getValidity() != PredicateJoinIntent.Validity.UNKNOWN) { 175 continue; 176 } 177 List<OracleLegacyOuterJoinFacts.MarkerFact> markers = 178 sortedMarkers(atom.getMarkers()); 179 OracleLegacyOuterJoinFacts.MarkerFact primary = markers.get(0); 180 state.report( 181 OracleOuterJoinViolation.Kind.INTENT_NOT_LOWERABLE, 182 "GSP found an Oracle (+) predicate whose relation " 183 + "direction cannot be lowered safely; the " 184 + "compatibility result retains it as a filter.", 185 firstNonNull(primary.getSpan(), atom.getSpan()), 186 Collections.<RelatedLocation>emptyList(), 187 atom.getExpression()); 188 state.poison(markers); 189 } 190 } 191 192 private static void checkBooleanContext( 193 OracleLegacyOuterJoinFacts facts, 194 ValidationState state) { 195 Map<TExpression, List<OracleLegacyOuterJoinFacts.MarkerFact>> byContainer = 196 new IdentityHashMap<TExpression, 197 List<OracleLegacyOuterJoinFacts.MarkerFact>>(); 198 List<TExpression> containers = new ArrayList<TExpression>(); 199 Map<TExpression, Set<Integer>> refsByContainer = 200 new IdentityHashMap<TExpression, Set<Integer>>(); 201 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 202 TExpression container = atom.getForbiddenBooleanContainer(); 203 if (container == null) continue; 204 for (OracleLegacyOuterJoinFacts.MarkerFact marker : atom.getMarkers()) { 205 if (state.poisonedMarkerIds.contains( 206 Integer.valueOf(marker.getMarkerId()))) { 207 continue; 208 } 209 List<OracleLegacyOuterJoinFacts.MarkerFact> grouped = 210 byContainer.get(container); 211 if (grouped == null) { 212 grouped = new ArrayList< 213 OracleLegacyOuterJoinFacts.MarkerFact>(); 214 byContainer.put(container, grouped); 215 containers.add(container); 216 } 217 grouped.add(marker); 218 } 219 Set<Integer> refs = refsByContainer.get(container); 220 if (refs == null) { 221 refs = new LinkedHashSet<Integer>(); 222 refsByContainer.put(container, refs); 223 } 224 refs.addAll(atom.getReferencedRelationInstanceIds()); 225 } 226 227 Collections.sort(containers, new Comparator<TExpression>() { 228 @Override 229 public int compare(TExpression left, TExpression right) { 230 return compareSpans(SourceSpan.of(left), SourceSpan.of(right)); 231 } 232 }); 233 for (TExpression container : containers) { 234 List<OracleLegacyOuterJoinFacts.MarkerFact> markers = 235 sortedMarkers(byContainer.get(container)); 236 if (markers.isEmpty()) continue; 237 OracleLegacyOuterJoinFacts.MarkerFact primary = markers.get(0); 238 List<RelatedLocation> related = new ArrayList<RelatedLocation>(); 239 for (int i = 1; i < markers.size(); i++) { 240 addRelated(related, "additional-marker", 241 "another (+) marker in this boolean expression is here", 242 markers.get(i).getSpan()); 243 } 244 String display = primary.getDisplayName() == null 245 ? "a marked column" : primary.getDisplayName() + "(+)"; 246 state.report( 247 OracleOuterJoinViolation.Kind.BOOLEAN_CONTEXT_NOT_ALLOWED, 248 "GSP found " + display 249 + " under an OR or IN expression.", 250 firstNonNull(primary.getSpan(), 251 SourceSpan.of(container)), 252 related, container); 253 state.poison(markers); 254 Set<Integer> refs = refsByContainer.get(container); 255 state.intents.put(container, 256 PredicateJoinIntent.invalid(refs)); 257 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 258 if (atom.getForbiddenBooleanContainer() == container) { 259 state.intents.put(atom.getExpression(), 260 PredicateJoinIntent.invalid( 261 atom.getReferencedRelationInstanceIds())); 262 } 263 } 264 } 265 } 266 267 private static void checkReciprocalDirections( 268 OracleLegacyOuterJoinFacts facts, 269 ValidationState state) { 270 Map<String, DirectionEvidence> firstByPair = 271 new LinkedHashMap<String, DirectionEvidence>(); 272 for (OracleLegacyOuterJoinFacts.AtomFact atom : facts.getAtoms()) { 273 PredicateJoinIntent intent = state.intents.get(atom.getExpression()); 274 if (intent == null 275 || intent.getValidity() != PredicateJoinIntent.Validity.VALID 276 || intent.isNullGeneratedFilter() 277 || intent.getPreservedRelationInstanceId() == null) { 278 continue; 279 } 280 int preserved = intent.getPreservedRelationInstanceId().intValue(); 281 int generated = intent.getNullGeneratedRelationInstanceId().intValue(); 282 String pair = Math.min(preserved, generated) + ":" 283 + Math.max(preserved, generated); 284 DirectionEvidence prior = firstByPair.get(pair); 285 if (prior == null) { 286 firstByPair.put(pair, 287 new DirectionEvidence(atom, preserved, generated)); 288 continue; 289 } 290 if (prior.preserved == generated && prior.generated == preserved) { 291 List<RelatedLocation> related = new ArrayList<RelatedLocation>(); 292 addRelated(related, "reciprocal-predicate", 293 "the opposite null-generation direction starts here", 294 prior.atom.getSpan()); 295 state.report( 296 OracleOuterJoinViolation.Kind.RECIPROCAL_NULL_GENERATION, 297 "GSP found reciprocal (+) directions between " 298 + markerDisplayList(sortedMarkers( 299 prior.atom.getMarkers())) + " and " 300 + markerDisplayList(sortedMarkers( 301 atom.getMarkers())) + ".", 302 atom.getSpan(), related, atom.getExpression()); 303 state.intents.put(prior.atom.getExpression(), 304 PredicateJoinIntent.invalid( 305 prior.atom.getReferencedRelationInstanceIds())); 306 state.intents.put(atom.getExpression(), 307 PredicateJoinIntent.invalid( 308 atom.getReferencedRelationInstanceIds())); 309 } 310 } 311 } 312 313 private static Integer soleOtherRelation(Set<Integer> references, 314 Integer marked) { 315 if (marked == null || references.size() != 2) return null; 316 Integer other = null; 317 for (Integer relation : references) { 318 if (!relation.equals(marked)) { 319 if (other != null) return null; 320 other = relation; 321 } 322 } 323 return other; 324 } 325 326 private static List<OracleLegacyOuterJoinFacts.MarkerFact> sortedMarkers( 327 List<OracleLegacyOuterJoinFacts.MarkerFact> markers) { 328 List<OracleLegacyOuterJoinFacts.MarkerFact> sorted = 329 new ArrayList<OracleLegacyOuterJoinFacts.MarkerFact>(markers); 330 Collections.sort(sorted, 331 new Comparator<OracleLegacyOuterJoinFacts.MarkerFact>() { 332 @Override 333 public int compare( 334 OracleLegacyOuterJoinFacts.MarkerFact left, 335 OracleLegacyOuterJoinFacts.MarkerFact right) { 336 return compareSpans(left.getSpan(), right.getSpan()); 337 } 338 }); 339 return sorted; 340 } 341 342 private static int compareSpans(SourceSpan left, SourceSpan right) { 343 if (left == right) return 0; 344 if (left == null) return 1; 345 if (right == null) return -1; 346 int c = Long.compare(left.getStartLine(), right.getStartLine()); 347 if (c != 0) return c; 348 c = Long.compare(left.getStartColumn(), right.getStartColumn()); 349 if (c != 0) return c; 350 c = Long.compare(left.getEndLine(), right.getEndLine()); 351 if (c != 0) return c; 352 return Long.compare(left.getEndColumn(), right.getEndColumn()); 353 } 354 355 private static SourceSpan firstNonNull(SourceSpan first, 356 SourceSpan fallback) { 357 return first == null ? fallback : first; 358 } 359 360 private static String markerDisplayList( 361 List<OracleLegacyOuterJoinFacts.MarkerFact> markers) { 362 List<String> displays = new ArrayList<String>(); 363 for (OracleLegacyOuterJoinFacts.MarkerFact marker : markers) { 364 String display = marker.getDisplayName(); 365 displays.add((display == null || display.isEmpty() 366 ? "marked column" : display) + "(+)"); 367 } 368 return displays.toString(); 369 } 370 371 private static void addRelated(List<RelatedLocation> out, String role, 372 String message, SourceSpan span) { 373 if (span != null) { 374 out.add(RelatedLocation.of(role, message, span)); 375 } 376 } 377 378 private static final class DirectionEvidence { 379 private final OracleLegacyOuterJoinFacts.AtomFact atom; 380 private final int preserved; 381 private final int generated; 382 383 private DirectionEvidence(OracleLegacyOuterJoinFacts.AtomFact atom, 384 int preserved, int generated) { 385 this.atom = atom; 386 this.preserved = preserved; 387 this.generated = generated; 388 } 389 } 390 391 private static final class ValidationState { 392 private final OracleSemanticProfile profile; 393 private final TSelectSqlStatement select; 394 private final List<Diagnostic> diagnostics = 395 new ArrayList<Diagnostic>(); 396 private final List<RecoveryMetadata.Entry> recoveryEntries = 397 new ArrayList<RecoveryMetadata.Entry>(); 398 private final IdentityHashMap<TExpression, PredicateJoinIntent> intents = 399 new IdentityHashMap<TExpression, PredicateJoinIntent>(); 400 private final Set<Integer> poisonedMarkerIds = 401 new LinkedHashSet<Integer>(); 402 403 private ValidationState(OracleSemanticProfile profile, 404 TSelectSqlStatement select) { 405 this.profile = profile; 406 this.select = select; 407 } 408 409 private Diagnostic report( 410 OracleOuterJoinViolation.Kind kind, 411 String message, 412 SourceSpan primarySpan, 413 List<RelatedLocation> related, 414 TExpression recoveredSubject) { 415 Diagnostic diagnostic = OracleOuterJoinDiagnosticCatalog.toDiagnostic( 416 profile, new OracleOuterJoinViolation( 417 kind, message, primarySpan, related)); 418 diagnostics.add(diagnostic); 419 recoveryEntries.add(new RecoveryMetadata.Entry( 420 evidenceId(diagnostic), "JOIN_INTENT", 421 subjectId(recoveredSubject), diagnostic)); 422 return diagnostic; 423 } 424 425 private void poison( 426 List<OracleLegacyOuterJoinFacts.MarkerFact> markers) { 427 for (OracleLegacyOuterJoinFacts.MarkerFact marker : markers) { 428 poisonedMarkerIds.add(Integer.valueOf(marker.getMarkerId())); 429 } 430 } 431 432 private String evidenceId(Diagnostic diagnostic) { 433 return "oracle-plus:" + diagnostic.getCode().name() + ":" 434 + spanKey(diagnostic.getSpan()) + ":" + diagnostics.size(); 435 } 436 437 private String subjectId(TExpression expression) { 438 return "query-block:" + spanKey(SourceSpan.of(select)) 439 + "/predicate:" + spanKey(SourceSpan.of(expression)); 440 } 441 442 private void sortDiagnosticsAndRecovery() { 443 final Map<Diagnostic, RecoveryMetadata.Entry> recoveryByRoot = 444 new IdentityHashMap<Diagnostic, RecoveryMetadata.Entry>(); 445 for (RecoveryMetadata.Entry entry : recoveryEntries) { 446 recoveryByRoot.put(entry.getRootDiagnostic(), entry); 447 } 448 Collections.sort(diagnostics, new Comparator<Diagnostic>() { 449 @Override 450 public int compare(Diagnostic left, Diagnostic right) { 451 int c = compareSpans(left.getSpan(), right.getSpan()); 452 if (c != 0) return c; 453 c = left.getCode().name().compareTo(right.getCode().name()); 454 if (c != 0) return c; 455 String leftVendor = left.getVendorError() == null 456 ? "" : left.getVendorError().getCode(); 457 String rightVendor = right.getVendorError() == null 458 ? "" : right.getVendorError().getCode(); 459 return leftVendor.compareTo(rightVendor); 460 } 461 }); 462 recoveryEntries.clear(); 463 for (Diagnostic diagnostic : diagnostics) { 464 RecoveryMetadata.Entry entry = recoveryByRoot.get(diagnostic); 465 if (entry != null) recoveryEntries.add(entry); 466 } 467 } 468 469 private static String spanKey(SourceSpan span) { 470 if (span == null) return "unknown"; 471 return span.getStartLine() + ":" + span.getStartColumn() 472 + "-" + span.getEndLine() + ":" + span.getEndColumn(); 473 } 474 } 475}