Skip to content
Closed
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
859 changes: 859 additions & 0 deletions PLAN.md

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions libpg-query-13/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
LIBPG_QUERY_TAG := 13-2.2.0

UNAME_S := $(shell uname -s)
UNAME_M := $(shell uname -m)

ifeq ($(UNAME_S),Linux)
PLATFORM := linux
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := darwin
endif

ifeq ($(UNAME_M),x86_64)
ARCH := x64
endif
ifeq ($(UNAME_M),arm64)
ARCH := arm64
endif

LIBPG_QUERY_DIR := libpg_query
LIBPG_QUERY_LIB := $(LIBPG_QUERY_DIR)/libpg_query.a

WASM_DIR := wasm
WASM_FILE := $(WASM_DIR)/libpg-query.wasm
WASM_JS_FILE := $(WASM_DIR)/libpg-query.js

.PHONY: build clean clean-cache

build: $(WASM_FILE)

$(WASM_FILE): $(LIBPG_QUERY_LIB) src/wasm_wrapper_light.c
@echo "Building WASM module for PostgreSQL 13..."
emcc -O3 \
-I$(LIBPG_QUERY_DIR) \
-I$(LIBPG_QUERY_DIR)/src/postgres/include \
src/wasm_wrapper_light.c \
$(LIBPG_QUERY_LIB) \
-o $(WASM_FILE) \
-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']" \
-sEXPORTED_RUNTIME_METHODS="['UTF8ToString','stringToUTF8','lengthBytesUTF8']" \
-sMODULARIZE=1 \
-sEXPORT_NAME="PgQueryModule" \
-sENVIRONMENT=web,node \
-sALLOW_MEMORY_GROWTH=1 \
-sINITIAL_MEMORY=16777216 \
-sSTACK_SIZE=1048576 \
-sNO_FILESYSTEM=1 \
-sNO_EXIT_RUNTIME=1

$(LIBPG_QUERY_LIB):
@echo "Cloning and building libpg_query $(LIBPG_QUERY_TAG)..."
rm -rf $(LIBPG_QUERY_DIR)
git clone --depth 1 --branch $(LIBPG_QUERY_TAG) https://github.com/pganalyze/libpg_query.git $(LIBPG_QUERY_DIR)
cd $(LIBPG_QUERY_DIR) && make build

clean:
rm -rf $(WASM_DIR)/*.wasm $(WASM_DIR)/*.js $(WASM_DIR)/*.wast

clean-cache:
rm -rf $(LIBPG_QUERY_DIR)
34 changes: 34 additions & 0 deletions libpg-query-13/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# libpg-query-13

PostgreSQL 13 query parser (lightweight version)

## Features

- Parse SQL queries into AST
- Generate query fingerprints
- Normalize queries
- Parse PL/pgSQL functions

## Installation

```bash
npm install libpg-query-13
```

## Usage

```javascript
import { parse, fingerprint, normalize } from 'libpg-query-13';

const ast = await parse('SELECT * FROM users');
const fp = await fingerprint('SELECT * FROM users WHERE id = $1');
const normalized = await normalize('SELECT * FROM users WHERE id = 123');
```

## Limitations

This is a lightweight version that does not include:
- Deparse functionality
- Scan functionality

For full functionality, use `libpg-query-full` or `libpg-query-multi`.
58 changes: 58 additions & 0 deletions libpg-query-13/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "libpg-query-13",
"version": "13.2.0",
"description": "PostgreSQL 13 query parser (lightweight - no deparse/scan)",
"homepage": "https://github.com/launchql/libpg-query-node",
"main": "./wasm/index.cjs",
"typings": "./wasm/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"wasm/*"
],
"exports": {
".": {
"types": "./wasm/index.d.ts",
"import": "./wasm/index.js",
"require": "./wasm/index.cjs"
}
},
"scripts": {
"clean": "yarn wasm:clean && rimraf cjs esm",
"build:js": "node scripts/build.js",
"build": "yarn clean; yarn wasm:build; yarn build:js",
"wasm:make": "docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk emmake make",
"wasm:build": "yarn wasm:make build",
"wasm:rebuild": "yarn wasm:make rebuild",
"wasm:clean": "yarn wasm:make clean",
"wasm:clean-cache": "yarn wasm:make clean-cache",
"test": "mocha test/*.test.js --timeout 5000"
},
"author": "Dan Lynch <[email protected]> (http://github.com/pyramation)",
"license": "LICENSE IN LICENSE",
"repository": {
"type": "git",
"url": "git://github.com/launchql/libpg-query-node.git"
},
"devDependencies": {
"@launchql/proto-cli": "1.25.0",
"@yamlize/cli": "^0.8.0",
"chai": "^3.5.0",
"mocha": "^11.7.0",
"rimraf": "5.0.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@pgsql/types": "^13.0.0"
},
"keywords": [
"sql",
"postgres",
"postgresql",
"pg",
"query",
"plpgsql",
"database"
]
}
213 changes: 213 additions & 0 deletions libpg-query-13/src/wasm_wrapper_light.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
#include "pg_query.h"
#include <emscripten.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>

static int validate_input(const char* input) {
return input != NULL && strlen(input) > 0;
}

static char* safe_strdup(const char* str) {
if (!str) return NULL;
char* result = strdup(str);
if (!result) {
return NULL;
}
return result;
}

static void* safe_malloc(size_t size) {
void* ptr = malloc(size);
if (!ptr && size > 0) {
return NULL;
}
return ptr;
}

EMSCRIPTEN_KEEPALIVE
char* wasm_parse_query(const char* input) {
if (!validate_input(input)) {
return safe_strdup("Invalid input: query cannot be null or empty");
}

PgQueryParseResult result = pg_query_parse(input);

if (result.error) {
char* error_msg = safe_strdup(result.error->message);
pg_query_free_parse_result(result);
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
}

char* parse_tree = safe_strdup(result.parse_tree);
pg_query_free_parse_result(result);
return parse_tree;
}

EMSCRIPTEN_KEEPALIVE
char* wasm_parse_plpgsql(const char* input) {
if (!validate_input(input)) {
return safe_strdup("Invalid input: query cannot be null or empty");
}

PgQueryPlpgsqlParseResult result = pg_query_parse_plpgsql(input);

if (result.error) {
char* error_msg = safe_strdup(result.error->message);
pg_query_free_plpgsql_parse_result(result);
return error_msg ? error_msg : safe_strdup("Memory allocation failed");
}

if (!result.plpgsql_funcs) {
pg_query_free_plpgsql_parse_result(result);
return safe_strdup("{\"plpgsql_funcs\":[]}");
}

size_t funcs_len = strlen(result.plpgsql_funcs);
size_t json_len = strlen("{\"plpgsql_funcs\":}") + funcs_len + 1;
char* wrapped_result = safe_malloc(json_len);

if (!wrapped_result) {
pg_query_free_plpgsql_parse_result(result);
return safe_strdup("Memory allocation failed");
}

int written = snprintf(wrapped_result, json_len, "{\"plpgsql_funcs\":%s}", result.plpgsql_funcs);

if (written >= json_len) {
free(wrapped_result);
pg_query_free_plpgsql_parse_result(result);
return safe_strdup("Buffer overflow prevented");
}

pg_query_free_plpgsql_parse_result(result);
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;
char* funcname;
char* filename;
int lineno;
int cursorpos;
char* context;
char* data;
size_t data_len;
} WasmDetailedResult;

EMSCRIPTEN_KEEPALIVE
WasmDetailedResult* wasm_parse_query_detailed(const char* input) {
WasmDetailedResult* result = safe_malloc(sizeof(WasmDetailedResult));
if (!result) {
return NULL;
}
memset(result, 0, sizeof(WasmDetailedResult));

if (!validate_input(input)) {
result->has_error = 1;
result->message = safe_strdup("Invalid input: query cannot be null or empty");
return result;
}

PgQueryParseResult parse_result = pg_query_parse(input);

if (parse_result.error) {
result->has_error = 1;
size_t message_len = strlen("Parse error: at line , position ") + strlen(parse_result.error->message) + 20;
char* prefixed_message = safe_malloc(message_len);
if (!prefixed_message) {
result->has_error = 1;
result->message = safe_strdup("Memory allocation failed");
pg_query_free_parse_result(parse_result);
return result;
}
snprintf(prefixed_message, message_len,
"Parse error: %s at line %d, position %d",
parse_result.error->message,
parse_result.error->lineno,
parse_result.error->cursorpos);
result->message = prefixed_message;
char* funcname_copy = parse_result.error->funcname ? safe_strdup(parse_result.error->funcname) : NULL;
char* filename_copy = parse_result.error->filename ? safe_strdup(parse_result.error->filename) : NULL;
char* context_copy = parse_result.error->context ? safe_strdup(parse_result.error->context) : NULL;

result->funcname = funcname_copy;
result->filename = filename_copy;
result->lineno = parse_result.error->lineno;
result->cursorpos = parse_result.error->cursorpos;
result->context = context_copy;
} else {
result->data = safe_strdup(parse_result.parse_tree);
if (result->data) {
result->data_len = strlen(result->data);
} else {
result->has_error = 1;
result->message = safe_strdup("Memory allocation failed");
}
}

pg_query_free_parse_result(parse_result);
return result;
}

EMSCRIPTEN_KEEPALIVE
void wasm_free_detailed_result(WasmDetailedResult* result) {
if (result) {
free(result->message);
free(result->funcname);
free(result->filename);
free(result->context);
free(result->data);
free(result);
}
}

EMSCRIPTEN_KEEPALIVE
void wasm_free_string(char* str) {
free(str);
}
Loading