001package gudusoft.gsqlparser.nodes.structured;
002
003import gudusoft.gsqlparser.EDbVendor;
004
005import java.util.Collections;
006import java.util.EnumMap;
007import java.util.EnumSet;
008import java.util.Map;
009import java.util.Set;
010
011/**
012 * What one dialect lets a query do with nested values (STRUCT fields, ARRAY elements, MAP entries), as the axes the
013 * nested-access models branch on. This is the one table of vendor rules for nested columns: the parser's access-path
014 * normalizer, resolver2 and lineage ask it instead of comparing {@link EDbVendor} values, so adding a dialect is one
015 * entry here (docs/refactor/nested_column_access_unification_plan.md, section 4.6).
016 * <p>
017 * A Trino analysis parses with the Presto grammar, so its nodes carry {@link EDbVendor#dbvpresto} while the analysis
018 * vendor is {@link EDbVendor#dbvtrino}; every axis gives the two the same answer except {@link #usesPrestoUnnestPath()},
019 * which is asked with the analysis vendor.
020 */
021public final class NestedAccessProfile {
022
023    /** How a column's DDL declares a nested type. */
024    public enum DeclaredTypeSyntax {
025        /** {@code STRUCT<a:STRING>}, {@code ARRAY<T>}, {@code MAP<K,V>}: Hive, Impala, Spark SQL, Databricks, Athena. */
026        ANGLE_COLON,
027        /** {@code STRUCT<a STRING>}, {@code ARRAY<T>}: BigQuery. */
028        ANGLE_SPACE,
029        /** {@code ROW(a VARCHAR)}, {@code ARRAY(T)}, {@code MAP(K,V)}: Presto, Trino. */
030        ROW_PAREN,
031        /** {@code CREATE TYPE t AS (...)}: PostgreSQL (extension point). */
032        COMPOSITE_TYPE,
033        /** {@code VARIANT}, {@code OBJECT}, {@code SUPER}: no declared fields (Snowflake, Redshift). */
034        SEMI_STRUCTURED,
035        NONE
036    }
037
038    /** The parse shapes a nested read takes in a dialect's grammar (plan section 3, gap G1). */
039    public enum AccessShape {
040        /** One name with a {@code TIndirection}: Databricks, the Trino family, PostgreSQL, Redshift, Snowflake. */
041        INDIRECTION,
042        /** {@code field_access_t} / {@code array_access_expr_t} around the name: Hive, Impala. */
043        EXPRESSION_CHAIN,
044        /** A dotted name in the name slots: every dialect. */
045        DOTTED,
046        /** BigQuery's {@code array_t} expression over a subscripted name. */
047        BIGQUERY_ARRAY_EXPR,
048        /** PostgreSQL {@code (customer).name} (extension point). */
049        PARENTHESISED,
050        /** Snowflake {@code customer:address.city} (extension point). */
051        COLON_PATH
052    }
053
054    /** Which element a bare subscript ({@code items[1]}) of a column of unknown type reads. */
055    public enum SubscriptRule {
056        /** A subscript reads the element node, with or without a field after it. */
057        ALWAYS,
058        /** A subscript reads the column whole. */
059        NEVER
060    }
061
062    /** What a table-generating construct of the dialect produces from an array. */
063    public enum UnnestExpansion {
064        /** BigQuery {@code UNNEST(x) AS i}: one value column, the element. */
065        VALUE_TABLE,
066        /** Trino {@code UNNEST(a, b) AS t(x, y)}: aliases pair with the inputs by position. */
067        POSITIONAL_EXPANSION,
068        /** Hive {@code LATERAL VIEW explode(x) lv AS e}: a table-generating function. */
069        GENERATOR,
070        /** Snowflake {@code FLATTEN}: fixed output columns (extension point). */
071        FIXED_COLUMNS,
072        NONE
073    }
074
075    private static final Map<EDbVendor, NestedAccessProfile> PROFILES =
076            new EnumMap<EDbVendor, NestedAccessProfile>(EDbVendor.class);
077
078    static {
079        for (EDbVendor vendor : EDbVendor.values()) {
080            PROFILES.put(vendor, new NestedAccessProfile(vendor));
081        }
082    }
083
084    private final EDbVendor vendor;
085    private final boolean trinoFamily;
086    private final boolean readsFieldChains;
087    private final boolean structFieldVendor;
088    private final boolean readsAttributePaths;
089    private final boolean publishesPropertyNames;
090    private final boolean buildsDeclaredTree;
091    private final SubscriptRule subscriptRule;
092    private final UnnestExpansion unnestExpansion;
093    private final boolean namedStructConstructor;
094    private final boolean rowCastConstructor;
095    private final DeclaredTypeSyntax declaredTypeSyntax;
096    private final Set<AccessShape> accessShapes;
097
098    private NestedAccessProfile(EDbVendor vendor) {
099        this.vendor = vendor;
100        this.trinoFamily = vendor == EDbVendor.dbvathena || vendor == EDbVendor.dbvpresto
101                || vendor == EDbVendor.dbvtrino;
102        boolean hiveFamily = vendor == EDbVendor.dbvhive || vendor == EDbVendor.dbvsparksql
103                || vendor == EDbVendor.dbvdatabricks;
104        // Impala reads payload.old_field as the field of a STRUCT column, as Hive does (Mantis 4917)
105        this.readsFieldChains = trinoFamily || hiveFamily || vendor == EDbVendor.dbvimpala;
106        this.structFieldVendor = vendor == EDbVendor.dbvbigquery || vendor == EDbVendor.dbvsnowflake;
107        this.readsAttributePaths = vendor == EDbVendor.dbvredshift || vendor == EDbVendor.dbvoracle;
108        this.publishesPropertyNames = vendor == EDbVendor.dbvbigquery || vendor == EDbVendor.dbvredshift;
109        // Impala declares STRUCT, ARRAY and MAP columns as Hive does; its fields were published as columns before the
110        // tree (Mantis 4884)
111        this.buildsDeclaredTree = vendor == EDbVendor.dbvbigquery || readsFieldChains || vendor == EDbVendor.dbvimpala;
112        // plan rule R6 (approved 2026-10-07): a subscript reads the element in every tree dialect, BigQuery's
113        // items[OFFSET(0)] and Hive's items[0] included, not only when a field follows
114        this.subscriptRule = buildsDeclaredTree ? SubscriptRule.ALWAYS : SubscriptRule.NEVER;
115        if (vendor == EDbVendor.dbvbigquery) {
116            this.unnestExpansion = UnnestExpansion.VALUE_TABLE;
117        } else if (trinoFamily) {
118            this.unnestExpansion = UnnestExpansion.POSITIONAL_EXPANSION;
119        } else if (hiveFamily || vendor == EDbVendor.dbvimpala) {
120            this.unnestExpansion = UnnestExpansion.GENERATOR;
121        } else if (vendor == EDbVendor.dbvsnowflake) {
122            this.unnestExpansion = UnnestExpansion.FIXED_COLUMNS;
123        } else {
124            this.unnestExpansion = UnnestExpansion.NONE;
125        }
126        this.namedStructConstructor = hiveFamily;
127        this.rowCastConstructor = trinoFamily || hiveFamily;
128        if (vendor == EDbVendor.dbvbigquery) {
129            this.declaredTypeSyntax = DeclaredTypeSyntax.ANGLE_SPACE;
130        } else if (vendor == EDbVendor.dbvpresto || vendor == EDbVendor.dbvtrino) {
131            this.declaredTypeSyntax = DeclaredTypeSyntax.ROW_PAREN;
132        } else if (hiveFamily || vendor == EDbVendor.dbvimpala || vendor == EDbVendor.dbvathena) {
133            this.declaredTypeSyntax = DeclaredTypeSyntax.ANGLE_COLON;
134        } else if (vendor == EDbVendor.dbvpostgresql) {
135            this.declaredTypeSyntax = DeclaredTypeSyntax.COMPOSITE_TYPE;
136        } else if (vendor == EDbVendor.dbvsnowflake || vendor == EDbVendor.dbvredshift) {
137            this.declaredTypeSyntax = DeclaredTypeSyntax.SEMI_STRUCTURED;
138        } else {
139            this.declaredTypeSyntax = DeclaredTypeSyntax.NONE;
140        }
141        EnumSet<AccessShape> shapes = EnumSet.of(AccessShape.DOTTED);
142        if (vendor == EDbVendor.dbvhive || vendor == EDbVendor.dbvimpala || vendor == EDbVendor.dbvsparksql) {
143            shapes.add(AccessShape.EXPRESSION_CHAIN);
144        }
145        if (vendor == EDbVendor.dbvdatabricks || trinoFamily || vendor == EDbVendor.dbvpostgresql
146                || vendor == EDbVendor.dbvredshift || vendor == EDbVendor.dbvsnowflake) {
147            shapes.add(AccessShape.INDIRECTION);
148        }
149        if (vendor == EDbVendor.dbvbigquery) {
150            shapes.add(AccessShape.BIGQUERY_ARRAY_EXPR);
151            shapes.add(AccessShape.INDIRECTION);
152        }
153        if (vendor == EDbVendor.dbvpostgresql) {
154            shapes.add(AccessShape.PARENTHESISED);
155        }
156        if (vendor == EDbVendor.dbvsnowflake) {
157            shapes.add(AccessShape.COLON_PATH);
158        }
159        this.accessShapes = Collections.unmodifiableSet(shapes);
160    }
161
162    /** The profile of {@code vendor}; a null vendor gets the profile of a dialect without nested access. */
163    public static NestedAccessProfile forVendor(EDbVendor vendor) {
164        if (vendor == null) {
165            return PROFILES.get(EDbVendor.dbvgeneric);
166        }
167        return PROFILES.get(vendor);
168    }
169
170    public EDbVendor getVendor() {
171        return vendor;
172    }
173
174    /** Athena, Presto, Trino: one grammar, Trino's name-reading rule (Mantis 4811, 4822). */
175    public boolean isTrinoFamily() {
176        return trinoFamily;
177    }
178
179    /**
180     * A dotted name is read by what its qualifiers name: a qualifier that names a relation in scope is that
181     * relation, otherwise the first part is a column and every later part a field (the Trino family; Hive, Spark
182     * SQL, Databricks since Mantis 4821).
183     */
184    public boolean readsFieldChains() {
185        return readsFieldChains;
186    }
187
188    /** resolver2 reads a two-part name whose head is no relation as a column with a field: BigQuery, Snowflake. */
189    public boolean isStructFieldVendor() {
190        return structFieldVendor;
191    }
192
193    /** An attribute path after a relation-qualified column ({@code o.customer.address.city}): Redshift, Oracle. */
194    public boolean readsAttributePaths() {
195        return readsAttributePaths;
196    }
197
198    /** Lineage names a field read of a flat column by its whole dotted path: BigQuery, Redshift (legacy naming). */
199    public boolean publishesPropertyNames() {
200        return publishesPropertyNames;
201    }
202
203    /** Lineage models a column with a declared STRUCT, ARRAY or MAP type as a tree of field nodes. */
204    public boolean buildsDeclaredTree() {
205        return buildsDeclaredTree;
206    }
207
208    /** Which node a subscript of a column of unknown type reads; see {@link SubscriptRule}. */
209    public SubscriptRule getSubscriptRule() {
210        return subscriptRule;
211    }
212
213    public UnnestExpansion getUnnestExpansion() {
214        return unnestExpansion;
215    }
216
217    /**
218     * Presto's own UNNEST analysis, kept as a compatibility adapter: asked with the analysis vendor, true for
219     * {@link EDbVendor#dbvpresto} only (a Trino or Athena analysis uses the positional-expansion path).
220     */
221    public boolean usesPrestoUnnestPath() {
222        return vendor == EDbVendor.dbvpresto;
223    }
224
225    /** {@code named_struct('a', x, ...)} builds a STRUCT: Hive, Spark SQL, Databricks. */
226    public boolean hasNamedStructConstructor() {
227        return namedStructConstructor;
228    }
229
230    /** {@code CAST(ROW(...) AS ROW(a T, ...))} builds a STRUCT with named fields: the tree dialects but BigQuery. */
231    public boolean hasRowCastConstructor() {
232        return rowCastConstructor;
233    }
234
235    public DeclaredTypeSyntax getDeclaredTypeSyntax() {
236        return declaredTypeSyntax;
237    }
238
239    public Set<AccessShape> getAccessShapes() {
240        return accessShapes;
241    }
242}