11import { SqlComponent , SqlComponentVisitor } from '../models/SqlComponent' ;
2- import { CommonTable , SubQuerySource , TableSource , SelectClause , SelectItem , FromClause } from '../models/Clause' ;
2+ import { CommonTable , SubQuerySource , TableSource } from '../models/Clause' ;
33import { SimpleSelectQuery } from '../models/SimpleSelectQuery' ;
44import { CTECollector } from './CTECollector' ;
55import { SelectableColumnCollector , DuplicateDetectionMode } from './SelectableColumnCollector' ;
6- import { SelectValueCollector } from './SelectValueCollector' ;
76import { ColumnReference , ValueComponent } from '../models/ValueComponent' ;
8- import { BinarySelectQuery , SelectQuery } from '../models/SelectQuery' ;
7+ import { BinarySelectQuery } from '../models/SelectQuery' ;
98import { SourceExpression } from '../models/Clause' ;
109import { TableColumnResolver } from './TableColumnResolver' ;
1110
@@ -192,18 +191,30 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
192191 } ) ;
193192 }
194193
195- private handleTableSource ( source : SourceExpression , queryColumns : { table : string , column : string } [ ] , includeUnnamed : boolean ) : void {
194+ private handleSourceExpression ( source : SourceExpression , queryColumns : { table : string , column : string } [ ] , includeUnnamed : boolean ) : void {
196195 if ( source . datasource instanceof TableSource ) {
197196 const tableName = source . datasource . getSourceName ( ) ;
198197 const cte = this . commonTables . filter ( ( table ) => table . getSourceAliasName ( ) === tableName ) ;
199198 if ( cte . length > 0 ) {
199+ // Process the CTE query recursively
200200 cte [ 0 ] . query . accept ( this ) ;
201+
202+ // Also collect schema information for the CTE itself
203+ const cteAlias = source . getAliasName ( ) ?? tableName ;
204+ this . processCTETableSchema ( cte [ 0 ] , cteAlias , queryColumns , includeUnnamed ) ;
201205 } else {
202206 const tableAlias = source . getAliasName ( ) ?? tableName ;
203207 this . processCollectTableSchema ( tableName , tableAlias , queryColumns , includeUnnamed ) ;
204208 }
209+ } else if ( source . datasource instanceof SubQuerySource ) {
210+ // Process subqueries recursively
211+ this . visitNode ( source . datasource . query ) ;
212+
213+ // For subqueries, we don't add schema information directly as they're derived
214+ // The schema will be collected from the inner query
205215 } else {
206- throw new Error ( "Datasource is not an instance of TableSource" ) ;
216+ // For other source types (FunctionSource, ParenSource), we skip schema collection
217+ // as they don't represent table schemas in the traditional sense
207218 }
208219 }
209220
@@ -294,7 +305,7 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
294305
295306 // Handle the main FROM clause table
296307 if ( query . fromClause . source . datasource instanceof TableSource ) {
297- this . handleTableSource ( query . fromClause . source , queryColumns , true ) ;
308+ this . handleSourceExpression ( query . fromClause . source , queryColumns , true ) ;
298309 } else if ( query . fromClause . source . datasource instanceof SubQuerySource ) {
299310 query . fromClause . source . datasource . query . accept ( this ) ;
300311 }
@@ -303,7 +314,7 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
303314 if ( query . fromClause ?. joins ) {
304315 for ( const join of query . fromClause . joins ) {
305316 if ( join . source . datasource instanceof TableSource ) {
306- this . handleTableSource ( join . source , queryColumns , false ) ;
317+ this . handleSourceExpression ( join . source , queryColumns , false ) ;
307318 } else if ( join . source . datasource instanceof SubQuerySource ) {
308319 join . source . datasource . query . accept ( this ) ;
309320 }
@@ -337,6 +348,7 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
337348 return selectColumns ;
338349 }
339350
351+
340352 private processCollectTableSchema ( tableName : string , tableAlias : string , queryColumns : { table : string , column : string } [ ] , includeUnnamed : boolean = false ) : void {
341353 // Check if wildcard is present and handle based on configuration
342354 if ( this . tableColumnResolver === null ) {
@@ -375,4 +387,136 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
375387 const tableSchema = new TableSchema ( tableName , tableColumns ) ;
376388 this . tableSchemas . push ( tableSchema ) ;
377389 }
390+
391+ private processCTETableSchema ( cte : CommonTable , cteAlias : string , queryColumns : { table : string , column : string } [ ] , includeUnnamed : boolean = false ) : void {
392+ const cteName = cte . getSourceAliasName ( ) ;
393+
394+ // Get the columns that the CTE exposes by analyzing its SELECT clause
395+ const cteColumns = this . getCTEColumns ( cte ) ;
396+
397+ // Filter query columns that reference this CTE
398+ const cteReferencedColumns = queryColumns
399+ . filter ( ( columnRef ) => columnRef . table === cteAlias || ( includeUnnamed && columnRef . table === "" ) )
400+ . map ( ( columnRef ) => columnRef . column ) ;
401+
402+ // Handle wildcards for CTEs
403+ if ( cteReferencedColumns . includes ( "*" ) ) {
404+ if ( this . tableColumnResolver !== null ) {
405+ // Try to resolve columns using the resolver first
406+ const resolvedColumns = this . tableColumnResolver ( cteName ) ;
407+ if ( resolvedColumns . length > 0 ) {
408+ const tableSchema = new TableSchema ( cteName , resolvedColumns ) ;
409+ this . tableSchemas . push ( tableSchema ) ;
410+ return ;
411+ }
412+ }
413+
414+ // If we can determine CTE columns, use them for wildcard expansion
415+ if ( cteColumns . length > 0 ) {
416+ const tableSchema = new TableSchema ( cteName , cteColumns ) ;
417+ this . tableSchemas . push ( tableSchema ) ;
418+ return ;
419+ } else if ( this . allowWildcardWithoutResolver ) {
420+ // Allow wildcards but with empty columns since we can't determine them
421+ const tableSchema = new TableSchema ( cteName , [ ] ) ;
422+ this . tableSchemas . push ( tableSchema ) ;
423+ return ;
424+ } else {
425+ // Handle wildcard error
426+ const errorMessage = `Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards. Target table: ${ cteName } ` ;
427+ if ( this . isAnalyzeMode ) {
428+ this . analysisError = errorMessage ;
429+ this . unresolvedColumns . push ( cteAlias ? `${ cteAlias } .*` : "*" ) ;
430+ } else {
431+ throw new Error ( errorMessage ) ;
432+ }
433+ return ;
434+ }
435+ }
436+
437+ // Process specific column references
438+ let tableColumns = cteReferencedColumns . filter ( ( column ) => column !== "*" ) ;
439+
440+ // Validate column references against CTE columns in analyze mode
441+ if ( this . isAnalyzeMode ) {
442+ let availableColumns = cteColumns ;
443+
444+ // Try to get columns from resolver first if available
445+ if ( this . tableColumnResolver ) {
446+ const resolvedColumns = this . tableColumnResolver ( cteName ) ;
447+ if ( resolvedColumns . length > 0 ) {
448+ availableColumns = resolvedColumns ;
449+ }
450+ }
451+
452+ const invalidColumns = tableColumns . filter ( ( column ) => ! availableColumns . includes ( column ) ) ;
453+ if ( invalidColumns . length > 0 ) {
454+ this . unresolvedColumns . push ( ...invalidColumns ) ;
455+ if ( ! this . analysisError ) {
456+ this . analysisError = `Undefined column(s) found in CTE "${ cteName } ": ${ invalidColumns . join ( ', ' ) } ` ;
457+ }
458+ }
459+ }
460+
461+ // Add the CTE schema
462+ const tableSchema = new TableSchema ( cteName , tableColumns ) ;
463+ this . tableSchemas . push ( tableSchema ) ;
464+ }
465+
466+ private getCTEColumns ( cte : CommonTable ) : string [ ] {
467+ try {
468+ // Try to get select items from the CTE query
469+ if ( cte . query instanceof SimpleSelectQuery && cte . query . selectClause ) {
470+ const selectItems = cte . query . selectClause . items ;
471+ const columns : string [ ] = [ ] ;
472+
473+ for ( const item of selectItems ) {
474+ if ( item . value instanceof ColumnReference ) {
475+ const columnName = item . identifier ?. name || item . value . column . name ;
476+ if ( item . value . column . name === "*" ) {
477+ // For wildcards in CTE definitions, we need special handling
478+ const tableNamespace = item . value . getNamespace ( ) ;
479+ if ( tableNamespace ) {
480+ // Try to find the referenced CTE or table
481+ const referencedCTE = this . commonTables . find ( cte => cte . getSourceAliasName ( ) === tableNamespace ) ;
482+ if ( referencedCTE ) {
483+ // Recursively get columns from the referenced CTE
484+ const referencedColumns = this . getCTEColumns ( referencedCTE ) ;
485+ if ( referencedColumns . length > 0 ) {
486+ columns . push ( ...referencedColumns ) ;
487+ continue ;
488+ }
489+ }
490+ }
491+ // If we can't resolve the wildcard, we mark this CTE as having unknown columns
492+ // This will be handled by the wildcard processing logic later
493+ return [ ] ;
494+ } else {
495+ columns . push ( columnName ) ;
496+ }
497+ } else {
498+ // For expressions, functions, etc., use the identifier if available
499+ if ( item . identifier ) {
500+ columns . push ( item . identifier . name ) ;
501+ }
502+ }
503+ }
504+
505+ return columns . filter ( ( name , index , array ) => array . indexOf ( name ) === index ) ; // Remove duplicates
506+ }
507+
508+ // Fallback: try using SelectableColumnCollector
509+ const columnCollector = new SelectableColumnCollector ( null , true , DuplicateDetectionMode . FullName ) ;
510+ const columns = columnCollector . collect ( cte . query ) ;
511+
512+ return columns
513+ . filter ( ( column ) => column . value instanceof ColumnReference )
514+ . map ( column => column . value as ColumnReference )
515+ . map ( columnRef => columnRef . column . name )
516+ . filter ( ( name , index , array ) => array . indexOf ( name ) === index ) ; // Remove duplicates
517+ } catch ( error ) {
518+ // If we can't determine the columns, return empty array
519+ return [ ] ;
520+ }
521+ }
378522}
0 commit comments