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}