001package gudusoft.gsqlparser.pp2.layout.rules; 002 003import gudusoft.gsqlparser.ETokenType; 004import gudusoft.gsqlparser.pp.para.GFmtOpt; 005import gudusoft.gsqlparser.pp.para.styleenums.TAlignStyle; 006import gudusoft.gsqlparser.pp2.island.ClausePart; 007import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 008import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult; 009import gudusoft.gsqlparser.pp2.layout.LayoutContext; 010import gudusoft.gsqlparser.pp2.layout.LayoutDecisionView; 011import gudusoft.gsqlparser.pp2.layout.LayoutPriorities; 012import gudusoft.gsqlparser.pp2.layout.LayoutRule; 013import gudusoft.gsqlparser.pp2.token.Pp2Token; 014import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 015import gudusoft.gsqlparser.pp2.token.TokenRole; 016 017import java.util.List; 018 019/** 020 * When {@link GFmtOpt#alignAliasInSelectList} is set (and the list is stacked, 021 * {@link TAlignStyle#AsStacked}), aligns each stacked SELECT-list continuation 022 * column under the first column (i.e. just past the {@code SELECT} keyword) 023 * rather than at a flat indent: 024 * 025 * <pre> 026 * SELECT a, 027 * b, 028 * c 029 * </pre> 030 * 031 * <p>The alignment column is {@code indentOf(SELECT) + len(SELECT) + 1}. This 032 * overrides {@link IndentRules}' flat indent for those columns (priority 033 * {@link LayoutPriorities#ALIGNMENT} > {@link LayoutPriorities#INDENT}). 034 * 035 * <p>Only SELECT-list continuation columns that begin a line are aligned (the 036 * first column stays on the SELECT line). Needs S20 sql + S21 clause analyses. 037 * Iterative; read-only over tokens. 038 * 039 * <p>Plan reference: §7.3/S28, §7.4/S28. 040 */ 041public final class AlignmentRules implements LayoutRule { 042 043 @Override 044 public int priority() { return LayoutPriorities.ALIGNMENT; } 045 046 @Override 047 public String name() { return "AlignmentRules"; } 048 049 @Override 050 public void apply(LayoutContext context) { 051 GFmtOpt opt = context.getOptions().toGFmtOpt(); 052 if (!opt.alignAliasInSelectList) return; 053 if (opt.selectColumnlistStyle != TAlignStyle.AsStacked) return; 054 ClauseScopeResult clause = context.getClauseScope(); 055 SqlScopeResult sql = context.getSqlScope(); 056 if (clause == null || sql == null) return; 057 058 int width = indentWidth(opt); 059 Pp2TokenStream stream = context.getStream(); 060 List<Integer> masters = clause.getMasterIndices(); 061 int n = min(stream.size(), clause.size(), sql.size()); 062 for (int i = 0; i < n; i++) { 063 if (clause.partAt(i) != ClausePart.SELECT_LIST) continue; 064 if (effectiveLinebreaks(context, stream, i) <= 0) continue; // first column stays inline 065 if (stream.get(i).hasRole(TokenRole.KEYWORD_MASTER)) continue; // the SELECT keyword itself 066 // Only a genuine stacked column start (the token right after a 067 // top-level list comma) aligns — not CASE/expression continuations 068 // inside a column, which keep IndentRules' indent. 069 int prev = prevSolid(stream, i); 070 if (prev < 0 || stream.get(prev).getSourceToken().tokentype != ETokenType.ttcomma) { 071 continue; 072 } 073 int selectIdx = selectMasterFor(stream, sql, masters, i); 074 if (selectIdx < 0) continue; 075 // Only align under a SELECT that itself begins a line (or is the very 076 // first token); an inline subquery SELECT's real column is unknown 077 // here, so fall back to IndentRules' flat indent for it. 078 if (selectIdx != 0 && effectiveLinebreaks(context, stream, selectIdx) <= 0) { 079 continue; 080 } 081 int selectIndent = sql.levelAt(selectIdx) * width; 082 int alignCol = selectIndent + length(stream.get(selectIdx)) + 1; 083 context.requestBlanksBefore(i, alignCol); 084 } 085 } 086 087 private static int prevSolid(Pp2TokenStream stream, int i) { 088 for (int j = i - 1; j >= 0; j--) { 089 ETokenType type = stream.get(j).getSourceToken().tokentype; 090 if (type == ETokenType.ttsimplecomment 091 || type == ETokenType.ttbracketedcomment 092 || type == ETokenType.ttCPPComment) { 093 continue; 094 } 095 return j; 096 } 097 return -1; 098 } 099 100 /** The nearest preceding SELECT master at the same SQL level as token i. */ 101 private static int selectMasterFor(Pp2TokenStream stream, SqlScopeResult sql, 102 List<Integer> masters, int i) { 103 int best = -1; 104 for (int m : masters) { 105 if (m > i) break; 106 if (sql.levelAt(m) == sql.levelAt(i) && isSelect(stream.get(m))) best = m; 107 } 108 return best; 109 } 110 111 private static boolean isSelect(Pp2Token t) { 112 String s = t.getText(); 113 return s != null && "SELECT".equalsIgnoreCase(s); 114 } 115 116 private static int length(Pp2Token t) { 117 String s = t.getText(); 118 return s == null ? 0 : s.length(); 119 } 120 121 private static int indentWidth(GFmtOpt opt) { 122 Integer w = opt.indentLen; 123 return (w == null || w.intValue() < 0) ? 2 : w.intValue(); 124 } 125 126 private static int effectiveLinebreaks(LayoutContext ctx, Pp2TokenStream stream, int i) { 127 LayoutDecisionView d = ctx.decisionAt(i); 128 return d.isLinebreaksDecided() ? d.getLinebreaksBefore() 129 : stream.get(i).getPrecedingLinebreaks(); 130 } 131 132 private static int min(int a, int b, int c) { 133 return Math.min(Math.min(a, b), c); 134 } 135}