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}