Skip to content

Commit 89b5e6b

Browse files
authored
Merge pull request #131 from launchql/feat/errors
Error Positions
2 parents ca03818 + cc39b07 commit 89b5e6b

Some content is hidden

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

57 files changed

+4650
-285
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
if [ "${{ matrix.package.name }}" = "v13" ]; then
5656
# Download prebuilt WASM for v13 since it can't build in CI
5757
mkdir -p wasm
58-
curl -o v13.tgz "https://registry.npmjs.org/@libpg-query/v13/-/v13-13.5.2.tgz"
58+
curl -o v13.tgz "https://registry.npmjs.org/@libpg-query/v13/-/v13-13.5.7.tgz"
5959
tar -xzf v13.tgz --strip-components=1 package/wasm
6060
rm v13.tgz
6161
else

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ pnpm run test
185185
- Ensure Emscripten SDK is properly installed and configured
186186
- Check that all required build dependencies are available
187187

188+
### Template System
189+
190+
To avoid duplication across PostgreSQL versions, common files are maintained in the `templates/` directory:
191+
- `LICENSE`, `Makefile`, `src/index.ts`, `src/libpg-query.d.ts`, `src/wasm_wrapper.c`
192+
193+
To update version-specific files from templates:
194+
```bash
195+
npm run copy:templates
196+
```
197+
198+
This ensures consistency while allowing version-specific customizations (e.g., patches for version 13).
199+
188200
### Build Artifacts
189201

190202
The build process generates these files:

REPO_NOTES.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
⚠️ Due to the managing of many versions, we do have some duplication, please beware!
2+
3+
There is a templates/ dir to solve some of this.
4+
5+
## Code Duplication 📋
6+
7+
### 1. Identical Test Files
8+
- All `versions/*/test/errors.test.js` files are identical (324 lines each)
9+
- All `versions/*/test/parsing.test.js` files are identical (89 lines each)
10+
- **Recommendation**: Consider using the template approach mentioned by the user
11+
12+
### 2. Nearly Identical Source Files
13+
- `versions/*/src/index.ts` are nearly identical except for version numbers
14+
- `versions/*/src/wasm_wrapper.c` are identical
15+
- `versions/*/Makefile` differ only in:
16+
- `LIBPG_QUERY_TAG` version
17+
- Version 13 has an extra emscripten patch
18+
19+
## Consistency Issues 🔧
20+
21+
### 1. Version 13 Makefile Difference
22+
- Version 13 applies an extra patch: `emscripten_disable_spinlocks.patch`
23+
- Other versions don't have this patch
24+
- **Status**: Patch file exists and is likely needed for v13 compatibility

full/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ ifdef EMSCRIPTEN
5757
-I$(LIBPG_QUERY_DIR) \
5858
-I$(LIBPG_QUERY_DIR)/vendor \
5959
-L$(LIBPG_QUERY_DIR) \
60-
-sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_wasm_parse_query_protobuf','_wasm_get_protobuf_len','_wasm_deparse_protobuf','_wasm_parse_plpgsql','_wasm_fingerprint','_wasm_normalize_query','_wasm_scan','_wasm_parse_query_detailed','_wasm_free_detailed_result','_wasm_free_string']" \
61-
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','UTF8ToString','HEAPU8','HEAPU32']" \
60+
-sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_wasm_parse_query_protobuf','_wasm_get_protobuf_len','_wasm_deparse_protobuf','_wasm_parse_plpgsql','_wasm_fingerprint','_wasm_normalize_query','_wasm_scan','_wasm_parse_query_detailed','_wasm_free_detailed_result','_wasm_free_string','_wasm_parse_query_raw','_wasm_free_parse_result']" \
61+
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','UTF8ToString','getValue','HEAPU8','HEAPU32']" \
6262
-sEXPORT_NAME="$(WASM_MODULE_NAME)" \
6363
-sENVIRONMENT="web,node" \
6464
-sMODULARIZE=1 \

full/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"wasm:rebuild": "pnpm wasm:make rebuild",
2323
"wasm:clean": "pnpm wasm:make clean",
2424
"wasm:clean-cache": "pnpm wasm:make clean-cache",
25-
"test": "node --test test/parsing.test.js test/deparsing.test.js test/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js",
25+
"test": "node --test test/parsing.test.js test/deparsing.test.js test/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js test/errors.test.js",
2626
"yamlize": "node ./scripts/yamlize.js",
2727
"protogen": "node ./scripts/protogen.js"
2828
},

full/src/index.ts

Lines changed: 189 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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
20117
import 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

87187
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+
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
}

full/src/wasm_wrapper.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,25 @@ char* wasm_parse_query(const char* input) {
4646
return parse_tree;
4747
}
4848

49+
EMSCRIPTEN_KEEPALIVE
50+
PgQueryParseResult* wasm_parse_query_raw(const char* input) {
51+
PgQueryParseResult* result = (PgQueryParseResult*)malloc(sizeof(PgQueryParseResult));
52+
if (!result) {
53+
return NULL;
54+
}
55+
56+
*result = pg_query_parse(input);
57+
return result;
58+
}
59+
60+
EMSCRIPTEN_KEEPALIVE
61+
void wasm_free_parse_result(PgQueryParseResult* result) {
62+
if (result) {
63+
pg_query_free_parse_result(*result);
64+
free(result);
65+
}
66+
}
67+
4968
EMSCRIPTEN_KEEPALIVE
5069
char* wasm_deparse_protobuf(const char* protobuf_data, size_t data_len) {
5170
if (!protobuf_data || data_len == 0) {

0 commit comments

Comments
 (0)