diff --git a/.github/workflows/build-wasm-no-docker.yaml b/.github/workflows/build-wasm-no-docker.yaml
index 71d20294..4cbceebb 100644
--- a/.github/workflows/build-wasm-no-docker.yaml
+++ b/.github/workflows/build-wasm-no-docker.yaml
@@ -42,15 +42,15 @@ jobs:
./emsdk install 3.1.59
./emsdk activate 3.1.59
source ./emsdk_env.sh
- working-directory: full
+ working-directory: versions/18
- name: Build with Emscripten π
run: |
source ./emsdk/emsdk_env.sh
emmake make
emmake make build
- working-directory: full
+ working-directory: versions/18
- name: Archive production artifacts π
uses: actions/upload-artifact@v4
with:
name: wasm-artifacts
- path: full/wasm
+ path: versions/18/wasm
diff --git a/.github/workflows/build-wasm.yml b/.github/workflows/build-wasm.yml
index d5200325..54d3bd4d 100644
--- a/.github/workflows/build-wasm.yml
+++ b/.github/workflows/build-wasm.yml
@@ -38,11 +38,11 @@ jobs:
- name: Build WASM π
run: pnpm run build
- working-directory: full
+ working-directory: versions/18
- name: Archive production artifacts π
uses: actions/upload-artifact@v4
with:
name: wasm-artifacts
- path: full/wasm/
+ path: versions/18/wasm/
retention-days: 7
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dac525b4..67b79d5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,6 @@ jobs:
strategy:
matrix:
package:
- - { name: 'full', path: 'full', version: '18' }
- { name: 'v13', path: 'versions/13', version: '13' }
- { name: 'v14', path: 'versions/14', version: '14' }
- { name: 'v15', path: 'versions/15', version: '15' }
@@ -78,7 +77,6 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
package:
- - { name: 'full', path: 'full', version: '18' }
- { name: 'v13', path: 'versions/13', version: '13' }
- { name: 'v14', path: 'versions/14', version: '14' }
- { name: 'v15', path: 'versions/15', version: '15' }
diff --git a/PUBLISH.md b/PUBLISH.md
index 956384ae..e86e4703 100644
--- a/PUBLISH.md
+++ b/PUBLISH.md
@@ -10,7 +10,6 @@ pnpm run publish:versions
This interactive script will:
- Check for uncommitted changes (will error if any exist)
- Let you select which versions to publish (or all)
-- Also includes the full package (@libpg-query/parser)
- Ask for version bump type (patch or minor only)
- Ask if you want to skip the build step (useful if already built)
- Always run tests (even if build is skipped)
@@ -163,32 +162,16 @@ npm install libpg-query@pg16 # PostgreSQL 16 specific
npm install libpg-query # Latest/default version
```
-## Full Package (@libpg-query/parser)
+## Full API on PG 18+
-### Quick Publish
-```bash
-cd full
-pnpm version patch
-git add . && git commit -m "release: bump @libpg-query/parser version"
-pnpm build
-pnpm test
-pnpm publish --tag pg17
-```
+The former `full/` package (`@libpg-query/parser`) has been retired. Starting with
+PostgreSQL 18, the regular `libpg-query` package (`versions/18`) ships the full API:
+`parse`, `parsePlPgSQL`, `scan`, `fingerprint`, `normalize` + sync variants.
+Versions 13β17 remain slim (parse only).
-### Promote to latest (optional)
-```bash
-npm dist-tag add @libpg-query/parser@pg17 latest
-```
-
-### What it does
-- Publishes `@libpg-query/parser` with tag `pg17`
-- Currently based on PostgreSQL 17
-- Includes full parser with all features
-
-### Install published package
```bash
-npm install @libpg-query/parser@pg17 # PostgreSQL 17 specific
-npm install @libpg-query/parser # Latest version
+npm install libpg-query@pg18 # full API
+npm install libpg-query@pg17 # parse only
```
## Parser Package (@pgsql/parser)
diff --git a/README.md b/README.md
index 21f928f4..072f72b8 100644
--- a/README.md
+++ b/README.md
@@ -73,11 +73,10 @@ This repository contains multiple packages to support different PostgreSQL versi
| Package | Description | PostgreSQL Versions | npm Package |
|---------|-------------|---------------------|-------------|
-| **[libpg-query](https://github.com/constructive-io/libpg-query-node/tree/main/versions)** | Lightweight parser (parse only) | 13, 14, 15, 16, 17, 18 | [`libpg-query`](https://www.npmjs.com/package/libpg-query) |
+| **[libpg-query](https://github.com/constructive-io/libpg-query-node/tree/main/versions)** | PostgreSQL parser (full API on PG 18+, parse-only on 13β17) | 13, 14, 15, 16, 17, 18 | [`libpg-query`](https://www.npmjs.com/package/libpg-query) |
| **[@pgsql/parser](https://github.com/constructive-io/libpg-query-node/tree/main/parser)** | Multi-version parser (runtime selection) | 15, 16, 17, 18 | [`@pgsql/parser`](https://www.npmjs.com/package/@pgsql/parser) |
| **[@pgsql/types](https://github.com/constructive-io/libpg-query-node/tree/main/types)** | TypeScript type definitions | 13, 14, 15, 16, 17, 18 | [`@pgsql/types`](https://www.npmjs.com/package/@pgsql/types) |
| **[@pgsql/enums](https://github.com/constructive-io/libpg-query-node/tree/main/enums)** | TypeScript enum definitions | 13, 14, 15, 16, 17, 18 | [`@pgsql/enums`](https://www.npmjs.com/package/@pgsql/enums) |
-| **[@libpg-query/parser](https://github.com/constructive-io/libpg-query-node/tree/main/full)** | Full parser with all features | 17 only | [`@libpg-query/parser`](https://www.npmjs.com/package/@libpg-query/parser) |
### Version Tags
@@ -99,10 +98,10 @@ npm install @pgsql/enums
### Which Package Should I Use?
-- **Just need to parse SQL?** β Use `libpg-query` (lightweight, all PG versions)
+- **Just need to parse SQL?** β Use `libpg-query` (all PG versions)
- **Need multiple versions at runtime?** β Use `@pgsql/parser` (dynamic version selection)
- **Need TypeScript types?** β Add `@pgsql/types` and/or `@pgsql/enums`
-- **Need fingerprint, normalize, scan, or PL/pgSQL parsing?** β Use `@libpg-query/parser` (PG 18)
+- **Need fingerprint, normalize, scan, or PL/pgSQL parsing?** β Use `libpg-query@pg18` β the full API ships on PG 18+ (13β17 remain parse-only)
## API Documentation
@@ -113,7 +112,6 @@ For detailed API documentation and usage examples, see the package-specific READ
- **@pgsql/parser** - [Multi-Version Parser Documentation](https://github.com/constructive-io/libpg-query-node/tree/main/parser)
- **@pgsql/types** - [Types Documentation](https://github.com/constructive-io/libpg-query-node/tree/main/types/18)
- **@pgsql/enums** - [Enums Documentation](https://github.com/constructive-io/libpg-query-node/tree/main/enums/18)
-- **@libpg-query/parser** - [Full Parser Documentation](https://github.com/constructive-io/libpg-query-node/tree/main/full)
## Build Instructions
diff --git a/REPO_NOTES.md b/REPO_NOTES.md
index dd3fa3a2..4ba54e9e 100644
--- a/REPO_NOTES.md
+++ b/REPO_NOTES.md
@@ -2,6 +2,14 @@
There is a templates/ dir to solve some of this.
+Versions with `x-publish.fullApi: true` (currently 18) are generated from the
+full-API templates in `templates/full/` and export the full API
+(parse, parsePlPgSQL, scan, fingerprint, normalize + sync variants);
+the other versions use the slim (parse-only) templates.
+β οΈ Note: the Makefiles for 15/17/18 have been hand-edited to point at
+constructive-io/libpg_query branches; `copy:templates` regenerates Makefiles
+with the pganalyze repo URL, so re-apply the repo override if you regenerate.
+
## Code Duplication π
### 1. Identical Test Files
diff --git a/full/.npmignore b/full/.npmignore
deleted file mode 100644
index 5099528c..00000000
--- a/full/.npmignore
+++ /dev/null
@@ -1,18 +0,0 @@
-# project files
-test
-.gitignore
-package.json
-build
-
-*.log
-npm-debug.log*
-
-# Dependency directories
-node_modules
-
-libpg_query/*
-
-# npm package lock
-package-lock.json
-yarn.lock
-
diff --git a/full/CHANGELOG.md b/full/CHANGELOG.md
deleted file mode 100644
index 1e26c64c..00000000
--- a/full/CHANGELOG.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# 17.2.1
-* Add normalize() and normalizeSync() functions for SQL normalization
-* Add fingerprint() and fingerprintSync() functions for query fingerprinting
-* Add isReady() function to check WASM module initialization status
-* Improve memory management and error handling in WASM wrapper
-* Remove unused dependencies (lodash, deasync, @launchql/protobufjs)
-* Split test suite into separate files for better organization
-* Update documentation with comprehensive function examples
-
-# 1.2.1
-* Rename package
-
-# 1.2.0
-* Add TS typings
-
-# 1.1.2 (07/23/19)
-* Fix build script
-
-# 1.1.0 (Republish, 07/23/19)
-* Rewrite to use Node-Addons-API (C++ wrapper for the stable N-API)
-* Support PL/PGSql Parsing
-* Support async parsing
-
-## 0.0.5 (November 19, 2015)
-* Update libpg_query to include latest A_CONST fixes
-
-## 0.0.4 (November 16, 2015)
-* Update libpg_query to include fix for empty string constants
-* Warning fixed in 0.0.3 is back for now until libpg_query has support for custom CFLAGS
-
-## 0.0.3 (November 14, 2015)
-* Update OSX static library to include `-mmacosx-version-min=10.5` to fix warnings on installation
-
-## 0.0.2 (November 14, 2015)
-* Rename repo to pg-query-native
-
-## 0.0.1 (November 14, 2015)
-* First version
diff --git a/full/Makefile b/full/Makefile
deleted file mode 100644
index e3284679..00000000
--- a/full/Makefile
+++ /dev/null
@@ -1,96 +0,0 @@
-WASM_OUT_DIR := wasm
-WASM_OUT_NAME := libpg-query
-WASM_MODULE_NAME := PgQueryModule
-# LIBPG_QUERY_REPO := https://github.com/pganalyze/libpg_query.git
-# LIBPG_QUERY_TAG := 17-6.1.0
-LIBPG_QUERY_REPO := https://github.com/constructive-io/libpg_query.git
-LIBPG_QUERY_TAG := 17-constructive
-
-CACHE_DIR := .cache
-
-OS ?= $(shell uname -s)
-ARCH ?= $(shell uname -m)
-
-ifdef EMSCRIPTEN
-PLATFORM := emscripten
-else ifeq ($(OS),Darwin)
-PLATFORM := darwin
-else ifeq ($(OS),Linux)
-PLATFORM := linux
-else
-$(error Unsupported platform: $(OS))
-endif
-
-ifdef EMSCRIPTEN
-ARCH := wasm
-endif
-
-PLATFORM_ARCH := $(PLATFORM)-$(ARCH)
-SRC_FILES := src/wasm_wrapper.c
-LIBPG_QUERY_DIR := $(CACHE_DIR)/$(PLATFORM_ARCH)/libpg_query/$(LIBPG_QUERY_TAG)
-LIBPG_QUERY_ARCHIVE := $(LIBPG_QUERY_DIR)/libpg_query.a
-LIBPG_QUERY_HEADER := $(LIBPG_QUERY_DIR)/pg_query.h
-CXXFLAGS := -O3
-
-ifdef EMSCRIPTEN
-OUT_FILES := $(foreach EXT,.js .wasm,$(WASM_OUT_DIR)/$(WASM_OUT_NAME)$(EXT))
-else
-$(error Native builds are no longer supported. Use EMSCRIPTEN=1 for WASM builds only.)
-endif
-
-# Clone libpg_query source (lives in CACHE_DIR)
-$(LIBPG_QUERY_DIR):
- mkdir -p $(CACHE_DIR)
- git clone -b $(LIBPG_QUERY_TAG) --single-branch $(LIBPG_QUERY_REPO) $(LIBPG_QUERY_DIR)
-
-$(LIBPG_QUERY_HEADER): $(LIBPG_QUERY_DIR)
-
-# Build libpg_query
-$(LIBPG_QUERY_ARCHIVE): $(LIBPG_QUERY_DIR)
- cd $(LIBPG_QUERY_DIR); $(MAKE) build
-
-# Build libpg-query-node WASM module
-$(OUT_FILES): $(LIBPG_QUERY_ARCHIVE) $(LIBPG_QUERY_HEADER) $(SRC_FILES)
-ifdef EMSCRIPTEN
- mkdir -p $(WASM_OUT_DIR)
- $(CC) \
- -v \
- $(CXXFLAGS) \
- -I$(LIBPG_QUERY_DIR) \
- -I$(LIBPG_QUERY_DIR)/vendor \
- -L$(LIBPG_QUERY_DIR) \
- -sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_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,worker" \
- -sASSERTIONS=0 \
- -sSINGLE_FILE=0 \
- -sMODULARIZE=1 \
- -sEXPORT_ES6=0 \
- -sALLOW_MEMORY_GROWTH=1 \
- -sINITIAL_MEMORY=134217728 \
- -sMAXIMUM_MEMORY=1073741824 \
- -sSTACK_SIZE=33554432 \
- -lpg_query \
- -o $@ \
- $(SRC_FILES)
-else
-$(error Native builds are no longer supported. Use EMSCRIPTEN=1 for WASM builds only.)
-endif
-
-# Commands
-build: $(OUT_FILES)
-
-build-cache: $(LIBPG_QUERY_ARCHIVE) $(LIBPG_QUERY_HEADER)
-
-rebuild: clean build
-
-rebuild-cache: clean-cache build-cache
-
-clean:
- -@ rm -r $(OUT_FILES) > /dev/null 2>&1
-
-clean-cache:
- -@ rm -rf $(LIBPG_QUERY_DIR)
-
-.PHONY: build build-cache rebuild rebuild-cache clean clean-cache
diff --git a/full/README.md b/full/README.md
deleted file mode 100644
index 2091d08a..00000000
--- a/full/README.md
+++ /dev/null
@@ -1,358 +0,0 @@
-# @libpg-query/parser
-
-
-
-
-
-
-
-
-
- 
-
-
-
-
-
-
-# The Real PostgreSQL Parser for JavaScript
-
-### Bring the power of PostgreSQLβs native parser to your JavaScript projects β no native builds, no platform headaches.
-
-This is the official PostgreSQL parser, compiled to WebAssembly (WASM) for seamless, cross-platform compatibility. Use it in Node.js or the browser, on Linux, Windows, or anywhere JavaScript runs.
-
-Built to power [pgsql-parser](https://github.com/constructive-io/pgsql-parser), this library delivers full fidelity with the Postgres C codebase β no rewrites, no shortcuts.
-
-### Features
-
-* π§ **Powered by PostgreSQL** β Uses the official Postgres C parser compiled to WebAssembly
-* π₯οΈ **Cross-Platform** β Runs smoothly on macOS, Linux, and Windows
-* π **Node.js & Browser Support** β Consistent behavior in any JS environment
-* π¦ **No Native Builds Required** β No compilation, no system-specific dependencies
-* π§ **Spec-Accurate Parsing** β Produces faithful, standards-compliant ASTs
-* π **Production-Grade** β Millions of downloads and trusted by countless projects and top teams
-
-## π For Round-trip Codegen
-
-> π― **Want to parse + deparse (full round trip)?**
-> We highly recommend using [`pgsql-parser`](https://github.com/constructive-io/pgsql-parser) which leverages a pure TypeScript deparser that has been battle-tested against 23,000+ SQL statements and is built on top of libpg-query.
-
-## Installation
-
-```sh
-npm install @libpg-query/parser
-```
-
-## Usage
-
-### `parse(query: string): Promise`
-
-Parses the SQL and returns a Promise for the parse tree. May reject with a parse error.
-
-```typescript
-import { parse } from '@libpg-query/parser';
-
-const result = await parse('SELECT * FROM users WHERE active = true');
-// Returns: ParseResult - parsed query object
-```
-
-### `parseSync(query: string): ParseResult`
-
-Synchronous version that returns the parse tree directly. May throw a parse error.
-
-```typescript
-import { parseSync } from '@libpg-query/parser';
-
-const result = parseSync('SELECT * FROM users WHERE active = true');
-// Returns: ParseResult - parsed query object
-```
-
-### `parsePlPgSQL(funcsSql: string): Promise`
-
-Parses the contents of a PL/pgSQL function from a `CREATE FUNCTION` declaration. Returns a Promise for the parse tree.
-
-```typescript
-import { parsePlPgSQL } from '@libpg-query/parser';
-
-const functionSql = `
-CREATE FUNCTION get_user_count() RETURNS integer AS $$
-BEGIN
- RETURN (SELECT COUNT(*) FROM users);
-END;
-$$ LANGUAGE plpgsql;
-`;
-
-const result = await parsePlPgSQL(functionSql);
-```
-
-### `parsePlPgSQLSync(funcsSql: string): ParseResult`
-
-Synchronous version of PL/pgSQL parsing.
-
-```typescript
-import { parsePlPgSQLSync } from '@libpg-query/parser';
-
-const result = parsePlPgSQLSync(functionSql);
-```
-
-### `fingerprint(sql: string): Promise`
-
-Generates a unique fingerprint for a SQL query that can be used for query identification and caching. Returns a Promise for a 16-character fingerprint string.
-
-```typescript
-import { fingerprint } from '@libpg-query/parser';
-
-const fp = await fingerprint('SELECT * FROM users WHERE active = $1');
-// Returns: string - unique 16-character fingerprint (e.g., "50fde20626009aba")
-```
-
-### `fingerprintSync(sql: string): string`
-
-Synchronous version that generates a unique fingerprint for a SQL query directly.
-
-```typescript
-import { fingerprintSync } from '@libpg-query/parser';
-
-const fp = fingerprintSync('SELECT * FROM users WHERE active = $1');
-// Returns: string - unique 16-character fingerprint
-```
-
-### `normalize(sql: string): Promise`
-
-Normalizes a SQL query by removing comments, standardizing whitespace, and converting to a canonical form. Returns a Promise for the normalized SQL string.
-
-```typescript
-import { normalize } from '@libpg-query/parser';
-
-const normalized = await normalize('SELECT * FROM users WHERE active = true');
-// Returns: string - normalized SQL query
-```
-
-### `normalizeSync(sql: string): string`
-
-Synchronous version that normalizes a SQL query directly.
-
-```typescript
-import { normalizeSync } from '@libpg-query/parser';
-
-const normalized = normalizeSync('SELECT * FROM users WHERE active = true');
-// Returns: string - normalized SQL query
-```
-
-### `scan(sql: string): Promise`
-
-Scans (tokenizes) a SQL query and returns detailed information about each token. Returns a Promise for a ScanResult containing all tokens with their positions, types, and classifications.
-
-```typescript
-import { scan } from '@libpg-query/parser';
-
-const result = await scan('SELECT * FROM users WHERE id = $1');
-// Returns: ScanResult - detailed tokenization information
-console.log(result.tokens[0]); // { start: 0, end: 6, text: "SELECT", tokenType: 651, tokenName: "UNKNOWN", keywordKind: 4, keywordName: "RESERVED_KEYWORD" }
-```
-
-### `scanSync(sql: string): ScanResult`
-
-Synchronous version that scans (tokenizes) a SQL query directly.
-
-```typescript
-import { scanSync } from '@libpg-query/parser';
-
-const result = scanSync('SELECT * FROM users WHERE id = $1');
-// Returns: ScanResult - detailed tokenization information
-```
-
-### Initialization
-
-The library provides both async and sync methods. Async methods handle initialization automatically, while sync methods require explicit initialization.
-
-#### Async Methods (Recommended)
-
-Async methods handle initialization automatically and are always safe to use:
-
-```typescript
-import { parse, scan } from '@libpg-query/parser';
-
-// These handle initialization automatically
-const result = await parse('SELECT * FROM users');
-const tokens = await scan('SELECT * FROM users');
-```
-
-#### Sync Methods
-
-Sync methods require explicit initialization using `loadModule()`:
-
-```typescript
-import { loadModule, parseSync, scanSync } from '@libpg-query/parser';
-
-// Initialize first
-await loadModule();
-
-// Now safe to use sync methods
-const result = parseSync('SELECT * FROM users');
-const tokens = scanSync('SELECT * FROM users');
-```
-
-### `loadModule(): Promise`
-
-Explicitly initializes the WASM module. Required before using any sync methods.
-
-```typescript
-import { loadModule, parseSync, scanSync } from '@libpg-query/parser';
-
-// Initialize before using sync methods
-await loadModule();
-const result = parseSync('SELECT * FROM users');
-const tokens = scanSync('SELECT * FROM users');
-```
-
-Note: We recommend using async methods as they handle initialization automatically. Use sync methods only when necessary, and always call `loadModule()` first.
-
-### Type Definitions
-
-```typescript
-interface ParseResult {
- version: number;
- stmts: Statement[];
-}
-
-interface Statement {
- stmt_type: string;
- stmt_len: number;
- stmt_location: number;
- query: string;
-}
-
-interface ScanResult {
- version: number;
- tokens: ScanToken[];
-}
-
-interface ScanToken {
- start: number; // Starting position in the SQL string
- end: number; // Ending position in the SQL string
- text: string; // The actual token text
- tokenType: number; // Numeric token type identifier
- tokenName: string; // Human-readable token type name
- keywordKind: number; // Numeric keyword classification
- keywordName: string; // Human-readable keyword classification
-}
-```
-
-**Note:** The return value is an array, as multiple queries may be provided in a single string (semicolon-delimited, as PostgreSQL expects).
-
-## Build Instructions
-
-This package uses a **WASM-only build system** for true cross-platform compatibility without native compilation dependencies.
-
-### Prerequisites
-
-- Node.js (version 16 or higher recommended)
-- [pnpm](https://pnpm.io/) (v8+ recommended)
-
-### Building WASM Artifacts
-
-1. **Install dependencies:**
- ```bash
- pnpm install
- ```
-
-2. **Build WASM artifacts:**
- ```bash
- pnpm run build
- ```
-
-3. **Clean WASM build (if needed):**
- ```bash
- pnpm run clean
- ```
-
-4. **Rebuild WASM artifacts from scratch:**
- ```bash
- pnpm run clean && pnpm run build
- ```
-
-### Build Process Details
-
-The WASM build process:
-- Uses Emscripten SDK for compilation
-- Compiles C wrapper code to WebAssembly
-- Generates `wasm/libpg-query.js` and `wasm/libpg-query.wasm` files
-- No native compilation or node-gyp dependencies required
-
-## Testing
-
-### Running Tests
-
-```bash
-pnpm run test
-```
-
-### Test Requirements
-
-- WASM artifacts must be built before running tests
-- If tests fail with "fetch failed" errors, rebuild WASM artifacts:
- ```bash
- pnpm run clean && pnpm run build && pnpm run test
- ```
-
-
-
-## Versions
-
-Built from the `17-constructive` branch of [constructive-io/libpg_query](https://github.com/constructive-io/libpg_query) (PostgreSQL 17, with PL/pgSQL serializer fixes).
-
-
-## Troubleshooting
-
-### Common Issues
-
-**"fetch failed" errors during tests:**
-- This indicates stale or missing WASM artifacts
-- Solution: `pnpm run clean && pnpm run build`
-
-**"WASM module not initialized" errors:**
-- Ensure you call an async method first to initialize the WASM module
-- Or use the async versions of methods which handle initialization automatically
-
-**Build environment issues:**
-- Ensure Emscripten SDK is properly installed and configured
-- Check that all required build dependencies are available
-
-### Build Artifacts
-
-The build process generates these files:
-- `wasm/libpg-query.js` - Emscripten-generated JavaScript loader
-- `wasm/libpg-query.wasm` - WebAssembly binary
-- `wasm/index.js` - ES module exports
-- `wasm/index.cjs` - CommonJS exports with sync wrappers
-
-## Credits
-
-Built on the excellent work of several contributors:
-
-* **[Dan Lynch](https://github.com/pyramation)** β official maintainer since 2018 and architect of the current implementation
-* **[Lukas Fittl](https://github.com/lfittl)** for [libpg_query](https://github.com/pganalyze/libpg_query) β the core PostgreSQL parser that powers this project
-* **[Greg Richardson](https://github.com/gregnr)** for AST guidance and pushing the transition to WASM and multiple PG runtimes for better interoperability
-* **[Ethan Resnick](https://github.com/ethanresnick)** for the original Node.js N-API bindings
-* **[Zac McCormick](https://github.com/zhm)** for the foundational [node-pg-query-native](https://github.com/zhm/node-pg-query-native) parser
-
-**π Built by the [Constructive](https://constructive.io) team β creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
-
-
-## Related
-
-* [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): The real PostgreSQL parser for Node.js, providing symmetric parsing and deparsing of SQL statements with actual PostgreSQL parser integration.
-* [pgsql-deparser](https://www.npmjs.com/package/pgsql-deparser): A streamlined tool designed for converting PostgreSQL ASTs back into SQL queries, focusing solely on deparser functionality to complement `pgsql-parser`.
-* [@pgsql/parser](https://www.npmjs.com/package/@pgsql/parser): Multi-version PostgreSQL parser with dynamic version selection at runtime, supporting PostgreSQL 15, 16, and 17 in a single package.
-* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): Offers TypeScript type definitions for PostgreSQL AST nodes, facilitating type-safe construction, analysis, and manipulation of ASTs.
-* [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): Provides TypeScript enum definitions for PostgreSQL constants, enabling type-safe usage of PostgreSQL enums and constants in your applications.
-* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): A comprehensive utility library for PostgreSQL, offering type-safe AST node creation and enum value conversions, simplifying the construction and manipulation of PostgreSQL ASTs.
-* [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): A TypeScript tool that parses PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
-* [libpg-query](https://github.com/constructive-io/libpg-query-node): The real PostgreSQL parser exposed for Node.js, used primarily in `pgsql-parser` for parsing and deparsing SQL queries.
-
-## Disclaimer
-
-AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
-
-No developer or entity involved in creating Software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the Software code or Software CLI, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
\ No newline at end of file
diff --git a/full/libpg_query/protobuf/.gitkeep b/full/libpg_query/protobuf/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/full/package.json b/full/package.json
deleted file mode 100644
index 91baefef..00000000
--- a/full/package.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "name": "@libpg-query/parser",
- "version": "17.8.0",
- "description": "The real PostgreSQL query parser",
- "homepage": "https://github.com/constructive-io/libpg-query-node",
- "main": "./wasm/index.cjs",
- "module": "./wasm/index.js",
- "typings": "./wasm/index.d.ts",
- "publishConfig": {
- "access": "public"
- },
- "files": [
- "wasm/*"
- ],
- "scripts": {
- "clean": "pnpm wasm:clean && rimraf cjs esm",
- "build:js": "node scripts/build.js",
- "build": "pnpm clean; pnpm wasm:build; pnpm build:js",
- "wasm:make": "docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) emscripten/emsdk emmake make",
- "wasm:build": "pnpm wasm:make build",
- "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/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js test/errors.test.js",
- "yamlize": "node ./scripts/yamlize.js"
- },
- "author": "Constructive ",
- "license": "MIT",
- "repository": {
- "type": "git",
- "url": "git://github.com/constructive-io/libpg-query-node.git"
- },
- "devDependencies": {
- "@yamlize/cli": "^0.8.0"
- },
- "dependencies": {
- "@pgsql/types": "^17.6.2"
- },
- "keywords": [
- "sql",
- "postgres",
- "postgresql",
- "pg",
- "query",
- "plpgsql",
- "database"
- ]
-}
diff --git a/full/scripts/build.js b/full/scripts/build.js
deleted file mode 100644
index 55d83240..00000000
--- a/full/scripts/build.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const { execSync } = require('child_process');
-
-// Run TypeScript compilation
-console.log('Compiling TypeScript...');
-execSync('tsc', { stdio: 'inherit' });
-execSync('tsc -p tsconfig.esm.json', { stdio: 'inherit' });
-
-// Rename files to have correct extensions
-const wasmDir = path.join(__dirname, '../wasm');
-const cjsDir = path.join(__dirname, '../cjs');
-const esmDir = path.join(__dirname, '../esm');
-
-// Ensure wasm directory exists
-if (!fs.existsSync(wasmDir)) {
- fs.mkdirSync(wasmDir, { recursive: true });
-}
-
-// Rename CommonJS files
-fs.renameSync(
- path.join(cjsDir, 'index.js'),
- path.join(wasmDir, 'index.cjs')
-);
-
-// Rename ESM files
-fs.renameSync(
- path.join(esmDir, 'index.js'),
- path.join(wasmDir, 'index.js')
-);
-
-// Rename declaration files
-fs.renameSync(
- path.join(cjsDir, 'index.d.ts'),
- path.join(wasmDir, 'index.d.ts')
-);
-
-console.log('Build completed successfully!');
\ No newline at end of file
diff --git a/full/src/proto.d.ts b/full/src/proto.d.ts
deleted file mode 100644
index 55a504f3..00000000
--- a/full/src/proto.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-declare module '../proto.js' {
- export namespace pg_query {
- interface ParseResult {
- version: number;
- stmts: Statement[];
- }
-
- interface Statement {
- stmt_type: string;
- stmt_len: number;
- stmt_location: number;
- query: string;
- }
-
- class ParseResult {
- static fromObject(obj: ParseResult): ParseResult;
- static encode(msg: ParseResult): { finish(): Uint8Array };
- }
- }
-}
\ No newline at end of file
diff --git a/full/test/errors.test.js b/full/test/errors.test.js
deleted file mode 100644
index f6c0fd61..00000000
--- a/full/test/errors.test.js
+++ /dev/null
@@ -1,325 +0,0 @@
-const { describe, it, before } = require('node:test');
-const assert = require('node:assert/strict');
-const { parseSync, loadModule, formatSqlError, hasSqlDetails } = require('../wasm/index.cjs');
-
-describe('Enhanced Error Handling', () => {
- before(async () => {
- await loadModule();
- });
-
- describe('Error Details Structure', () => {
- it('should include sqlDetails property on parse errors', () => {
- assert.throws(() => {
- parseSync('SELECT * FROM users WHERE id = @');
- });
-
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- } catch (error) {
- assert.ok('sqlDetails' in error);
- assert.ok('message' in error.sqlDetails);
- assert.ok('cursorPosition' in error.sqlDetails);
- assert.ok('fileName' in error.sqlDetails);
- assert.ok('functionName' in error.sqlDetails);
- assert.ok('lineNumber' in error.sqlDetails);
- }
- });
-
- it('should have correct cursor position (0-based)', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 32);
- }
- });
-
- it('should identify error source file', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.fileName, 'scan.l');
- assert.equal(error.sqlDetails.functionName, 'scanner_yyerror');
- }
- });
- });
-
- describe('Error Position Accuracy', () => {
- const positionTests = [
- { query: '@ SELECT * FROM users', expectedPos: 0, desc: 'error at start' },
- { query: 'SELECT @ FROM users', expectedPos: 9, desc: 'error after SELECT' },
- { query: 'SELECT * FROM users WHERE @ = 1', expectedPos: 28, desc: 'error after WHERE' },
- { query: 'SELECT * FROM users WHERE id = @', expectedPos: 32, desc: 'error at end' },
- { query: 'INSERT INTO users (id, name) VALUES (1, @)', expectedPos: 41, desc: 'error in VALUES' },
- { query: 'UPDATE users SET name = @ WHERE id = 1', expectedPos: 26, desc: 'error in SET' },
- { query: 'CREATE TABLE test (id INT, name @)', expectedPos: 32, desc: 'error in CREATE TABLE' },
- ];
-
- positionTests.forEach(({ query, expectedPos, desc }) => {
- it(`should correctly identify position for ${desc}`, () => {
- try {
- parseSync(query);
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, expectedPos);
- }
- });
- });
- });
-
- describe('Error Types', () => {
- it('should handle unterminated string literals', () => {
- try {
- parseSync("SELECT * FROM users WHERE name = 'unclosed");
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error.message.includes('unterminated quoted string'));
- assert.equal(error.sqlDetails.cursorPosition, 33);
- }
- });
-
- it('should handle unterminated quoted identifiers', () => {
- try {
- parseSync('SELECT * FROM users WHERE name = "unclosed');
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error.message.includes('unterminated quoted identifier'));
- assert.equal(error.sqlDetails.cursorPosition, 33);
- }
- });
-
- it('should handle invalid tokens', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = $');
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error.message.includes('syntax error at or near "$"'));
- assert.equal(error.sqlDetails.cursorPosition, 31);
- }
- });
-
- it('should handle reserved keywords', () => {
- try {
- parseSync('SELECT * FROM table');
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error.message.includes('syntax error at or near "table"'));
- assert.equal(error.sqlDetails.cursorPosition, 14);
- }
- });
-
- it('should handle syntax error in WHERE clause', () => {
- try {
- parseSync('SELECT * FROM users WHERE');
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error.message.includes('syntax error at end of input'));
- assert.equal(error.sqlDetails.cursorPosition, 25);
- }
- });
- });
-
- describe('formatSqlError Helper', () => {
- it('should format error with position indicator', () => {
- try {
- parseSync("SELECT * FROM users WHERE id = 'unclosed");
- assert.fail('Expected error');
- } catch (error) {
- const formatted = formatSqlError(error, "SELECT * FROM users WHERE id = 'unclosed");
- assert.ok(formatted.includes('Error: unterminated quoted string'));
- assert.ok(formatted.includes('Position: 31'));
- assert.ok(formatted.includes("SELECT * FROM users WHERE id = 'unclosed"));
- assert.ok(formatted.includes(' ^'));
- }
- });
-
- it('should respect showPosition option', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- const formatted = formatSqlError(error, 'SELECT * FROM users WHERE id = @', {
- showPosition: false
- });
- assert.ok(!formatted.includes('^'));
- assert.ok(formatted.includes('Position: 32'));
- }
- });
-
- it('should respect showQuery option', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- const formatted = formatSqlError(error, 'SELECT * FROM users WHERE id = @', {
- showQuery: false
- });
- assert.ok(!formatted.includes('SELECT * FROM users'));
- assert.ok(formatted.includes('Error:'));
- assert.ok(formatted.includes('Position:'));
- }
- });
-
- it('should truncate long queries', () => {
- const longQuery = 'SELECT ' + 'a, '.repeat(50) + 'z FROM users WHERE id = @';
- try {
- parseSync(longQuery);
- assert.fail('Expected error');
- } catch (error) {
- const formatted = formatSqlError(error, longQuery, { maxQueryLength: 50 });
- assert.ok(formatted.includes('...'));
- const lines = formatted.split('\n');
- const queryLine = lines.find(line => line.includes('...'));
- assert.ok(queryLine.length <= 56); // 50 + 2*3 for ellipsis
- }
- });
-
- it('should handle color option without breaking output', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- const formatted = formatSqlError(error, 'SELECT * FROM users WHERE id = @', {
- color: true
- });
- assert.ok(formatted.includes('Error:'));
- assert.ok(formatted.includes('Position:'));
- // Should contain ANSI codes but still be readable
- const cleanFormatted = formatted.replace(/\x1b\[[0-9;]*m/g, '');
- assert.ok(cleanFormatted.includes('syntax error'));
- }
- });
- });
-
- describe('hasSqlDetails Type Guard', () => {
- it('should return true for SQL parse errors', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(hasSqlDetails(error), true);
- }
- });
-
- it('should return false for regular errors', () => {
- const regularError = new Error('Regular error');
- assert.equal(hasSqlDetails(regularError), false);
- });
-
- it('should return false for non-Error objects', () => {
- assert.equal(hasSqlDetails('string'), false);
- assert.equal(hasSqlDetails(123), false);
- assert.equal(hasSqlDetails(null), false);
- assert.equal(hasSqlDetails(undefined), false);
- assert.equal(hasSqlDetails({}), false);
- });
-
- it('should return false for Error with incomplete sqlDetails', () => {
- const error = new Error('Test');
- error.sqlDetails = { message: 'test' }; // Missing cursorPosition
- assert.equal(hasSqlDetails(error), false);
- });
- });
-
- describe('Edge Cases', () => {
- it('should handle empty query', () => {
- assert.throws(() => parseSync(''), {
- message: 'Query cannot be empty'
- });
- });
-
- it('should handle null query', () => {
- assert.throws(() => parseSync(null), {
- message: 'Query cannot be null or undefined'
- });
- });
-
- it('should handle undefined query', () => {
- assert.throws(() => parseSync(undefined), {
- message: 'Query cannot be null or undefined'
- });
- });
-
- it('should handle @ in comments', () => {
- const query = 'SELECT * FROM users /* @ in comment */ WHERE id = 1';
- assert.doesNotThrow(() => parseSync(query));
- });
-
- it('should handle @ in strings', () => {
- const query = 'SELECT * FROM users WHERE email = \'user@example.com\'';
- assert.doesNotThrow(() => parseSync(query));
- });
- });
-
- describe('Complex Error Scenarios', () => {
- it('should handle errors in CASE statements', () => {
- try {
- parseSync('SELECT CASE WHEN id = 1 THEN "one" WHEN id = 2 THEN @ ELSE "other" END FROM users');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 54);
- }
- });
-
- it('should handle errors in subqueries', () => {
- try {
- parseSync('SELECT * FROM users WHERE id IN (SELECT @ FROM orders)');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 42);
- }
- });
-
- it('should handle errors in function calls', () => {
- try {
- parseSync('SELECT COUNT(@) FROM users');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 14);
- }
- });
-
- it('should handle errors in second statement', () => {
- try {
- parseSync('SELECT * FROM users; SELECT * FROM orders WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 54);
- }
- });
-
- it('should handle errors in CTE', () => {
- try {
- parseSync('WITH cte AS (SELECT * FROM users WHERE id = @) SELECT * FROM cte');
- assert.fail('Expected error');
- } catch (error) {
- assert.equal(error.sqlDetails.cursorPosition, 45);
- }
- });
- });
-
- describe('Backward Compatibility', () => {
- it('should maintain Error instance', () => {
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- assert.fail('Expected error');
- } catch (error) {
- assert.ok(error instanceof Error);
- assert.ok(error.message);
- assert.ok(error.stack);
- }
- });
-
- it('should work with standard error handling', () => {
- let caught = false;
- try {
- parseSync('SELECT * FROM users WHERE id = @');
- } catch (e) {
- caught = true;
- assert.ok(e.message.includes('syntax error'));
- }
- assert.equal(caught, true);
- });
- });
-});
\ No newline at end of file
diff --git a/full/test/parsing.test.js b/full/test/parsing.test.js
deleted file mode 100644
index 5477c6a0..00000000
--- a/full/test/parsing.test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-const query = require("../");
-const { describe, it, before, after, beforeEach, afterEach } = require('node:test');
-const assert = require('node:assert/strict');
-
-function removeLocationProperties(obj) {
- if (typeof obj !== 'object' || obj === null) {
- return obj;
- }
-
- if (Array.isArray(obj)) {
- return obj.map(item => removeLocationProperties(item));
- }
-
- const result = {};
- for (const key in obj) {
- if (obj.hasOwnProperty(key)) {
- if (key === 'location' || key === 'stmt_len' || key === 'stmt_location') {
- continue; // Skip location-related properties
- }
- result[key] = removeLocationProperties(obj[key]);
- }
- }
- return result;
-}
-
-describe("Query Parsing", () => {
- before(async () => {
- await query.parse("SELECT 1");
- });
-
- describe("Sync Parsing", () => {
- it("should return a single-item parse result for common queries", () => {
- const queries = ["select 1", "select null", "select ''", "select a, b"];
- const results = queries.map(query.parseSync);
- results.forEach((res) => {
- assert.equal(res.stmts.length, 1);
- });
-
- const selectedDatas = results.map(
- (it) => it.stmts[0].stmt.SelectStmt.targetList
- );
-
- assert.equal(selectedDatas[0][0].ResTarget.val.A_Const.ival.ival, 1);
- assert.equal(selectedDatas[1][0].ResTarget.val.A_Const.isnull, true);
- assert.equal(selectedDatas[2][0].ResTarget.val.A_Const.sval.sval, "");
- assert.equal(selectedDatas[3].length, 2);
- });
-
- it("should support parsing multiple queries", () => {
- const res = query.parseSync("select 1; select null;");
- assert.deepEqual(res.stmts.map(removeLocationProperties), [
- ...query.parseSync("select 1;").stmts.map(removeLocationProperties),
- ...query.parseSync("select null;").stmts.map(removeLocationProperties),
- ]);
- });
-
- it("should not parse a bogus query", () => {
- assert.throws(() => query.parseSync("NOT A QUERY"), Error);
- });
- });
-
- describe("Async parsing", () => {
- it("should return a promise resolving to same result", async () => {
- const testQuery = "select * from john;";
- const resPromise = query.parse(testQuery);
- const res = await resPromise;
-
- assert.ok(resPromise instanceof Promise);
- assert.deepEqual(res, query.parseSync(testQuery));
- });
-
- it("should reject on bogus queries", async () => {
- return query.parse("NOT A QUERY").then(
- () => {
- throw new Error("should have rejected");
- },
- (e) => {
- assert.ok(e instanceof Error);
- assert.match(e.message, /NOT/);
- }
- );
- });
- });
-});
diff --git a/full/tsconfig.esm.json b/full/tsconfig.esm.json
deleted file mode 100644
index 63012c17..00000000
--- a/full/tsconfig.esm.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "module": "es2022",
- "rootDir": "src/",
- "declaration": false,
- "outDir": "esm/"
- }
- }
\ No newline at end of file
diff --git a/full/tsconfig.json b/full/tsconfig.json
deleted file mode 100644
index d7da207b..00000000
--- a/full/tsconfig.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "compilerOptions": {
- "target": "es2022",
- "module": "commonjs",
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "strict": true,
- "strictNullChecks": false,
- "skipLibCheck": true,
- "sourceMap": false,
- "declaration": true,
- "resolveJsonModule": true,
- "moduleResolution": "node",
- "outDir": "cjs/",
- "rootDir": "src"
- },
- "exclude": ["cjs", "esm", "wasm", "node_modules", "**/*.test.ts"]
-}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 712b71b5..eef64ba6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -72,16 +72,6 @@ importers:
version: 1.28.2
publishDirectory: dist
- full:
- dependencies:
- '@pgsql/types':
- specifier: ^17.6.2
- version: 17.6.2
- devDependencies:
- '@yamlize/cli':
- specifier: ^0.8.0
- version: 0.8.0
-
parser:
devDependencies:
cross-env:
@@ -465,17 +455,6 @@ packages:
undici-types: 6.21.0
dev: true
- /@yamlize/cli@0.8.0:
- resolution: {integrity: sha512-OuhQ/gYLCuMjENdLMF8UXgM32p7blBB0FxwS4R2Mw4jk/9uvv87uCz2ptq9VB7GjNTNbnRTQKw+bAbwCXyngCA==}
- hasBin: true
- dependencies:
- chalk: 4.1.0
- inquirerer: 1.9.1
- js-yaml: 4.1.0
- minimist: 1.2.8
- yamlize: 0.8.0
- dev: true
-
/acorn-walk@8.3.4:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
@@ -515,10 +494,6 @@ packages:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
dev: true
- /argparse@2.0.1:
- resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
- dev: true
-
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
dev: true
@@ -541,22 +516,6 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
- /chalk@4.1.0:
- resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==}
- engines: {node: '>=10'}
- dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
- dev: true
-
- /chalk@4.1.2:
- resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
- engines: {node: '>=10'}
- dependencies:
- ansi-styles: 4.3.0
- supports-color: 7.2.0
- dev: true
-
/cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
dependencies:
@@ -716,11 +675,6 @@ packages:
engines: {node: '>=4'}
dev: true
- /has-flag@4.0.0:
- resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
- engines: {node: '>=8'}
- dev: true
-
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@@ -733,15 +687,6 @@ packages:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
dev: true
- /inquirerer@1.9.1:
- resolution: {integrity: sha512-c7N3Yd9warVEpWdyX04dJUtYSad1qZFnNQYsKdqk0Av4qRg83lmxSnhWLn8Ok+UNzj87xXxo/ww0ReIL3ZO92g==}
- dependencies:
- chalk: 4.1.2
- deepmerge: 4.3.1
- js-yaml: 4.1.0
- minimist: 1.2.8
- dev: true
-
/is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
@@ -778,13 +723,6 @@ packages:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: true
- /js-yaml@4.1.0:
- resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
- hasBin: true
- dependencies:
- argparse: 2.0.1
- dev: true
-
/jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'}
@@ -828,10 +766,6 @@ packages:
brace-expansion: 2.0.2
dev: true
- /minimist@1.2.8:
- resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
- dev: true
-
/minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -843,12 +777,6 @@ packages:
hasBin: true
dev: true
- /mkdirp@3.0.1:
- resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
- engines: {node: '>=10'}
- hasBin: true
- dev: true
-
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: true
@@ -1026,13 +954,6 @@ packages:
ansi-regex: 6.1.0
dev: true
- /supports-color@7.2.0:
- resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
- engines: {node: '>=8'}
- dependencies:
- has-flag: 4.0.0
- dev: true
-
/through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
dependencies:
@@ -1139,14 +1060,6 @@ packages:
engines: {node: '>=10'}
dev: true
- /yamlize@0.8.0:
- resolution: {integrity: sha512-2sXxXTr4gZuIP1TmVmm9yJc/WirEKsqctk/gk4MzPGuochfSAY4+OxKXXqFj02HejQmEgAFRun7b0Ec6YjlE7A==}
- dependencies:
- js-yaml: 4.1.0
- mkdirp: 3.0.1
- nested-obj: 0.0.1
- dev: true
-
/yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 71353548..953012e7 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,6 +1,5 @@
packages:
- 'parser'
- - 'full'
- 'versions/18'
- 'versions/17'
- 'versions/16'
diff --git a/scripts/analyze-sizes.js b/scripts/analyze-sizes.js
index 674d8642..c93e45df 100644
--- a/scripts/analyze-sizes.js
+++ b/scripts/analyze-sizes.js
@@ -74,9 +74,7 @@ function analyzePackage(packagePath, packageName) {
// Get all version packages dynamically
function getVersionPackages() {
const versionsDir = './versions';
- const packages = [
- { path: './full', name: 'full (Full)', version: 'original' }
- ];
+ const packages = [];
if (fs.existsSync(versionsDir)) {
const versions = fs.readdirSync(versionsDir)
diff --git a/scripts/copy-templates.js b/scripts/copy-templates.js
index a2a6fb79..6d8d312d 100755
--- a/scripts/copy-templates.js
+++ b/scripts/copy-templates.js
@@ -32,13 +32,15 @@ function loadVersionConfigs() {
const packageData = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
const version = packageData['x-publish']?.pgVersion;
const libpgQueryTag = packageData['x-publish']?.libpgQueryTag;
+ const fullApi = packageData['x-publish']?.fullApi === true;
if (version && libpgQueryTag) {
configs[version] = {
tag: libpgQueryTag,
- hasEmscriptenPatch: version === '13' // Only version 13 needs the patch
+ hasEmscriptenPatch: version === '13', // Only version 13 needs the patch
+ fullApi
};
- console.log(` Version ${version}: tag ${libpgQueryTag}`);
+ console.log(` Version ${version}: tag ${libpgQueryTag}${fullApi ? ' (full API)' : ''}`);
} else {
console.warn(` Warning: Missing x-publish data in ${packageFile}`);
}
@@ -54,14 +56,18 @@ function loadVersionConfigs() {
// Load configurations
const VERSION_CONFIGS = loadVersionConfigs();
-// Files to copy from templates
+// Files to copy from templates. Full-API versions use the variants in
+// templates/full/ for the source files.
const TEMPLATE_FILES = [
- { src: 'LICENSE', dest: 'LICENSE', header: false },
- { src: 'wasm_wrapper.c', dest: 'src/wasm_wrapper.c', header: HEADER },
- { src: 'libpg-query.d.ts', dest: 'src/libpg-query.d.ts', header: HEADER },
- { src: 'index.ts', dest: 'src/index.ts', header: HEADER }
+ { src: 'LICENSE', dest: 'LICENSE', header: false, hasFullVariant: false },
+ { src: 'wasm_wrapper.c', dest: 'src/wasm_wrapper.c', header: HEADER, hasFullVariant: true },
+ { src: 'libpg-query.d.ts', dest: 'src/libpg-query.d.ts', header: HEADER, hasFullVariant: true },
+ { src: 'index.ts', dest: 'src/index.ts', header: HEADER, hasFullVariant: true }
];
+const SLIM_EXPORTED_FUNCTIONS = "['_malloc','_free','_wasm_parse_query_raw','_wasm_free_parse_result']";
+const FULL_EXPORTED_FUNCTIONS = "['_malloc','_free','_wasm_parse_query','_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']";
+
function copyTemplates() {
const templatesDir = path.join(__dirname, '..', 'templates');
const versionsDir = path.join(__dirname, '..', 'versions');
@@ -73,7 +79,9 @@ function copyTemplates() {
// Copy template files
for (const file of TEMPLATE_FILES) {
- const srcPath = path.join(templatesDir, file.src);
+ const srcPath = (config.fullApi && file.hasFullVariant)
+ ? path.join(templatesDir, 'full', file.src)
+ : path.join(templatesDir, file.src);
const destPath = path.join(versionDir, file.dest);
// Ensure destination directory exists
@@ -98,6 +106,10 @@ function copyTemplates() {
// Process Makefile template
const makefileTemplate = fs.readFileSync(path.join(templatesDir, 'Makefile.template'), 'utf8');
let makefileContent = makefileTemplate.replace(/{{VERSION_TAG}}/g, config.tag);
+ makefileContent = makefileContent.replace(
+ /{{EXPORTED_FUNCTIONS}}/g,
+ config.fullApi ? FULL_EXPORTED_FUNCTIONS : SLIM_EXPORTED_FUNCTIONS
+ );
// Handle the USE_EMSCRIPTEN_PATCH placeholder
if (config.hasEmscriptenPatch) {
diff --git a/scripts/publish-versions.js b/scripts/publish-versions.js
index ed9d8e0b..f9e08a86 100755
--- a/scripts/publish-versions.js
+++ b/scripts/publish-versions.js
@@ -29,25 +29,16 @@ async function main() {
.filter(dir => /^\d+$/.test(dir))
.sort((a, b) => parseInt(b) - parseInt(a)); // Sort descending
- // Also check for full package
- const fullPackagePath = path.join(__dirname, '..', 'full', 'package.json');
- const hasFullPackage = fs.existsSync(fullPackagePath);
-
console.log('π¦ Available packages:');
versionDirs.forEach(v => console.log(` - PostgreSQL ${v} (versions/${v})`));
- if (hasFullPackage) {
- console.log(` - Full package (./full) - PostgreSQL 17`);
- }
console.log();
// Ask which versions to publish
const publishAll = await question('Publish all packages? (y/N): ');
let selectedVersions = [];
- let includeFullPackage = false;
if (publishAll.toLowerCase() === 'y') {
selectedVersions = versionDirs;
- includeFullPackage = hasFullPackage;
} else {
// Let user select versions
for (const version of versionDirs) {
@@ -56,14 +47,9 @@ async function main() {
selectedVersions.push(version);
}
}
-
- if (hasFullPackage) {
- const publishFull = await question(`Publish full package (PostgreSQL 17)? (y/N): `);
- includeFullPackage = publishFull.toLowerCase() === 'y';
- }
}
- if (selectedVersions.length === 0 && !includeFullPackage) {
+ if (selectedVersions.length === 0) {
console.log('\nβ No packages selected for publishing.');
rl.close();
return;
@@ -78,9 +64,6 @@ async function main() {
console.log(`\nπ Will publish:`);
selectedVersions.forEach(v => console.log(` - PostgreSQL ${v} (${bump} bump)`));
- if (includeFullPackage) {
- console.log(` - Full package (${bump} bump)`);
- }
// Ask about building
const skipBuild = await question('\nSkip build step? (y/N): ');
@@ -141,71 +124,20 @@ async function main() {
}
}
- // Process full package if selected
- if (includeFullPackage) {
- console.log(`\nπ¦ Publishing full package...`);
- const fullPath = path.join(__dirname, '..', 'full');
-
- try {
- // Version bump
- console.log(` π Bumping version (${bump})...`);
- execSync(`pnpm version ${bump}`, { cwd: fullPath, stdio: 'inherit' });
-
- // Commit
- console.log(` πΎ Committing version bump...`);
- execSync(`git add package.json`, { cwd: fullPath });
- execSync(`git commit -m "release: bump @libpg-query/parser version"`, { stdio: 'inherit' });
-
- // Build (if not skipped)
- if (shouldBuild) {
- console.log(` π¨ Building...`);
- execSync('pnpm build', { cwd: fullPath, stdio: 'inherit' });
- } else {
- console.log(` βοΈ Skipping build step`);
- }
-
- // Test (always run)
- console.log(` π§ͺ Running tests...`);
- execSync('pnpm test', { cwd: fullPath, stdio: 'inherit' });
-
- // Publish with pg17 tag
- console.log(` π€ Publishing to npm with pg17 tag...`);
- // use npm so staged changes are OK
- execSync('npm publish --tag pg17', { cwd: fullPath, stdio: 'inherit' });
-
- console.log(` β
Full package published successfully with pg17 tag!`);
- } catch (error) {
- console.error(` β Failed to publish full package:`, error.message);
- }
- }
-
// Ask about promoting to latest
- if (selectedVersions.includes('17') || includeFullPackage) {
+ if (selectedVersions.includes('17')) {
console.log('\nπ·οΈ Tag Management');
- if (selectedVersions.includes('17')) {
- const promoteVersions = await question('Promote libpg-query@pg17 to latest? (y/N): ');
- if (promoteVersions.toLowerCase() === 'y') {
- try {
- execSync('npm dist-tag add libpg-query@pg17 latest', { stdio: 'inherit' });
- console.log('β
libpg-query@pg17 promoted to latest');
- } catch (error) {
- console.error('β Failed to promote tag:', error.message);
- }
- }
- }
-
- if (includeFullPackage) {
- const promoteFullPackage = await question('Promote @libpg-query/parser@pg17 to latest? (y/N): ');
- if (promoteFullPackage.toLowerCase() === 'y') {
- try {
- execSync('npm dist-tag add @libpg-query/parser@pg17 latest', { stdio: 'inherit' });
- console.log('β
@libpg-query/parser@pg17 promoted to latest');
- } catch (error) {
- console.error('β Failed to promote tag:', error.message);
- }
+ const promoteVersions = await question('Promote libpg-query@pg17 to latest? (y/N): ');
+ if (promoteVersions.toLowerCase() === 'y') {
+ try {
+ execSync('npm dist-tag add libpg-query@pg17 latest', { stdio: 'inherit' });
+ console.log('β
libpg-query@pg17 promoted to latest');
+ } catch (error) {
+ console.error('β Failed to promote tag:', error.message);
}
}
+
}
console.log('\n⨠Publishing complete!');
diff --git a/templates/Makefile.template b/templates/Makefile.template
index 422f286b..340d51ff 100644
--- a/templates/Makefile.template
+++ b/templates/Makefile.template
@@ -62,7 +62,7 @@ ifdef EMSCRIPTEN
-I$(LIBPG_QUERY_DIR) \
-I$(LIBPG_QUERY_DIR)/vendor \
-L$(LIBPG_QUERY_DIR) \
- -sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query_raw','_wasm_free_parse_result']" \
+ -sEXPORTED_FUNCTIONS="{{EXPORTED_FUNCTIONS}}" \
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','getValue','UTF8ToString','HEAPU8','HEAPU32']" \
-sEXPORT_NAME="$(WASM_MODULE_NAME)" \
-sENVIRONMENT="web,node,worker" \
diff --git a/full/src/index.ts b/templates/full/index.ts
similarity index 100%
rename from full/src/index.ts
rename to templates/full/index.ts
diff --git a/full/src/libpg-query.d.ts b/templates/full/libpg-query.d.ts
similarity index 100%
rename from full/src/libpg-query.d.ts
rename to templates/full/libpg-query.d.ts
diff --git a/full/src/wasm_wrapper.c b/templates/full/wasm_wrapper.c
similarity index 100%
rename from full/src/wasm_wrapper.c
rename to templates/full/wasm_wrapper.c
diff --git a/versions/18/Makefile b/versions/18/Makefile
index 27bec91b..2074e8ec 100644
--- a/versions/18/Makefile
+++ b/versions/18/Makefile
@@ -64,7 +64,7 @@ ifdef EMSCRIPTEN
-I$(LIBPG_QUERY_DIR) \
-I$(LIBPG_QUERY_DIR)/vendor \
-L$(LIBPG_QUERY_DIR) \
- -sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query_raw','_wasm_free_parse_result']" \
+ -sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_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','getValue','UTF8ToString','HEAPU8','HEAPU32']" \
-sEXPORT_NAME="$(WASM_MODULE_NAME)" \
-sENVIRONMENT="web,node,worker" \
diff --git a/versions/18/README.md b/versions/18/README.md
index 45a1156e..be93bf2f 100644
--- a/versions/18/README.md
+++ b/versions/18/README.md
@@ -54,7 +54,9 @@ const result = await parse('SELECT * FROM users WHERE active = true');
## Versions
-Our latest is built with the `18-latest` branch from libpg_query
+Our latest is built with the `18-constructive` branch of [constructive-io/libpg_query](https://github.com/constructive-io/libpg_query)
+
+Starting with PostgreSQL 18, this package ships the **full API**: `parse`, `parsePlPgSQL`, `scan`, `fingerprint`, `normalize` and their sync variants. Versions 13β17 are parse-only.
| PG Major Version | libpg_query | npm dist-tag
|--------------------------|-------------|---------|
@@ -89,7 +91,56 @@ const result = parseSync('SELECT * FROM users WHERE active = true');
// Returns: ParseResult - parsed query object
```
-β **Note:** If you need additional functionality like `fingerprint`, `scan`, `deparse`, or `normalize`, check out the full package (`@libpg-query/parser`) in the [./full](https://github.com/constructive-io/libpg-query-node/tree/main/full) folder of the repo.
+### `parsePlPgSQL(funcsSql: string): Promise` / `parsePlPgSQLSync`
+
+Parses the contents of a PL/pgSQL function from a `CREATE FUNCTION` declaration.
+
+```typescript
+import { parsePlPgSQL } from 'libpg-query';
+
+const result = await parsePlPgSQL(`
+CREATE FUNCTION get_count() RETURNS integer AS $$
+BEGIN
+ RETURN (SELECT COUNT(*) FROM users);
+END;
+$$ LANGUAGE plpgsql;
+`);
+// { plpgsql_funcs: [...] }
+```
+
+### `scan(sql: string): Promise` / `scanSync`
+
+Tokenizes the SQL, returning token positions, text, types, and keyword kinds. See [SCAN.md](./SCAN.md).
+
+```typescript
+import { scan } from 'libpg-query';
+
+const result = await scan('SELECT id FROM users');
+// { version: 180004, tokens: [{ start, end, text, tokenType, tokenName, keywordKind, keywordName }, ...] }
+```
+
+### `fingerprint(sql: string): Promise` / `fingerprintSync`
+
+Generates a unique fingerprint for query identification/caching (equivalent queries share a fingerprint).
+
+```typescript
+import { fingerprint } from 'libpg-query';
+
+await fingerprint('SELECT * FROM users WHERE id = 1'); // e.g. 'a0ead580058af585'
+```
+
+### `normalize(sql: string): Promise` / `normalizeSync`
+
+Normalizes the SQL, replacing constants with placeholders.
+
+```typescript
+import { normalize } from 'libpg-query';
+
+await normalize("SELECT * FROM users WHERE name = 'alice'");
+// SELECT * FROM users WHERE name = $1
+```
+
+β **Note:** the full API (`parsePlPgSQL`, `scan`, `fingerprint`, `normalize`) is available on `pg18`+ only; `pg13`β`pg17` builds expose `parse`/`parseSync` only.
### Initialization
diff --git a/full/SCAN.md b/versions/18/SCAN.md
similarity index 100%
rename from full/SCAN.md
rename to versions/18/SCAN.md
diff --git a/full/libpg_query.md b/versions/18/libpg_query.md
similarity index 100%
rename from full/libpg_query.md
rename to versions/18/libpg_query.md
diff --git a/versions/18/package.json b/versions/18/package.json
index 132f0570..ea50f34b 100644
--- a/versions/18/package.json
+++ b/versions/18/package.json
@@ -13,7 +13,8 @@
"publishName": "libpg-query",
"pgVersion": "18",
"distTag": "pg18",
- "libpgQueryTag": "18.0.0"
+ "libpgQueryTag": "18-constructive",
+ "fullApi": true
},
"files": [
"wasm/*"
@@ -28,7 +29,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/errors.test.js"
+ "test": "node --test test/parsing.test.js test/pg18.test.js test/fingerprint.test.js test/normalize.test.js test/plpgsql.test.js test/scan.test.js test/errors.test.js"
},
"author": "Constructive ",
"license": "MIT",
diff --git a/versions/18/src/index.ts b/versions/18/src/index.ts
index 27eeb49f..0dd8e982 100644
--- a/versions/18/src/index.ts
+++ b/versions/18/src/index.ts
@@ -5,34 +5,36 @@
* npm run copy:templates
*/
+import { ParseResult } from "@pgsql/types";
export * from "@pgsql/types";
-// @ts-ignore
-import PgQueryModule from './libpg-query.js';
+export interface ScanToken {
+ start: number;
+ end: number;
+ text: string;
+ tokenType: number;
+ tokenName: string;
+ keywordKind: number;
+ keywordName: string;
+}
-let wasmModule: any;
+export interface ScanResult {
+ version: number;
+ tokens: ScanToken[];
+}
-// SQL error details interface
export interface SqlErrorDetails {
message: string;
- cursorPosition: number; // 0-based position in the query
- fileName?: string; // Source file where error occurred (e.g., 'scan.l', 'gram.y')
- functionName?: string; // Internal function name
- lineNumber?: number; // Line number in source file
- context?: string; // Additional context
-}
-
-// Options for formatting SQL errors
-export interface SqlErrorFormatOptions {
- showPosition?: boolean; // Show the error position marker (default: true)
- showQuery?: boolean; // Show the query text (default: true)
- color?: boolean; // Use ANSI colors (default: false)
- maxQueryLength?: number; // Max query length to display (default: no limit)
+ 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';
@@ -40,21 +42,19 @@ export class SqlError extends Error {
}
}
-
-
-// Helper function to classify error source
-function getErrorSource(filename: string | null): string {
- if (!filename) return 'unknown';
- if (filename === 'scan.l') return 'lexer'; // Lexical analysis errors
- if (filename === 'gram.y') return 'parser'; // Grammar/parsing errors
- return filename;
+export function hasSqlDetails(error: unknown): error is SqlError {
+ return error instanceof SqlError && error.sqlDetails !== undefined;
}
-// Format SQL error with visual position indicator
export function formatSqlError(
- error: Error & { sqlDetails?: SqlErrorDetails },
+ error: SqlError,
query: string,
- options: SqlErrorFormatOptions = {}
+ options: {
+ showPosition?: boolean;
+ showQuery?: boolean;
+ color?: boolean;
+ maxQueryLength?: number;
+ } = {}
): string {
const {
showPosition = true,
@@ -64,23 +64,23 @@ export function formatSqlError(
} = 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}`);
@@ -88,26 +88,25 @@ export function formatSqlError(
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) +
+ displayQuery = (start > 0 ? '...' : '') +
+ query.substring(start, end) +
(end < query.length ? '...' : '');
// Adjust cursor position for truncation
- const adjustedPosition = cursorPosition - start + (start > 0 ? 3 : 0);
- lines.push(displayQuery);
- lines.push(' '.repeat(adjustedPosition) + `${yellow}^${reset}`);
- } else {
- lines.push(displayQuery);
- lines.push(' '.repeat(cursorPosition) + `${yellow}^${reset}`);
+ 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
@@ -117,21 +116,33 @@ export function formatSqlError(
}
lines.push(`Query: ${displayQuery}`);
}
-
+
return lines.join('\n');
}
-// Check if an error has SQL details
-export function hasSqlDetails(error: any): error is Error & { sqlDetails: SqlErrorDetails } {
- return error instanceof Error &&
- 'sqlDetails' in error &&
- typeof (error as any).sqlDetails === 'object' &&
- (error as any).sqlDetails !== null &&
- 'message' in (error as any).sqlDetails &&
- 'cursorPosition' in (error as any).sqlDetails;
+// @ts-ignore
+import PgQueryModule from './libpg-query.js';
+
+interface WasmModule {
+ _malloc: (size: number) => number;
+ _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_parse_plpgsql: (queryPtr: number) => number;
+ _wasm_fingerprint: (queryPtr: number) => number;
+ _wasm_normalize_query: (queryPtr: number) => number;
+ _wasm_scan: (queryPtr: number) => number;
+ lengthBytesUTF8: (str: string) => number;
+ stringToUTF8: (str: string, ptr: number, len: number) => void;
+ UTF8ToString: (ptr: number) => string;
+ getValue: (ptr: number, type: string) => number;
}
-const initPromise = PgQueryModule().then((module: any) => {
+let wasmModule: WasmModule;
+
+const initPromise = PgQueryModule().then((module: WasmModule) => {
wasmModule = module;
});
@@ -139,13 +150,13 @@ function ensureLoaded() {
if (!wasmModule) throw new Error("WASM module not initialized. Call `loadModule()` first.");
}
-export async function loadModule() {
+export async function loadModule(): Promise {
if (!wasmModule) {
await initPromise;
}
}
-function awaitInit any>(fn: T): T {
+function awaitInit Promise>(fn: T): T {
return (async (...args: Parameters) => {
await initPromise;
return fn(...args);
@@ -176,68 +187,58 @@ function ptrToString(ptr: number): string {
return wasmModule.UTF8ToString(ptr);
}
-export const parse = awaitInit(async (query: string) => {
- // Pre-validation
+export const parse = awaitInit(async (query: string): Promise => {
+ // Input validation
if (query === null || query === undefined) {
throw new Error('Query cannot be null or undefined');
}
- if (typeof query !== 'string') {
- throw new Error(`Query must be a string, got ${typeof query}`);
- }
- if (query.trim() === '') {
+
+ if (query === '') {
throw new Error('Query cannot be empty');
}
-
+
const queryPtr = stringToPtr(query);
let resultPtr = 0;
try {
- // Call the raw function that returns a struct pointer
resultPtr = wasmModule._wasm_parse_query_raw(queryPtr);
if (!resultPtr) {
- throw new Error('Failed to allocate memory for parse result');
+ throw new Error('Failed to parse query: memory allocation failed');
}
- // Read the PgQueryParseResult struct fields
- // struct { char* parse_tree; char* stderr_buffer; PgQueryError* error; }
- const parseTreePtr = wasmModule.getValue(resultPtr, 'i32'); // offset 0
- const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32'); // offset 4
- const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32'); // offset 8
+ // Read the PgQueryParseResult struct
+ const parseTreePtr = wasmModule.getValue(resultPtr, 'i32');
+ const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32');
+ const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32');
- // Check for error
if (errorPtr) {
- // Read PgQueryError struct fields
- // struct { char* message; char* funcname; char* filename; int lineno; int cursorpos; char* context; }
- const messagePtr = wasmModule.getValue(errorPtr, 'i32'); // offset 0
- const funcnamePtr = wasmModule.getValue(errorPtr + 4, 'i32'); // offset 4
- const filenamePtr = wasmModule.getValue(errorPtr + 8, 'i32'); // offset 8
- const lineno = wasmModule.getValue(errorPtr + 12, 'i32'); // offset 12
- const cursorpos = wasmModule.getValue(errorPtr + 16, 'i32'); // offset 16
- const contextPtr = wasmModule.getValue(errorPtr + 20, 'i32'); // offset 20
+ // 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 filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : null;
+ const funcname = funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined;
+ const filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : undefined;
- const errorDetails: SqlErrorDetails = {
- message: message,
+ throw new SqlError(message, {
+ message,
cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, // Convert to 0-based
- fileName: filename || undefined,
- functionName: funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined,
- lineNumber: lineno > 0 ? lineno : undefined,
- context: contextPtr ? wasmModule.UTF8ToString(contextPtr) : undefined
- };
-
- throw new SqlError(message, errorDetails);
+ fileName: filename,
+ functionName: funcname,
+ lineNumber: lineno > 0 ? lineno : undefined
+ });
}
if (!parseTreePtr) {
- throw new Error('Parse result is null');
+ throw new Error('No parse tree generated');
}
- const parseTree = wasmModule.UTF8ToString(parseTreePtr);
- return JSON.parse(parseTree);
- }
- finally {
+ const parseTreeStr = wasmModule.UTF8ToString(parseTreePtr);
+ return JSON.parse(parseTreeStr);
+ } finally {
wasmModule._free(queryPtr);
if (resultPtr) {
wasmModule._wasm_free_parse_result(resultPtr);
@@ -245,71 +246,246 @@ export const parse = awaitInit(async (query: string) => {
}
});
-export function parseSync(query: string) {
- // Pre-validation
+export const parsePlPgSQL = awaitInit(async (query: string): Promise => {
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_parse_plpgsql(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return JSON.parse(resultStr);
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+});
+
+export const fingerprint = awaitInit(async (query: string): Promise => {
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_fingerprint(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return resultStr;
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+});
+
+export const normalize = awaitInit(async (query: string): Promise => {
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = 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);
+ }
+
+ return resultStr;
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+});
+
+// Sync versions
+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 (typeof query !== 'string') {
- throw new Error(`Query must be a string, got ${typeof query}`);
- }
- if (query.trim() === '') {
+
+ if (query === '') {
throw new Error('Query cannot be empty');
}
-
+
const queryPtr = stringToPtr(query);
let resultPtr = 0;
try {
- // Call the raw function that returns a struct pointer
resultPtr = wasmModule._wasm_parse_query_raw(queryPtr);
if (!resultPtr) {
- throw new Error('Failed to allocate memory for parse result');
+ throw new Error('Failed to parse query: memory allocation failed');
}
- // Read the PgQueryParseResult struct fields
- // struct { char* parse_tree; char* stderr_buffer; PgQueryError* error; }
- const parseTreePtr = wasmModule.getValue(resultPtr, 'i32'); // offset 0
- const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32'); // offset 4
- const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32'); // offset 8
+ // Read the PgQueryParseResult struct
+ const parseTreePtr = wasmModule.getValue(resultPtr, 'i32');
+ const stderrBufferPtr = wasmModule.getValue(resultPtr + 4, 'i32');
+ const errorPtr = wasmModule.getValue(resultPtr + 8, 'i32');
- // Check for error
if (errorPtr) {
- // Read PgQueryError struct fields
- // struct { char* message; char* funcname; char* filename; int lineno; int cursorpos; char* context; }
- const messagePtr = wasmModule.getValue(errorPtr, 'i32'); // offset 0
- const funcnamePtr = wasmModule.getValue(errorPtr + 4, 'i32'); // offset 4
- const filenamePtr = wasmModule.getValue(errorPtr + 8, 'i32'); // offset 8
- const lineno = wasmModule.getValue(errorPtr + 12, 'i32'); // offset 12
- const cursorpos = wasmModule.getValue(errorPtr + 16, 'i32'); // offset 16
- const contextPtr = wasmModule.getValue(errorPtr + 20, 'i32'); // offset 20
+ // 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 filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : null;
+ const funcname = funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined;
+ const filename = filenamePtr ? wasmModule.UTF8ToString(filenamePtr) : undefined;
- const errorDetails: SqlErrorDetails = {
- message: message,
+ throw new SqlError(message, {
+ message,
cursorPosition: cursorpos > 0 ? cursorpos - 1 : 0, // Convert to 0-based
- fileName: filename || undefined,
- functionName: funcnamePtr ? wasmModule.UTF8ToString(funcnamePtr) : undefined,
- lineNumber: lineno > 0 ? lineno : undefined,
- context: contextPtr ? wasmModule.UTF8ToString(contextPtr) : undefined
- };
-
- throw new SqlError(message, errorDetails);
+ fileName: filename,
+ functionName: funcname,
+ lineNumber: lineno > 0 ? lineno : undefined
+ });
}
if (!parseTreePtr) {
- throw new Error('Parse result is null');
+ throw new Error('No parse tree generated');
}
- const parseTree = wasmModule.UTF8ToString(parseTreePtr);
- return JSON.parse(parseTree);
- }
- finally {
+ const parseTreeStr = wasmModule.UTF8ToString(parseTreePtr);
+ return JSON.parse(parseTreeStr);
+ } finally {
wasmModule._free(queryPtr);
if (resultPtr) {
wasmModule._wasm_free_parse_result(resultPtr);
}
}
-}
\ No newline at end of file
+}
+
+export function parsePlPgSQLSync(query: string): ParseResult {
+ if (!wasmModule) {
+ throw new Error('WASM module not initialized. Call loadModule() first.');
+ }
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_parse_plpgsql(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return JSON.parse(resultStr);
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+}
+
+export function fingerprintSync(query: string): string {
+ if (!wasmModule) {
+ throw new Error('WASM module not initialized. Call loadModule() first.');
+ }
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_fingerprint(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return resultStr;
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+}
+
+export function normalizeSync(query: string): string {
+ if (!wasmModule) {
+ throw new Error('WASM module not initialized. Call loadModule() first.');
+ }
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = 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);
+ }
+
+ return resultStr;
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+}
+
+export const scan = awaitInit(async (query: string): Promise => {
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_scan(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return JSON.parse(resultStr);
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+});
+
+export function scanSync(query: string): ScanResult {
+ if (!wasmModule) {
+ throw new Error('WASM module not initialized. Call loadModule() first.');
+ }
+ const queryPtr = stringToPtr(query);
+ let resultPtr = 0;
+
+ try {
+ resultPtr = wasmModule._wasm_scan(queryPtr);
+ const resultStr = ptrToString(resultPtr);
+
+ if (resultStr.startsWith('syntax error') || resultStr.startsWith('deparse error') || resultStr.startsWith('ERROR')) {
+ throw new Error(resultStr);
+ }
+
+ return JSON.parse(resultStr);
+ } finally {
+ wasmModule._free(queryPtr);
+ if (resultPtr) {
+ wasmModule._wasm_free_string(resultPtr);
+ }
+ }
+}
\ No newline at end of file
diff --git a/versions/18/src/libpg-query.d.ts b/versions/18/src/libpg-query.d.ts
index 2098ee1c..b00f1e7f 100644
--- a/versions/18/src/libpg-query.d.ts
+++ b/versions/18/src/libpg-query.d.ts
@@ -11,10 +11,12 @@ declare module './libpg-query.js' {
_free: (ptr: number) => void;
_wasm_free_string: (ptr: number) => void;
_wasm_parse_query: (queryPtr: number) => number;
+ _wasm_parse_plpgsql: (queryPtr: number) => number;
+ _wasm_fingerprint: (queryPtr: number) => number;
+ _wasm_normalize_query: (queryPtr: number) => number;
lengthBytesUTF8: (str: string) => number;
stringToUTF8: (str: string, ptr: number, len: number) => void;
UTF8ToString: (ptr: number) => string;
- HEAPU8: Uint8Array;
}
const PgQueryModule: () => Promise;
diff --git a/versions/18/src/wasm_wrapper.c b/versions/18/src/wasm_wrapper.c
index 38151683..ca6ea740 100644
--- a/versions/18/src/wasm_wrapper.c
+++ b/versions/18/src/wasm_wrapper.c
@@ -5,15 +5,26 @@
* npm run copy:templates
*/
+#include "pg_query.h"
+#include "protobuf/pg_query.pb-c.h"
#include
-#include
#include
#include
+#include
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) {
@@ -22,14 +33,28 @@ static void* safe_malloc(size_t size) {
return ptr;
}
-// Raw struct access functions for parse
EMSCRIPTEN_KEEPALIVE
-PgQueryParseResult* wasm_parse_query_raw(const char* input) {
+char* wasm_parse_query(const char* input) {
if (!validate_input(input)) {
- return NULL;
+ 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");
}
- PgQueryParseResult* result = (PgQueryParseResult*)safe_malloc(sizeof(PgQueryParseResult));
+ char* parse_tree = safe_strdup(result.parse_tree);
+ pg_query_free_parse_result(result);
+ return parse_tree;
+}
+
+EMSCRIPTEN_KEEPALIVE
+PgQueryParseResult* wasm_parse_query_raw(const char* input) {
+ PgQueryParseResult* result = (PgQueryParseResult*)malloc(sizeof(PgQueryParseResult));
if (!result) {
return NULL;
}
@@ -44,4 +69,334 @@ void wasm_free_parse_result(PgQueryParseResult* result) {
pg_query_free_parse_result(*result);
free(result);
}
-}
\ No newline at end of file
+}
+
+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);
+ }
+}
+
+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
+ switch(token_type) {
+ case 258: return "IDENT";
+ case 261: return "SCONST";
+ case 266: return "ICONST";
+ case 260: return "FCONST";
+ case 267: return "PARAM";
+ case 40: return "ASCII_40"; // (
+ case 41: return "ASCII_41"; // )
+ case 42: return "ASCII_42"; // *
+ case 44: return "ASCII_44"; // ,
+ case 59: return "ASCII_59"; // ;
+ case 61: return "ASCII_61"; // =
+ case 268: return "TYPECAST";
+ case 272: return "LESS_EQUALS";
+ case 273: return "GREATER_EQUALS";
+ case 274: return "NOT_EQUALS";
+ case 275: return "SQL_COMMENT";
+ case 276: return "C_COMMENT";
+ default: return "UNKNOWN";
+ }
+}
+
+static const char* get_keyword_name(PgQuery__KeywordKind keyword_kind) {
+ switch(keyword_kind) {
+ case 0: return "NO_KEYWORD";
+ case 1: return "UNRESERVED_KEYWORD";
+ case 2: return "COL_NAME_KEYWORD";
+ case 3: return "TYPE_FUNC_NAME_KEYWORD";
+ case 4: return "RESERVED_KEYWORD";
+ default: return "UNKNOWN_KEYWORD";
+ }
+}
+
+static char* build_scan_json(PgQuery__ScanResult *scan_result, const char* original_sql) {
+ if (!scan_result || !original_sql) {
+ return safe_strdup("{\"version\":0,\"tokens\":[]}");
+ }
+
+ // Calculate rough JSON size estimate
+ size_t estimated_size = 1024 + (scan_result->n_tokens * 200);
+ char* json = safe_malloc(estimated_size);
+ if (!json) {
+ return safe_strdup("{\"version\":0,\"tokens\":[]}");
+ }
+
+ // Start building JSON
+ int pos = snprintf(json, estimated_size, "{\"version\":%d,\"tokens\":[", scan_result->version);
+
+ for (size_t i = 0; i < scan_result->n_tokens; i++) {
+ PgQuery__ScanToken *token = scan_result->tokens[i];
+
+ // Extract token text from original SQL
+ 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;
+
+ if (token_length > 0) {
+ strncpy(token_text, &original_sql[token->start], token_length);
+ }
+ token_text[token_length] = '\0';
+
+ // Escape token text for JSON
+ char* escaped_text = safe_malloc(token_length * 2 + 1);
+ if (!escaped_text) {
+ free(token_text);
+ continue;
+ }
+
+ int escaped_pos = 0;
+ for (int j = 0; j < token_length; j++) {
+ char c = token_text[j];
+ if (c == '"' || c == '\\') {
+ escaped_text[escaped_pos++] = '\\';
+ escaped_text[escaped_pos++] = c;
+ } else if (c == '\n') {
+ escaped_text[escaped_pos++] = '\\';
+ escaped_text[escaped_pos++] = 'n';
+ } else if (c == '\r') {
+ escaped_text[escaped_pos++] = '\\';
+ escaped_text[escaped_pos++] = 'r';
+ } else if (c == '\t') {
+ escaped_text[escaped_pos++] = '\\';
+ escaped_text[escaped_pos++] = 't';
+ } else {
+ escaped_text[escaped_pos++] = c;
+ }
+ }
+ escaped_text[escaped_pos] = '\0';
+
+ // Get token type name and keyword kind name
+ const char* token_name = get_token_name(token->token);
+ const char* keyword_name = get_keyword_name(token->keyword_kind);
+
+ // Add comma if not first token
+ if (i > 0) {
+ pos += snprintf(json + pos, estimated_size - pos, ",");
+ }
+
+ // Add token object to JSON
+ pos += snprintf(json + pos, estimated_size - pos,
+ "{\"start\":%d,\"end\":%d,\"text\":\"%s\",\"tokenType\":%d,\"tokenName\":\"%s\",\"keywordKind\":%d,\"keywordName\":\"%s\"}",
+ token->start, token->end, escaped_text, token->token, token_name, token->keyword_kind, keyword_name);
+
+ free(token_text);
+ free(escaped_text);
+
+ // Check if we're running out of space
+ if (pos >= estimated_size - 200) {
+ char* new_json = realloc(json, estimated_size * 2);
+ if (!new_json) break;
+ json = new_json;
+ estimated_size *= 2;
+ }
+ }
+
+ // Close JSON
+ snprintf(json + pos, estimated_size - pos, "]}");
+
+ return json;
+}
+
+EMSCRIPTEN_KEEPALIVE
+char* wasm_scan(const char* input) {
+ if (!validate_input(input)) {
+ return safe_strdup("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");
+ }
+
+ // 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");
+ }
+
+ // 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\":[]}");
+}
+
+EMSCRIPTEN_KEEPALIVE
+void wasm_free_string(char* str) {
+ free(str);
+}
diff --git a/full/test/fingerprint.test.js b/versions/18/test/fingerprint.test.js
similarity index 100%
rename from full/test/fingerprint.test.js
rename to versions/18/test/fingerprint.test.js
diff --git a/full/test/normalize.test.js b/versions/18/test/normalize.test.js
similarity index 100%
rename from full/test/normalize.test.js
rename to versions/18/test/normalize.test.js
diff --git a/versions/18/test/pg18.test.js b/versions/18/test/pg18.test.js
new file mode 100644
index 00000000..fb49bae0
--- /dev/null
+++ b/versions/18/test/pg18.test.js
@@ -0,0 +1,69 @@
+const { describe, it, before } = require("node:test");
+const assert = require("node:assert");
+const { parse, parseSync, parsePlPgSQL, scan, loadModule } = require("../");
+
+describe("PostgreSQL 18 syntax", () => {
+ before(async () => {
+ await loadModule();
+ });
+
+ it("should report parser version 180004", async () => {
+ const result = await parse("SELECT 1");
+ assert.equal(result.version, 180004);
+ });
+
+ it("should parse RETURNING OLD/NEW aliases", async () => {
+ const result = await parse(
+ "UPDATE t SET a = 1 RETURNING OLD.a AS oa, NEW.a AS na"
+ );
+ const stmt = result.stmts[0].stmt.UpdateStmt;
+ const targets = stmt.returningClause.exprs.map(
+ (rt) => rt.ResTarget.name
+ );
+ assert.deepEqual(targets, ["oa", "na"]);
+ });
+
+ it("should parse VIRTUAL generated columns", () => {
+ const result = parseSync(
+ "CREATE TABLE tt (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL)"
+ );
+ const columns = result.stmts[0].stmt.CreateStmt.tableElts;
+ const generated = columns[1].ColumnDef.constraints.find(
+ (c) => c.Constraint.contype === "CONSTR_GENERATED"
+ );
+ assert.ok(generated);
+ assert.equal(generated.Constraint.generated_kind, "v");
+ });
+
+ it("should parse ALTER CONSTRAINT ... NOT ENFORCED", async () => {
+ const result = await parse(
+ "ALTER TABLE t ALTER CONSTRAINT c NOT ENFORCED"
+ );
+ const cmd = result.stmts[0].stmt.AlterTableStmt.cmds[0].AlterTableCmd;
+ assert.equal(cmd.subtype, "AT_AlterConstraint");
+ assert.equal(cmd.def.ATAlterConstraint.conname, "c");
+ assert.equal(cmd.def.ATAlterConstraint.alterEnforceability, true);
+ });
+
+ it("should parse PL/pgSQL on the 18 build", async () => {
+ const result = await parsePlPgSQL(`
+ CREATE FUNCTION f() RETURNS int AS $$
+ BEGIN
+ RETURN 1;
+ END;
+ $$ LANGUAGE plpgsql;
+ `);
+ assert.ok(Array.isArray(result.plpgsql_funcs));
+ assert.equal(result.plpgsql_funcs.length, 1);
+ });
+
+ it("should scan PG18-specific SQL", async () => {
+ const result = await scan(
+ "UPDATE t SET a = 1 RETURNING OLD.a AS oa, NEW.a AS na"
+ );
+ assert.equal(result.version, 180004);
+ const texts = result.tokens.map((t) => t.text);
+ assert.ok(texts.includes("OLD"));
+ assert.ok(texts.includes("NEW"));
+ });
+});
diff --git a/full/test/plpgsql.test.js b/versions/18/test/plpgsql.test.js
similarity index 100%
rename from full/test/plpgsql.test.js
rename to versions/18/test/plpgsql.test.js
diff --git a/full/test/scan.test.js b/versions/18/test/scan.test.js
similarity index 100%
rename from full/test/scan.test.js
rename to versions/18/test/scan.test.js