@@ -16,6 +16,103 @@ export interface ScanResult {
16
16
tokens : ScanToken [ ] ;
17
17
}
18
18
19
+ export interface SqlErrorDetails {
20
+ message : string ;
21
+ cursorPosition : number ;
22
+ fileName ?: string ;
23
+ functionName ?: string ;
24
+ lineNumber ?: number ;
25
+ context ?: string ;
26
+ }
27
+
28
+ export class SqlError extends Error {
29
+ sqlDetails ?: SqlErrorDetails ;
30
+
31
+ constructor ( message : string , details ?: SqlErrorDetails ) {
32
+ super ( message ) ;
33
+ this . name = 'SqlError' ;
34
+ this . sqlDetails = details ;
35
+ }
36
+ }
37
+
38
+ export function hasSqlDetails ( error : unknown ) : error is SqlError {
39
+ return error instanceof SqlError && error . sqlDetails !== undefined ;
40
+ }
41
+
42
+ export function formatSqlError (
43
+ error : SqlError ,
44
+ query : string ,
45
+ options : {
46
+ showPosition ?: boolean ;
47
+ showQuery ?: boolean ;
48
+ color ?: boolean ;
49
+ maxQueryLength ?: number ;
50
+ } = { }
51
+ ) : string {
52
+ const {
53
+ showPosition = true ,
54
+ showQuery = true ,
55
+ color = false ,
56
+ maxQueryLength
57
+ } = options ;
58
+
59
+ const lines : string [ ] = [ ] ;
60
+
61
+ // ANSI color codes
62
+ const red = color ? '\x1b[31m' : '' ;
63
+ const yellow = color ? '\x1b[33m' : '' ;
64
+ const reset = color ? '\x1b[0m' : '' ;
65
+
66
+ // Add error message
67
+ lines . push ( `${ red } Error: ${ error . message } ${ reset } ` ) ;
68
+
69
+ // Add SQL details if available
70
+ if ( error . sqlDetails ) {
71
+ const { cursorPosition, fileName, functionName, lineNumber } = error . sqlDetails ;
72
+
73
+ if ( cursorPosition !== undefined && cursorPosition >= 0 ) {
74
+ lines . push ( `Position: ${ cursorPosition } ` ) ;
75
+ }
76
+
77
+ if ( fileName || functionName || lineNumber ) {
78
+ const details = [ ] ;
79
+ if ( fileName ) details . push ( `file: ${ fileName } ` ) ;
80
+ if ( functionName ) details . push ( `function: ${ functionName } ` ) ;
81
+ if ( lineNumber ) details . push ( `line: ${ lineNumber } ` ) ;
82
+ lines . push ( `Source: ${ details . join ( ', ' ) } ` ) ;
83
+ }
84
+
85
+ // Show query with position marker
86
+ if ( showQuery && showPosition && cursorPosition !== undefined && cursorPosition >= 0 ) {
87
+ let displayQuery = query ;
88
+ let adjustedPosition = cursorPosition ;
89
+
90
+ // Truncate if needed
91
+ if ( maxQueryLength && query . length > maxQueryLength ) {
92
+ const start = Math . max ( 0 , cursorPosition - Math . floor ( maxQueryLength / 2 ) ) ;
93
+ const end = Math . min ( query . length , start + maxQueryLength ) ;
94
+ displayQuery = ( start > 0 ? '...' : '' ) +
95
+ query . substring ( start , end ) +
96
+ ( end < query . length ? '...' : '' ) ;
97
+ // Adjust cursor position for truncation
98
+ adjustedPosition = cursorPosition - start + ( start > 0 ? 3 : 0 ) ;
99
+ }
100
+
101
+ lines . push ( displayQuery ) ;
102
+ lines . push ( ' ' . repeat ( adjustedPosition ) + `${ yellow } ^${ reset } ` ) ;
103
+ }
104
+ } else if ( showQuery ) {
105
+ // No SQL details, just show the query if requested
106
+ let displayQuery = query ;
107
+ if ( maxQueryLength && query . length > maxQueryLength ) {
108
+ displayQuery = query . substring ( 0 , maxQueryLength ) + '...' ;
109
+ }
110
+ lines . push ( `Query: ${ displayQuery } ` ) ;
111
+ }
112
+
113
+ return lines . join ( '\n' ) ;
114
+ }
115
+
19
116
// @ts -ignore
20
117
import PgQueryModule from './libpg-query.js' ;
21
118
// @ts -ignore
@@ -26,6 +123,8 @@ interface WasmModule {
26
123
_free : ( ptr : number ) => void ;
27
124
_wasm_free_string : ( ptr : number ) => void ;
28
125
_wasm_parse_query : ( queryPtr : number ) => number ;
126
+ _wasm_parse_query_raw : ( queryPtr : number ) => number ;
127
+ _wasm_free_parse_result : ( ptr : number ) => void ;
29
128
_wasm_deparse_protobuf : ( dataPtr : number , length : number ) => number ;
30
129
_wasm_parse_plpgsql : ( queryPtr : number ) => number ;
31
130
_wasm_fingerprint : ( queryPtr : number ) => number ;
@@ -34,6 +133,7 @@ interface WasmModule {
34
133
lengthBytesUTF8 : ( str : string ) => number ;
35
134
stringToUTF8 : ( str : string , ptr : number , len : number ) => void ;
36
135
UTF8ToString : ( ptr : number ) => string ;
136
+ getValue : ( ptr : number , type : string ) => number ;
37
137
HEAPU8 : Uint8Array ;
38
138
}
39
139
@@ -85,22 +185,60 @@ function ptrToString(ptr: number): string {
85
185
}
86
186
87
187
export const parse = awaitInit ( async ( query : string ) : Promise < ParseResult > => {
188
+ // Input validation
189
+ if ( query === null || query === undefined ) {
190
+ throw new Error ( 'Query cannot be null or undefined' ) ;
191
+ }
192
+
193
+ if ( query === '' ) {
194
+ throw new Error ( 'Query cannot be empty' ) ;
195
+ }
196
+
88
197
const queryPtr = stringToPtr ( query ) ;
89
198
let resultPtr = 0 ;
90
199
91
200
try {
92
- resultPtr = wasmModule . _wasm_parse_query ( queryPtr ) ;
93
- const resultStr = ptrToString ( resultPtr ) ;
201
+ resultPtr = wasmModule . _wasm_parse_query_raw ( queryPtr ) ;
202
+ if ( ! resultPtr ) {
203
+ throw new Error ( 'Failed to parse query: memory allocation failed' ) ;
204
+ }
94
205
95
- if ( resultStr . startsWith ( 'syntax error' ) || resultStr . startsWith ( 'deparse error' ) || resultStr . startsWith ( 'ERROR' ) ) {
96
- throw new Error ( resultStr ) ;
206
+ // Read the PgQueryParseResult struct
207
+ const parseTreePtr = wasmModule . getValue ( resultPtr , 'i32' ) ;
208
+ const stderrBufferPtr = wasmModule . getValue ( resultPtr + 4 , 'i32' ) ;
209
+ const errorPtr = wasmModule . getValue ( resultPtr + 8 , 'i32' ) ;
210
+
211
+ if ( errorPtr ) {
212
+ // Read PgQueryError struct
213
+ const messagePtr = wasmModule . getValue ( errorPtr , 'i32' ) ;
214
+ const funcnamePtr = wasmModule . getValue ( errorPtr + 4 , 'i32' ) ;
215
+ const filenamePtr = wasmModule . getValue ( errorPtr + 8 , 'i32' ) ;
216
+ const lineno = wasmModule . getValue ( errorPtr + 12 , 'i32' ) ;
217
+ const cursorpos = wasmModule . getValue ( errorPtr + 16 , 'i32' ) ;
218
+
219
+ const message = messagePtr ? wasmModule . UTF8ToString ( messagePtr ) : 'Unknown error' ;
220
+ const funcname = funcnamePtr ? wasmModule . UTF8ToString ( funcnamePtr ) : undefined ;
221
+ const filename = filenamePtr ? wasmModule . UTF8ToString ( filenamePtr ) : undefined ;
222
+
223
+ throw new SqlError ( message , {
224
+ message,
225
+ cursorPosition : cursorpos > 0 ? cursorpos - 1 : 0 , // Convert to 0-based
226
+ fileName : filename ,
227
+ functionName : funcname ,
228
+ lineNumber : lineno > 0 ? lineno : undefined
229
+ } ) ;
97
230
}
98
231
99
- return JSON . parse ( resultStr ) ;
232
+ if ( ! parseTreePtr ) {
233
+ throw new Error ( 'No parse tree generated' ) ;
234
+ }
235
+
236
+ const parseTreeStr = wasmModule . UTF8ToString ( parseTreePtr ) ;
237
+ return JSON . parse ( parseTreeStr ) ;
100
238
} finally {
101
239
wasmModule . _free ( queryPtr ) ;
102
240
if ( resultPtr ) {
103
- wasmModule . _wasm_free_string ( resultPtr ) ;
241
+ wasmModule . _wasm_free_parse_result ( resultPtr ) ;
104
242
}
105
243
}
106
244
} ) ;
@@ -202,22 +340,61 @@ export function parseSync(query: string): ParseResult {
202
340
if ( ! wasmModule ) {
203
341
throw new Error ( 'WASM module not initialized. Call loadModule() first.' ) ;
204
342
}
343
+
344
+ // Input validation
345
+ if ( query === null || query === undefined ) {
346
+ throw new Error ( 'Query cannot be null or undefined' ) ;
347
+ }
348
+
349
+ if ( query === '' ) {
350
+ throw new Error ( 'Query cannot be empty' ) ;
351
+ }
352
+
205
353
const queryPtr = stringToPtr ( query ) ;
206
354
let resultPtr = 0 ;
207
355
208
356
try {
209
- resultPtr = wasmModule . _wasm_parse_query ( queryPtr ) ;
210
- const resultStr = ptrToString ( resultPtr ) ;
357
+ resultPtr = wasmModule . _wasm_parse_query_raw ( queryPtr ) ;
358
+ if ( ! resultPtr ) {
359
+ throw new Error ( 'Failed to parse query: memory allocation failed' ) ;
360
+ }
211
361
212
- if ( resultStr . startsWith ( 'syntax error' ) || resultStr . startsWith ( 'deparse error' ) || resultStr . startsWith ( 'ERROR' ) ) {
213
- throw new Error ( resultStr ) ;
362
+ // Read the PgQueryParseResult struct
363
+ const parseTreePtr = wasmModule . getValue ( resultPtr , 'i32' ) ;
364
+ const stderrBufferPtr = wasmModule . getValue ( resultPtr + 4 , 'i32' ) ;
365
+ const errorPtr = wasmModule . getValue ( resultPtr + 8 , 'i32' ) ;
366
+
367
+ if ( errorPtr ) {
368
+ // Read PgQueryError struct
369
+ const messagePtr = wasmModule . getValue ( errorPtr , 'i32' ) ;
370
+ const funcnamePtr = wasmModule . getValue ( errorPtr + 4 , 'i32' ) ;
371
+ const filenamePtr = wasmModule . getValue ( errorPtr + 8 , 'i32' ) ;
372
+ const lineno = wasmModule . getValue ( errorPtr + 12 , 'i32' ) ;
373
+ const cursorpos = wasmModule . getValue ( errorPtr + 16 , 'i32' ) ;
374
+
375
+ const message = messagePtr ? wasmModule . UTF8ToString ( messagePtr ) : 'Unknown error' ;
376
+ const funcname = funcnamePtr ? wasmModule . UTF8ToString ( funcnamePtr ) : undefined ;
377
+ const filename = filenamePtr ? wasmModule . UTF8ToString ( filenamePtr ) : undefined ;
378
+
379
+ throw new SqlError ( message , {
380
+ message,
381
+ cursorPosition : cursorpos > 0 ? cursorpos - 1 : 0 , // Convert to 0-based
382
+ fileName : filename ,
383
+ functionName : funcname ,
384
+ lineNumber : lineno > 0 ? lineno : undefined
385
+ } ) ;
214
386
}
215
387
216
- return JSON . parse ( resultStr ) ;
388
+ if ( ! parseTreePtr ) {
389
+ throw new Error ( 'No parse tree generated' ) ;
390
+ }
391
+
392
+ const parseTreeStr = wasmModule . UTF8ToString ( parseTreePtr ) ;
393
+ return JSON . parse ( parseTreeStr ) ;
217
394
} finally {
218
395
wasmModule . _free ( queryPtr ) ;
219
396
if ( resultPtr ) {
220
- wasmModule . _wasm_free_string ( resultPtr ) ;
397
+ wasmModule . _wasm_free_parse_result ( resultPtr ) ;
221
398
}
222
399
}
223
400
}
0 commit comments