Skip to content

fix(v18): throw SqlError from fingerprint/normalize/scan on every libpg_query error - #183

Open
pyramation wants to merge 4 commits into
mainfrom
devin/1792898986-fingerprint-normalize-errors
Open

pyramation wants to merge 4 commits into
mainfrom
devin/1792898986-fingerprint-normalize-errors

Conversation

@pyramation

@pyramation pyramation commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

wasm_fingerprint, wasm_normalize_query and wasm_scan returned either the result or the error message as one bare string. The JS side guessed which it was from the prefix (syntax error / deparse error / ERROR). Any other libpg_query error was therefore returned as if it were the result:

const q = 'select percentile_cont(p order by p) within group (order by x) from t';
parseSync(q)        // throws SqlError "cannot use multiple ORDER BY clauses with WITHIN GROUP"
fingerprintSync(q)  // returned "cannot use multiple ORDER BY clauses with WITHIN GROUP"
normalizeSync(q)    // returned the same string
scanSync("SELECT 'abc")  // threw SyntaxError: Unexpected token 'u', "unterminat"... is not valid JSON
scanSync('SELECT a, b FROM t WHERE c = 1;\n'.repeat(340000))  // 10.4 MB: returned { version: 0, tokens: [] }

Running the PostgreSQL regression corpus (libpg_query test/sql/**, 50,928 statements) showed 76 statements where fingerprint/normalize returned an error message. All of them are grammar ereports that aren't syntax errors, e.g. column number must be in range from 1 to 32767 and unrecognized partitioning strategy "magic". 18.2.0 and 18.2.1 behave the same.

Change:

  • wasm_fingerprint, wasm_normalize_query and wasm_scan now return the existing WasmDetailedResult (has_error, message, funcname, filename, lineno, cursorpos, context, data), freed with wasm_free_detailed_result.
  • A new readDetailedResult() in templates/full/index.ts throws the same SqlError that parseSync throws, including cursorPosition (0-based). The prefix check is gone.
  • build_scan_json now escapes the other control characters (< 0x20) as \u00XX, with buffers sized at 6 bytes per input byte. Before, scanSync("SELECT E'a\u0002b'") produced invalid JSON (corpus case from libpg_query's parse_tests.c).
  • Out of memory: when build_scan_json, the scan unpack or the result copy fails to allocate, the function returns NULL, and JS throws libpg_query returned no result: out of WASM memory …. Before, the whole token list came back empty, and any single token whose copy failed was skipped. The buffer-size sum is also checked for 32-bit size_t overflow.

v13–17 are unaffected: they only export parse/parseSync, which already read the structured PgQueryParseResult.

Tests (v18)

New cases in test/fingerprint.test.js, test/normalize.test.js and test/scan.test.js:

  • sync and async must throw SqlError with the exact message, including lexer errors (SELECT 'abc, SELECT "x, SELECT $$x);
  • empty input throws SqlError Invalid input…;
  • an 11 MB scanSync/scan must throw out of WASM memory, and the next call must still work;
  • scanSync('SELECT 1;\n'.repeat(2000000)) (6M tokens, repro from fix(scan): throw when the scan result does not fit in memory #184) must throw instead of returning tokens: [], while 1M repeats still returns all 3M tokens;
  • scan token text must survive a control character.

Results:

  • Run against the published 18.2.1 wasm/ (current main): tests 52, pass 40, fail 12 (actual: 'SyntaxError', expected: 'SqlError' etc.). The control-character test also fails before the escape fix: Bad control character in string literal in JSON.
  • The 11 MB and 6M-token out-of-memory tests fail before 6610a93 (Missing expected exception: published 18.2.1 returns tokens: [] for both). The other new tests in that commit (lexer errors in fingerprint/normalize, empty input) already passed at ed28cca and pin that behaviour.
  • With this PR (pnpm build, emsdk 6.0.3): pnpm test passes 165/165.
  • Corpus re-check with this build: errors from parse, fingerprint and normalize all now total 1,592, and every scan error is a SqlError.

Known, not fixed here

From about 50 MB of short statements (≈18.6M tokens), scanSync aborts with RuntimeError: Aborted(Assertion failed: <binary>) or table index is out of bounds. Sync calls in the same tick then keep failing until the replacement instance loads; async calls recover.

The cause is in the libpg_query fork's pg_query_scan (src/pg_query_scan.c): it never checks its mallocs for output_tokens, each token, and result.pbuf.data. Once the 1 GiB heap is exhausted, malloc(result.pbuf.len) returns NULL, and pg_query__scan_result__pack writes the protobuf to address 0. In wasm that does not trap; it overwrites static data. The "assertion text" is literally packed ScanToken bytes (\b, \u0010, , ( field tags).

Because the instance's memory is corrupt, the wrapper's stale-instance sync path cannot be used safely; this is not a wrapper recovery bug. Repro: scanSync('SELECT a, b FROM t WHERE c = 1;\n'.repeat(1690000)). 39.7 MB still throws the clean out-of-memory error. The fix belongs in the fork: return an "out of memory" PgQueryError when those mallocs fail.

@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Review complete. No blocking issues — approved ✅; 2 nitpicks below.

🧹 Nitpicks (2) — 🟢 2 low
  • 🟢 Add _wasm_free_detailed_result to libpg-query.d.ts (libpg-query.d.ts:2) — The new WASM export _wasm_free_detailed_result was added to the inline WasmModule interfaces in templates/full/index.ts:130 and versions/18/src/index.ts, but the ambient declaration in templates/full/libpg-query.d.ts:2-14 was not updated and still lacks it.
  • 🟢 Error tests never assert sqlDetails source fields (scan.test.js:322) — The new scan errors, fingerprint errors, and normalize errors suites assert only err.name, err.message, and cursorPosition (versions/18/test/scan.test.js:325); the fileName, functionName, lineNumber, and context fields of SqlErrorDetails — the core of the new contract, populated from the PgQueryError struct in readDetailedResult — are never checked.

This PR replaces error-string plumbing with a structured SqlError carrying detailed SQL error fields, and switches scan result serialization to a JSON-escaping helper in the C wrapper. Template and PG18 sources were kept in sync; tests cover sync/async happy paths and error shapes.

Files Change
templates/full/index.ts, versions/18/src/index.ts Add readDetailedResult reading the new WASM struct and throwing SqlError with fileName/functionName/lineNumber/context/cursorPosition; apply to parse, normalize, and fingerprint call sites
templates/full/wasm_wrapper.c, versions/18/src/wasm_wrapper.c Add WasmDetailedResult struct, copy helpers, _wasm_free_detailed_result, and a JSON-escaping path for wasm_scan token text
versions/18/test/fingerprint.test.js, normalize.test.js, scan.test.js Cover sync/async fingerprint/normalize/scan pairs plus error-name/message/cursorPosition assertions
templates/full/libpg-query.d.ts Not updated for the new _wasm_free_detailed_result export (see finding-001)

Reviewed commit: 229f315

@devin-ai-integration

Copy link
Copy Markdown

Folded into #190

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant