001package gudusoft.gsqlparser.resolver2.namespace; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.nodes.TTable; 005import gudusoft.gsqlparser.nodes.TColumnDefinition; 006import gudusoft.gsqlparser.nodes.TColumnDefinitionList; 007import gudusoft.gsqlparser.nodes.TObjectName; 008import gudusoft.gsqlparser.resolver2.ColumnLevel; 009import gudusoft.gsqlparser.resolver2.matcher.INameMatcher; 010import gudusoft.gsqlparser.resolver2.model.ColumnSource; 011import gudusoft.gsqlparser.resolver2.model.QualifiedName; 012import gudusoft.gsqlparser.resolver2.model.QualifiedNameResolver; 013import gudusoft.gsqlparser.sqlenv.TSQLCatalog; 014import gudusoft.gsqlparser.sqlenv.TSQLEnv; 015import gudusoft.gsqlparser.sqlenv.TSQLTable; 016import gudusoft.gsqlparser.sqlenv.TSQLColumn; 017 018import java.util.LinkedHashMap; 019import java.util.Map; 020 021/** 022 * Namespace representing a physical table. 023 * Provides column information from table metadata. 024 * 025 * Column sources come from: 026 * 1. Table metadata (if available from DDL) 027 * 2. External metadata providers (SQLEnv) 028 * 3. Inferred from usage (when no metadata available) 029 */ 030public class TableNamespace extends AbstractNamespace { 031 032 private final TTable table; 033 034 /** TSQLEnv for looking up table metadata */ 035 private TSQLEnv sqlEnv; 036 037 /** Database vendor for qualified name resolution */ 038 private EDbVendor vendor; 039 040 /** Qualified name resolver for normalizing table references */ 041 private QualifiedNameResolver qualifiedNameResolver; 042 043 /** The qualified name of this table (with defaults applied) */ 044 private QualifiedName qualifiedName; 045 046 /** Flag to track if this table has actual metadata (vs. only inferred columns) */ 047 private boolean hasMetadata = false; 048 049 /** The resolved TSQLTable from SQLEnv (if found) */ 050 private TSQLTable resolvedTable; 051 052 public TableNamespace(TTable table, INameMatcher nameMatcher) { 053 super(table, nameMatcher); 054 this.table = table; 055 } 056 057 public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv) { 058 super(table, nameMatcher); 059 this.table = table; 060 this.sqlEnv = sqlEnv; 061 // Default vendor - use setVendor() to override 062 if (sqlEnv != null) { 063 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, EDbVendor.dbvoracle); 064 } 065 } 066 067 /** 068 * Create a TableNamespace with full qualified name resolution support. 069 * 070 * @param table The table AST node 071 * @param nameMatcher The name matcher for case sensitivity 072 * @param sqlEnv The SQL environment for metadata lookup 073 * @param vendor The database vendor 074 */ 075 public TableNamespace(TTable table, INameMatcher nameMatcher, TSQLEnv sqlEnv, EDbVendor vendor) { 076 super(table, nameMatcher); 077 this.table = table; 078 this.sqlEnv = sqlEnv; 079 this.vendor = vendor; 080 if (sqlEnv != null) { 081 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 082 } 083 } 084 085 public TableNamespace(TTable table) { 086 super(table); 087 this.table = table; 088 } 089 090 /** 091 * Set the TSQLEnv for metadata lookup. 092 * @param sqlEnv the SQL environment containing table metadata 093 */ 094 public void setSqlEnv(TSQLEnv sqlEnv) { 095 this.sqlEnv = sqlEnv; 096 if (sqlEnv != null && vendor != null) { 097 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 098 } 099 // Reset cached qualified name so it's recomputed with new env 100 this.qualifiedName = null; 101 } 102 103 /** 104 * Get the TSQLEnv used for metadata lookup. 105 * @return the SQL environment, or null if not set 106 */ 107 public TSQLEnv getSqlEnv() { 108 return sqlEnv; 109 } 110 111 /** 112 * Set the database vendor. 113 * @param vendor the database vendor 114 */ 115 public void setVendor(EDbVendor vendor) { 116 this.vendor = vendor; 117 if (sqlEnv != null) { 118 this.qualifiedNameResolver = new QualifiedNameResolver(sqlEnv, vendor); 119 } 120 // Reset cached qualified name 121 this.qualifiedName = null; 122 } 123 124 /** 125 * Get the database vendor. 126 * @return the database vendor, or null if not set 127 */ 128 public EDbVendor getVendor() { 129 return vendor; 130 } 131 132 /** 133 * Get the qualified name resolver. 134 * @return the resolver, or null if sqlEnv is not set 135 */ 136 public QualifiedNameResolver getQualifiedNameResolver() { 137 return qualifiedNameResolver; 138 } 139 140 /** 141 * Get the fully qualified name of this table. 142 * 143 * <p>The qualified name is computed by applying defaults from TSQLEnv 144 * to the table's partial name. 145 * 146 * @return the qualified name, or null if table name is unavailable 147 */ 148 public QualifiedName getQualifiedName() { 149 if (qualifiedName != null) { 150 return qualifiedName; 151 } 152 153 TObjectName tableName = table.getTableName(); 154 if (tableName == null) { 155 return null; 156 } 157 158 if (qualifiedNameResolver != null) { 159 qualifiedName = qualifiedNameResolver.resolve(tableName); 160 } else { 161 // Fallback: build from table name parts without defaults 162 String catalog = tableName.getDatabaseString(); 163 String schema = tableName.getSchemaString(); 164 String name = tableName.getObjectString(); 165 if (name == null || name.isEmpty()) { 166 name = tableName.toString(); 167 } 168 if (name != null && !name.isEmpty()) { 169 qualifiedName = QualifiedName.forTable(catalog, schema, name); 170 } 171 } 172 173 return qualifiedName; 174 } 175 176 /** 177 * Get the resolved TSQLTable from SQLEnv. 178 * @return the resolved table, or null if not found in SQLEnv 179 */ 180 public TSQLTable getResolvedTable() { 181 return resolvedTable; 182 } 183 184 @Override 185 public String getDisplayName() { 186 String alias = table.getAliasName(); 187 if (alias != null && !alias.isEmpty()) { 188 return alias; 189 } 190 String fullName = table.getFullName(); 191 if (fullName != null && !fullName.isEmpty()) { 192 return fullName; 193 } 194 // Fallback to simple table name 195 String name = table.getName(); 196 if (name != null && !name.isEmpty()) { 197 return name; 198 } 199 // Last resort: try table name object 200 TObjectName tableName = table.getTableName(); 201 if (tableName != null) { 202 String tableStr = tableName.getTableString(); 203 if (tableStr != null && !tableStr.isEmpty()) { 204 return tableStr; 205 } 206 String tableNameStr = tableName.toString(); 207 if (tableNameStr != null && !tableNameStr.isEmpty()) { 208 return tableNameStr; 209 } 210 } 211 return ""; 212 } 213 214 @Override 215 public TTable getFinalTable() { 216 return table; 217 } 218 219 @Override 220 public TTable getSourceTable() { 221 return table; 222 } 223 224 @Override 225 protected void doValidate() { 226 // Load columns from metadata 227 columnSources = new LinkedHashMap<>(); 228 hasMetadata = false; 229 resolvedTable = null; 230 231 // Priority 1: Check if table has column list (from DDL like CREATE TABLE) 232 TColumnDefinitionList columnDefs = table.getColumnDefinitions(); 233 if (columnDefs != null && columnDefs.size() > 0) { 234 // Has explicit column list from CREATE TABLE or metadata 235 hasMetadata = true; 236 for (int i = 0; i < columnDefs.size(); i++) { 237 TColumnDefinition colDef = columnDefs.getColumn(i); 238 TObjectName colNameObj = colDef.getColumnName(); 239 if (colNameObj == null) continue; 240 241 String colName = colNameObj.toString(); 242 243 ColumnSource source = new ColumnSource( 244 this, 245 colName, 246 colDef, 247 1.0, // Definite - from DDL metadata 248 "ddl_metadata" 249 ); 250 columnSources.put(colName, source); 251 } 252 return; // DDL metadata takes priority 253 } 254 255 // Priority 2: Try to load from TSQLEnv if available 256 if (sqlEnv != null) { 257 TObjectName tableName = table.getTableName(); 258 TSQLTable sqlTable = null; 259 if (tableName != null) { 260 sqlTable = sqlEnv.searchTable(tableName); 261 } 262 263 if (sqlTable != null) { 264 // Found table in SQLEnv - load all columns 265 resolvedTable = sqlTable; 266 hasMetadata = true; 267 268 for (TSQLColumn column : sqlTable.getColumnList()) { 269 String colName = column.getName(); 270 271 ColumnSource source = new ColumnSource( 272 this, 273 colName, 274 null, // No TColumnDefinition node from SQLEnv 275 1.0, // Definite - from SQLEnv metadata 276 "sqlenv_metadata" 277 ); 278 columnSources.put(colName, source); 279 } 280 return; // SQLEnv metadata found 281 } 282 } 283 284 // Priority 3: Check for known table-valued functions with well-defined output columns 285 if (table.getTableType() == gudusoft.gsqlparser.ETableSource.function) { 286 String funcName = getTableFunctionName(); 287 if (funcName != null) { 288 String upperName = funcName.toUpperCase(); 289 if (upperName.equals("STRING_SPLIT")) { 290 // STRING_SPLIT returns: value, ordinal (ordinal is SQL Server 2022+) 291 hasMetadata = true; 292 addKnownFunctionColumn("value"); 293 addKnownFunctionColumn("ordinal"); 294 return; 295 } 296 // Add more known table functions here as needed 297 // e.g., OPENJSON, OPENXML, etc. 298 } 299 } 300 301 // Priority 4: No metadata available 302 // For tables without metadata, we'll handle this as MAYBE in hasColumn 303 // Columns will be inferred from usage in resolveColumn() 304 } 305 306 /** 307 * Get the function name for table-valued functions. 308 */ 309 private String getTableFunctionName() { 310 if (table.getTableName() != null) { 311 return table.getTableName().toString(); 312 } 313 if (table.getFuncCall() != null && table.getFuncCall().getFunctionName() != null) { 314 return table.getFuncCall().getFunctionName().toString(); 315 } 316 return table.getName(); 317 } 318 319 /** 320 * Add a known column for a table-valued function. 321 */ 322 private void addKnownFunctionColumn(String columnName) { 323 ColumnSource source = new ColumnSource( 324 this, 325 columnName, 326 null, 327 1.0, // Definite - known function output column 328 "known_function_column" 329 ); 330 columnSources.put(columnName, source); 331 } 332 333 @Override 334 public ColumnLevel hasColumn(String columnName) { 335 ensureValidated(); 336 337 // Check if column exists in known columns (including inferred ones) 338 for (String existingCol : columnSources.keySet()) { 339 if (nameMatcher.matches(existingCol, columnName)) { 340 return ColumnLevel.EXISTS; 341 } 342 } 343 344 // If we don't have metadata, any column MAYBE exists 345 // (we can't definitively say it doesn't exist) 346 if (!hasMetadata) { 347 return ColumnLevel.MAYBE; 348 } 349 350 return ColumnLevel.NOT_EXISTS; 351 } 352 353 @Override 354 public ColumnSource resolveColumn(String columnName) { 355 ensureValidated(); 356 357 // First try to find in known columns 358 ColumnSource existing = super.resolveColumn(columnName); 359 if (existing != null) { 360 return existing; 361 } 362 363 // If no metadata available, create an inferred ColumnSource 364 // This allows columns to be resolved against tables without metadata 365 // Note: hasMetadata is false when we don't have DDL or SQLEnv metadata 366 if (!hasMetadata && columnName != null && !columnName.isEmpty()) { 367 // Create a column source with moderate confidence 368 // Since we don't have metadata, we're inferring this column exists 369 ColumnSource inferred = new ColumnSource( 370 this, 371 columnName, 372 null, // No definition node 373 0.8, // Moderate confidence - we don't have proof this column exists 374 "inferred_from_usage" 375 ); 376 377 // Cache it for future lookups 378 columnSources.put(columnName, inferred); 379 380 return inferred; 381 } 382 383 return null; 384 } 385 386 public TTable getTable() { 387 return table; 388 } 389 390 /** 391 * Returns true if this namespace has actual metadata (from DDL or SQLEnv). 392 * When false, column resolution will infer columns from usage. 393 * 394 * @return true if metadata is available, false if columns are inferred 395 */ 396 public boolean hasMetadata() { 397 ensureValidated(); 398 return hasMetadata; 399 } 400 401 /** 402 * Slice S4: authoritative metadata-state tri-value (plan §5.5). 403 * 404 * <p>Distinguishes:</p> 405 * <ul> 406 * <li>{@link MetadataState#FOUND} — DDL columns present, or 407 * {@link TSQLEnv} returned a table with a non-empty column list, or 408 * a known table-valued function (e.g. STRING_SPLIT) populated its 409 * columns.</li> 410 * <li>{@link MetadataState#NOT_FOUND_IN_CATALOG} — a non-empty 411 * {@code TSQLEnv} was consulted but the table itself is absent. 412 * Never used when no environment has been configured.</li> 413 * <li>{@link MetadataState#METADATA_UNAVAILABLE} — no environment, an 414 * empty environment (no catalogs registered), the lookup returned a 415 * table without expanded columns (Q8 view-without-columns), or any 416 * other unauthoritative state.</li> 417 * </ul> 418 */ 419 @Override 420 public MetadataState getMetadataState() { 421 ensureValidated(); 422 423 // Authoritative: DDL columns or TSQLEnv-resolved table with columns. 424 if (hasMetadata && columnSources != null && !columnSources.isEmpty()) { 425 return MetadataState.FOUND; 426 } 427 428 // Found-but-unexpanded: TSQLEnv returned the table but no columns came 429 // through. Q8 view-without-columns falls here. Never NOT_FOUND_IN_CATALOG 430 // and never UNKNOWN_COLUMN — strict mode may emit a WARNING in S6. 431 if (resolvedTable != null) { 432 return MetadataState.METADATA_UNAVAILABLE; 433 } 434 435 // Lookup ran against a populated environment and missed entirely. The 436 // env has at least one catalog registered, so we trust the absence as 437 // an authoritative table-not-found signal. Bridges pre-register an 438 // empty default catalog/schema during construction (see 439 // CatalogRuntimeToSQLEnvBridge.applyDefaults), which preserves this 440 // behavior under lazy materialization. 441 if (sqlEnv != null && hasAnyCatalogContent(sqlEnv)) { 442 return MetadataState.NOT_FOUND_IN_CATALOG; 443 } 444 445 return MetadataState.METADATA_UNAVAILABLE; 446 } 447 448 private static boolean hasAnyCatalogContent(TSQLEnv env) { 449 if (env == null) { 450 return false; 451 } 452 java.util.List<TSQLCatalog> catalogs = env.getCatalogList(); 453 return catalogs != null && !catalogs.isEmpty(); 454 } 455 456 /** 457 * Add a USING clause column to this table's namespace. 458 * This is called during scope building when a JOIN...USING clause 459 * includes columns from this table. The USING column exists in BOTH 460 * tables of the join. 461 * 462 * @param columnName the name of the USING column 463 */ 464 public void addUsingColumn(String columnName) { 465 ensureValidated(); 466 467 // Only add if not already present 468 if (hasColumn(columnName) == ColumnLevel.EXISTS) { 469 return; 470 } 471 472 ColumnSource source = new ColumnSource( 473 this, 474 columnName, 475 null, // No definition node 476 1.0, // Definite - USING clause semantics guarantee the column exists 477 "using_clause_column" 478 ); 479 columnSources.put(columnName, source); 480 } 481 482 @Override 483 public String toString() { 484 return "TableNamespace(" + getDisplayName() + ", columns=" + 485 (columnSources != null ? columnSources.size() : "?") + ")"; 486 } 487}