[PGLite-Native]: 嵌入式版本 PostgreSQL

源码: 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

交互式 Shell

除了 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 的一个完整使用示例。


完整 C 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:


内部原理

PostgreSQL 单用户模式:嵌入的基础

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 的核心思路:

  1. 把这个单用户 backend 留在进程内,不让它退出
  2. 每次执行 SQL 时写入一个临时文件,rewind 后当作输入流喂给 backend
  3. 通过 hook 拦截 wire protocol 消息,把结果收集到内存
  4. 后端处理完 SQL 后回到就绪状态,等待下一条

原理不复杂,但要让它稳定工作,需要处理 PostgreSQL 内部大量的进程级假设。

Hack 1:阻止 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()

Hack 2:嵌入式 initdb

标准 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 模式

Hack 3:用临时文件模拟 stdin/stdout

每次 API 调用都会:

  1. 创建临时文件 tmpfile()
  2. 把 SQL 写入 + rewind()
  3. 把文件指针作为 stdin 传给 single-user backend
  4. Backend 从文件读取 SQL → 执行 → 结果通过 hook 返回内存
  5. 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

Hack 4:拦截 libpq 协议到内存

标准模式: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;

Hack 5:会话复用

标准 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

每次 API 调用返回时,runtime 总是处于确定的 stable state(READY / BROKEN / CLOSED),不会有半清理状态泄漏给宿主。

结果分四类:

类别语义示例
SUCCESS操作成功正常查询返回
OPERATION_ERRORSQL 级错误,runtime 仍可用语法错误、约束违反
RUNTIME_BROKENruntime 已死FATAL / PANIC
API_MISUSE调用方违反 API 约束重入、无效 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_executePrepared Statements
t_txn_rollback事务回滚
t_nulls_and_typesNULL 值与类型处理
t_large_resultset大结果集
t_multi_result_sets多语句执行
t_rfc0002_error_recoverySQL ERROR 后恢复
t_rfc0002_panic_boundaryPANIC 不杀宿主进程
t_rfc0002_proc_exit_boundaryproc_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_unsupportedCOPY 拒绝

运行测试:

./build.sh test

当前限制


设计文档

项目的 rfcs/ 目录下有完整的设计文档系列:


总结

PGLite-Native 把一个完整的 PostgreSQL 17 backend 压缩进一个 .so 文件,用 几个 hack(proc_exit 拦截、嵌入式 initdb、临时文件 stdin、协议 hook、会话复用)实现了类似 SQLite 的嵌入式使用体验。

虽然目前只支持 C API、单连接、同步执行,但底层跑的是完整的 PostgreSQL——这意味着你可以用 PostgreSQL 的全部 SQL 能力、扩展系统、事务语义,而不需要启动任何服务端进程。

[infioAI] Obsidian 中的 Cursor
[LanBuffer]: 基于对象存储的高性能检索引擎