@@ -16,6 +16,103 @@ export interface ScanResult {
1616 tokens : ScanToken [ ] ;
1717}
1818
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+
19116// @ts -ignore
20117import PgQueryModule from './libpg-query.js' ;
21118// @ts -ignore
@@ -26,6 +123,8 @@ interface WasmModule {
26123 _free : ( ptr : number ) => void ;
27124 _wasm_free_string : ( ptr : number ) => void ;
28125 _wasm_parse_query : ( queryPtr : number ) => number ;
126+ _wasm_parse_query_raw : ( queryPtr : number ) => number ;
127+ _wasm_free_parse_result : ( ptr : number ) => void ;
29128 _wasm_deparse_protobuf : ( dataPtr : number , length : number ) => number ;
30129 _wasm_parse_plpgsql : ( queryPtr : number ) => number ;
31130 _wasm_fingerprint : ( queryPtr : number ) => number ;
@@ -34,6 +133,7 @@ interface WasmModule {
34133 lengthBytesUTF8 : ( str : string ) => number ;
35134 stringToUTF8 : ( str : string , ptr : number , len : number ) => void ;
36135 UTF8ToString : ( ptr : number ) => string ;
136+ getValue : ( ptr : number , type : string ) => number ;
37137 HEAPU8 : Uint8Array ;
38138}
39139
@@ -85,22 +185,60 @@ function ptrToString(ptr: number): string {
85185}
86186
87187export 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+
88197 const queryPtr = stringToPtr ( query ) ;
89198 let resultPtr = 0 ;
90199
91200 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+ }
94205
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+ } ) ;
97230 }
98231
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 ) ;
100238 } finally {
101239 wasmModule . _free ( queryPtr ) ;
102240 if ( resultPtr ) {
103- wasmModule . _wasm_free_string ( resultPtr ) ;
241+ wasmModule . _wasm_free_parse_result ( resultPtr ) ;
104242 }
105243 }
106244} ) ;
@@ -202,22 +340,61 @@ export function parseSync(query: string): ParseResult {
202340 if ( ! wasmModule ) {
203341 throw new Error ( 'WASM module not initialized. Call loadModule() first.' ) ;
204342 }
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+
205353 const queryPtr = stringToPtr ( query ) ;
206354 let resultPtr = 0 ;
207355
208356 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+ }
211361
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+ } ) ;
214386 }
215387
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 ) ;
217394 } finally {
218395 wasmModule . _free ( queryPtr ) ;
219396 if ( resultPtr ) {
220- wasmModule . _wasm_free_string ( resultPtr ) ;
397+ wasmModule . _wasm_free_parse_result ( resultPtr ) ;
221398 }
222399 }
223400}
0 commit comments