001package gudusoft.gsqlparser.nodes.structured; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.ESqlClause; 005import gudusoft.gsqlparser.ETokenType; 006import gudusoft.gsqlparser.TSourceToken; 007import gudusoft.gsqlparser.common.structured.StructuredColumnPath; 008import gudusoft.gsqlparser.common.structured.StructuredPathSegment; 009import gudusoft.gsqlparser.common.structured.StructuredPathSegmentKind; 010import gudusoft.gsqlparser.nodes.TIndices; 011import gudusoft.gsqlparser.nodes.TIndirection; 012import gudusoft.gsqlparser.nodes.TObjectName; 013import gudusoft.gsqlparser.nodes.TTable; 014import gudusoft.gsqlparser.resolver2.model.ColumnSource; 015import gudusoft.gsqlparser.resolver2.model.ResolutionResult; 016import gudusoft.gsqlparser.resolver2.model.StructFieldHint; 017import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 018import gudusoft.gsqlparser.util.SQLUtil; 019 020import java.util.ArrayList; 021import java.util.List; 022 023/** 024 * The one producer of the nested access a column name denotes (plan section 4.1): which token is the column, and the 025 * path of fields, array elements and map values below it. 026 * <p> 027 * The input is the name's {@link NestedAccessSyntax} (what the parse wrote) and what TSQLResolver2 proved about it 028 * (where the column ends). The steps after the identifier chain come from the name's indirection, which carries every 029 * parse shape: the Databricks and Trino-family grammars write it directly, BigQuery's subscript expression and the 030 * Hive/Impala expression chain record their steps on it. A subscript is first a 031 * {@link StructuredPathSegmentKind#SUBSCRIPT} step and is refined by the dialect's 032 * {@link NestedAccessProfile.SubscriptRule}. 033 */ 034public final class NestedAccessNormalizer { 035 036 private NestedAccessNormalizer() { 037 } 038 039 /** 040 * The nested read or write {@code name} denotes, or null when it reads a whole column. 041 * 042 * @param name a column reference 043 * @return the base column token and the path below it; null for a plain column 044 */ 045 public static NestedAccess normalize(TObjectName name) { 046 NestedAccessSyntax syntax = name.getNestedAccessSyntax(); 047 NestedAccessProfile profile = NestedAccessProfile.forVendor(name.dbvendor); 048 List<String> fields = null; 049 NestedAccess.Evidence evidence = NestedAccess.Evidence.VENDOR_RULE; 050 ColumnSource source = exactSource(name); 051 StructFieldHint hint = name.getStructFieldHint(); 052 if (source != null && source.isStructFieldAccess() && source.hasFieldPath()) { 053 fields = source.getFieldPath().getSegments(); 054 evidence = NestedAccess.Evidence.RESOLVED; 055 } else if (hint != null && hint.getFieldPath() != null && !hint.getFieldPath().isEmpty()) { 056 fields = hint.getFieldPath().getSegments(); 057 evidence = NestedAccess.Evidence.HINT; 058 } 059 List<TSourceToken> chain = syntax.getChain(); 060 if (!partsWritten(name, chain)) { 061 return null; 062 } 063 int base; 064 if (fields == null && name.getLocation() == ESqlClause.set && name.getSourceTable() != null 065 && profile.buildsDeclaredTree()) { 066 // A target of UPDATE or MERGE SET is a column of the target table, after the target's name or alias when 067 // written, and a name after the column is a field below it (BigQuery SET customer.name = ...). The 068 // resolver does not read targets; the parse bound this one to its table by its position. 069 base = targetQualifierLength(name.dbvendor, chain, name.getSourceTable()); 070 } else { 071 int k = fields == null ? 0 : fields.size(); 072 base = chain.size() - 1 - k; 073 } 074 if (base < 0 || base >= chain.size()) { 075 return null; 076 } 077 List<StructuredPathSegment> segments = new ArrayList<StructuredPathSegment>(); 078 for (int i = base + 1; i < chain.size(); i++) { 079 segments.add(StructuredPathSegment.field(chain.get(i).toString())); 080 } 081 segments.addAll(refineSubscripts(profile, stepsAfterChain(syntax.getIndirection(), chain))); 082 if (segments.isEmpty()) { 083 return null; 084 } 085 TSourceToken baseToken = chain.get(base); 086 return new NestedAccess(baseToken, new StructuredColumnPath(baseToken.toString(), segments), evidence); 087 } 088 089 /** 090 * Whether the parts of a name are the name as written: they all lie in the text the name spans. Impala parses 091 * {@code e.payload.address.*} into a name that keeps e and address but not payload (Mantis 4917, Codex review), 092 * and a path read from those parts names a column the SQL does not. 093 */ 094 private static boolean partsWritten(TObjectName name, List<TSourceToken> parts) { 095 TSourceToken start = name.getStartToken(); 096 TSourceToken end = name.getEndToken(); 097 if ((start == null) || (end == null) || (start.posinlist < 0) || (end.posinlist < start.posinlist)) { 098 return true; 099 } 100 for (TSourceToken part : parts) { 101 if ((part.posinlist >= 0) && ((part.posinlist < start.posinlist) || (part.posinlist > end.posinlist))) { 102 return false; 103 } 104 } 105 return true; 106 } 107 108 /** 109 * The STRUCT column or field whose fields a wildcard below it reads ({@code customer.*}, 110 * {@code e.payload.*}), as a path ending with a STAR step; null when the name is not a wildcard below a column. 111 */ 112 public static NestedAccess normalizeStar(TObjectName name) { 113 NestedAccessSyntax syntax = name.getNestedAccessSyntax(); 114 List<TSourceToken> chain = syntax.getStarQualifierChain(); 115 if (chain.isEmpty() || !partsWritten(name, chain)) { 116 return null; 117 } 118 ColumnSource source = exactSource(name); 119 StructFieldHint hint = name.getStructFieldHint(); 120 int base; 121 NestedAccess.Evidence evidence; 122 if (source != null && source.isStructFieldAccess() && source.hasFieldPath()) { 123 // the field path read ends at the wildcard, which is the last part of this name 124 base = chain.size() - source.getFieldPath().getSegments().size(); 125 evidence = NestedAccess.Evidence.RESOLVED; 126 } else if (hint != null && hint.getFieldPath() != null && !hint.getFieldPath().isEmpty()) { 127 base = chain.size() - hint.getFieldPath().getSegments().size(); 128 evidence = NestedAccess.Evidence.HINT; 129 } else if (source != null && name.getSourceTable() != null 130 && NestedAccessProfile.forVendor(name.dbvendor).readsFieldChains()) { 131 // e.payload.*: the column payload of e, the wildcard right after it 132 base = targetQualifierLength(name.dbvendor, chain, name.getSourceTable()); 133 if (base != chain.size() - 1 || source.getExposedName() == null || !SQLUtil.sameName(name.dbvendor, 134 ESQLDataObjectType.dotColumn, source.getExposedName(), chain.get(base).toString())) { 135 return null; 136 } 137 evidence = NestedAccess.Evidence.VENDOR_RULE; 138 } else { 139 return null; 140 } 141 if (base < 0 || base >= chain.size()) { 142 return null; 143 } 144 List<StructuredPathSegment> segments = new ArrayList<StructuredPathSegment>(); 145 for (int i = base + 1; i < chain.size(); i++) { 146 segments.add(StructuredPathSegment.field(chain.get(i).toString())); 147 } 148 segments.add(StructuredPathSegment.star()); 149 TSourceToken baseToken = chain.get(base); 150 return new NestedAccess(baseToken, new StructuredColumnPath(baseToken.toString(), segments), evidence); 151 } 152 153 /** The path without its trailing STAR step: the column or field a wildcard expands. */ 154 public static StructuredColumnPath withoutStar(StructuredColumnPath path) { 155 List<StructuredPathSegment> segments = new ArrayList<StructuredPathSegment>(path.getSegments()); 156 if (!segments.isEmpty() && segments.get(segments.size() - 1).getKind() == StructuredPathSegmentKind.STAR) { 157 segments.remove(segments.size() - 1); 158 } 159 return new StructuredColumnPath(path.getRootColumn(), segments); 160 } 161 162 private static ColumnSource exactSource(TObjectName name) { 163 ResolutionResult resolution = name.getResolution(); 164 return resolution != null && resolution.isExactMatch() ? resolution.getColumnSource() : null; 165 } 166 167 /** 168 * Refine the SUBSCRIPT steps by the dialect's rule (rule R6 of the plan). Without a rule that proves the 169 * container, the steps from the first subscript on are dropped and the read stays at the column before it. 170 */ 171 static List<StructuredPathSegment> refineSubscripts(NestedAccessProfile profile, List<StructuredPathSegment> after) { 172 List<StructuredPathSegment> refined = new ArrayList<StructuredPathSegment>(); 173 switch (profile.getSubscriptRule()) { 174 case ALWAYS: 175 for (StructuredPathSegment segment : after) { 176 refined.add(segment.getKind() == StructuredPathSegmentKind.SUBSCRIPT 177 ? StructuredPathSegment.arrayElement() : segment); 178 } 179 return refined; 180 case NEVER: 181 default: 182 return refined; 183 } 184 } 185 186 /** 187 * The steps of the indirection after {@code chain}, which stops at the first subscript: a SUBSCRIPT for each 188 * subscript and a FIELD for each later name. Empty when the chain was not read from the indirection. 189 */ 190 static List<StructuredPathSegment> stepsAfterChain(TIndirection indirection, List<TSourceToken> chain) { 191 List<StructuredPathSegment> segments = new ArrayList<StructuredPathSegment>(); 192 if (indirection == null || indirection.getIndices() == null || chain.isEmpty()) { 193 return segments; 194 } 195 int consumed = chain.size() - 1; 196 if (indirection.getIndices().getElement(0).getIndicesType() != TIndices.IndicesType.QUALIFIED_NAME) { 197 // the indirection starts after the chain, at a subscript: BigQuery o.items[OFFSET(0)].item_name keeps 198 // o and items in the name slots (Mantis 4833) 199 consumed = 0; 200 } 201 if (consumed > indirection.getIndices().size()) { 202 return segments; 203 } 204 for (int i = 0; i < consumed; i++) { 205 TIndices index = indirection.getIndices().getElement(i); 206 if (index.getAttributeName() == null || index.getAttributeName().getPartToken() != chain.get(i + 1)) { 207 return segments; 208 } 209 } 210 for (int i = consumed; i < indirection.getIndices().size(); i++) { 211 TIndices index = indirection.getIndices().getElement(i); 212 if (index.getIndicesType() == TIndices.IndicesType.ARRAY_ACCESS) { 213 segments.add(StructuredPathSegment.subscript()); 214 } else if (index.getIndicesType() == TIndices.IndicesType.QUALIFIED_NAME && index.getAttributeName() != null 215 && index.getAttributeName().getPartToken() != null 216 && index.getAttributeName().getPartToken().tokentype != ETokenType.ttasterisk) { 217 segments.add(StructuredPathSegment.field(index.getAttributeName().getPartToken().toString())); 218 } else { 219 break; 220 } 221 } 222 return segments; 223 } 224 225 /** 226 * The number of leading names of {@code chain} that name {@code target}: its alias, or its name with the 227 * qualifiers written before it ({@code orders_raw}, {@code ds.orders_raw}), the longest first. Zero when the chain 228 * starts with a column; a column must follow the names counted. 229 */ 230 static int targetQualifierLength(EDbVendor vendor, List<TSourceToken> chain, TTable target) { 231 String alias = target.getAliasName(); 232 if (alias != null && !alias.isEmpty() && chain.size() > 1 233 && SQLUtil.sameName(vendor, ESQLDataObjectType.dotTable, alias, chain.get(0).toString())) { 234 return 1; 235 } 236 TObjectName name = target.getTableName(); 237 if (name == null || name.getTableToken() == null) { 238 return 0; 239 } 240 List<TSourceToken> parts = new ArrayList<TSourceToken>(); 241 List<ESQLDataObjectType> kinds = new ArrayList<ESQLDataObjectType>(); 242 if (name.getDatabaseToken() != null) { 243 parts.add(name.getDatabaseToken()); 244 kinds.add(ESQLDataObjectType.dotCatalog); 245 } 246 if (name.getSchemaToken() != null) { 247 parts.add(name.getSchemaToken()); 248 kinds.add(ESQLDataObjectType.dotSchema); 249 } 250 parts.add(name.getTableToken()); 251 kinds.add(ESQLDataObjectType.dotTable); 252 for (int k = parts.size(); k >= 1; k--) { 253 if (chain.size() <= k) { 254 continue; 255 } 256 boolean same = true; 257 for (int i = 0; i < k && same; i++) { 258 int part = parts.size() - k + i; 259 same = SQLUtil.sameName(vendor, kinds.get(part), parts.get(part).toString(), chain.get(i).toString()); 260 } 261 if (same) { 262 return k; 263 } 264 } 265 return 0; 266 } 267}