Skip to content

Commit 410d9da

Browse files
feat: implement multi-version PostgreSQL support with folder-based structure
- Create lightweight versions for PG13-16 without deparse/scan functionality - Add full-featured PG17 version (libpg-query-full) - Implement libpg-query-multi package with PG15/16/17 and Parser class API - Add master Makefile for coordinated builds across all versions - Generate version-specific TypeScript types using PgProtoParser - Maintain existing test patterns for each version Key features: - Lightweight WASM builds exclude wasm_deparse_protobuf and wasm_scan functions - Parser class supports runtime version selection with error handling - Version-specific libpg_query tags: 13-2.2.0, 14-3.0.0, 15-4.2.4, 16-5.2.0, 17-6.1.0 - Type generation from GitHub proto files using pg-proto-parser - Comprehensive test suites for all packages - Bundle size optimization for lightweight versions Co-Authored-By: Dan Lynch <[email protected]>
1 parent 875ca29 commit 410d9da

Some content is hidden

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

46 files changed

+5087
-0
lines changed

PLAN.md

Lines changed: 859 additions & 0 deletions
Large diffs are not rendered by default.

libpg-query-13/Makefile

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
LIBPG_QUERY_TAG := 13-2.2.0
2+
3+
UNAME_S := $(shell uname -s)
4+
UNAME_M := $(shell uname -m)
5+
6+
ifeq ($(UNAME_S),Linux)
7+
PLATFORM := linux
8+
endif
9+
ifeq ($(UNAME_S),Darwin)
10+
PLATFORM := darwin
11+
endif
12+
13+
ifeq ($(UNAME_M),x86_64)
14+
ARCH := x64
15+
endif
16+
ifeq ($(UNAME_M),arm64)
17+
ARCH := arm64
18+
endif
19+
20+
LIBPG_QUERY_DIR := libpg_query
21+
LIBPG_QUERY_LIB := $(LIBPG_QUERY_DIR)/libpg_query.a
22+
23+
WASM_DIR := wasm
24+
WASM_FILE := $(WASM_DIR)/libpg-query.wasm
25+
WASM_JS_FILE := $(WASM_DIR)/libpg-query.js
26+
27+
.PHONY: build clean clean-cache
28+
29+
build: $(WASM_FILE)
30+
31+
$(WASM_FILE): $(LIBPG_QUERY_LIB) src/wasm_wrapper_light.c
32+
@echo "Building WASM module for PostgreSQL 13..."
33+
emcc -O3 \
34+
-I$(LIBPG_QUERY_DIR) \
35+
-I$(LIBPG_QUERY_DIR)/src/postgres/include \
36+
src/wasm_wrapper_light.c \
37+
$(LIBPG_QUERY_LIB) \
38+
-o $(WASM_FILE) \
39+
-sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_wasm_parse_plpgsql','_wasm_fingerprint','_wasm_normalize_query','_wasm_parse_query_detailed','_wasm_free_detailed_result','_wasm_free_string']" \
40+
-sEXPORTED_RUNTIME_METHODS="['UTF8ToString','stringToUTF8','lengthBytesUTF8']" \
41+
-sMODULARIZE=1 \
42+
-sEXPORT_NAME="PgQueryModule" \
43+
-sENVIRONMENT=web,node \
44+
-sALLOW_MEMORY_GROWTH=1 \
45+
-sINITIAL_MEMORY=16777216 \
46+
-sSTACK_SIZE=1048576 \
47+
-sNO_FILESYSTEM=1 \
48+
-sNO_EXIT_RUNTIME=1
49+
50+
$(LIBPG_QUERY_LIB):
51+
@echo "Cloning and building libpg_query $(LIBPG_QUERY_TAG)..."
52+
rm -rf $(LIBPG_QUERY_DIR)
53+
git clone --depth 1 --branch $(LIBPG_QUERY_TAG) https://github.com/pganalyze/libpg_query.git $(LIBPG_QUERY_DIR)
54+
cd $(LIBPG_QUERY_DIR) && make build
55+
56+
clean:
57+
rm -rf $(WASM_DIR)/*.wasm $(WASM_DIR)/*.js $(WASM_DIR)/*.wast
58+
59+
clean-cache:
60+
rm -rf $(LIBPG_QUERY_DIR)

libpg-query-13/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# libpg-query-13
2+
3+
PostgreSQL 13 query parser (lightweight version)
4+
5+
## Features
6+
7+
- Parse SQL queries into AST
8+
- Generate query fingerprints
9+
- Normalize queries
10+
- Parse PL/pgSQL functions
11+
12+
## Installation
13+
14+
```bash
15+
npm install libpg-query-13
16+
```
17+
18+
## Usage
19+
20+
```javascript
21+
import { parse, fingerprint, normalize } from 'libpg-query-13';
22+
23+
const ast = await parse('SELECT * FROM users');
24+
const fp = await fingerprint('SELECT * FROM users WHERE id = $1');
25+
const normalized = await normalize('SELECT * FROM users WHERE id = 123');
26+
```
27+
28+
## Limitations
29+
30+
This is a lightweight version that does not include:
31+
- Deparse functionality
32+
- Scan functionality
33+
34+
For full functionality, use `libpg-query-full` or `libpg-query-multi`.

libpg-query-13/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "libpg-query-13",
3+
"version": "13.2.0",
4+
"description": "PostgreSQL 13 query parser (lightweight - no deparse/scan)",
5+
"main": "wasm/index.js",
6+
"types": "wasm/index.d.ts",
7+
"type": "module",
8+
"exports": {
9+
".": {
10+
"import": "./wasm/index.js",
11+
"types": "./wasm/index.d.ts"
12+
}
13+
},
14+
"files": [
15+
"wasm/",
16+
"README.md"
17+
],
18+
"scripts": {
19+
"build": "EMSCRIPTEN=1 make build",
20+
"clean": "make clean",
21+
"test": "mocha test/*.test.js --timeout 5000"
22+
},
23+
"dependencies": {
24+
"@pgsql/types": "^13.0.0"
25+
},
26+
"devDependencies": {
27+
"chai": "^4.3.7",
28+
"mocha": "^10.2.0"
29+
},
30+
"keywords": ["postgresql", "parser", "sql", "pg13"],
31+
"author": "Dan Lynch",
32+
"license": "MIT"
33+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
#include "pg_query.h"
2+
#include <emscripten.h>
3+
#include <stdlib.h>
4+
#include <string.h>
5+
#include <stdio.h>
6+
#include <ctype.h>
7+
8+
static int validate_input(const char* input) {
9+
return input != NULL && strlen(input) > 0;
10+
}
11+
12+
static char* safe_strdup(const char* str) {
13+
if (!str) return NULL;
14+
char* result = strdup(str);
15+
if (!result) {
16+
return NULL;
17+
}
18+
return result;
19+
}
20+
21+
static void* safe_malloc(size_t size) {
22+
void* ptr = malloc(size);
23+
if (!ptr && size > 0) {
24+
return NULL;
25+
}
26+
return ptr;
27+
}
28+
29+
EMSCRIPTEN_KEEPALIVE
30+
char* wasm_parse_query(const char* input) {
31+
if (!validate_input(input)) {
32+
return safe_strdup("Invalid input: query cannot be null or empty");
33+
}
34+
35+
PgQueryParseResult result = pg_query_parse(input);
36+
37+
if (result.error) {
38+
char* error_msg = safe_strdup(result.error->message);
39+
pg_query_free_parse_result(result);
40+
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
41+
}
42+
43+
char* parse_tree = safe_strdup(result.parse_tree);
44+
pg_query_free_parse_result(result);
45+
return parse_tree;
46+
}
47+
48+
EMSCRIPTEN_KEEPALIVE
49+
char* wasm_parse_plpgsql(const char* input) {
50+
if (!validate_input(input)) {
51+
return safe_strdup("Invalid input: query cannot be null or empty");
52+
}
53+
54+
PgQueryPlpgsqlParseResult result = pg_query_parse_plpgsql(input);
55+
56+
if (result.error) {
57+
char* error_msg = safe_strdup(result.error->message);
58+
pg_query_free_plpgsql_parse_result(result);
59+
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
60+
}
61+
62+
if (!result.plpgsql_funcs) {
63+
pg_query_free_plpgsql_parse_result(result);
64+
return safe_strdup("{\"plpgsql_funcs\":[]}");
65+
}
66+
67+
size_t funcs_len = strlen(result.plpgsql_funcs);
68+
size_t json_len = strlen("{\"plpgsql_funcs\":}") + funcs_len + 1;
69+
char* wrapped_result = safe_malloc(json_len);
70+
71+
if (!wrapped_result) {
72+
pg_query_free_plpgsql_parse_result(result);
73+
return safe_strdup("Memory allocation failed");
74+
}
75+
76+
int written = snprintf(wrapped_result, json_len, "{\"plpgsql_funcs\":%s}", result.plpgsql_funcs);
77+
78+
if (written >= json_len) {
79+
free(wrapped_result);
80+
pg_query_free_plpgsql_parse_result(result);
81+
return safe_strdup("Buffer overflow prevented");
82+
}
83+
84+
pg_query_free_plpgsql_parse_result(result);
85+
return wrapped_result;
86+
}
87+
88+
EMSCRIPTEN_KEEPALIVE
89+
char* wasm_fingerprint(const char* input) {
90+
if (!validate_input(input)) {
91+
return safe_strdup("Invalid input: query cannot be null or empty");
92+
}
93+
94+
PgQueryFingerprintResult result = pg_query_fingerprint(input);
95+
96+
if (result.error) {
97+
char* error_msg = safe_strdup(result.error->message);
98+
pg_query_free_fingerprint_result(result);
99+
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
100+
}
101+
102+
char* fingerprint_str = safe_strdup(result.fingerprint_str);
103+
pg_query_free_fingerprint_result(result);
104+
return fingerprint_str;
105+
}
106+
107+
EMSCRIPTEN_KEEPALIVE
108+
char* wasm_normalize_query(const char* input) {
109+
if (!validate_input(input)) {
110+
return safe_strdup("Invalid input: query cannot be null or empty");
111+
}
112+
113+
PgQueryNormalizeResult result = pg_query_normalize(input);
114+
115+
if (result.error) {
116+
char* error_msg = safe_strdup(result.error->message);
117+
pg_query_free_normalize_result(result);
118+
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
119+
}
120+
121+
char* normalized = safe_strdup(result.normalized_query);
122+
pg_query_free_normalize_result(result);
123+
124+
if (!normalized) {
125+
return safe_strdup("Memory allocation failed");
126+
}
127+
128+
return normalized;
129+
}
130+
131+
typedef struct {
132+
int has_error;
133+
char* message;
134+
char* funcname;
135+
char* filename;
136+
int lineno;
137+
int cursorpos;
138+
char* context;
139+
char* data;
140+
size_t data_len;
141+
} WasmDetailedResult;
142+
143+
EMSCRIPTEN_KEEPALIVE
144+
WasmDetailedResult* wasm_parse_query_detailed(const char* input) {
145+
WasmDetailedResult* result = safe_malloc(sizeof(WasmDetailedResult));
146+
if (!result) {
147+
return NULL;
148+
}
149+
memset(result, 0, sizeof(WasmDetailedResult));
150+
151+
if (!validate_input(input)) {
152+
result->has_error = 1;
153+
result->message = safe_strdup("Invalid input: query cannot be null or empty");
154+
return result;
155+
}
156+
157+
PgQueryParseResult parse_result = pg_query_parse(input);
158+
159+
if (parse_result.error) {
160+
result->has_error = 1;
161+
size_t message_len = strlen("Parse error: at line , position ") + strlen(parse_result.error->message) + 20;
162+
char* prefixed_message = safe_malloc(message_len);
163+
if (!prefixed_message) {
164+
result->has_error = 1;
165+
result->message = safe_strdup("Memory allocation failed");
166+
pg_query_free_parse_result(parse_result);
167+
return result;
168+
}
169+
snprintf(prefixed_message, message_len,
170+
"Parse error: %s at line %d, position %d",
171+
parse_result.error->message,
172+
parse_result.error->lineno,
173+
parse_result.error->cursorpos);
174+
result->message = prefixed_message;
175+
char* funcname_copy = parse_result.error->funcname ? safe_strdup(parse_result.error->funcname) : NULL;
176+
char* filename_copy = parse_result.error->filename ? safe_strdup(parse_result.error->filename) : NULL;
177+
char* context_copy = parse_result.error->context ? safe_strdup(parse_result.error->context) : NULL;
178+
179+
result->funcname = funcname_copy;
180+
result->filename = filename_copy;
181+
result->lineno = parse_result.error->lineno;
182+
result->cursorpos = parse_result.error->cursorpos;
183+
result->context = context_copy;
184+
} else {
185+
result->data = safe_strdup(parse_result.parse_tree);
186+
if (result->data) {
187+
result->data_len = strlen(result->data);
188+
} else {
189+
result->has_error = 1;
190+
result->message = safe_strdup("Memory allocation failed");
191+
}
192+
}
193+
194+
pg_query_free_parse_result(parse_result);
195+
return result;
196+
}
197+
198+
EMSCRIPTEN_KEEPALIVE
199+
void wasm_free_detailed_result(WasmDetailedResult* result) {
200+
if (result) {
201+
free(result->message);
202+
free(result->funcname);
203+
free(result->filename);
204+
free(result->context);
205+
free(result->data);
206+
free(result);
207+
}
208+
}
209+
210+
EMSCRIPTEN_KEEPALIVE
211+
void wasm_free_string(char* str) {
212+
free(str);
213+
}

0 commit comments

Comments
 (0)