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.island.SqlScopeDetector.SqlScopeResult; 008import gudusoft.gsqlparser.pp2.layout.LayoutContext; 009import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 010import gudusoft.gsqlparser.pp2.layout.LayoutRule; 011import gudusoft.gsqlparser.pp2.token.Pp2Token; 012import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 013 014import java.util.EnumSet; 015import java.util.Locale; 016import java.util.Set; 017 018/** 019 * Breaks a predicate before each top-level {@code AND} / {@code OR} so a long 020 * WHERE/HAVING/ON condition lays out one conjunct per line: 021 * 022 * <pre> 023 * WHERE a = 1 024 * AND b = 2 025 * OR c = 3 026 * </pre> 027 * 028 * <p>Only top-level boolean operators break: an {@code AND}/{@code OR} whose 029 * block (paren) depth equals its clause run's base depth. Operators inside a 030 * parenthesised sub-predicate ({@code (a AND b)}) or a function stay inline. 031 * Limited to predicate clauses ({@code WHERE}/{@code HAVING}/{@code JOIN}). 032 * 033 * <p>Concatenation operators ({@code ||}, {@code +}) are left to the spacing 034 * rule (no break) — keeping concatenated expressions on one line. 035 * 036 * <h2>Deep input safety</h2> 037 * 038 * <p>This is a single linear pass over the token stream — there is no recursion 039 * over the boolean expression tree — so a 2000-deep {@code AND}/{@code OR} chain 040 * is handled in O(n) without {@code StackOverflowError} (plan §13/R3). 041 * 042 * <p>Priority {@link LayoutPriorities#CASE_ANDOR}. Needs S19 block + S20 sql + 043 * S21 clause analyses. Read-only. 044 * 045 * <p>Plan reference: §7.3/S27, §7.4/S27. 046 */ 047public final class AndOrConcatRules implements LayoutRule { 048 049 private static final Set<ClausePart> PREDICATE_CLAUSES = EnumSet.of( 050 ClausePart.WHERE, ClausePart.HAVING, ClausePart.JOIN); 051 052 @Override 053 public int priority() { return LayoutPriorities.CASE_ANDOR; } 054 055 @Override 056 public String name() { return "AndOrConcatRules"; } 057 058 @Override 059 public void apply(LayoutContext context) { 060 ClauseScopeResult clause = context.getClauseScope(); 061 SqlScopeResult sql = context.getSqlScope(); 062 BlockScopeResult block = context.getBlockScope(); 063 if (clause == null || sql == null || block == null) return; 064 Pp2TokenStream stream = context.getStream(); 065 int n = min(stream.size(), clause.size(), sql.size(), block.size()); 066 067 // Single O(n) forward pass: track the current clause run's base paren 068 // depth (the depth at the run's first token) instead of re-scanning back 069 // for every AND/OR, and a BETWEEN "debt" so the AND of a BETWEEN ... AND 070 // is not mistaken for a boolean operator. 071 ClausePart prevPart = null; 072 int prevLevel = -1; 073 int runBaseDepth = 0; 074 int betweenDebt = 0; 075 for (int i = 0; i < n; i++) { 076 ClausePart part = clause.partAt(i); 077 int level = sql.levelAt(i); 078 if (part != prevPart || level != prevLevel) { 079 runBaseDepth = block.depthAt(i); // new clause run starts here 080 betweenDebt = 0; 081 } 082 prevPart = part; 083 prevLevel = level; 084 085 if (!PREDICATE_CLAUSES.contains(part)) continue; 086 boolean atBase = block.depthAt(i) == runBaseDepth; 087 if (!atBase) continue; 088 089 String u = upper(stream.get(i)); 090 if ("BETWEEN".equals(u)) { 091 betweenDebt++; 092 } else if ("AND".equals(u)) { 093 if (betweenDebt > 0) { 094 betweenDebt--; // this AND belongs to a BETWEEN ... AND 095 } else if (i > 0) { 096 context.requestLinebreaksBefore(i, 1); 097 context.requestBlanksBefore(i, 0); 098 } 099 } else if ("OR".equals(u) && i > 0) { 100 context.requestLinebreaksBefore(i, 1); 101 context.requestBlanksBefore(i, 0); 102 } 103 } 104 } 105 106 private static String upper(Pp2Token t) { 107 if (t.getSourceToken().tokentype != ETokenType.ttkeyword) return ""; 108 String s = t.getText(); 109 return s == null ? "" : s.toUpperCase(Locale.ROOT); 110 } 111 112 private static int min(int a, int b, int c, int d) { 113 return Math.min(Math.min(a, b), Math.min(c, d)); 114 } 115}