Skip to content

Commit 8606ab5

Browse files
Ajit Pratap Singhclaude
authored andcommitted
test: add integration tests for error propagation chain
Add comprehensive integration tests that verify: - Error codes propagate correctly from tokenizer to parser - Error code extraction works with IsCode() helper - Error location information is preserved through parsing pipeline Tests cover various SQL error scenarios: - Unterminated string literals (tokenizer error) - Incomplete SQL statements - Missing table names (INSERT INTO VALUES) - Unexpected token usage (SELECT FROM users) - Multiline error location tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent dd893d9 commit 8606ab5

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

pkg/errors/integration_test.go

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package errors_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ajitpratap0/GoSQLX/pkg/errors"
7+
"github.com/ajitpratap0/GoSQLX/pkg/sql/parser"
8+
"github.com/ajitpratap0/GoSQLX/pkg/sql/tokenizer"
9+
)
10+
11+
// TestErrorPropagation_TokenizerToParser tests that errors from the tokenizer
12+
// propagate correctly with error codes intact through the parsing pipeline.
13+
func TestErrorPropagation_TokenizerToParser(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
sql string
17+
expectedCode errors.ErrorCode
18+
expectedInMsg string
19+
checkTokenizer bool // if true, we expect tokenizer to catch the error
20+
}{
21+
{
22+
name: "unterminated string literal",
23+
sql: "SELECT * FROM users WHERE name = 'unterminated",
24+
expectedCode: errors.ErrCodeUnterminatedString,
25+
expectedInMsg: "unterminated",
26+
},
27+
{
28+
name: "unexpected token in SELECT",
29+
sql: "SELECT * FROM",
30+
expectedCode: errors.ErrCodeExpectedToken, // Expects table name after FROM
31+
expectedInMsg: "expected",
32+
},
33+
{
34+
name: "incomplete SQL statement",
35+
sql: "",
36+
expectedCode: errors.ErrCodeIncompleteStatement,
37+
expectedInMsg: "incomplete",
38+
},
39+
{
40+
name: "invalid syntax - missing table name",
41+
sql: "INSERT INTO VALUES (1, 2)",
42+
expectedCode: errors.ErrCodeExpectedToken, // Parser expects table name after INSERT INTO
43+
expectedInMsg: "expected",
44+
},
45+
{
46+
name: "unexpected keyword usage",
47+
sql: "SELECT FROM users",
48+
expectedCode: errors.ErrCodeUnexpectedToken,
49+
expectedInMsg: "unexpected",
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
// Get tokenizer from pool
56+
tkz := tokenizer.GetTokenizer()
57+
defer tokenizer.PutTokenizer(tkz)
58+
59+
// Tokenize the input
60+
tokens, tokenErr := tkz.Tokenize([]byte(tt.sql))
61+
62+
// If tokenizer caught an error with a code, verify it
63+
if tokenErr != nil {
64+
if err, ok := tokenErr.(*errors.Error); ok {
65+
if err.Code != "" {
66+
t.Logf("Tokenizer caught error with code: %s", err.Code)
67+
// Verify the error code is what we expected
68+
if tt.checkTokenizer && err.Code != tt.expectedCode {
69+
t.Errorf("Tokenizer error code mismatch: expected %s, got %s", tt.expectedCode, err.Code)
70+
}
71+
}
72+
}
73+
return // Don't continue to parser if tokenizer failed
74+
}
75+
76+
// Convert tokens and parse
77+
parserTokens, _ := parser.ConvertTokensForParser(tokens)
78+
p := parser.NewParser()
79+
_, parseErr := p.Parse(parserTokens)
80+
81+
// Verify parser error
82+
if parseErr == nil {
83+
t.Errorf("Expected parse error for SQL: %s", tt.sql)
84+
return
85+
}
86+
87+
// Check if error matches expected code using IsCode
88+
if !errors.IsCode(parseErr, tt.expectedCode) {
89+
// Check if error is structured
90+
if err, ok := parseErr.(*errors.Error); ok {
91+
t.Errorf("Error code mismatch: expected %s, got %s", tt.expectedCode, err.Code)
92+
} else {
93+
t.Logf("Parse error is not structured error type: %T - %v", parseErr, parseErr)
94+
}
95+
}
96+
97+
// Verify error message contains expected text
98+
if tt.expectedInMsg != "" {
99+
errMsg := parseErr.Error()
100+
found := false
101+
for i := 0; i <= len(errMsg)-len(tt.expectedInMsg); i++ {
102+
if errMsg[i:i+len(tt.expectedInMsg)] == tt.expectedInMsg {
103+
found = true
104+
break
105+
}
106+
}
107+
if !found {
108+
t.Errorf("Error message should contain '%s', got: %s", tt.expectedInMsg, errMsg)
109+
}
110+
}
111+
112+
t.Logf("Error propagated correctly: %v", parseErr)
113+
})
114+
}
115+
}
116+
117+
// TestErrorCodeExtraction tests that error codes can be reliably extracted
118+
// from errors returned by the parser using the IsCode helper.
119+
func TestErrorCodeExtraction(t *testing.T) {
120+
testCases := []struct {
121+
name string
122+
sql string
123+
expectedCode errors.ErrorCode
124+
}{
125+
{
126+
name: "unexpected token after SELECT",
127+
sql: "SELECT *** FROM users",
128+
expectedCode: errors.ErrCodeExpectedToken, // Parser expects FROM, semicolon, or end of statement
129+
},
130+
{
131+
name: "missing FROM clause",
132+
sql: "SELECT * users",
133+
expectedCode: errors.ErrCodeExpectedToken, // Parser expects FROM keyword
134+
},
135+
{
136+
name: "invalid WHERE clause",
137+
sql: "SELECT * FROM users WHERE",
138+
expectedCode: errors.ErrCodeUnexpectedToken, // Unexpected EOF after WHERE
139+
},
140+
}
141+
142+
for _, tc := range testCases {
143+
t.Run(tc.name, func(t *testing.T) {
144+
// Tokenize
145+
tkz := tokenizer.GetTokenizer()
146+
defer tokenizer.PutTokenizer(tkz)
147+
148+
tokens, err := tkz.Tokenize([]byte(tc.sql))
149+
if err != nil {
150+
t.Skipf("Tokenizer error: %v", err)
151+
}
152+
153+
// Parse
154+
parserTokens, _ := parser.ConvertTokensForParser(tokens)
155+
p := parser.NewParser()
156+
_, parseErr := p.Parse(parserTokens)
157+
158+
if parseErr == nil {
159+
t.Fatalf("Expected error for SQL: %s", tc.sql)
160+
}
161+
162+
// Check error code using IsCode
163+
if !errors.IsCode(parseErr, tc.expectedCode) {
164+
if structErr, ok := parseErr.(*errors.Error); ok {
165+
t.Errorf("IsCode returned false for code %s, error has code %s", tc.expectedCode, structErr.Code)
166+
} else {
167+
t.Errorf("Error is not structured, cannot extract code: %v", parseErr)
168+
}
169+
}
170+
171+
t.Logf("Successfully verified error code %s matches error: %v", tc.expectedCode, parseErr)
172+
})
173+
}
174+
}
175+
176+
// TestErrorLocationPropagation tests that error location information
177+
// propagates correctly from tokenizer through parser.
178+
func TestErrorLocationPropagation(t *testing.T) {
179+
testCases := []struct {
180+
name string
181+
sql string
182+
expectLine int
183+
expectMinCol int
184+
checkLocation bool
185+
}{
186+
{
187+
name: "error on first line",
188+
sql: "SELECT *** FROM users",
189+
expectLine: 1,
190+
expectMinCol: 7,
191+
checkLocation: true,
192+
},
193+
{
194+
name: "error on second line",
195+
sql: "SELECT *\nFROM",
196+
expectLine: 2,
197+
expectMinCol: 1,
198+
checkLocation: true,
199+
},
200+
{
201+
name: "multiline with error",
202+
sql: "SELECT *\nFROM users\nWHERE",
203+
expectLine: 3,
204+
expectMinCol: 1,
205+
checkLocation: true,
206+
},
207+
}
208+
209+
for _, tc := range testCases {
210+
t.Run(tc.name, func(t *testing.T) {
211+
// Tokenize
212+
tkz := tokenizer.GetTokenizer()
213+
defer tokenizer.PutTokenizer(tkz)
214+
215+
tokens, err := tkz.Tokenize([]byte(tc.sql))
216+
if err != nil {
217+
t.Skipf("Tokenizer error: %v", err)
218+
}
219+
220+
// Parse
221+
parserTokens, _ := parser.ConvertTokensForParser(tokens)
222+
p := parser.NewParser()
223+
_, parseErr := p.Parse(parserTokens)
224+
225+
if parseErr == nil {
226+
t.Fatalf("Expected error for SQL: %s", tc.sql)
227+
}
228+
229+
// Check location if expected
230+
if tc.checkLocation {
231+
parsedErr, ok := parseErr.(*errors.Error)
232+
if ok && parsedErr.Location.Line > 0 {
233+
if parsedErr.Location.Line != tc.expectLine {
234+
t.Errorf("Expected line %d, got %d", tc.expectLine, parsedErr.Location.Line)
235+
}
236+
if parsedErr.Location.Column < tc.expectMinCol {
237+
t.Errorf("Expected column >= %d, got %d", tc.expectMinCol, parsedErr.Location.Column)
238+
}
239+
t.Logf("Location propagated correctly: line=%d, column=%d",
240+
parsedErr.Location.Line, parsedErr.Location.Column)
241+
}
242+
}
243+
})
244+
}
245+
}

0 commit comments

Comments
 (0)