Skip to content

Commit 7f944e1

Browse files
authored
Merge pull request #186 from mk3008/feat_static_analysis
fix: implement missing SchemaCollector methods and enhance schema collection
2 parents 5ea922e + a5da37d commit 7f944e1

File tree

6 files changed

+543
-22
lines changed

6 files changed

+543
-22
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rawsql-ts",
3-
"version": "0.11.27-beta",
3+
"version": "0.11.28-beta",
44
"description": "[beta]High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.",
55
"main": "dist/src/index.js",
66
"module": "dist/esm/index.js",

packages/core/src/transformers/SchemaCollector.ts

Lines changed: 151 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { SqlComponent, SqlComponentVisitor } from '../models/SqlComponent';
2-
import { CommonTable, SubQuerySource, TableSource, SelectClause, SelectItem, FromClause } from '../models/Clause';
2+
import { CommonTable, SubQuerySource, TableSource } from '../models/Clause';
33
import { SimpleSelectQuery } from '../models/SimpleSelectQuery';
44
import { CTECollector } from './CTECollector';
55
import { SelectableColumnCollector, DuplicateDetectionMode } from './SelectableColumnCollector';
6-
import { SelectValueCollector } from './SelectValueCollector';
76
import { ColumnReference, ValueComponent } from '../models/ValueComponent';
8-
import { BinarySelectQuery, SelectQuery } from '../models/SelectQuery';
7+
import { BinarySelectQuery } from '../models/SelectQuery';
98
import { SourceExpression } from '../models/Clause';
109
import { 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
}

packages/core/tests/transformers/CTEComposer.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,8 @@ describe("CTEComposer", () => {
228228
// Arrange
229229
const schema = {
230230
users: ["id", "name", "email", "active"],
231-
orders: ["id", "user_id", "total"]
231+
orders: ["id", "user_id", "total"],
232+
active_users: ["id", "name"] // CTE schema
232233
};
233234
const validatingComposer = new CTEComposer({
234235
validateSchema: true,

0 commit comments

Comments
 (0)