001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType; 005import gudusoft.gsqlparser.util.SQLUtil; 006import gudusoft.gsqlparser.nodes.TTable; 007import gudusoft.gsqlparser.nodes.TColumnDefinition; 008import gudusoft.gsqlparser.nodes.TColumnDefinitionList; 009import gudusoft.gsqlparser.nodes.TExpression; 010import gudusoft.gsqlparser.nodes.TFunctionCall; 011import gudusoft.gsqlparser.nodes.TObjectName; 012import gudusoft.gsqlparser.nodes.TObjectNameList; 013import gudusoft.gsqlparser.resolver2.ColumnLevel; 014import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 015import gudusoft.gsqlparser.resolver2.model.ColumnSource; 016import gudusoft.gsqlparser.resolver2.model.QualifiedName; 017import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver; 018import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 019import gudusoft.gsqlparser.sqlenv.TSQLEnv; 020import gudusoft.gsqlparser.sqlenv.TSQLTable; 021import gudusoft.gsqlparser.sqlenv.TSQLColumn; 022 023import java.util.LinkedHashMap; 024import java.util.Map; 025 026/** 027 * Namespace representing a physical table. 028 * Provides column information from table metadata. 029 * 030 * Column sources come from: 031 * 1. Table metadata (if available from DDL) 032 * 2. External metadata providers (SQLEnv) 033 * 3. Inferred from usage (when no metadata available) 034 */ 035public class TableNamespace extends AbstractNamespace { 036 037 private final TTable table; 038 039 /** TSQLEnv for looking up table metadata */ 040 private TSQLEnv sqlEnv; 041 042 /** Database vendor for qualified name resolution */ 043 private EDbVendor vendor; 044 045 /** Qualified name resolver for normalizing table references */ 046 private QualifiedNameResolver qualifiedNameResolver; 047 048 /** The qualified name of this table (with defaults applied) */ 049 private QualifiedName qualifiedName; 050 051 /** Flag to track if this table has actual metadata (vs. only inferred columns) */ 052 private boolean hasMetadata = false; 053 054 /** The resolved TSQLTable from SQLEnv (if found) */ 055 private TSQLTable resolvedTable; 056 057 public TableNamespace(TTable table, INameMatcher nameMatcher) { 058 super(table, nameMatcher); 059 this.table = table; 060 } 061 062 public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv) { 063 super(table, nameMatcher); 064 this.table = table; 065 this.sqlEnv = sqlEnv; 066 // Default vendor - use setVendor() to override 067 if (sqlEnv != null) { 068 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, EDbVendor.dbvoracle); 069 } 070 } 071 072 /** 073 * Create a TableNamespace with full qualified name resolution support. 074 * 075 * @param table The table AST node 076 * @param nameMatcher The name matcher for case sensitivity 077 * @param sqlEnv The SQL environment for metadata lookup 078 * @param vendor The database vendor 079 */ 080 public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv, EDbVendor vendor) { 081 super(table, nameMatcher); 082 this.table = table; 083 this.sqlEnv = sqlEnv; 084 this.vendor = vendor; 085 if (sqlEnv != null) { 086 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 087 } 088 } 089 090 public TableNamespace(TTable table) { 091 super(table); 092 this.table = table; 093 } 094 095 /** 096 * Set the TSQLEnv for metadata lookup. 097 * @param sqlEnv the SQL environment containing table metadata 098 */ 099 public void setSqlEnv(TSQLEnv sqlEnv) { 100 this.sqlEnv = sqlEnv; 101 if (sqlEnv != null && vendor != null) { 102 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 103 } 104 // Reset cached qualified name so it's recomputed with new env 105 this.qualifiedName = null; 106 } 107 108 /** 109 * Get the TSQLEnv used for metadata lookup. 110 * @return the SQL environment, or null if not set 111 */ 112 public TSQLEnv getSqlEnv() { 113 return sqlEnv; 114 } 115 116 /** 117 * Set the database vendor. 118 * @param vendor the database vendor 119 */ 120 public void setVendor(EDbVendor vendor) { 121 this.vendor = vendor; 122 if (sqlEnv != null) { 123 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 124 } 125 // Reset cached qualified name 126 this.qualifiedName = null; 127 } 128 129 /** 130 * Get the database vendor. 131 * @return the database vendor, or null if not set 132 */ 133 public EDbVendor getVendor() { 134 return vendor; 135 } 136 137 /** 138 * Get the qualified name resolver. 139 * @return the resolver, or null if sqlEnv is not set 140 */ 141 public QualifiedNameResolver getQualifiedNameResolver() { 142 return qualifiedNameResolver; 143 } 144 145 /** 146 * Get the fully qualified name of this table. 147 * 148 * <p>The qualified name is computed by applying defaults from TSQLEnv 149 * to the table's partial name. 150 * 151 * @return the qualified name, or null if table name is unavailable 152 */ 153 public QualifiedName getQualifiedName() { 154 if (qualifiedName != null) { 155 return qualifiedName; 156 } 157 158 TObjectName tableName = table.getTableName(); 159 if (tableName == null) { 160 return null; 161 } 162 163 if (qualifiedNameResolver != null) { 164 qualifiedName = qualifiedNameResolver.resolve(tableName); 165 } else { 166 // Fallback: build from table name parts without defaults 167 String catalog = tableName.getDatabaseString(); 168 String schema = tableName.getSchemaString(); 169 String name = tableName.getObjectString(); 170 if (name == null || name.isEmpty()) { 171 name = tableName.toString(); 172 } 173 if (name != null && !name.isEmpty()) { 174 qualifiedName = QualifiedName.forTable(catalog, schema, name); 175 } 176 } 177 178 return qualifiedName; 179 } 180 181 /** 182 * Get the resolved TSQLTable from SQLEnv. 183 * @return the resolved table, or null if not found in SQLEnv 184 */ 185 public TSQLTable getResolvedTable() { 186 return resolvedTable; 187 } 188 189 @Override 190 public String getDisplayName() { 191 String alias = table.getAliasName(); 192 if (alias != null && !alias.isEmpty()) { 193 return alias; 194 } 195 String fullName = table.getFullName(); 196 if (fullName != null && !fullName.isEmpty()) { 197 return fullName; 198 } 199 // Fallback to simple table name 200 String name = table.getName(); 201 if (name != null && !name.isEmpty()) { 202 return name; 203 } 204 // Last resort: try table name object 205 TObjectName tableName = table.getTableName(); 206 if (tableName != null) { 207 String tableStr = tableName.getTableString(); 208 if (tableStr != null && !tableStr.isEmpty()) { 209 return tableStr; 210 } 211 String tableNameStr = tableName.toString(); 212 if (tableNameStr != null && !tableNameStr.isEmpty()) { 213 return tableNameStr; 214 } 215 } 216 return ""; 217 } 218 219 @Override 220 public TTable getFinalTable() { 221 return table; 222 } 223 224 @Override 225 public TTable getSourceTable() { 226 return table; 227 } 228 229 @Override 230 protected void doValidate() { 231 // Load columns from metadata 232 columnSources = new LinkedHashMap<>(); 233 hasMetadata = false; 234 resolvedTable = null; 235 236 // Priority 1: Check if table has column list (from DDL like CREATE TABLE) 237 TColumnDefinitionList columnDefs = table.getColumnDefinitions(); 238 if (columnDefs != null && columnDefs.size() > 0) { 239 // Has explicit column list from CREATE TABLE or metadata 240 hasMetadata = true; 241 for (int i = 0; i < columnDefs.size(); i++) { 242 TColumnDefinition colDef = columnDefs.getColumn(i); 243 TObjectName colNameObj = colDef.getColumnName(); 244 if (colNameObj == null) continue; 245 246 String colName = colNameObj.toString(); 247 248 ColumnSource source = new ColumnSource( 249 this, 250 colName, 251 colDef, 252 1.0, // Definite - from DDL metadata 253 "ddl_metadata" 254 ); 255 columnSources.put(colName, source); 256 } 257 return; // DDL metadata takes priority 258 } 259 260 // Priority 2: Try to load from TSQLEnv if available 261 if (sqlEnv != null) { 262 TObjectName tableName = table.getTableName(); 263 TSQLTable sqlTable = null; 264 if (tableName != null) { 265 sqlTable = sqlEnv.searchTable(tableName); 266 } 267 268 if (sqlTable != null) { 269 // Found table in SQLEnv - load all columns 270 resolvedTable = sqlTable; 271 hasMetadata = true; 272 273 for (TSQLColumn column : sqlTable.getColumnList()) { 274 String colName = column.getName(); 275 276 ColumnSource source = new ColumnSource( 277 this, 278 colName, 279 null, // No TColumnDefinition node from SQLEnv 280 1.0, // Definite - from SQLEnv metadata 281 "sqlenv_metadata" 282 ); 283 columnSources.put(colName, source); 284 } 285 return; // SQLEnv metadata found 286 } 287 } 288 289 // Priority 3: Table-valued functions. 290 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) { 291 String[] knownColumns = knownTableFunctionColumns(); 292 TObjectNameList aliasColumns = functionAliasColumns(); 293 294 // 3a: an explicit column alias list renames the function's output columns 295 // POSITIONALLY, e.g. FROM TABLE(FLATTEN(input => t.c)) f(s, k). 296 if (aliasColumns != null) { 297 for (int i = 0; i < aliasColumns.size(); i++) { 298 TObjectName colName = aliasColumns.getObjectName(i); 299 if (colName != null) { 300 addKnownFunctionColumn(colName.toString()); 301 } 302 } 303 if (knownColumns != null) { 304 // A partial list renames only the leading columns; the rest keep 305 // their built-in names, so the exposed set is still fully known. 306 for (int i = aliasColumns.size(); i < knownColumns.length; i++) { 307 addKnownFunctionColumn(knownColumns[i]); 308 } 309 hasMetadata = true; 310 } else { 311 // The function's own output schema is unknown, so a partial list 312 // leaves unnamed columns that keep names we cannot enumerate. 313 // Stay non-authoritative: those must remain inferable rather than 314 // being reported as non-existent. 315 hasMetadata = false; 316 } 317 return; 318 } 319 320 // 3b: known table-valued functions with well-defined output columns 321 if (knownColumns != null) { 322 hasMetadata = true; 323 for (String knownColumn : knownColumns) { 324 addKnownFunctionColumn(knownColumn); 325 } 326 return; 327 } 328 } 329 330 // Priority 4: No metadata available 331 // For tables without metadata, we'll handle this as MAYBE in hasColumn 332 // Columns will be inferred from usage in resolveColumn() 333 } 334 335 /** SQL Server STRING_SPLIT output columns (ordinal is SQL Server 2022+). */ 336 private static final String[] STRING_SPLIT_COLUMNS = {"value", "ordinal"}; 337 338 /** 339 * Snowflake FLATTEN output columns. FLATTEN is a built-in table function with a 340 * fixed, documented output schema: 341 * https://docs.snowflake.com/en/sql-reference/functions/flatten 342 */ 343 private static final String[] SNOWFLAKE_FLATTEN_COLUMNS = 344 {"SEQ", "KEY", "PATH", "INDEX", "VALUE", "THIS"}; 345 346 /** 347 * Snowflake SPLIT_TO_TABLE output columns — same fixed-schema shape as FLATTEN: 348 * https://docs.snowflake.com/en/sql-reference/functions/split_to_table 349 */ 350 private static final String[] SNOWFLAKE_SPLIT_TO_TABLE_COLUMNS = {"SEQ", "INDEX", "VALUE"}; 351 352 /** 353 * The explicit column alias list of this table, or null when there is none. 354 * 355 * <p>Snowflake, PostgreSQL and Presto/Trino all allow renaming a table 356 * function's output columns: {@code FROM f(...) alias (c1, c2, ...)}. The list 357 * may be shorter than the function's output — the remaining columns then keep 358 * their original names.</p> 359 */ 360 private TObjectNameList functionAliasColumns() { 361 if (table.getAliasClause() == null) { 362 return null; 363 } 364 TObjectNameList columns = table.getAliasClause().getColumns(); 365 if (columns == null || columns.size() == 0) { 366 return null; 367 } 368 return columns; 369 } 370 371 /** 372 * Output columns of a built-in table-valued function whose schema is fixed by 373 * the vendor, or null when this function has no known schema (the caller then 374 * falls back to inferring columns from usage). 375 * 376 * <p>Only an unqualified function name is matched: a schema-qualified call such 377 * as {@code myschema.flatten(...)} is a user-defined function that merely shares 378 * the name, and must keep the inferring behaviour.</p> 379 */ 380 private String[] knownTableFunctionColumns() { 381 EDbVendor tableVendor = getTableVendor(); 382 383 if (tableVendor == EDbVendor.dbvsnowflake) { 384 // Mantis 4663: an unqualified reference to a FLATTEN output column (VALUE, 385 // SEQ, ...) must resolve to the FLATTEN table rather than being left 386 // ambiguous against the other FROM-clause items. Snowflake spells these 387 // built-ins both as LATERAL f(...) and as TABLE(f(...)), so the TABLE 388 // wrapper is transparent here. 389 String snowflakeName = getUnqualifiedTableFunctionName(true); 390 if (snowflakeName != null && !snowflakeName.isEmpty()) { 391 if (SQLUtil.sameName(tableVendor, ESQLDataObjectType.dotFunction, 392 snowflakeName, "FLATTEN")) { 393 return SNOWFLAKE_FLATTEN_COLUMNS; 394 } 395 if (SQLUtil.sameName(tableVendor, ESQLDataObjectType.dotFunction, 396 snowflakeName, "SPLIT_TO_TABLE")) { 397 return SNOWFLAKE_SPLIT_TO_TABLE_COLUMNS; 398 } 399 } 400 } 401 402 // STRING_SPLIT is matched on the directly named function only. Looking through 403 // a TABLE(...) wrapper here would newly claim SQL Server's schema for a 404 // same-named user-defined table function in another dialect, whose real output 405 // columns would then be reported as non-existent. 406 String directName = getUnqualifiedTableFunctionName(false); 407 if (directName != null && !directName.isEmpty() 408 && SQLUtil.sameName(getTableVendor(), ESQLDataObjectType.dotFunction, 409 directName, "STRING_SPLIT")) { 410 return STRING_SPLIT_COLUMNS; 411 } 412 413 // Add more known table functions here as needed 414 // e.g., OPENJSON, OPENXML, etc. 415 return null; 416 } 417 418 /** 419 * The vendor of the parsed statement this table belongs to. Prefers the vendor 420 * the namespace was constructed with and falls back to the AST node's own 421 * vendor, because {@link gudusoft.gsqlparser.resolver2.ScopeBuilder} builds 422 * table namespaces through constructors that do not carry the vendor. 423 */ 424 private EDbVendor getTableVendor() { 425 if (vendor != null) { 426 return vendor; 427 } 428 return table.dbvendor; 429 } 430 431 /** 432 * Get the function name for table-valued functions, but only when the call is 433 * unqualified. Returns null for a schema/database-qualified call. 434 * 435 * @param lookThroughTableKeyword when true, an explicit {@code TABLE(...)} wrapper 436 * is transparent: Snowflake spells the same table function both ways — 437 * {@code LATERAL FLATTEN(input => c)} and {@code TABLE(FLATTEN(input => c))} 438 * — and both expose the same output columns. 439 */ 440 private String getUnqualifiedTableFunctionName(boolean lookThroughTableKeyword) { 441 if (lookThroughTableKeyword) { 442 TFunctionCall unwrapped = unwrapTableKeywordFunction(table.getFuncCall()); 443 if (unwrapped != null && unwrapped != table.getFuncCall() 444 && unwrapped.getFunctionName() != null) { 445 TObjectName funcName = unwrapped.getFunctionName(); 446 return isQualified(funcName) ? null : funcName.toString(); 447 } 448 } 449 450 TObjectName tableName = table.getTableName(); 451 if (tableName != null) { 452 if (isQualified(tableName)) { 453 return null; 454 } 455 return tableName.toString(); 456 } 457 if (table.getFuncCall() != null && table.getFuncCall().getFunctionName() != null) { 458 TObjectName funcName = table.getFuncCall().getFunctionName(); 459 if (isQualified(funcName)) { 460 return null; 461 } 462 return funcName.toString(); 463 } 464 return table.getName(); 465 } 466 467 /** 468 * Unwrap {@code TABLE(f(...))} to the inner call {@code f(...)}, or return the 469 * call unchanged when it is not such a wrapper. Only a single-argument 470 * {@code TABLE(...)} whose sole argument is itself a function call is unwrapped, 471 * so a genuine user function named TABLE with other arguments is left alone. 472 */ 473 private TFunctionCall unwrapTableKeywordFunction(TFunctionCall funcCall) { 474 if (funcCall == null || funcCall.getFunctionName() == null) { 475 return funcCall; 476 } 477 if (isQualified(funcCall.getFunctionName())) { 478 return funcCall; 479 } 480 if (!SQLUtil.sameName(getTableVendor(), ESQLDataObjectType.dotFunction, 481 funcCall.getFunctionName().toString(), "TABLE")) { 482 return funcCall; 483 } 484 if (funcCall.getArgs() == null || funcCall.getArgs().size() != 1) { 485 return funcCall; 486 } 487 TExpression arg = funcCall.getArgs().getExpression(0); 488 if (arg == null || arg.getFunctionCall() == null) { 489 return funcCall; 490 } 491 return arg.getFunctionCall(); 492 } 493 494 private static boolean isQualified(TObjectName name) { 495 String schema = name.getSchemaString(); 496 if (schema != null && !schema.isEmpty()) { 497 return true; 498 } 499 String database = name.getDatabaseString(); 500 return database != null && !database.isEmpty(); 501 } 502 503 504 /** 505 * Add a known column for a table-valued function. 506 */ 507 private void addKnownFunctionColumn(String columnName) { 508 ColumnSource source = new ColumnSource( 509 this, 510 columnName, 511 null, 512 1.0, // Definite - known function output column 513 "known_function_column" 514 ); 515 columnSources.put(columnName, source); 516 noteColumnSourceAdded(columnName); 517 } 518 519 @Override 520 public ColumnLevel hasColumn(String columnName) { 521 ensureValidated(); 522 523 // Check if column exists in known columns (including inferred ones). 524 // Canonical-key indexed lookup (Mantis 4684): the previous matcher loop 525 // over the inferred-column map made wide select lists O(N²), with a 526 // collator comparison per entry on SQL Server. 527 if (findColumnKeyByMatcher(columnName) != null) { 528 return ColumnLevel.EXISTS; 529 } 530 531 // If we don't have metadata, any column MAYBE exists 532 // (we can't definitively say it doesn't exist) 533 if (!hasMetadata) { 534 return ColumnLevel.MAYBE; 535 } 536 537 return ColumnLevel.NOT_EXISTS; 538 } 539 540 @Override 541 public ColumnSource resolveColumn(String columnName) { 542 ensureValidated(); 543 544 // First try to find in known columns 545 ColumnSource existing = super.resolveColumn(columnName); 546 if (existing != null) { 547 return existing; 548 } 549 550 // If no metadata available, create an inferred ColumnSource 551 // This allows columns to be resolved against tables without metadata 552 // Note: hasMetadata is false when we don't have DDL or SQLEnv metadata 553 if (!hasMetadata && columnName != null && !columnName.isEmpty()) { 554 // Create a column source with moderate confidence 555 // Since we don't have metadata, we're inferring this column exists 556 ColumnSource inferred = new ColumnSource( 557 this, 558 columnName, 559 null, // No definition node 560 0.8, // Moderate confidence - we don't have proof this column exists 561 "inferred_from_usage" 562 ); 563 564 // Cache it for future lookups 565 columnSources.put(columnName, inferred); 566 noteColumnSourceAdded(columnName); 567 568 return inferred; 569 } 570 571 return null; 572 } 573 574 public TTable getTable() { 575 return table; 576 } 577 578 /** 579 * Returns true if this namespace has actual metadata (from DDL or SQLEnv). 580 * When false, column resolution will infer columns from usage. 581 * 582 * @return true if metadata is available, false if columns are inferred 583 */ 584 public boolean hasMetadata() { 585 ensureValidated(); 586 return hasMetadata; 587 } 588 589 /** 590 * Slice S4: authoritative metadata-state tri-value (plan §5.5). 591 * 592 * <p>Distinguishes:</p> 593 * <ul> 594 * <li>{@link MetadataState#FOUND} — DDL columns present, or 595 * {@link TSQLEnv} returned a table with a non-empty column list, or 596 * a known table-valued function (e.g. STRING_SPLIT) populated its 597 * columns.</li> 598 * <li>{@link MetadataState#NOT_FOUND_IN_CATALOG} — a non-empty 599 * {@code TSQLEnv} was consulted but the table itself is absent. 600 * Never used when no environment has been configured.</li> 601 * <li>{@link MetadataState#METADATA_UNAVAILABLE} — no environment, an 602 * empty environment (no catalogs registered), the lookup returned a 603 * table without expanded columns (Q8 view-without-columns), or any 604 * other unauthoritative state.</li> 605 * </ul> 606 */ 607 @Override 608 public MetadataState getMetadataState() { 609 ensureValidated(); 610 611 // Authoritative: DDL columns or TSQLEnv-resolved table with columns. 612 if (hasMetadata && columnSources != null && !columnSources.isEmpty()) { 613 return MetadataState.FOUND; 614 } 615 616 // Found-but-unexpanded: TSQLEnv returned the table but no columns came 617 // through. Q8 view-without-columns falls here. Never NOT_FOUND_IN_CATALOG 618 // and never UNKNOWN_COLUMN — strict mode may emit a WARNING in S6. 619 if (resolvedTable != null) { 620 return MetadataState.METADATA_UNAVAILABLE; 621 } 622 623 // Lookup ran against a populated environment and missed entirely. The 624 // env has at least one catalog registered, so we trust the absence as 625 // an authoritative table-not-found signal. Bridges pre-register an 626 // empty default catalog/schema during construction (see 627 // CatalogRuntimeToSQLEnvBridge.applyDefaults), which preserves this 628 // behavior under lazy materialization. 629 if (sqlEnv != null && hasAnyCatalogContent(sqlEnv)) { 630 return MetadataState.NOT_FOUND_IN_CATALOG; 631 } 632 633 return MetadataState.METADATA_UNAVAILABLE; 634 } 635 636 private static boolean hasAnyCatalogContent(TSQLEnv env) { 637 if (env == null) { 638 return false; 639 } 640 java.util.List<TSQLCatalog> catalogs = env.getCatalogList(); 641 return catalogs != null && !catalogs.isEmpty(); 642 } 643 644 /** 645 * Add a USING clause column to this table's namespace. 646 * This is called during scope building when a JOIN...USING clause 647 * includes columns from this table. The USING column exists in BOTH 648 * tables of the join. 649 * 650 * @param columnName the name of the USING column 651 */ 652 public void addUsingColumn(String columnName) { 653 ensureValidated(); 654 655 // Only add if not already present 656 if (hasColumn(columnName) == ColumnLevel.EXISTS) { 657 return; 658 } 659 660 ColumnSource source = new ColumnSource( 661 this, 662 columnName, 663 null, // No definition node 664 1.0, // Definite - USING clause semantics guarantee the column exists 665 "using_clause_column" 666 ); 667 columnSources.put(columnName, source); 668 noteColumnSourceAdded(columnName); 669 } 670 671 @Override 672 public String toString() { 673 return "TableNamespace(" + getDisplayName() + ", columns=" + 674 (columnSources != null ? columnSources.size() : "?") + ")"; 675 } 676}