From 229f31505160ef10fac7094a3e0f0019455e0be0 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 03:35:02 +0000 Subject: [PATCH 1/4] fix(v18): throw SqlError from fingerprint/normalize/scan on every libpg_query error --- templates/full/index.ts | 71 ++++++----- templates/full/wasm_wrapper.c | 180 +++++++++++++++------------ versions/18/src/index.ts | 71 ++++++----- versions/18/src/wasm_wrapper.c | 180 +++++++++++++++------------ versions/18/test/fingerprint.test.js | 32 +++++ versions/18/test/normalize.test.js | 32 +++++ versions/18/test/scan.test.js | 44 +++++++ 7 files changed, 384 insertions(+), 226 deletions(-) diff --git a/templates/full/index.ts b/templates/full/index.ts index e97b6096..767089ae 100644 --- a/templates/full/index.ts +++ b/templates/full/index.ts @@ -127,6 +127,7 @@ interface WasmModule { _wasm_fingerprint: (queryPtr: number) => number; _wasm_normalize_query: (queryPtr: number) => number; _wasm_scan: (queryPtr: number) => number; + _wasm_free_detailed_result: (ptr: number) => void; lengthBytesUTF8: (str: string) => number; stringToUTF8: (str: string, ptr: number, len: number) => void; UTF8ToString: (ptr: number) => string; @@ -244,6 +245,28 @@ function ptrToString(ptr: number): string { return wasmModule.UTF8ToString(ptr); } +function readDetailedResult(resultPtr: number): string { + if (!resultPtr) { + throw new Error('libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'); + } + const ptrAt = (offset: number) => wasmModule.getValue(resultPtr + offset, 'i32'); + const str = (ptr: number) => (ptr ? wasmModule.UTF8ToString(ptr) : undefined); + if (ptrAt(0)) { + const message = str(ptrAt(4)) ?? 'Unknown error'; + const lineno = ptrAt(16); + const cursorpos = ptrAt(20); + throw new SqlError(message, { + message, + cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, + fileName: str(ptrAt(12)), + functionName: str(ptrAt(8)), + lineNumber: lineno > 0 ? lineno : undefined, + context: str(ptrAt(24)) + }); + } + return str(ptrAt(28)) ?? ''; +} + export const parse = awaitInit(async (query: string): Promise => { // Input validation if (query === null || query === undefined) { @@ -338,18 +361,14 @@ export const fingerprint = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_fingerprint(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -362,18 +381,14 @@ export const normalize = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_normalize_query(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -484,18 +499,14 @@ export function fingerprintSync(query: string): string { try { resultPtr = callWasm(() => wasmModule._wasm_fingerprint(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -511,18 +522,14 @@ export function normalizeSync(query: string): string { try { resultPtr = callWasm(() => wasmModule._wasm_normalize_query(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -535,18 +542,14 @@ export const scan = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return JSON.parse(resultStr); } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -562,18 +565,14 @@ export function scanSync(query: string): ScanResult { try { resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return JSON.parse(resultStr); } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } diff --git a/templates/full/wasm_wrapper.c b/templates/full/wasm_wrapper.c index 268d2893..81911fab 100644 --- a/templates/full/wasm_wrapper.c +++ b/templates/full/wasm_wrapper.c @@ -104,53 +104,6 @@ char* wasm_parse_plpgsql(const char* input) { return wrapped_result; } -EMSCRIPTEN_KEEPALIVE -char* wasm_fingerprint(const char* input) { - if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); - } - - PgQueryFingerprintResult result = pg_query_fingerprint(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_fingerprint_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); - } - - char* fingerprint_str = safe_strdup(result.fingerprint_str); - pg_query_free_fingerprint_result(result); - return fingerprint_str; -} - -EMSCRIPTEN_KEEPALIVE -char* wasm_normalize_query(const char* input) { - if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); - } - - PgQueryNormalizeResult result = pg_query_normalize(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_normalize_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); - } - - char* normalized = safe_strdup(result.normalized_query); - pg_query_free_normalize_result(result); - - if (!normalized) { - return safe_strdup("Memory allocation failed"); - } - - return normalized; -} - - - - - typedef struct { int has_error; char* message; @@ -230,6 +183,80 @@ void wasm_free_detailed_result(WasmDetailedResult* result) { } } +static WasmDetailedResult* detailed_result_new(void) { + WasmDetailedResult* result = safe_malloc(sizeof(WasmDetailedResult)); + if (result) { + memset(result, 0, sizeof(WasmDetailedResult)); + } + return result; +} + +static WasmDetailedResult* detailed_set_error(WasmDetailedResult* result, PgQueryError* error) { + result->has_error = 1; + result->message = safe_strdup(error->message); + result->funcname = error->funcname ? safe_strdup(error->funcname) : NULL; + result->filename = error->filename ? safe_strdup(error->filename) : NULL; + result->context = error->context ? safe_strdup(error->context) : NULL; + result->lineno = error->lineno; + result->cursorpos = error->cursorpos; + return result; +} + +static WasmDetailedResult* detailed_set_message(WasmDetailedResult* result, const char* message) { + result->has_error = 1; + result->message = safe_strdup(message); + return result; +} + +static WasmDetailedResult* detailed_set_data(WasmDetailedResult* result, char* data) { + if (!data) { + return detailed_set_message(result, "Memory allocation failed"); + } + result->data = data; + result->data_len = strlen(data); + return result; +} + +EMSCRIPTEN_KEEPALIVE +WasmDetailedResult* wasm_fingerprint(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } + if (!validate_input(input)) { + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); + } + + PgQueryFingerprintResult fingerprint_result = pg_query_fingerprint(input); + if (fingerprint_result.error) { + detailed_set_error(result, fingerprint_result.error); + } else { + detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); + } + pg_query_free_fingerprint_result(fingerprint_result); + return result; +} + +EMSCRIPTEN_KEEPALIVE +WasmDetailedResult* wasm_normalize_query(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } + if (!validate_input(input)) { + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); + } + + PgQueryNormalizeResult normalize_result = pg_query_normalize(input); + if (normalize_result.error) { + detailed_set_error(result, normalize_result.error); + } else { + detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); + } + pg_query_free_normalize_result(normalize_result); + return result; +} + static const char* get_token_name(PgQuery__Token token_type) { // Map some common token types to readable names // Note: This is a simplified mapping - full enum lookup would require more complexity @@ -272,14 +299,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } // Size the buffer from token text length, not token count: each token - // contributes at most ~170 bytes of fixed JSON plus up to 2 bytes per input - // byte once escaped. + // contributes at most ~170 bytes of fixed JSON plus up to 6 bytes per input + // byte once escaped (\\u00XX for control characters). size_t estimated_size = 1024; for (size_t i = 0; i < scan_result->n_tokens; i++) { PgQuery__ScanToken *token = scan_result->tokens[i]; int token_length = token->end - token->start; if (token_length < 0) token_length = 0; - estimated_size += (size_t) token_length * 2 + 200; + estimated_size += (size_t) token_length * 6 + 200; } char* json = safe_malloc(estimated_size); if (!json) { @@ -305,7 +332,7 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi token_text[token_length] = '\0'; // Escape token text for JSON - char* escaped_text = safe_malloc(token_length * 2 + 1); + char* escaped_text = safe_malloc(token_length * 6 + 1); if (!escaped_text) { free(token_text); continue; @@ -326,6 +353,8 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } else if (c == '\t') { escaped_text[escaped_pos++] = '\\'; escaped_text[escaped_pos++] = 't'; + } else if ((unsigned char) c < 0x20) { + escaped_pos += snprintf(escaped_text + escaped_pos, 7, "\\u%04x", (unsigned char) c); } else { escaped_text[escaped_pos++] = c; } @@ -364,36 +393,33 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } EMSCRIPTEN_KEEPALIVE -char* wasm_scan(const char* input) { +WasmDetailedResult* wasm_scan(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); } - - PgQueryScanResult result = pg_query_scan(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_scan_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); + + PgQueryScanResult scan_result = pg_query_scan(input); + if (scan_result.error) { + detailed_set_error(result, scan_result.error); + pg_query_free_scan_result(scan_result); + return result; } - - // Unpack protobuf data - PgQuery__ScanResult *scan_result = pg_query__scan_result__unpack( - NULL, result.pbuf.len, (void *) result.pbuf.data); - - if (!scan_result) { - pg_query_free_scan_result(result); - return safe_strdup("Failed to unpack scan result"); + + PgQuery__ScanResult *unpacked = pg_query__scan_result__unpack( + NULL, scan_result.pbuf.len, (void *) scan_result.pbuf.data); + if (!unpacked) { + pg_query_free_scan_result(scan_result); + return detailed_set_message(result, "Failed to unpack scan result"); } - - // Convert to JSON - char* json_result = build_scan_json(scan_result, input); - - // Clean up - pg_query__scan_result__free_unpacked(scan_result, NULL); - pg_query_free_scan_result(result); - - return json_result ? json_result : safe_strdup("{\"version\":0,\"tokens\":[]}"); + + detailed_set_data(result, build_scan_json(unpacked, input)); + pg_query__scan_result__free_unpacked(unpacked, NULL); + pg_query_free_scan_result(scan_result); + return result; } EMSCRIPTEN_KEEPALIVE diff --git a/versions/18/src/index.ts b/versions/18/src/index.ts index 56c93d61..31ce9660 100644 --- a/versions/18/src/index.ts +++ b/versions/18/src/index.ts @@ -134,6 +134,7 @@ interface WasmModule { _wasm_fingerprint: (queryPtr: number) => number; _wasm_normalize_query: (queryPtr: number) => number; _wasm_scan: (queryPtr: number) => number; + _wasm_free_detailed_result: (ptr: number) => void; lengthBytesUTF8: (str: string) => number; stringToUTF8: (str: string, ptr: number, len: number) => void; UTF8ToString: (ptr: number) => string; @@ -251,6 +252,28 @@ function ptrToString(ptr: number): string { return wasmModule.UTF8ToString(ptr); } +function readDetailedResult(resultPtr: number): string { + if (!resultPtr) { + throw new Error('libpg_query returned no result: out of WASM memory (heap limit is WASM_MAXIMUM_MEMORY, 1 GiB); parse large inputs in smaller pieces'); + } + const ptrAt = (offset: number) => wasmModule.getValue(resultPtr + offset, 'i32'); + const str = (ptr: number) => (ptr ? wasmModule.UTF8ToString(ptr) : undefined); + if (ptrAt(0)) { + const message = str(ptrAt(4)) ?? 'Unknown error'; + const lineno = ptrAt(16); + const cursorpos = ptrAt(20); + throw new SqlError(message, { + message, + cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, + fileName: str(ptrAt(12)), + functionName: str(ptrAt(8)), + lineNumber: lineno > 0 ? lineno : undefined, + context: str(ptrAt(24)) + }); + } + return str(ptrAt(28)) ?? ''; +} + export const parse = awaitInit(async (query: string): Promise => { // Input validation if (query === null || query === undefined) { @@ -345,18 +368,14 @@ export const fingerprint = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_fingerprint(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -369,18 +388,14 @@ export const normalize = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_normalize_query(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -491,18 +506,14 @@ export function fingerprintSync(query: string): string { try { resultPtr = callWasm(() => wasmModule._wasm_fingerprint(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -518,18 +529,14 @@ export function normalizeSync(query: string): string { try { resultPtr = callWasm(() => wasmModule._wasm_normalize_query(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return resultStr; } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -542,18 +549,14 @@ export const scan = awaitInit(async (query: string): Promise => { try { resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return JSON.parse(resultStr); } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } @@ -569,18 +572,14 @@ export function scanSync(query: string): ScanResult { try { resultPtr = callWasm(() => wasmModule._wasm_scan(queryPtr)); - const resultStr = ptrToString(resultPtr); - - if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) { - throw new Error(resultStr); - } + const resultStr = readDetailedResult(resultPtr); return JSON.parse(resultStr); } finally { if (failures === failuresBefore) { wasmModule._free(queryPtr); if (resultPtr) { - wasmModule._wasm_free_string(resultPtr); + wasmModule._wasm_free_detailed_result(resultPtr); } } } diff --git a/versions/18/src/wasm_wrapper.c b/versions/18/src/wasm_wrapper.c index 63ae5dab..2ee54101 100644 --- a/versions/18/src/wasm_wrapper.c +++ b/versions/18/src/wasm_wrapper.c @@ -111,53 +111,6 @@ char* wasm_parse_plpgsql(const char* input) { return wrapped_result; } -EMSCRIPTEN_KEEPALIVE -char* wasm_fingerprint(const char* input) { - if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); - } - - PgQueryFingerprintResult result = pg_query_fingerprint(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_fingerprint_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); - } - - char* fingerprint_str = safe_strdup(result.fingerprint_str); - pg_query_free_fingerprint_result(result); - return fingerprint_str; -} - -EMSCRIPTEN_KEEPALIVE -char* wasm_normalize_query(const char* input) { - if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); - } - - PgQueryNormalizeResult result = pg_query_normalize(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_normalize_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); - } - - char* normalized = safe_strdup(result.normalized_query); - pg_query_free_normalize_result(result); - - if (!normalized) { - return safe_strdup("Memory allocation failed"); - } - - return normalized; -} - - - - - typedef struct { int has_error; char* message; @@ -237,6 +190,80 @@ void wasm_free_detailed_result(WasmDetailedResult* result) { } } +static WasmDetailedResult* detailed_result_new(void) { + WasmDetailedResult* result = safe_malloc(sizeof(WasmDetailedResult)); + if (result) { + memset(result, 0, sizeof(WasmDetailedResult)); + } + return result; +} + +static WasmDetailedResult* detailed_set_error(WasmDetailedResult* result, PgQueryError* error) { + result->has_error = 1; + result->message = safe_strdup(error->message); + result->funcname = error->funcname ? safe_strdup(error->funcname) : NULL; + result->filename = error->filename ? safe_strdup(error->filename) : NULL; + result->context = error->context ? safe_strdup(error->context) : NULL; + result->lineno = error->lineno; + result->cursorpos = error->cursorpos; + return result; +} + +static WasmDetailedResult* detailed_set_message(WasmDetailedResult* result, const char* message) { + result->has_error = 1; + result->message = safe_strdup(message); + return result; +} + +static WasmDetailedResult* detailed_set_data(WasmDetailedResult* result, char* data) { + if (!data) { + return detailed_set_message(result, "Memory allocation failed"); + } + result->data = data; + result->data_len = strlen(data); + return result; +} + +EMSCRIPTEN_KEEPALIVE +WasmDetailedResult* wasm_fingerprint(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } + if (!validate_input(input)) { + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); + } + + PgQueryFingerprintResult fingerprint_result = pg_query_fingerprint(input); + if (fingerprint_result.error) { + detailed_set_error(result, fingerprint_result.error); + } else { + detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); + } + pg_query_free_fingerprint_result(fingerprint_result); + return result; +} + +EMSCRIPTEN_KEEPALIVE +WasmDetailedResult* wasm_normalize_query(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } + if (!validate_input(input)) { + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); + } + + PgQueryNormalizeResult normalize_result = pg_query_normalize(input); + if (normalize_result.error) { + detailed_set_error(result, normalize_result.error); + } else { + detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); + } + pg_query_free_normalize_result(normalize_result); + return result; +} + static const char* get_token_name(PgQuery__Token token_type) { // Map some common token types to readable names // Note: This is a simplified mapping - full enum lookup would require more complexity @@ -279,14 +306,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } // Size the buffer from token text length, not token count: each token - // contributes at most ~170 bytes of fixed JSON plus up to 2 bytes per input - // byte once escaped. + // contributes at most ~170 bytes of fixed JSON plus up to 6 bytes per input + // byte once escaped (\\u00XX for control characters). size_t estimated_size = 1024; for (size_t i = 0; i < scan_result->n_tokens; i++) { PgQuery__ScanToken *token = scan_result->tokens[i]; int token_length = token->end - token->start; if (token_length < 0) token_length = 0; - estimated_size += (size_t) token_length * 2 + 200; + estimated_size += (size_t) token_length * 6 + 200; } char* json = safe_malloc(estimated_size); if (!json) { @@ -312,7 +339,7 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi token_text[token_length] = '\0'; // Escape token text for JSON - char* escaped_text = safe_malloc(token_length * 2 + 1); + char* escaped_text = safe_malloc(token_length * 6 + 1); if (!escaped_text) { free(token_text); continue; @@ -333,6 +360,8 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } else if (c == '\t') { escaped_text[escaped_pos++] = '\\'; escaped_text[escaped_pos++] = 't'; + } else if ((unsigned char) c < 0x20) { + escaped_pos += snprintf(escaped_text + escaped_pos, 7, "\\u%04x", (unsigned char) c); } else { escaped_text[escaped_pos++] = c; } @@ -371,36 +400,33 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi } EMSCRIPTEN_KEEPALIVE -char* wasm_scan(const char* input) { +WasmDetailedResult* wasm_scan(const char* input) { + WasmDetailedResult* result = detailed_result_new(); + if (!result) { + return NULL; + } if (!validate_input(input)) { - return safe_strdup("Invalid input: query cannot be null or empty"); + return detailed_set_message(result, "Invalid input: query cannot be null or empty"); } - - PgQueryScanResult result = pg_query_scan(input); - - if (result.error) { - char* error_msg = safe_strdup(result.error->message); - pg_query_free_scan_result(result); - return error_msg ? error_msg : safe_strdup("Memory allocation failed"); + + PgQueryScanResult scan_result = pg_query_scan(input); + if (scan_result.error) { + detailed_set_error(result, scan_result.error); + pg_query_free_scan_result(scan_result); + return result; } - - // Unpack protobuf data - PgQuery__ScanResult *scan_result = pg_query__scan_result__unpack( - NULL, result.pbuf.len, (void *) result.pbuf.data); - - if (!scan_result) { - pg_query_free_scan_result(result); - return safe_strdup("Failed to unpack scan result"); + + PgQuery__ScanResult *unpacked = pg_query__scan_result__unpack( + NULL, scan_result.pbuf.len, (void *) scan_result.pbuf.data); + if (!unpacked) { + pg_query_free_scan_result(scan_result); + return detailed_set_message(result, "Failed to unpack scan result"); } - - // Convert to JSON - char* json_result = build_scan_json(scan_result, input); - - // Clean up - pg_query__scan_result__free_unpacked(scan_result, NULL); - pg_query_free_scan_result(result); - - return json_result ? json_result : safe_strdup("{\"version\":0,\"tokens\":[]}"); + + detailed_set_data(result, build_scan_json(unpacked, input)); + pg_query__scan_result__free_unpacked(unpacked, NULL); + pg_query_free_scan_result(scan_result); + return result; } EMSCRIPTEN_KEEPALIVE diff --git a/versions/18/test/fingerprint.test.js b/versions/18/test/fingerprint.test.js index 07171c82..e38a0df5 100644 --- a/versions/18/test/fingerprint.test.js +++ b/versions/18/test/fingerprint.test.js @@ -65,3 +65,35 @@ describe("Query Fingerprinting", () => { }); }); }); + +describe("fingerprint errors", () => { + before(async () => { + await query.parse("SELECT 1"); + }); + + // Errors raised outside the grammar's "syntax error" path used to come back + // as the fingerprint result instead of being thrown. + const failing = [ + ["select percentile_cont(p order by p) within group (order by x) from t", "cannot use multiple ORDER BY clauses with WITHIN GROUP"], + ["CREATE TABLE p (a int) PARTITION BY MAGIC (a)", 'unrecognized partitioning strategy "magic"'], + ]; + + for (const [sql, message] of failing) { + it(`fingerprintSync throws SqlError for ${sql}`, () => { + assert.throws(() => query.fingerprintSync(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + return true; + }); + }); + + it(`fingerprint rejects with SqlError for ${sql}`, async () => { + await assert.rejects(query.fingerprint(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + return true; + }); + }); + } +}); diff --git a/versions/18/test/normalize.test.js b/versions/18/test/normalize.test.js index 14f0a431..68932a68 100644 --- a/versions/18/test/normalize.test.js +++ b/versions/18/test/normalize.test.js @@ -67,3 +67,35 @@ describe("Query Normalization", () => { }); }); }); + +describe("normalize errors", () => { + before(async () => { + await query.parse("SELECT 1"); + }); + + // Errors raised outside the grammar's "syntax error" path used to come back + // as the normalize result instead of being thrown. + const failing = [ + ["select percentile_cont(p order by p) within group (order by x) from t", "cannot use multiple ORDER BY clauses with WITHIN GROUP"], + ["CREATE TABLE p (a int) PARTITION BY MAGIC (a)", 'unrecognized partitioning strategy "magic"'], + ]; + + for (const [sql, message] of failing) { + it(`normalizeSync throws SqlError for ${sql}`, () => { + assert.throws(() => query.normalizeSync(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + return true; + }); + }); + + it(`normalize rejects with SqlError for ${sql}`, async () => { + await assert.rejects(query.normalize(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + return true; + }); + }); + } +}); diff --git a/versions/18/test/scan.test.js b/versions/18/test/scan.test.js index bd927874..5d1a3424 100644 --- a/versions/18/test/scan.test.js +++ b/versions/18/test/scan.test.js @@ -305,3 +305,47 @@ comment */ SELECT 2`; }); }); }); + +describe("scan errors", () => { + before(async () => { + await query.parse("SELECT 1"); + }); + + // Scanner errors used to surface as a JSON.parse SyntaxError on the message. + const failing = [ + ["SELECT 'abc", "unterminated quoted string at or near \"'abc\""], + ["SELECT $$x", "unterminated dollar-quoted string at or near \"$$x\""], + ]; + + for (const [sql, message] of failing) { + it(`scanSync throws SqlError for ${sql}`, () => { + assert.throws(() => query.scanSync(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + return true; + }); + }); + + it(`scan rejects with SqlError for ${sql}`, async () => { + await assert.rejects(query.scan(sql), (err) => { + assert.equal(err.name, "SqlError"); + assert.equal(err.message, message); + return true; + }); + }); + } +}); + +describe("scan control characters", () => { + before(async () => { + await query.parse("SELECT 1"); + }); + + it("escapes control characters in token text", () => { + const sql = "SELECT E'a\u0002b'"; + const result = query.scanSync(sql); + const literal = result.tokens.find((t) => t.tokenName === "SCONST"); + assert.equal(literal.text, "E'a\u0002b'"); + }); +}); From ed28cca9fe98fb613480228d56c382b84afb422d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 03:41:22 +0000 Subject: [PATCH 2/4] test: assert SqlError source fields; declare new WASM exports in libpg-query.d.ts --- templates/full/libpg-query.d.ts | 3 +++ versions/18/src/libpg-query.d.ts | 3 +++ versions/18/test/fingerprint.test.js | 3 +++ versions/18/test/normalize.test.js | 3 +++ versions/18/test/scan.test.js | 3 +++ 5 files changed, 15 insertions(+) diff --git a/templates/full/libpg-query.d.ts b/templates/full/libpg-query.d.ts index 731e1a99..dd0cd1c4 100644 --- a/templates/full/libpg-query.d.ts +++ b/templates/full/libpg-query.d.ts @@ -7,9 +7,12 @@ declare module './libpg-query.js' { _wasm_parse_plpgsql: (queryPtr: number) => number; _wasm_fingerprint: (queryPtr: number) => number; _wasm_normalize_query: (queryPtr: number) => number; + _wasm_scan: (queryPtr: number) => number; + _wasm_free_detailed_result: (ptr: number) => void; lengthBytesUTF8: (str: string) => number; stringToUTF8: (str: string, ptr: number, len: number) => void; UTF8ToString: (ptr: number) => string; + getValue: (ptr: number, type: string) => number; HEAPU8: Uint8Array; } diff --git a/versions/18/src/libpg-query.d.ts b/versions/18/src/libpg-query.d.ts index 152d8414..e19be14e 100644 --- a/versions/18/src/libpg-query.d.ts +++ b/versions/18/src/libpg-query.d.ts @@ -14,9 +14,12 @@ declare module './libpg-query.js' { _wasm_parse_plpgsql: (queryPtr: number) => number; _wasm_fingerprint: (queryPtr: number) => number; _wasm_normalize_query: (queryPtr: number) => number; + _wasm_scan: (queryPtr: number) => number; + _wasm_free_detailed_result: (ptr: number) => void; lengthBytesUTF8: (str: string) => number; stringToUTF8: (str: string, ptr: number, len: number) => void; UTF8ToString: (ptr: number) => string; + getValue: (ptr: number, type: string) => number; HEAPU8: Uint8Array; } diff --git a/versions/18/test/fingerprint.test.js b/versions/18/test/fingerprint.test.js index e38a0df5..e3132948 100644 --- a/versions/18/test/fingerprint.test.js +++ b/versions/18/test/fingerprint.test.js @@ -84,6 +84,9 @@ describe("fingerprint errors", () => { assert.equal(err.name, "SqlError"); assert.equal(err.message, message); assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + assert.equal(typeof err.sqlDetails.fileName, "string"); + assert.equal(typeof err.sqlDetails.functionName, "string"); + assert.ok(err.sqlDetails.lineNumber > 0); return true; }); }); diff --git a/versions/18/test/normalize.test.js b/versions/18/test/normalize.test.js index 68932a68..404ee341 100644 --- a/versions/18/test/normalize.test.js +++ b/versions/18/test/normalize.test.js @@ -86,6 +86,9 @@ describe("normalize errors", () => { assert.equal(err.name, "SqlError"); assert.equal(err.message, message); assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + assert.equal(typeof err.sqlDetails.fileName, "string"); + assert.equal(typeof err.sqlDetails.functionName, "string"); + assert.ok(err.sqlDetails.lineNumber > 0); return true; }); }); diff --git a/versions/18/test/scan.test.js b/versions/18/test/scan.test.js index 5d1a3424..f008c9cd 100644 --- a/versions/18/test/scan.test.js +++ b/versions/18/test/scan.test.js @@ -323,6 +323,9 @@ describe("scan errors", () => { assert.equal(err.name, "SqlError"); assert.equal(err.message, message); assert.equal(typeof err.sqlDetails.cursorPosition, "number"); + assert.equal(typeof err.sqlDetails.fileName, "string"); + assert.equal(typeof err.sqlDetails.functionName, "string"); + assert.ok(err.sqlDetails.lineNumber > 0); return true; }); }); From 6610a93a656bd417c4cdd19356d5bf90921a0a79 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 03:54:16 +0000 Subject: [PATCH 3/4] fix(v18): throw out of WASM memory from scan instead of returning partial or empty tokens --- templates/full/wasm_wrapper.c | 36 ++++++++++++++++++---------- versions/18/src/wasm_wrapper.c | 36 ++++++++++++++++++---------- versions/18/test/fingerprint.test.js | 6 +++++ versions/18/test/normalize.test.js | 6 +++++ versions/18/test/scan.test.js | 20 ++++++++++++++++ 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/templates/full/wasm_wrapper.c b/templates/full/wasm_wrapper.c index 81911fab..ec87a0e7 100644 --- a/templates/full/wasm_wrapper.c +++ b/templates/full/wasm_wrapper.c @@ -4,6 +4,7 @@ #include #include #include +#include static int validate_input(const char* input) { return input != NULL && strlen(input) > 0; @@ -208,9 +209,11 @@ static WasmDetailedResult* detailed_set_message(WasmDetailedResult* result, cons return result; } +// Out of memory returns NULL, which the JS side reports as out of WASM memory. static WasmDetailedResult* detailed_set_data(WasmDetailedResult* result, char* data) { if (!data) { - return detailed_set_message(result, "Memory allocation failed"); + wasm_free_detailed_result(result); + return NULL; } result->data = data; result->data_len = strlen(data); @@ -231,7 +234,7 @@ WasmDetailedResult* wasm_fingerprint(const char* input) { if (fingerprint_result.error) { detailed_set_error(result, fingerprint_result.error); } else { - detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); + result = detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); } pg_query_free_fingerprint_result(fingerprint_result); return result; @@ -251,7 +254,7 @@ WasmDetailedResult* wasm_normalize_query(const char* input) { if (normalize_result.error) { detailed_set_error(result, normalize_result.error); } else { - detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); + result = detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); } pg_query_free_normalize_result(normalize_result); return result; @@ -293,9 +296,10 @@ static const char* get_keyword_name(PgQuery__KeywordKind keyword_kind) { } } +// Returns NULL when out of memory: a partial token list must never pass as a result. static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* original_sql) { if (!scan_result || !original_sql) { - return safe_strdup("{\"version\":0,\"tokens\":[]}"); + return NULL; } // Size the buffer from token text length, not token count: each token @@ -306,11 +310,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi PgQuery__ScanToken *token = scan_result->tokens[i]; int token_length = token->end - token->start; if (token_length < 0) token_length = 0; + if ((size_t) token_length > (SIZE_MAX - 200 - estimated_size) / 6) { + return NULL; + } estimated_size += (size_t) token_length * 6 + 200; } char* json = safe_malloc(estimated_size); if (!json) { - return safe_strdup("{\"version\":0,\"tokens\":[]}"); + return NULL; } // Start building JSON @@ -323,8 +330,11 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi int token_length = token->end - token->start; if (token_length < 0) token_length = 0; // Safety check - char* token_text = safe_malloc(token_length + 1); - if (!token_text) continue; + char* token_text = safe_malloc((size_t) token_length + 1); + if (!token_text) { + free(json); + return NULL; + } if (token_length > 0) { strncpy(token_text, &original_sql[token->start], token_length); @@ -332,10 +342,11 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi token_text[token_length] = '\0'; // Escape token text for JSON - char* escaped_text = safe_malloc(token_length * 6 + 1); + char* escaped_text = safe_malloc((size_t) token_length * 6 + 1); if (!escaped_text) { free(token_text); - continue; + free(json); + return NULL; } int escaped_pos = 0; @@ -413,13 +424,14 @@ WasmDetailedResult* wasm_scan(const char* input) { NULL, scan_result.pbuf.len, (void *) scan_result.pbuf.data); if (!unpacked) { pg_query_free_scan_result(scan_result); - return detailed_set_message(result, "Failed to unpack scan result"); + wasm_free_detailed_result(result); + return NULL; } - detailed_set_data(result, build_scan_json(unpacked, input)); + char* json = build_scan_json(unpacked, input); pg_query__scan_result__free_unpacked(unpacked, NULL); pg_query_free_scan_result(scan_result); - return result; + return detailed_set_data(result, json); } EMSCRIPTEN_KEEPALIVE diff --git a/versions/18/src/wasm_wrapper.c b/versions/18/src/wasm_wrapper.c index 2ee54101..e3ea6a92 100644 --- a/versions/18/src/wasm_wrapper.c +++ b/versions/18/src/wasm_wrapper.c @@ -11,6 +11,7 @@ #include #include #include +#include static int validate_input(const char* input) { return input != NULL && strlen(input) > 0; @@ -215,9 +216,11 @@ static WasmDetailedResult* detailed_set_message(WasmDetailedResult* result, cons return result; } +// Out of memory returns NULL, which the JS side reports as out of WASM memory. static WasmDetailedResult* detailed_set_data(WasmDetailedResult* result, char* data) { if (!data) { - return detailed_set_message(result, "Memory allocation failed"); + wasm_free_detailed_result(result); + return NULL; } result->data = data; result->data_len = strlen(data); @@ -238,7 +241,7 @@ WasmDetailedResult* wasm_fingerprint(const char* input) { if (fingerprint_result.error) { detailed_set_error(result, fingerprint_result.error); } else { - detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); + result = detailed_set_data(result, safe_strdup(fingerprint_result.fingerprint_str)); } pg_query_free_fingerprint_result(fingerprint_result); return result; @@ -258,7 +261,7 @@ WasmDetailedResult* wasm_normalize_query(const char* input) { if (normalize_result.error) { detailed_set_error(result, normalize_result.error); } else { - detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); + result = detailed_set_data(result, safe_strdup(normalize_result.normalized_query)); } pg_query_free_normalize_result(normalize_result); return result; @@ -300,9 +303,10 @@ static const char* get_keyword_name(PgQuery__KeywordKind keyword_kind) { } } +// Returns NULL when out of memory: a partial token list must never pass as a result. static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* original_sql) { if (!scan_result || !original_sql) { - return safe_strdup("{\"version\":0,\"tokens\":[]}"); + return NULL; } // Size the buffer from token text length, not token count: each token @@ -313,11 +317,14 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi PgQuery__ScanToken *token = scan_result->tokens[i]; int token_length = token->end - token->start; if (token_length < 0) token_length = 0; + if ((size_t) token_length > (SIZE_MAX - 200 - estimated_size) / 6) { + return NULL; + } estimated_size += (size_t) token_length * 6 + 200; } char* json = safe_malloc(estimated_size); if (!json) { - return safe_strdup("{\"version\":0,\"tokens\":[]}"); + return NULL; } // Start building JSON @@ -330,8 +337,11 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi int token_length = token->end - token->start; if (token_length < 0) token_length = 0; // Safety check - char* token_text = safe_malloc(token_length + 1); - if (!token_text) continue; + char* token_text = safe_malloc((size_t) token_length + 1); + if (!token_text) { + free(json); + return NULL; + } if (token_length > 0) { strncpy(token_text, &original_sql[token->start], token_length); @@ -339,10 +349,11 @@ static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* origi token_text[token_length] = '\0'; // Escape token text for JSON - char* escaped_text = safe_malloc(token_length * 6 + 1); + char* escaped_text = safe_malloc((size_t) token_length * 6 + 1); if (!escaped_text) { free(token_text); - continue; + free(json); + return NULL; } int escaped_pos = 0; @@ -420,13 +431,14 @@ WasmDetailedResult* wasm_scan(const char* input) { NULL, scan_result.pbuf.len, (void *) scan_result.pbuf.data); if (!unpacked) { pg_query_free_scan_result(scan_result); - return detailed_set_message(result, "Failed to unpack scan result"); + wasm_free_detailed_result(result); + return NULL; } - detailed_set_data(result, build_scan_json(unpacked, input)); + char* json = build_scan_json(unpacked, input); pg_query__scan_result__free_unpacked(unpacked, NULL); pg_query_free_scan_result(scan_result); - return result; + return detailed_set_data(result, json); } EMSCRIPTEN_KEEPALIVE diff --git a/versions/18/test/fingerprint.test.js b/versions/18/test/fingerprint.test.js index e3132948..e6ce519a 100644 --- a/versions/18/test/fingerprint.test.js +++ b/versions/18/test/fingerprint.test.js @@ -76,8 +76,14 @@ describe("fingerprint errors", () => { const failing = [ ["select percentile_cont(p order by p) within group (order by x) from t", "cannot use multiple ORDER BY clauses with WITHIN GROUP"], ["CREATE TABLE p (a int) PARTITION BY MAGIC (a)", 'unrecognized partitioning strategy "magic"'], + ["SELECT 'abc", "unterminated quoted string at or near \"'abc\""], ]; + it("throws SqlError for empty input", async () => { + assert.throws(() => query.fingerprintSync(""), { name: "SqlError", message: /^Invalid input/ }); + await assert.rejects(query.fingerprint(""), { name: "SqlError", message: /^Invalid input/ }); + }); + for (const [sql, message] of failing) { it(`fingerprintSync throws SqlError for ${sql}`, () => { assert.throws(() => query.fingerprintSync(sql), (err) => { diff --git a/versions/18/test/normalize.test.js b/versions/18/test/normalize.test.js index 404ee341..a7d996d9 100644 --- a/versions/18/test/normalize.test.js +++ b/versions/18/test/normalize.test.js @@ -78,8 +78,14 @@ describe("normalize errors", () => { const failing = [ ["select percentile_cont(p order by p) within group (order by x) from t", "cannot use multiple ORDER BY clauses with WITHIN GROUP"], ["CREATE TABLE p (a int) PARTITION BY MAGIC (a)", 'unrecognized partitioning strategy "magic"'], + ['SELECT "x', "unterminated quoted identifier at or near \"\"x\""], ]; + it("throws SqlError for empty input", async () => { + assert.throws(() => query.normalizeSync(""), { name: "SqlError", message: /^Invalid input/ }); + await assert.rejects(query.normalize(""), { name: "SqlError", message: /^Invalid input/ }); + }); + for (const [sql, message] of failing) { it(`normalizeSync throws SqlError for ${sql}`, () => { assert.throws(() => query.normalizeSync(sql), (err) => { diff --git a/versions/18/test/scan.test.js b/versions/18/test/scan.test.js index f008c9cd..9f121d21 100644 --- a/versions/18/test/scan.test.js +++ b/versions/18/test/scan.test.js @@ -340,6 +340,26 @@ describe("scan errors", () => { } }); +describe("scan input limits", () => { + before(async () => { + await query.parse("SELECT 1"); + }); + + it("throws SqlError for empty input", async () => { + assert.throws(() => query.scanSync(""), { name: "SqlError", message: /^Invalid input/ }); + await assert.rejects(query.scan(""), { name: "SqlError", message: /^Invalid input/ }); + }); + + // About 11 MB: the token JSON no longer fits in the 1 GiB heap. This used to + // return { version: 0, tokens: [] } instead of throwing. + it("throws when the token JSON does not fit in WASM memory, then recovers", async () => { + const sql = "SELECT a, b FROM t WHERE c = 1;\n".repeat(360000); + assert.throws(() => query.scanSync(sql), /out of WASM memory/); + await assert.rejects(query.scan(sql), /out of WASM memory/); + assert.equal(query.scanSync("SELECT 1").tokens.length, 2); + }); +}); + describe("scan control characters", () => { before(async () => { await query.parse("SELECT 1"); From ab4aedb60d22f665eb60cf0dc59d5035fb747393 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sun, 25 Oct 2026 03:58:12 +0000 Subject: [PATCH 4/4] test(v18): scan of 6M short tokens throws out of WASM memory --- versions/18/test/scan.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/versions/18/test/scan.test.js b/versions/18/test/scan.test.js index 9f121d21..c96ef4eb 100644 --- a/versions/18/test/scan.test.js +++ b/versions/18/test/scan.test.js @@ -358,6 +358,12 @@ describe("scan input limits", () => { await assert.rejects(query.scan(sql), /out of WASM memory/); assert.equal(query.scanSync("SELECT 1").tokens.length, 2); }); + + // 6M one-character tokens (20 MB) used to return tokens: [] as well. + it("throws for many short tokens instead of returning none", () => { + assert.equal(query.scanSync("SELECT 1;\n".repeat(1000000)).tokens.length, 3000000); + assert.throws(() => query.scanSync("SELECT 1;\n".repeat(2000000)), /out of WASM memory/); + }); }); describe("scan control characters", () => {