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