源码: duanfuxiang0/postgres-pglite
PGLite 是 ElectricSQL 的项目,把完整的 PostgreSQL(含 pgvector 等扩展)编译成 WASM,可以直接跑在浏览器或 Node.js 里。对于前端向量搜索、离线优先应用来说几乎是最佳方案。
但它只能在 WASM 环境里跑。这个项目其实就是实现一个native版本, 把 PostgreSQL 嵌入一个原生 C/C++ 程序——就像用 SQLite 或 DuckDB 那样,链接一个 .so, 这个工作不是独立完成的, 是基于 https://github.com/electric-sql/pglite
我主要用来作为启动本地的测试环境, 不需要启动完整的 postgres 服务, 目前还有一些测试无法通过.
git clone https://github.com/duanfuxiang0/postgres-pglite.git
cd postgres-pglite
./build.sh # 完整构建 + 测试
# ./build.sh build # 只构建不测试
# DEBUG=true ./build.sh # 调试构建
构建产物在 dist/ 目录下,核心是 dist/lib/pglite.so。
#include "pglite.h"
int main(void) {
pglite_database db = NULL;
pglite_connection conn = NULL;
pglite_result result = NULL;
// 打开数据库(如果不存在会自动 initdb)
pglite_open("/tmp/my-embedded-pg", &db);
pglite_connect(db, &conn);
// 建表、插入数据
pglite_exec(conn,
"CREATE TABLE users (id INT, name TEXT);"
"INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob');",
NULL, NULL, NULL);
// 查询
pglite_query(conn, "SELECT id, name FROM users ORDER BY id;", &result);
// 读取结果
int64_t rows = pglite_result_row_count(result); // 2
int32_t cols = pglite_result_column_count(result); // 2
for (int64_t r = 0; r < rows; r++) {
const char *id = pglite_value_text(result, 0, r);
const char *name = pglite_value_text(result, 1, r);
printf("id=%s, name=%s\n", id, name);
}
// 输出:
// id=1, name=Alice
// id=2, name=Bob
// 清理
pglite_destroy_result(&result);
pglite_disconnect(&conn);
pglite_close(&db);
}
编译链接:
gcc -o demo demo.c -I dist/include -L dist/lib -lpglite -Wl,-rpath,dist/lib
除了 C API,项目还提供了一个类似 sqlite3 / duckdb 的交互式 shell 工具 pglite,可以直接在终端里操作嵌入式 PostgreSQL,方便本地调试和探索数据。
$ ./pglite /tmp/mydb
pglite> CREATE TABLE users (id serial, name text);
CREATE TABLE
pglite> INSERT INTO users (name) VALUES ('Alice'), ('Bob');
INSERT 0 2
pglite> SELECT * FROM users;
id | name
----+-------
1 | Alice
2 | Bob
(2 rows)
pglite> \q
Shell 支持三种使用模式:
# 交互式 REPL(终端直接输入)
./pglite /tmp/mydb
# 执行单条 SQL
./pglite /tmp/mydb -c "SELECT version();"
# 执行 SQL 文件
./pglite /tmp/mydb -f schema.sql
# 管道输入
echo "SELECT 1;" | ./pglite /tmp/mydb
# 调试模式(显示 backend 详细日志)
./pglite /tmp/mydb --debug
Shell 内部调用的就是 pglite_open() → pglite_connect() → pglite_query_ex() 这套公开 C API,本身也是这套 API 的一个完整使用示例。
// 生命周期
pglite_state pglite_open(const char *path, pglite_database *out_db);
void pglite_close(pglite_database *db);
pglite_state pglite_connect(pglite_database db, pglite_connection *out_conn);
void pglite_disconnect(pglite_connection *conn);
// 查询(结果收集到内存)
pglite_state pglite_query(pglite_connection conn, const char *sql,
pglite_result *out_result);
pglite_state pglite_query_ex(pglite_connection conn, const char *sql,
pglite_result *out_result, char **out_error);
// 执行(通过回调逐行处理)
pglite_state pglite_exec(pglite_connection conn, const char *sql,
pglite_exec_callback callback,
void *user_data, char **out_error);
// Prepared Statements
pglite_state pglite_prepare(pglite_connection conn, const char *name,
const char *sql, pglite_statement *out_stmt);
pglite_state pglite_execute_prepared(pglite_statement stmt,
const char *const *params,
size_t param_count,
pglite_result *out_result);
void pglite_statement_close(pglite_statement *stmt);
// 结果访问
int64_t pglite_result_row_count(pglite_result result);
int32_t pglite_result_column_count(pglite_result result);
const char * pglite_result_column_name(pglite_result result, int32_t col);
uint32_t pglite_result_column_type(pglite_result result, int32_t col);
const char * pglite_result_command_tag(pglite_result result);
bool pglite_value_is_null(pglite_result result, int32_t col, int64_t row);
const char * pglite_value_text(pglite_result result, int32_t col, int64_t row);
int32_t pglite_value_int32(pglite_result result, int32_t col, int64_t row);
int64_t pglite_value_int64(pglite_result result, int32_t col, int64_t row);
double pglite_value_double(pglite_result result, int32_t col, int64_t row);
// 清理
void pglite_destroy_result(pglite_result *result);
void pglite_free_error(char *error_message);
API:
open → connect → query/exec → disconnect → closedestroy_result 一次性释放PostgreSQL 有一个运行模式:单用户模式(--single)。不启动 postmaster,不监听任何 socket,直接在当前进程里运行一个 backend:
// src/backend/main/main.c
if (argc > 1 && strcmp(argv[1], "--single") == 0)
PostgresSingleUserMain(argc, argv, username);
PostgresSingleUserMain 依次完成初始化后进入 PostgresMain() 主循环。标准模式下输入来自 stdin,输出到 stdout。
PGLite 的核心思路:
原理不复杂,但要让它稳定工作,需要处理 PostgreSQL 内部大量的进程级假设。
proc_exit() 杀掉宿主进程PostgreSQL 遇到 FATAL/PANIC 或正常退出时会调用 proc_exit() 结束进程。嵌入场景下宿主进程不能被杀。
在 src/backend/storage/ipc/ipc.c 里拦截 proc_exit(),用 sigsetjmp/siglongjmp 把控制权交回调用方,并记录终止原因:
// 宿主边界设置跳转目标
pglite_set_proc_exit_jump(&boundary, &exit_code);
if (sigsetjmp(boundary, 1) == 0) {
// 正常执行 SQL ...
} else {
// PostgreSQL 内部调用了 proc_exit() 或 PANIC
// 控制流跳回到这里,宿主进程继续存活
// exit_code 记录了终止原因
}
同样,ereport(FATAL) 和 ereport(PANIC) 在 src/backend/utils/error/elog.c 里也做了拦截,不再直接 abort()。
标准 initdb 是独立的可执行文件,会 fork 子进程完成初始化。嵌入场景不能 fork。
pgl_initdb.c 把 initdb 源码直接 #include 进来,劫持 popen():
// initdb 原本: popen("postgres --boot -D /data", "w") → fork 子进程
// PGLite: pgl_popen() → fopen("initdb-boot.txt", "w") → 写入到文件
#define popen(command, mode) pgl_popen(command, mode)
initdb 把所有 bootstrap SQL 写入文件后,PGLite 用 freopen() 把该文件作为 stdin 喂给 BootstrapModeMain(),在同一个进程里顺序完成整个初始化。整个流程用 setjmp 边界串起来:
pgl_initdb()
├─ setjmp(boundary)
├─ pgl_initdb_main() // initdb 写 SQL 到文件
│ └─ proc_exit(66) → longjmp(boundary) // 拦截退出
├─ setjmp(boundary)
├─ freopen("initdb-boot.txt", "r", stdin)
├─ BootstrapModeMain() // 从文件读 SQL 执行
│ └─ proc_exit(66) → longjmp(boundary) // 拦截退出
└─ 初始化完成,进入 single-user 模式
每次 API 调用都会:
tmpfile()rewind()fclose() 自动删除临时文件pglite_query(conn, "SELECT * FROM users")
│
├─ tmpfile() → 创建临时文件
├─ fwrite("SELECT * FROM users\n")
├─ rewind() → 回到文件开头
├─ pgl_run_sql_stream(stream, dbname, username)
│ └─ PostgreSQL 从文件读取 SQL → 执行
│ └─ 结果通过 protocol hook → 收集到内存
├─ fclose(stream) → 删除临时文件
└─ 返回 pglite_result
标准模式:Backend → libpq wire protocol → 网络 → 客户端 单用户模式:Backend → printf() → stdout
PGLite 伪装成 postmaster 环境 + 安装协议 hook:
// 伪装成远程连接,让 backend 走 wire protocol 路径
IsPostmasterEnvironment = true;
MyProcPort = pq_init(&dummy_sock); // 假 socket
whereToSendOutput = DestRemote; // 输出到"远程客户端"
然后在 src/backend/utils/pglite_proto_hook.c 注册 hook,拦截 RowDescription、DataRow、CommandComplete、ErrorMessage:
typedef struct PgliteProtoHook {
void (*on_row_description)(void *arg, int field_count,
const PgliteResultField *fields);
void (*on_data_row)(void *arg, int field_count,
const char *const *values,
const int *lengths, const int16_t *formats);
void (*on_command_complete)(void *arg, const char *command_tag);
void (*on_error_message)(void *arg, const char *message);
void *arg;
} PgliteProtoHook;
标准 PostgreSQL 每个连接 fork 一个 backend 进程。PGLite 只有一个 backend,通过复用来处理多次请求:
static bool single_session_initialized = false;
if (!single_session_initialized) {
// 第一次:完整初始化 backend
process_postgres_switches(...);
InitPostgres(dbname, ...);
single_session_initialized = true;
}
// 每次调用:只处理新的 SQL 流
pgl_process_sql_stream(feed);
为了给宿主提供可靠的行为保证,pgl_capi.c 实现了一个显式的运行时状态机:
CLOSED → OPENING → BOOTSTRAPPING → READY ⇄ EXECUTING
↘
BROKEN → CLOSING → CLOSED
pglite_open() 合法每次 API 调用返回时,runtime 总是处于确定的 stable state(READY / BROKEN / CLOSED),不会有半清理状态泄漏给宿主。
结果分四类:
| 类别 | 语义 | 示例 |
|---|---|---|
SUCCESS | 操作成功 | 正常查询返回 |
OPERATION_ERROR | SQL 级错误,runtime 仍可用 | 语法错误、约束违反 |
RUNTIME_BROKEN | runtime 已死 | FATAL / PANIC |
API_MISUSE | 调用方违反 API 约束 | 重入、无效 handle |
还有一些安全约束:
open() 递增 generation,close() 后旧的 connection handle 自动失效1. pglite_open("/data", &db)
├─ pgl_native_startup() // 设置环境变量、PREFIX、PGDATA
└─ pgl_native_initdb_if_needed()
├─ 数据库已存在?→ 跳过
└─ pgl_initdb()
├─ setjmp + pgl_initdb_main() // initdb → 写 SQL 到文件
├─ setjmp + BootstrapModeMain() // 从文件读 SQL 执行
└─ 标记初始化完成
2. pglite_connect(db, &conn)
└─ 创建连接句柄(内存结构)
3. pglite_query(conn, "SELECT ...", &result)
├─ tmpfile() → 写入 SQL → rewind()
├─ 安装 protocol hook
├─ pgl_run_sql_stream()
│ └─ PostgreSQL backend 处理 SQL
│ ├─ hook.on_row_description() → 记录列信息
│ ├─ hook.on_data_row() → 收集每行数据
│ └─ hook.on_command_complete() → 记录 command tag
├─ 卸载 hook
├─ fclose(stream)
└─ 返回 pglite_result
4. pglite_disconnect(&conn) → 释放连接句柄
5. pglite_close(&db) → 释放数据库句柄,状态回到 CLOSED
项目包含 20+ 个原生测试用例,覆盖:
| 测试 | 验证内容 |
|---|---|
t_smoke | 基本 CRUD |
t_prepare_execute | Prepared Statements |
t_txn_rollback | 事务回滚 |
t_nulls_and_types | NULL 值与类型处理 |
t_large_resultset | 大结果集 |
t_multi_result_sets | 多语句执行 |
t_rfc0002_error_recovery | SQL ERROR 后恢复 |
t_rfc0002_panic_boundary | PANIC 不杀宿主进程 |
t_rfc0002_proc_exit_boundary | proc_exit 拦截 |
t_double_connect_rejected | 重复连接被拒 |
t_reentrant_exec_rejected | 重入保护 |
t_generation_guard | 过期 handle 拒绝 |
t_exec_callback_abort | 回调中止 |
t_session_identity | 会话身份锁定 |
t_contract_statement_timeout | 语句超时 |
t_contract_copy_unsupported | COPY 拒绝 |
运行测试:
./build.sh test
项目的 rfcs/ 目录下有完整的设计文档系列:
PGLite-Native 把一个完整的 PostgreSQL 17 backend 压缩进一个 .so 文件,用 几个 hack(proc_exit 拦截、嵌入式 initdb、临时文件 stdin、协议 hook、会话复用)实现了类似 SQLite 的嵌入式使用体验。
虽然目前只支持 C API、单连接、同步执行,但底层跑的是完整的 PostgreSQL——这意味着你可以用 PostgreSQL 的全部 SQL 能力、扩展系统、事务语义,而不需要启动任何服务端进程。