Skip to content

Commit 5ddb9fe

Browse files
committed
test(generic): add more sql test
- Add comprehensive syntax tests for all supported statement types - Add context collect tests for entity and semantic collectors - Add suggestion tests for token, syntax, and multi-statement scenarios - Add error strategy, listener, visitor, and validation tests - Fix entity collector to distinguish simple columns from expressions
1 parent c7c9ebc commit 5ddb9fe

41 files changed

Lines changed: 2492 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/parser/generic/genericEntityCollector.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class GenericEntityCollector extends EntityCollector implements GenericSq
7070
declareType: ColumnDeclareType.ALL,
7171
});
7272
} else {
73+
const isSimpleColumn = this.isSimpleColumnReference(ctx);
7374
this.pushEntity(
7475
ctx,
7576
EntityContextType.COLUMN,
@@ -80,12 +81,27 @@ export class GenericEntityCollector extends EntityCollector implements GenericSq
8081
},
8182
],
8283
{
83-
declareType: ColumnDeclareType.EXPRESSION,
84+
declareType: isSimpleColumn
85+
? ColumnDeclareType.LITERAL
86+
: ColumnDeclareType.EXPRESSION,
8487
}
8588
);
8689
}
8790
}
8891

92+
private isSimpleColumnReference(ctx: SelectItemContext): boolean {
93+
const text = ctx.getText();
94+
// Simple column reference: no parentheses, no operators, no spaces
95+
return (
96+
!text.includes('(') &&
97+
!text.includes(' ') &&
98+
!text.includes('+') &&
99+
!text.includes('-') &&
100+
!text.includes('*') &&
101+
!text.includes('/')
102+
);
103+
}
104+
89105
/** ===== Statement begin */
90106

91107
enterSingleStatement(ctx: SingleStatementContext) {
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { ParseTreeListener } from 'antlr4ng';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import { GenericSqlListener } from 'src/lib/generic/GenericSqlListener';
5+
import {
6+
ColumnDeclareType,
7+
CommonEntityContext,
8+
isCommonEntityContext,
9+
StmtContextType,
10+
} from 'src/parser/common/entityCollector';
11+
import { EntityContextType } from 'src/parser/common/types';
12+
import { GenericEntityCollector, GenericSQL, GenericSplitListener } from 'src/parser/generic';
13+
14+
const commonSql = fs.readFileSync(path.join(__dirname, 'fixtures', 'common.sql'), 'utf-8');
15+
16+
describe('GenericSQL entity collector tests', () => {
17+
const parser = new GenericSQL();
18+
const parseTree = parser.parse(commonSql);
19+
const splitListener = new GenericSplitListener();
20+
parser.listen(splitListener as GenericSqlListener, parseTree);
21+
22+
test('validate common sql', () => {
23+
expect(parser.validate(commonSql).length).toBe(0);
24+
});
25+
26+
test('split results', () => {
27+
expect(splitListener.statementsContext.length).toBe(10);
28+
});
29+
30+
test('create table with columns', () => {
31+
const testingContext = splitListener.statementsContext[0];
32+
33+
const collectListener = new GenericEntityCollector(commonSql);
34+
parser.listen(collectListener as ParseTreeListener, testingContext);
35+
36+
const allEntities = collectListener.getEntities();
37+
38+
expect(allEntities.length).toBe(1);
39+
40+
const tableCreateEntity = allEntities[0];
41+
42+
expect(tableCreateEntity.entityContextType).toBe(EntityContextType.TABLE_CREATE);
43+
expect(tableCreateEntity.text).toBe('orders');
44+
expect(tableCreateEntity.belongStmt.stmtContextType).toBe(
45+
StmtContextType.CREATE_TABLE_STMT
46+
);
47+
48+
if (isCommonEntityContext(tableCreateEntity)) {
49+
expect(tableCreateEntity.columns?.length).toBe(2);
50+
// GenericSQL column text includes data type
51+
expect(tableCreateEntity.columns[0].text).toContain('orderkey');
52+
expect(tableCreateEntity.columns[1].text).toContain('orderstatus');
53+
expect(tableCreateEntity.relatedEntities).toBeNull();
54+
}
55+
});
56+
57+
test('select from table', () => {
58+
const testingContext = splitListener.statementsContext[1];
59+
60+
const collectListener = new GenericEntityCollector(commonSql);
61+
parser.listen(collectListener as ParseTreeListener, testingContext);
62+
63+
const allEntities = collectListener.getEntities();
64+
65+
expect(allEntities.length).toBe(2);
66+
67+
const tableEntity = allEntities[0];
68+
69+
expect(tableEntity.entityContextType).toBe(EntityContextType.TABLE);
70+
expect(tableEntity.text).toBe('table1');
71+
expect(tableEntity.belongStmt.stmtContextType).toBe(StmtContextType.SELECT_STMT);
72+
73+
if (isCommonEntityContext(tableEntity)) {
74+
expect(tableEntity.columns).toBeUndefined();
75+
expect(tableEntity.relatedEntities).toBeNull();
76+
}
77+
});
78+
79+
test('insert into table as select', () => {
80+
const testingContext = splitListener.statementsContext[2];
81+
82+
const collectListener = new GenericEntityCollector(commonSql);
83+
parser.listen(collectListener as ParseTreeListener, testingContext);
84+
85+
const allEntities = collectListener.getEntities();
86+
87+
expect(allEntities.length).toBe(3);
88+
89+
const fromTableEntity = allEntities[0];
90+
const queryResultEntity = allEntities[1];
91+
const insertTableEntity = allEntities[2];
92+
93+
expect(insertTableEntity.entityContextType).toBe(EntityContextType.TABLE);
94+
expect(insertTableEntity.belongStmt.stmtContextType).toBe(StmtContextType.INSERT_STMT);
95+
expect(insertTableEntity.text).toBe('orders');
96+
97+
expect(queryResultEntity.entityContextType).toBe(EntityContextType.QUERY_RESULT);
98+
99+
expect(fromTableEntity.entityContextType).toBe(EntityContextType.TABLE);
100+
expect(fromTableEntity.belongStmt.stmtContextType).toBe(StmtContextType.SELECT_STMT);
101+
expect(fromTableEntity.text).toBe('new_orders');
102+
expect(fromTableEntity.belongStmt.parentStmt).toBe(insertTableEntity.belongStmt);
103+
});
104+
105+
test('select using alias', () => {
106+
const testingContext = splitListener.statementsContext[4];
107+
108+
const collectListener = new GenericEntityCollector(commonSql);
109+
parser.listen(collectListener as ParseTreeListener, testingContext);
110+
111+
const allEntities = collectListener.getEntities();
112+
113+
expect(allEntities.length).toBe(2);
114+
115+
const tableEntity = allEntities[0];
116+
117+
expect(tableEntity.entityContextType).toBe(EntityContextType.TABLE);
118+
expect(tableEntity.text).toBe('tb');
119+
expect(tableEntity.belongStmt.stmtContextType).toBe(StmtContextType.SELECT_STMT);
120+
121+
if (isCommonEntityContext(tableEntity)) {
122+
expect(tableEntity.columns).toBeUndefined();
123+
expect(tableEntity.relatedEntities).toBeNull();
124+
}
125+
});
126+
127+
test('should collect query result and columns', () => {
128+
const context = splitListener.statementsContext[5];
129+
130+
const collectListener = new GenericEntityCollector(commonSql);
131+
parser.listen(collectListener as ParseTreeListener, context);
132+
133+
const allEntities = collectListener.getEntities();
134+
const queryResult = allEntities.find(
135+
(e) => e.entityContextType === EntityContextType.QUERY_RESULT
136+
) as CommonEntityContext;
137+
138+
expect(queryResult).toBeDefined();
139+
expect(queryResult.relatedEntities?.length).toBe(1);
140+
expect(queryResult.relatedEntities?.[0].text).toBe('t1');
141+
142+
const columns = queryResult.columns;
143+
expect(columns?.length).toBe(3);
144+
expect(columns[0].text).toBe('id');
145+
expect(columns[0].declareType).toBe(ColumnDeclareType.LITERAL);
146+
// GenericSQL entity text includes alias when present
147+
expect(columns[1].text).toContain('age');
148+
expect(columns[1].declareType).toBe(ColumnDeclareType.LITERAL);
149+
});
150+
151+
test('should collect columns with multiple star symbol', () => {
152+
const context = splitListener.statementsContext[6];
153+
154+
const collectListener = new GenericEntityCollector(commonSql);
155+
parser.listen(collectListener as ParseTreeListener, context);
156+
157+
const allEntities = collectListener.getEntities();
158+
const queryResult = allEntities.find(
159+
(e) => e.entityContextType === EntityContextType.QUERY_RESULT
160+
) as CommonEntityContext;
161+
162+
expect(queryResult).toBeDefined();
163+
expect(queryResult.columns?.length).toBe(2);
164+
expect(queryResult.columns[0].declareType).toBe(ColumnDeclareType.ALL);
165+
expect(queryResult.columns[1].declareType).toBe(ColumnDeclareType.ALL);
166+
});
167+
168+
test('should collect columns with single star symbol', () => {
169+
const context = splitListener.statementsContext[7];
170+
171+
const collectListener = new GenericEntityCollector(commonSql);
172+
parser.listen(collectListener as ParseTreeListener, context);
173+
174+
const allEntities = collectListener.getEntities();
175+
const queryResult = allEntities.find(
176+
(e) => e.entityContextType === EntityContextType.QUERY_RESULT
177+
) as CommonEntityContext;
178+
179+
expect(queryResult).toBeDefined();
180+
expect(queryResult.columns?.length).toBe(1);
181+
expect(queryResult.columns[0].declareType).toBe(ColumnDeclareType.ALL);
182+
});
183+
184+
test('should collect derived table and derived column', () => {
185+
const context = splitListener.statementsContext[8];
186+
187+
const collectListener = new GenericEntityCollector(commonSql);
188+
parser.listen(collectListener as ParseTreeListener, context);
189+
190+
const allEntities = collectListener.getEntities();
191+
const tableEntities = allEntities.filter(
192+
(entity) => entity.entityContextType === EntityContextType.TABLE
193+
) as CommonEntityContext[];
194+
195+
// GenericSQL may not collect subquery-derived tables the same way as Trino
196+
expect(tableEntities.length).toBeGreaterThanOrEqual(2);
197+
198+
const queryResults = allEntities.filter(
199+
(entity) => entity.entityContextType === EntityContextType.QUERY_RESULT
200+
) as CommonEntityContext[];
201+
expect(queryResults.length).toBeGreaterThanOrEqual(1);
202+
});
203+
204+
test('should collect query result in where clause', () => {
205+
const context = splitListener.statementsContext[9];
206+
207+
const collectListener = new GenericEntityCollector(commonSql);
208+
parser.listen(collectListener as ParseTreeListener, context);
209+
210+
const allEntities = collectListener.getEntities();
211+
const queryResults = allEntities.filter(
212+
(e) => e.entityContextType === EntityContextType.QUERY_RESULT
213+
) as CommonEntityContext[];
214+
215+
expect(queryResults.length).toBe(2);
216+
});
217+
218+
test('table entities are accessible when caret is in outer query', () => {
219+
const sql = `SELECT id FROM t1, (SELECT name from t2) as t3`;
220+
221+
const entities = parser.getAllEntities(sql, {
222+
lineNumber: 1,
223+
column: 13,
224+
});
225+
226+
const accessibleTables = entities.filter(
227+
(e) => e.entityContextType === EntityContextType.TABLE && e.isAccessible
228+
);
229+
expect(accessibleTables.length).toBeGreaterThanOrEqual(1);
230+
231+
const t1 = accessibleTables.find((e) => e.text === 't1');
232+
expect(t1).toBeDefined();
233+
expect(t1?.isAccessible).toBeTruthy();
234+
});
235+
236+
test('table entities are not accessible when caret is in inner query', () => {
237+
const sql = `SELECT id FROM t1, (SELECT name from t2) as t3`;
238+
239+
const entities = parser.getAllEntities(sql, {
240+
lineNumber: 1,
241+
column: 29,
242+
});
243+
244+
const tables = entities.filter((e) => e.entityContextType === EntityContextType.TABLE);
245+
246+
const t2 = tables.find((e) => e.text === 't2');
247+
248+
expect(t2).toBeDefined();
249+
expect(t2?.isAccessible).toBeTruthy();
250+
});
251+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
CREATE TABLE orders (
2+
orderkey bigint,
3+
orderstatus varchar
4+
);
5+
6+
SELECT * FROM table1 GROUP BY a;
7+
8+
INSERT INTO orders SELECT * FROM new_orders;
9+
10+
INSERT INTO cities SELECT id, name FROM source_cities;
11+
12+
SELECT id AS col1, name AS col2 FROM tb AS tb_alias;
13+
14+
select id, age as new_age, sum(amount) as total from t1;
15+
16+
select t1.*, t2.* from t1, t2;
17+
18+
select * from t1;
19+
20+
select id, (select max(age) from t3) as max_age from (select id, name from t1) as derived_table, t2;
21+
22+
select id from a1 where name in (select name from b1);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
CREA
2+
3+
CREATE
4+
5+
CREATE TABLE a A
6+
7+
CREATE TABLE a AS
8+
9+
INSERT
10+
11+
SELECT id FROM t1;
12+
CRE
13+
14+
SELECT id FROM t2;
15+
16+
CREATE TABLE IF NOT EXISTS a1(id INT, )
17+
SEL
18+
19+
CREATE; SEL
20+
21+
CREATE TABLE a1(id INT);
22+
SEL
23+
INSERT INTO t1 VALUES(1);
24+
25+
CREATE TABLE a1(id INT);
26+
CREATE TABLE
27+
INSERT INTO t1 VALUES(1);
28+
29+
CREATE TABLE a1(id INT)
30+
CREATE TABLE

0 commit comments

Comments
 (0)