001package gudusoft.gsqlparser.resolver2.namespace;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.EExpressionType;
005import gudusoft.gsqlparser.ETableSource;
006import gudusoft.gsqlparser.nodes.TExpression;
007import gudusoft.gsqlparser.nodes.TObjectName;
008import gudusoft.gsqlparser.nodes.TTable;
009import gudusoft.gsqlparser.nodes.TUnnestClause;
010import gudusoft.gsqlparser.resolver2.ColumnLevel;
011import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
012import gudusoft.gsqlparser.resolver2.model.ColumnSource;
013
014import java.util.ArrayList;
015import java.util.Collections;
016import java.util.HashSet;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Set;
020
021/**
022 * Namespace representing an UNNEST table expression: BigQuery {@code UNNEST(arr) AS x}, the Trino family
023 * {@code CROSS JOIN UNNEST(arr) AS t(x)}, and the other dialects with an UNNEST relation.
024 *
025 * UNNEST flattens an array into rows. For example:
026 * - UNNEST(['a', 'b', 'c']) creates a virtual table with rows 'a', 'b', 'c'
027 * - SELECT value FROM UNNEST(array_column) - 'value' is an implicit column name
028 *
029 * The UNNEST namespace provides:
030 * 1. An implicit column for the unnested elements (named by alias or 'value')
031 * 2. Support for WITH OFFSET which adds an 'offset' column
032 * 3. Support for STRUCT arrays which expose struct field names
033 */
034public class UnnestNamespace extends AbstractNamespace {
035
036    private final TTable unnestTable;
037
038    /**
039     * How many output columns the clause generated, as opposed to how many distinct names they
040     * ended up under: {@code UNNEST(x) AS x WITH OFFSET AS x} generates two outputs that share one
041     * name, and a map keyed by name then holds one of them. The schema is complete only when
042     * every generated output has a name of its own.
043     */
044    private int generatedOutputs;
045    private final TUnnestClause unnestClause;
046    private final String alias;
047
048    /** The implicit column name for unnested elements */
049    private String implicitColumnName;
050
051    /** WITH OFFSET column name if present */
052    private String offsetColumnName;
053
054    /** Inferred columns for dynamic resolution */
055    private Set<String> inferredColumnNames;
056
057    /** Whether this UNNEST has an explicit alias (e.g., UNNEST(...) AS alias) */
058    private boolean hasExplicitAlias;
059
060    /** Whether the UNNEST array expression is a subquery (returns a struct that can be expanded) */
061    private boolean arrayExpressionIsSubquery;
062
063    public UnnestNamespace(TTable unnestTable, String alias, INameMatcher nameMatcher) {
064        super(unnestTable, nameMatcher);
065        this.unnestTable = unnestTable;
066        this.unnestClause = unnestTable.getUnnestClause();
067
068        // Determine if this is a real alias (from AS clause) or just the table name fallback
069        // For UNNEST tables, we should check if there's an explicit alias
070        String explicitAlias = unnestTable.getAliasName();
071        if (explicitAlias != null && !explicitAlias.isEmpty()) {
072            // Has explicit AS alias - use it for both table alias and implicit column
073            this.alias = explicitAlias;
074            this.implicitColumnName = explicitAlias;
075            this.hasExplicitAlias = true;
076        } else {
077            // No explicit alias - UNNEST without AS clause
078            // The implicit column name is "value" (BigQuery default for simple arrays)
079            this.alias = alias; // May be null or derived name
080            this.implicitColumnName = "value";
081            this.hasExplicitAlias = false;
082        }
083
084        // Check for WITH OFFSET
085        if (unnestClause != null && unnestClause.getWithOffset() != null) {
086            if (unnestClause.getWithOffsetAlais() != null &&
087                unnestClause.getWithOffsetAlais().getAliasName() != null) {
088                this.offsetColumnName = unnestClause.getWithOffsetAlais().getAliasName().toString();
089            } else {
090                this.offsetColumnName = "offset";
091            }
092        }
093
094        // Check if the array expression is a subquery (returns a struct)
095        // When UNNEST operates on a subquery that returns a struct, struct fields become accessible
096        // Example: UNNEST((SELECT struct_column FROM table))
097        this.arrayExpressionIsSubquery = false;
098        if (unnestClause != null) {
099            TExpression arrayExpr = unnestClause.getArrayExpr();
100            if (arrayExpr != null && arrayExpr.getExpressionType() == EExpressionType.subquery_t) {
101                this.arrayExpressionIsSubquery = true;
102            }
103        }
104    }
105
106    public UnnestNamespace(TTable unnestTable, String alias) {
107        this(unnestTable, alias, null);
108    }
109
110    @Override
111    public String getDisplayName() {
112        if (alias != null && !alias.isEmpty()) {
113            return alias;
114        }
115        return "(unnest table)";
116    }
117
118    @Override
119    public TTable getFinalTable() {
120        // UNNEST creates a virtual table, return the TTable representing it
121        return unnestTable;
122    }
123
124    @Override
125    public List<TTable> getAllFinalTables() {
126        List<TTable> tables = new ArrayList<>();
127        tables.add(unnestTable);
128        return tables;
129    }
130
131    /**
132     * Whether this namespace enumerates the UNNEST table's whole schema.
133     *
134     * <p>It does when the structure was DECLARED — explicit alias columns
135     * ({@code UNNEST(x) AS t(a, b)}) or a derived struct-field list — or when the element type
136     * is visibly scalar: an array literal whose every item is a plain constant
137     * ({@code UNNEST(['foo','bar'])}), for which the implicit element column and WITH OFFSET are
138     * the complete output. For anything else — a column, a function result, an array literal of
139     * {@code STRUCT(...)} items — the element may be a struct whose fields this namespace cannot
140     * list (they are resolved on demand as struct field access), so the schema is NOT complete
141     * and a star over the table still carries information. Any inferred field settles it the
142     * same way.
143     */
144    /**
145     * {@code FROM orders_raw, UNNEST(items)}: an unaliased value table whose element type is unknown exposes the
146     * element's fields under their own names, so an unqualified name no table proves may be one of them (Mantis
147     * 4828, STRUCT tree plan rule R3). BigQuery exposes the fields of a STRUCT element; Presto, Trino and Athena
148     * expand an ARRAY(ROW) into one column per field, named after the field. Other vendors (PostgreSQL names the
149     * one unnest column after the function) never expose fields this way.
150     */
151    public boolean mayHoldUnknownFields() {
152        if (hasExplicitAlias || arrayExpressionIsSubquery || !exposesElementFields(unnestTable.dbvendor)) {
153            return false;
154        }
155        return getMetadataState() == MetadataState.METADATA_UNAVAILABLE;
156    }
157
158    /** Whether the vendor's unaliased UNNEST exposes the fields of a STRUCT or ROW element as columns. */
159    private static boolean exposesElementFields(EDbVendor vendor) {
160        return vendor == EDbVendor.dbvbigquery || vendor == EDbVendor.dbvpresto || vendor == EDbVendor.dbvtrino
161                || vendor == EDbVendor.dbvathena;
162    }
163
164    @Override
165    public MetadataState getMetadataState() {
166        ensureValidated();
167        if (columnSources == null || columnSources.isEmpty()) {
168            return MetadataState.METADATA_UNAVAILABLE;
169        }
170        if (inferredColumnNames != null && !inferredColumnNames.isEmpty()) {
171            return MetadataState.METADATA_UNAVAILABLE;
172        }
173        if (generatedOutputs != columnSources.size()) {
174            return MetadataState.METADATA_UNAVAILABLE;   // two outputs collapsed onto one name
175        }
176        boolean declared = (unnestTable.getAliasClause() != null
177                && unnestTable.getAliasClause().getColumns() != null
178                && unnestTable.getAliasClause().getColumns().size() > 0)
179                || (unnestClause != null && unnestClause.getDerivedColumnList() != null
180                && unnestClause.getDerivedColumnList().size() > 0);
181        if (declared) {
182            return MetadataState.FOUND;
183        }
184        return elementIsScalarLiteral() ? MetadataState.FOUND : MetadataState.METADATA_UNAVAILABLE;
185    }
186
187    /** The UNNEST argument is an array literal whose every item is a plain constant. */
188    private boolean elementIsScalarLiteral() {
189        if (unnestClause == null) return false;
190        gudusoft.gsqlparser.nodes.TExpression array = unnestClause.getArrayExpr();
191        if (array == null || array.getExpressionType() != gudusoft.gsqlparser.EExpressionType.array_t) {
192            return false;
193        }
194        gudusoft.gsqlparser.nodes.TExpressionList items = array.getExprList();
195        if (items == null || items.size() == 0) return false;
196        for (int i = 0; i < items.size(); i++) {
197            gudusoft.gsqlparser.nodes.TExpression item = items.getExpression(i);
198            if (item == null || item.getExpressionType() != gudusoft.gsqlparser.EExpressionType.simple_constant_t) {
199                return false;
200            }
201        }
202        return true;
203    }
204
205    @Override
206    protected void doValidate() {
207        columnSources = new LinkedHashMap<>();
208        generatedOutputs = 0;
209
210        // Check for explicit column aliases in table's alias clause
211        // For Presto/Trino syntax: UNNEST(array) AS t (col1, col2)
212        // The column names are in the alias clause's column list
213        boolean hasExplicitColumns = false;
214        if (unnestTable.getAliasClause() != null &&
215            unnestTable.getAliasClause().getColumns() != null &&
216            unnestTable.getAliasClause().getColumns().size() > 0) {
217
218            hasExplicitColumns = true;
219            for (int i = 0; i < unnestTable.getAliasClause().getColumns().size(); i++) {
220                TObjectName colName = unnestTable.getAliasClause().getColumns().getObjectName(i);
221                if (colName != null) {
222                    String name = colName.toString();
223                    ColumnSource source = new ColumnSource(
224                        this,
225                        name,
226                        null,
227                        1.0,
228                        "unnest_explicit_column_alias"
229                    );
230                    columnSources.put(name, source);
231                    generatedOutputs++;
232                }
233            }
234        }
235
236        // Add the implicit column for unnested elements (only if no explicit columns)
237        if (!hasExplicitColumns && implicitColumnName != null) {
238            ColumnSource implicitSource = new ColumnSource(
239                this,
240                implicitColumnName,
241                null,
242                1.0,
243                "unnest_implicit_column"
244            );
245            columnSources.put(implicitColumnName, implicitSource);
246            generatedOutputs++;
247        }
248
249        // Add WITH OFFSET column if present
250        if (offsetColumnName != null) {
251            ColumnSource offsetSource = new ColumnSource(
252                this,
253                offsetColumnName,
254                null,
255                1.0,
256                "unnest_offset_column"
257            );
258            columnSources.put(offsetColumnName, offsetSource);
259            generatedOutputs++;
260        }
261
262        // Add derived columns from STRUCT types if available
263        if (unnestClause != null && unnestClause.getDerivedColumnList() != null) {
264            for (int i = 0; i < unnestClause.getDerivedColumnList().size(); i++) {
265                TObjectName derivedCol = unnestClause.getDerivedColumnList().getObjectName(i);
266                if (derivedCol != null) {
267                    String colName = derivedCol.toString();
268                    ColumnSource derivedSource = new ColumnSource(
269                        this,
270                        colName,
271                        null,
272                        1.0,
273                        "unnest_struct_field"
274                    );
275                    columnSources.put(colName, derivedSource);
276                    generatedOutputs++;
277                }
278            }
279        } else if (unnestTable != null) {
280            // The fields of the STRUCT literals the array holds, as the parse reads them (Mantis 4797):
281            // UNNEST([STRUCT(0 AS a, 1 AS b)]), or a CTE or derived column defined so, has the columns a and b.
282            java.util.List<String> fields = gudusoft.gsqlparser.stmt.TSelectSqlStatement.unnestStructFieldNames(unnestTable);
283            if (fields != null) {
284                for (String field : fields) {
285                    columnSources.put(field, new ColumnSource(this, field, null, 1.0, "unnest_struct_field"));
286                    generatedOutputs++;
287                }
288            }
289        }
290    }
291
292    @Override
293    public boolean hasStarColumn() {
294        // UNNEST tables support star expansion for SELECT *
295        // but have fixed columns (implicit + offset + struct fields)
296        return true;
297    }
298
299    @Override
300    public boolean supportsDynamicInference() {
301        // Only allow dynamic inference for anonymous UNNEST (no explicit alias) that operates
302        // on a subquery returning a struct. In this case, struct fields become accessible
303        // as unqualified columns.
304        //
305        // Example: SELECT field FROM UNNEST((SELECT struct_col FROM table))
306        // Here 'field' is a struct field that should be inferred from the UNNEST.
307        //
308        // For UNNEST with alias or UNNEST on a simple column, don't allow dynamic inference.
309        // This prevents external variables from being incorrectly attributed to the UNNEST.
310        return !hasExplicitAlias && arrayExpressionIsSubquery;
311    }
312
313    @Override
314    public boolean addInferredColumn(String columnName, double confidence, String evidence) {
315        if (columnName == null || columnName.isEmpty()) {
316            return false;
317        }
318
319        // Only allow inferred columns when dynamic inference is supported.
320        // UNNEST with explicit alias has a fixed set of columns (implicit column, offset, struct fields)
321        // and should NOT accept arbitrary inferred columns from star push-down or enhancement.
322        // This prevents external variables (like function parameters) from being incorrectly
323        // attributed to the UNNEST table.
324        if (!supportsDynamicInference()) {
325            return false;
326        }
327
328        if (inferredColumnNames == null) {
329            inferredColumnNames = new HashSet<>();
330        }
331
332        // Slice S1: dedupe via matcher-aware helpers so per-vendor identifier
333        // rules (BigQuery columns case-insensitive; Oracle quoted vs unquoted)
334        // govern collision detection.
335        if (containsColumnByMatcher(columnSources, columnName)) {
336            return false;
337        }
338
339        if (containsColumnNameByMatcher(inferredColumnNames, columnName)) {
340            return false;
341        }
342
343        inferredColumnNames.add(columnName);
344
345        // Also add to column sources
346        if (columnSources != null) {
347            ColumnSource source = new ColumnSource(
348                this,
349                columnName,
350                null,
351                confidence,
352                evidence
353            );
354            columnSources.put(columnName, source);
355        }
356
357        return true;
358    }
359
360    @Override
361    public Set<String> getInferredColumns() {
362        if (inferredColumnNames == null) {
363            return Collections.emptySet();
364        }
365        return Collections.unmodifiableSet(inferredColumnNames);
366    }
367
368    @Override
369    public ColumnLevel hasColumn(String columnName) {
370        ensureValidated();
371
372        // Check explicit columns (implicit column, offset, struct fields).
373        if (containsColumnByMatcher(columnSources, columnName)) {
374            return ColumnLevel.EXISTS;
375        }
376
377        // Check inferred columns. Slice S1: route through matcher-aware
378        // helper so case-only-different references don't drop to NOT_EXISTS
379        // on case-insensitive vendors.
380        if (containsColumnNameByMatcher(inferredColumnNames, columnName)) {
381            return ColumnLevel.EXISTS;
382        }
383
384        // For anonymous UNNEST on a subquery (struct expansion), return MAYBE to allow inference.
385        // The actual column will be inferred in resolveColumn().
386        if (!hasExplicitAlias && arrayExpressionIsSubquery) {
387            return ColumnLevel.MAYBE;
388        }
389
390        // For other UNNEST types, don't auto-infer unknown columns.
391        // Unknown columns should resolve to outer scopes (correlated references)
392        return ColumnLevel.NOT_EXISTS;
393    }
394
395    @Override
396    public ColumnSource resolveColumn(String columnName) {
397        ensureValidated();
398
399        // Check explicit columns (implicit column, offset, struct fields)
400        ColumnSource source = super.resolveColumn(columnName);
401        if (source != null) {
402            return source;
403        }
404
405        // Check inferred columns (added via addInferredColumn or resolveQualifiedStructField).
406        // Slice S1: matcher-aware containment check; lookup in columnSources is
407        // matcher-aware via super.resolveColumn() above.
408        if (containsColumnNameByMatcher(inferredColumnNames, columnName)) {
409            ColumnSource viaMatcher = super.resolveColumn(columnName);
410            if (viaMatcher != null) {
411                return viaMatcher;
412            }
413            return columnSources != null ? columnSources.get(columnName) : null;
414        }
415
416        // For anonymous UNNEST on a subquery (struct expansion), dynamically infer struct fields.
417        // This allows struct field access without qualification when there's no alias.
418        // Example: SELECT field FROM UNNEST((SELECT struct_col FROM table))
419        // Here 'field' is a struct field that should be inferred from the UNNEST.
420        if (!hasExplicitAlias && arrayExpressionIsSubquery) {
421            return resolveQualifiedStructField(columnName);
422        }
423
424        // For other UNNEST types, don't auto-infer unknown columns.
425        // Unknown unqualified columns should resolve to outer scopes or be marked as missed.
426        // For qualified struct field access (e.g., alias.field), use resolveQualifiedStructField().
427        return null;
428    }
429
430    /**
431     * Resolve a qualified struct field reference (e.g., "alias.field").
432     * This method infers struct fields when no metadata is available.
433     *
434     * <p>Use this method when resolving qualified column references like "x.field"
435     * where "x" is the UNNEST alias. This is appropriate for UNNEST of STRUCT arrays
436     * where the struct fields are accessed via the alias.
437     *
438     * <p>For unqualified column references, use {@link #resolveColumn(String)} which
439     * does NOT infer columns and returns null for unknown columns.
440     *
441     * @param columnName the column/field name (without the table qualifier)
442     * @return ColumnSource for the inferred struct field
443     */
444    public ColumnSource resolveQualifiedStructField(String columnName) {
445        if (columnName == null || columnName.isEmpty()) {
446            return null;
447        }
448
449        ensureValidated();
450
451        // Check if already known
452        ColumnSource existing = super.resolveColumn(columnName);
453        if (existing != null) {
454            return existing;
455        }
456
457        // Infer the column as a struct field
458        ColumnSource inferred = new ColumnSource(
459            this,
460            columnName,
461            null,  // No definition node
462            0.8,   // Moderate confidence - inferred from usage
463            "inferred_unnest_struct_field"
464        );
465
466        // Cache it for future lookups
467        if (inferredColumnNames == null) {
468            inferredColumnNames = new HashSet<>();
469        }
470        inferredColumnNames.add(columnName);
471        columnSources.put(columnName, inferred);
472        noteColumnSourceAdded(columnName);
473
474        return inferred;
475    }
476
477    /**
478     * Get the array expression being unnested.
479     * This is used to track the source of the unnested data.
480     */
481    public TExpression getArrayExpression() {
482        return unnestClause != null ? unnestClause.getArrayExpr() : null;
483    }
484
485    /**
486     * Get the implicit column name for the unnested elements.
487     */
488    public String getImplicitColumnName() {
489        return implicitColumnName;
490    }
491
492    /**
493     * Get the offset column name if WITH OFFSET is present.
494     */
495    public String getOffsetColumnName() {
496        return offsetColumnName;
497    }
498
499    public TTable getUnnestTable() {
500        return unnestTable;
501    }
502
503    /**
504     * {@inheritDoc}
505     * For UnnestNamespace, returns the TTable representing the UNNEST expression.
506     * This is the immediate source table for columns resolved through this UNNEST.
507     */
508    @Override
509    public TTable getSourceTable() {
510        return unnestTable;
511    }
512
513    @Override
514    public String toString() {
515        return String.format("UnnestNamespace(%s, implicitCol=%s, offsetCol=%s)",
516            getDisplayName(),
517            implicitColumnName,
518            offsetColumnName);
519    }
520}