Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
if [ "${{ matrix.package.name }}" = "v13" ]; then
# Download prebuilt WASM for v13 since it can't build in CI
mkdir -p wasm
curl -o v13.tgz "https://registry.npmjs.org/@libpg-query/v13/-/v13-13.5.2.tgz"
curl -o v13.tgz "https://registry.npmjs.org/@libpg-query/v13/-/v13-13.5.7.tgz"
tar -xzf v13.tgz --strip-components=1 package/wasm
rm v13.tgz
else
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,18 @@ pnpm run test
- Ensure Emscripten SDK is properly installed and configured
- Check that all required build dependencies are available

### Template System

To avoid duplication across PostgreSQL versions, common files are maintained in the `templates/` directory:
- `LICENSE`, `Makefile`, `src/index.ts`, `src/libpg-query.d.ts`, `src/wasm_wrapper.c`

To update version-specific files from templates:
```bash
npm run copy:templates
```

This ensures consistency while allowing version-specific customizations (e.g., patches for version 13).

### Build Artifacts

The build process generates these files:
Expand Down
24 changes: 24 additions & 0 deletions REPO_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
⚠️ Due to the managing of many versions, we do have some duplication, please beware!

There is a templates/ dir to solve some of this.

## Code Duplication 📋

### 1. Identical Test Files
- All `versions/*/test/errors.test.js` files are identical (324 lines each)
- All `versions/*/test/parsing.test.js` files are identical (89 lines each)
- **Recommendation**: Consider using the template approach mentioned by the user

### 2. Nearly Identical Source Files
- `versions/*/src/index.ts` are nearly identical except for version numbers
- `versions/*/src/wasm_wrapper.c` are identical
- `versions/*/Makefile` differ only in:
- `LIBPG_QUERY_TAG` version
- Version 13 has an extra emscripten patch

## Consistency Issues 🔧

### 1. Version 13 Makefile Difference
- Version 13 applies an extra patch: `emscripten_disable_spinlocks.patch`
- Other versions don't have this patch
- **Status**: Patch file exists and is likely needed for v13 compatibility
4 changes: 2 additions & 2 deletions full/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ ifdef EMSCRIPTEN
-I$(LIBPG_QUERY_DIR) \
-I$(LIBPG_QUERY_DIR)/vendor \
-L$(LIBPG_QUERY_DIR) \
-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']" \
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','UTF8ToString','HEAPU8','HEAPU32']" \
-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']" \
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','UTF8ToString','getValue','HEAPU8','HEAPU32']" \
-sEXPORT_NAME="$(WASM_MODULE_NAME)" \
-sENVIRONMENT="web,node" \
-sMODULARIZE=1 \
Expand Down
2 changes: 1 addition & 1 deletion full/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"wasm:rebuild": "pnpm wasm:make rebuild",
"wasm:clean": "pnpm wasm:make clean",
"wasm:clean-cache": "pnpm wasm:make clean-cache",
"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": "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",
"yamlize": "node ./scripts/yamlize.js",
"protogen": "node ./scripts/protogen.js"
},
Expand Down
201 changes: 189 additions & 12 deletions full/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,103 @@ export interface ScanResult {
tokens: ScanToken[];
}

export interface SqlErrorDetails {
message: string;
cursorPosition: number;
fileName?: string;
functionName?: string;
lineNumber?: number;
context?: string;
}

export class SqlError extends Error {
sqlDetails?: SqlErrorDetails;

constructor(message: string, details?: SqlErrorDetails) {
super(message);
this.name = 'SqlError';
this.sqlDetails = details;
}
}

export function hasSqlDetails(error: unknown): error is SqlError {
return error instanceof SqlError && error.sqlDetails !== undefined;
}

export function formatSqlError(
error: SqlError,
query: string,
options: {
showPosition?: boolean;
showQuery?: boolean;
color?: boolean;
maxQueryLength?: number;
} = {}
): string {
const {
showPosition = true,
showQuery = true,
color = false,
maxQueryLength
} = options;

const lines: string[] = [];

// ANSI color codes
const red = color ? '\x1b[31m' : '';
const yellow = color ? '\x1b[33m' : '';
const reset = color ? '\x1b[0m' : '';

// Add error message
lines.push(`${red}Error: ${error.message}${reset}`);

// Add SQL details if available
if (error.sqlDetails) {
const { cursorPosition, fileName, functionName, lineNumber } = error.sqlDetails;

if (cursorPosition !== undefined && cursorPosition >= 0) {
lines.push(`Position: ${cursorPosition}`);
}

if (fileName || functionName || lineNumber) {
const details = [];
if (fileName) details.push(`file: ${fileName}`);
if (functionName) details.push(`function: ${functionName}`);
if (lineNumber) details.push(`line: ${lineNumber}`);
lines.push(`Source: ${details.join(', ')}`);
}

// Show query with position marker
if (showQuery && showPosition && cursorPosition !== undefined && cursorPosition >= 0) {
let displayQuery = query;
let adjustedPosition = cursorPosition;

// Truncate if needed
if (maxQueryLength && query.length > maxQueryLength) {
const start = Math.max(0, cursorPosition - Math.floor(maxQueryLength / 2));
const end = Math.min(query.length, start + maxQueryLength);
displayQuery = (start > 0 ? '...' : '') +
query.substring(start, end) +
(end < query.length ? '...' : '');
// Adjust cursor position for truncation
adjustedPosition = cursorPosition - start + (start > 0 ? 3 : 0);
}

lines.push(displayQuery);
lines.push(' '.repeat(adjustedPosition) + `${yellow}^${reset}`);
}
} else if (showQuery) {
// No SQL details, just show the query if requested
let displayQuery = query;
if (maxQueryLength && query.length > maxQueryLength) {
displayQuery = query.substring(0, maxQueryLength) + '...';
}
lines.push(`Query: ${displayQuery}`);
}

return lines.join('\n');
}

// @ts-ignore
import PgQueryModule from './libpg-query.js';
// @ts-ignore
Expand All @@ -26,6 +123,8 @@ interface WasmModule {
_free: (ptr: number) => void;
_wasm_free_string: (ptr: number) => void;
_wasm_parse_query: (queryPtr: number) => number;
_wasm_parse_query_raw: (queryPtr: number) => number;
_wasm_free_parse_result: (ptr: number) => void;
_wasm_deparse_protobuf: (dataPtr: number, length: number) => number;
_wasm_parse_plpgsql: (queryPtr: number) => number;
_wasm_fingerprint: (queryPtr: number) => number;
Expand All @@ -34,6 +133,7 @@ interface WasmModule {
lengthBytesUTF8: (str: string) => number;
stringToUTF8: (str: string, ptr: number, len: number) => void;
UTF8ToString: (ptr: number) => string;
getValue: (ptr: number, type: string) => number;
HEAPU8: Uint8Array;
}

Expand Down Expand Up @@ -85,22 +185,60 @@ function ptrToString(ptr: number): string {
}

export const parse = awaitInit(async (query: string): Promise<ParseResult> => {
// Input validation
if (query === null || query === undefined) {
throw new Error('Query cannot be null or undefined');
}

if (query === '') {
throw new Error('Query cannot be empty');
}

const queryPtr = stringToPtr(query);
let resultPtr = 0;

try {
resultPtr = wasmModule._wasm_parse_query(queryPtr);
const resultStr = ptrToString(resultPtr);
resultPtr = wasmModule._wasm_parse_query_raw(queryPtr);
if (!resultPtr) {
throw new Error('Failed to parse query: memory allocation failed');
}

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
// Read the PgQueryParseResult struct
const parseTreePtr = wasmModule.getValue(resultPtr, 'i32');
const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32');
const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32');

if (errorPtr) {
// Read PgQueryError struct
const messagePtr = wasmModule.getValue(errorPtr, 'i32');
const funcnamePtr = wasmModule.getValue(errorPtr + 4, 'i32');
const filenamePtr = wasmModule.getValue(errorPtr + 8, 'i32');
const lineno = wasmModule.getValue(errorPtr + 12, 'i32');
const cursorpos = wasmModule.getValue(errorPtr + 16, 'i32');

const message = messagePtr ? wasmModule.UTF8ToString(messagePtr) : 'Unknown error';
const funcname = funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined;
const filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : undefined;

throw new SqlError(message, {
message,
cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, // Convert to 0-based
fileName: filename,
functionName: funcname,
lineNumber: lineno > 0 ? lineno : undefined
});
}

return JSON.parse(resultStr);
if (!parseTreePtr) {
throw new Error('No parse tree generated');
}

const parseTreeStr = wasmModule.UTF8ToString(parseTreePtr);
return JSON.parse(parseTreeStr);
} finally {
wasmModule._free(queryPtr);
if (resultPtr) {
wasmModule._wasm_free_string(resultPtr);
wasmModule._wasm_free_parse_result(resultPtr);
}
}
});
Expand Down Expand Up @@ -202,22 +340,61 @@ export function parseSync(query: string): ParseResult {
if (!wasmModule) {
throw new Error('WASM module not initialized. Call loadModule() first.');
}

// Input validation
if (query === null || query === undefined) {
throw new Error('Query cannot be null or undefined');
}

if (query === '') {
throw new Error('Query cannot be empty');
}

const queryPtr = stringToPtr(query);
let resultPtr = 0;

try {
resultPtr = wasmModule._wasm_parse_query(queryPtr);
const resultStr = ptrToString(resultPtr);
resultPtr = wasmModule._wasm_parse_query_raw(queryPtr);
if (!resultPtr) {
throw new Error('Failed to parse query: memory allocation failed');
}

if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
throw new Error(resultStr);
// Read the PgQueryParseResult struct
const parseTreePtr = wasmModule.getValue(resultPtr, 'i32');
const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32');
const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32');

if (errorPtr) {
// Read PgQueryError struct
const messagePtr = wasmModule.getValue(errorPtr, 'i32');
const funcnamePtr = wasmModule.getValue(errorPtr + 4, 'i32');
const filenamePtr = wasmModule.getValue(errorPtr + 8, 'i32');
const lineno = wasmModule.getValue(errorPtr + 12, 'i32');
const cursorpos = wasmModule.getValue(errorPtr + 16, 'i32');

const message = messagePtr ? wasmModule.UTF8ToString(messagePtr) : 'Unknown error';
const funcname = funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined;
const filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : undefined;

throw new SqlError(message, {
message,
cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, // Convert to 0-based
fileName: filename,
functionName: funcname,
lineNumber: lineno > 0 ? lineno : undefined
});
}

return JSON.parse(resultStr);
if (!parseTreePtr) {
throw new Error('No parse tree generated');
}

const parseTreeStr = wasmModule.UTF8ToString(parseTreePtr);
return JSON.parse(parseTreeStr);
} finally {
wasmModule._free(queryPtr);
if (resultPtr) {
wasmModule._wasm_free_string(resultPtr);
wasmModule._wasm_free_parse_result(resultPtr);
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions full/src/wasm_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ char* wasm_parse_query(const char* input) {
return parse_tree;
}

EMSCRIPTEN_KEEPALIVE
PgQueryParseResult* wasm_parse_query_raw(const char* input) {
PgQueryParseResult* result = (PgQueryParseResult*)malloc(sizeof(PgQueryParseResult));
if (!result) {
return NULL;
}

*result = pg_query_parse(input);
return result;
}

EMSCRIPTEN_KEEPALIVE
void wasm_free_parse_result(PgQueryParseResult* result) {
if (result) {
pg_query_free_parse_result(*result);
free(result);
}
}

EMSCRIPTEN_KEEPALIVE
char* wasm_deparse_protobuf(const char* protobuf_data, size_t data_len) {
if (!protobuf_data || data_len == 0) {
Expand Down
Loading