001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult; 005import gudusoft.gsqlparser.pp2.island.ClausePart; 006import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 007import gudusoft.gsqlparser.pp2.layout.LayoutContext; 008import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 009import gudusoft.gsqlparser.pp2.layout.LayoutRule; 010import gudusoft.gsqlparser.pp2.token.Pp2Token; 011import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 012 013import java.util.Arrays; 014import java.util.Collections; 015import java.util.HashSet; 016import java.util.Locale; 017import java.util.Set; 018 019/** 020 * Lays out joins vertically: a linebreak before the start of each join phrase 021 * ({@code JOIN}, {@code LEFT OUTER JOIN}, {@code INNER JOIN}, {@code CROSS APPLY}, 022 * ...) and before each {@code ON} / {@code USING} that opens a join condition. 023 * 024 * <h2>Robust join detection</h2> 025 * 026 * <ul> 027 * <li>A break before a join phrase fires only when the maximal forward run of 028 * join keywords actually contains the operator {@code JOIN} or 029 * {@code APPLY}. This stops a {@code LEFT(} string function inside an 030 * {@code ON} predicate ({@code ... ON left(x,1) = y}) from being mistaken 031 * for a {@code LEFT JOIN}.</li> 032 * <li>{@code ON}/{@code USING} break only at the join operator's own block 033 * (paren) depth, so a structural-looking keyword inside a function call 034 * (e.g. JSON {@code ... NULL ON ERROR ...}) does not trigger a break.</li> 035 * </ul> 036 * 037 * <p>Gated to tokens whose S21 {@link ClausePart} is {@link ClausePart#JOIN}. 038 * Priority {@link LayoutPriorities#JOIN_PAREN_COMMA}. Needs the S21 clause and 039 * S19 block analyses on the context. Iterative; read-only. 040 * 041 * <p>Plan reference: §7.3/S26, §7.4/S26. 042 */ 043public final class JoinRules implements LayoutRule { 044 045 private static final Set<String> JOIN_PHRASE_WORDS; 046 static { 047 Set<String> s = new HashSet<String>(Arrays.asList( 048 "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "CROSS", "NATURAL", "OUTER", "APPLY")); 049 JOIN_PHRASE_WORDS = Collections.unmodifiableSet(s); 050 } 051 052 @Override 053 public int priority() { return LayoutPriorities.JOIN_PAREN_COMMA; } 054 055 @Override 056 public String name() { return "JoinRules"; } 057 058 @Override 059 public void apply(LayoutContext context) { 060 ClauseScopeResult clause = context.getClauseScope(); 061 if (clause == null) return; 062 BlockScopeResult block = context.getBlockScope(); // may be null 063 Pp2TokenStream stream = context.getStream(); 064 int n = Math.min(stream.size(), clause.size()); 065 066 int lastJoinDepth = -1; // block depth of the most recent join operator 067 for (int i = 1; i < n; i++) { 068 if (clause.partAt(i) != ClausePart.JOIN) continue; 069 String u = upper(stream.get(i)); 070 if (JOIN_PHRASE_WORDS.contains(u)) { 071 int prev = prevSolid(stream, i); 072 String pu = prev < 0 ? null : upper(stream.get(prev)); 073 boolean phraseStart = pu == null || !JOIN_PHRASE_WORDS.contains(pu); 074 if (phraseStart && runContainsJoinOperator(stream, clause, i)) { 075 context.requestLinebreaksBefore(i, 1); 076 context.requestBlanksBefore(i, 0); 077 lastJoinDepth = depthAt(block, i); 078 } 079 } else if ("ON".equals(u) || "USING".equals(u)) { 080 // Only the join condition opener at the join operator's depth. 081 if (lastJoinDepth >= 0 && depthAt(block, i) == lastJoinDepth) { 082 context.requestLinebreaksBefore(i, 1); 083 context.requestBlanksBefore(i, 0); 084 } 085 } 086 } 087 } 088 089 /** Scan the maximal forward run of join keywords from {@code start}; true if it has JOIN/APPLY. */ 090 private static boolean runContainsJoinOperator(Pp2TokenStream stream, 091 ClauseScopeResult clause, int start) { 092 int n = Math.min(stream.size(), clause.size()); 093 for (int j = start; j < n; j++) { 094 if (clause.partAt(j) != ClausePart.JOIN) break; 095 String u = upper(stream.get(j)); 096 if (!JOIN_PHRASE_WORDS.contains(u)) break; 097 if ("JOIN".equals(u) || "APPLY".equals(u)) return true; 098 } 099 return false; 100 } 101 102 /** Block depth at i, or 0 when no block analysis is attached. */ 103 private static int depthAt(BlockScopeResult block, int i) { 104 if (block == null || i >= block.size()) return 0; 105 return block.depthAt(i); 106 } 107 108 private static String upper(Pp2Token t) { 109 if (t.getSourceToken().tokentype != ETokenType.ttkeyword) return ""; 110 String s = t.getText(); 111 return s == null ? "" : s.toUpperCase(Locale.ROOT); 112 } 113 114 private static int prevSolid(Pp2TokenStream stream, int i) { 115 for (int j = i - 1; j >= 0; j--) { 116 ETokenType type = stream.get(j).getSourceToken().tokentype; 117 if (type == ETokenType.ttsimplecomment 118 || type == ETokenType.ttbracketedcomment 119 || type == ETokenType.ttCPPComment) { 120 continue; 121 } 122 return j; 123 } 124 return -1; 125 } 126}