Skip to content

Commit f4abe53

Browse files
rootclaude
authored andcommitted
feat: add SchemaCollector.analyze method for non-throwing schema analysis
Added analyze() method to SchemaCollector that returns structured results instead of throwing errors. This allows graceful handling of unresolved columns and validation errors while maintaining backward compatibility. - Added SchemaAnalysisResult interface with success status, schemas, and unresolved columns - Implemented analyze() method that collects errors instead of throwing - Added comprehensive test coverage for the new functionality - Exported new types in index.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 0220408 commit f4abe53

File tree

5 files changed

+208
-6
lines changed

5 files changed

+208
-6
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.22-beta",
3+
"version": "0.11.23-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/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export * from './transformers/UpstreamSelectQueryFinder';
5252
export * from './transformers/TypeTransformationPostProcessor';
5353

5454
export * from './transformers/SchemaCollector';
55+
export { TableSchema, SchemaAnalysisResult } from './transformers/SchemaCollector';
5556
export * from './transformers/QueryFlowDiagramGenerator';
5657
export * from './transformers/SqlParamInjector';
5758
export * from './transformers/SqlSortInjector';

packages/core/src/transformers/SchemaCollector.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ export class TableSchema {
1919
}
2020
}
2121

22+
export interface SchemaAnalysisResult {
23+
success: boolean;
24+
schemas: TableSchema[];
25+
unresolvedColumns: string[];
26+
error?: string;
27+
}
28+
2229
/**
2330
* A visitor that collects schema information (table names and column names) from a SQL query structure.
2431
*/
@@ -29,6 +36,11 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
2936
private visitedNodes: Set<SqlComponent> = new Set();
3037
private commonTables: CommonTable[] = [];
3138
private running = false;
39+
40+
// For analyze method
41+
private unresolvedColumns: string[] = [];
42+
private analysisError: string | undefined = undefined;
43+
private isAnalyzeMode = false;
3244

3345
constructor(
3446
private tableColumnResolver: TableColumnResolver | null = null,
@@ -53,6 +65,34 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
5365
return this.tableSchemas;
5466
}
5567

68+
/**
69+
* Analyzes schema information from a SQL query structure without throwing errors.
70+
* Returns a result object containing successfully resolved schemas, unresolved columns,
71+
* and error information if any issues were encountered.
72+
*
73+
* @param arg The SQL query structure to analyze.
74+
* @returns Analysis result containing schemas, unresolved columns, and success status.
75+
*/
76+
public analyze(arg: SqlComponent): SchemaAnalysisResult {
77+
// Set analyze mode flag
78+
this.isAnalyzeMode = true;
79+
80+
try {
81+
this.visit(arg);
82+
83+
// If we got here without errors, it's a success
84+
return {
85+
success: this.unresolvedColumns.length === 0 && !this.analysisError,
86+
schemas: this.tableSchemas,
87+
unresolvedColumns: this.unresolvedColumns,
88+
error: this.analysisError
89+
};
90+
} finally {
91+
// Reset analyze mode flag
92+
this.isAnalyzeMode = false;
93+
}
94+
}
95+
5696
/**
5797
* Main entry point for the visitor pattern.
5898
* Implements the shallow visit pattern to distinguish between root and recursive visits.
@@ -122,6 +162,8 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
122162
this.tableSchemas = [];
123163
this.visitedNodes = new Set();
124164
this.commonTables = [];
165+
this.unresolvedColumns = [];
166+
this.analysisError = undefined;
125167
}
126168

127169
/**
@@ -235,11 +277,18 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
235277
}));
236278
}
237279

238-
// Throw an error if there are columns without table names in queries with joins
280+
// Handle columns without table names in queries with joins
239281
if (query.fromClause.joins !== null && query.fromClause.joins.length > 0) {
240282
const columnsWithoutTable = queryColumns.filter((columnRef) => columnRef.table === "").map((columnRef) => columnRef.column);
241283
if (columnsWithoutTable.length > 0) {
242-
throw new Error(`Column reference(s) without table name found in query: ${columnsWithoutTable.join(', ')}`);
284+
if (this.isAnalyzeMode) {
285+
// In analyze mode, collect unresolved columns
286+
this.unresolvedColumns.push(...columnsWithoutTable);
287+
this.analysisError = `Column reference(s) without table name found in query: ${columnsWithoutTable.join(', ')}`;
288+
} else {
289+
// In collect mode, throw error as before
290+
throw new Error(`Column reference(s) without table name found in query: ${columnsWithoutTable.join(', ')}`);
291+
}
243292
}
244293
}
245294

@@ -296,12 +345,25 @@ export class SchemaCollector implements SqlComponentVisitor<void> {
296345
.filter((columnRef) => columnRef.column === "*")
297346
.length > 0;
298347

299-
// Throw error if wildcard is found and allowWildcardWithoutResolver is false (default behavior)
348+
// Handle error if wildcard is found and allowWildcardWithoutResolver is false (default behavior)
300349
if (hasWildcard && !this.allowWildcardWithoutResolver) {
301350
const errorMessage = tableName
302351
? `Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards. Target table: ${tableName}`
303352
: "Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards.";
304-
throw new Error(errorMessage);
353+
354+
if (this.isAnalyzeMode) {
355+
// In analyze mode, record the error but continue processing
356+
this.analysisError = errorMessage;
357+
// Add wildcard columns to unresolved list
358+
const wildcardColumns = queryColumns
359+
.filter((columnRef) => columnRef.table === tableAlias || (includeUnnamed && columnRef.table === ""))
360+
.filter((columnRef) => columnRef.column === "*")
361+
.map((columnRef) => columnRef.table ? `${columnRef.table}.*` : "*");
362+
this.unresolvedColumns.push(...wildcardColumns);
363+
} else {
364+
// In collect mode, throw error as before
365+
throw new Error(errorMessage);
366+
}
305367
}
306368
}
307369

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { SelectQueryParser } from '../../src/parsers/SelectQueryParser';
3+
import { SchemaCollector } from '../../src/transformers/SchemaCollector';
4+
5+
describe('SchemaCollector.analyze', () => {
6+
test('should successfully analyze simple SELECT query', () => {
7+
// Arrange
8+
const sql = `SELECT u.id, u.name FROM users as u`;
9+
const query = SelectQueryParser.parse(sql);
10+
const collector = new SchemaCollector();
11+
12+
// Act
13+
const result = collector.analyze(query);
14+
15+
// Assert
16+
expect(result.success).toBe(true);
17+
expect(result.schemas.length).toBe(1);
18+
expect(result.schemas[0].name).toBe('users');
19+
expect(result.schemas[0].columns).toEqual(['id', 'name']);
20+
expect(result.unresolvedColumns).toEqual([]);
21+
expect(result.error).toBeUndefined();
22+
});
23+
24+
test('should detect unresolved columns in JOIN queries', () => {
25+
// Arrange
26+
const sql = `SELECT id, name FROM users u JOIN orders o ON u.id = o.user_id`;
27+
const query = SelectQueryParser.parse(sql);
28+
const collector = new SchemaCollector();
29+
30+
// Act
31+
const result = collector.analyze(query);
32+
33+
// Assert
34+
expect(result.success).toBe(false);
35+
expect(result.unresolvedColumns).toEqual(['id', 'name']);
36+
expect(result.error).toBe('Column reference(s) without table name found in query: id, name');
37+
expect(result.schemas.length).toBe(2); // Still collects table info
38+
});
39+
40+
test('should handle wildcard without resolver', () => {
41+
// Arrange
42+
const sql = `SELECT * FROM users`;
43+
const query = SelectQueryParser.parse(sql);
44+
const collector = new SchemaCollector(); // No resolver, default option
45+
46+
// Act
47+
const result = collector.analyze(query);
48+
49+
// Assert
50+
expect(result.success).toBe(false);
51+
expect(result.unresolvedColumns).toEqual(['*']);
52+
expect(result.error).toBe('Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards. Target table: users');
53+
expect(result.schemas.length).toBe(1); // Still collects table info
54+
});
55+
56+
test('should handle qualified wildcard without resolver', () => {
57+
// Arrange
58+
const sql = `SELECT u.* FROM users as u`;
59+
const query = SelectQueryParser.parse(sql);
60+
const collector = new SchemaCollector(null, false); // Explicitly false
61+
62+
// Act
63+
const result = collector.analyze(query);
64+
65+
// Assert
66+
expect(result.success).toBe(false);
67+
expect(result.unresolvedColumns).toEqual(['u.*']);
68+
expect(result.error).toBe('Wildcard (*) is used. A TableColumnResolver is required to resolve wildcards. Target table: users');
69+
});
70+
71+
test('should handle multiple unresolved columns from different tables', () => {
72+
// Arrange
73+
const sql = `SELECT id, name, order_id FROM users u JOIN orders o ON u.id = o.user_id`;
74+
const query = SelectQueryParser.parse(sql);
75+
const collector = new SchemaCollector();
76+
77+
// Act
78+
const result = collector.analyze(query);
79+
80+
// Assert
81+
expect(result.success).toBe(false);
82+
expect(result.unresolvedColumns).toEqual(['id', 'name', 'order_id']);
83+
expect(result.error).toBe('Column reference(s) without table name found in query: id, name, order_id');
84+
});
85+
86+
test('should successfully analyze query with proper table prefixes', () => {
87+
// Arrange
88+
const sql = `SELECT u.id, u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id`;
89+
const query = SelectQueryParser.parse(sql);
90+
const collector = new SchemaCollector();
91+
92+
// Act
93+
const result = collector.analyze(query);
94+
95+
// Assert
96+
expect(result.success).toBe(true);
97+
expect(result.schemas.length).toBe(2);
98+
expect(result.unresolvedColumns).toEqual([]);
99+
expect(result.error).toBeUndefined();
100+
});
101+
102+
test('should handle wildcards with allowWildcardWithoutResolver option', () => {
103+
// Arrange
104+
const sql = `SELECT * FROM users`;
105+
const query = SelectQueryParser.parse(sql);
106+
const collector = new SchemaCollector(null, true); // allowWildcardWithoutResolver = true
107+
108+
// Act
109+
const result = collector.analyze(query);
110+
111+
// Assert
112+
expect(result.success).toBe(true); // Should succeed with the option enabled
113+
expect(result.schemas.length).toBe(1);
114+
expect(result.schemas[0].name).toBe('users');
115+
expect(result.schemas[0].columns).toEqual([]); // Wildcards are excluded when no resolver
116+
expect(result.unresolvedColumns).toEqual([]);
117+
expect(result.error).toBeUndefined();
118+
});
119+
120+
test('should handle UNION queries', () => {
121+
// Arrange
122+
const sql = `
123+
SELECT u.id, u.name FROM users as u
124+
UNION
125+
SELECT c.id, c.email FROM customers c
126+
`;
127+
const query = SelectQueryParser.parse(sql);
128+
const collector = new SchemaCollector();
129+
130+
// Act
131+
const result = collector.analyze(query);
132+
133+
// Assert
134+
expect(result.success).toBe(true);
135+
expect(result.schemas.length).toBe(2);
136+
expect(result.unresolvedColumns).toEqual([]);
137+
expect(result.error).toBeUndefined();
138+
});
139+
});

0 commit comments

Comments
 (0)