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}