Loading

PostgreSQL的SPI接口用法

SPI(Server Programming Interface)是PostgreSQL内部C函数访问数据库的标准接口,允许在C函数中执行SQL语句、读取结果集。所有SPI调用遵循严格的生命周期模型

SPI_connect()  →  执行查询  →  读取结果  →  SPI_finish()

核心API分类

  1. 结果读取
    SPI_processed: 语句处理的行数
    SPI_tuptable: 结果集
    SPI_getvalue: 取字段值(string)
    SPI_getbinval: 取字段值(Datum)
    SPI_fnumber: 字段名->编号
  2. 游标操作
    SPI_cursor_open -> SPI_cursor_fetch -> SPI_cursor_close
  3. 查询执行(预编译)
    SPI_prepare -> SPI_execute_plan/SPI_execp -> SPI_freeplan/SPI_keepplan/SPI_saveplan
  4. 查询执行(简单)
    SPI_execute -> 直接传入SQL字符串
    SPI_execute_with_args -> SQL字符串 + 参数值/类型
  5. 生命周期管理
    SPI_connect -> SPI_finish

简单SQL执行

Datum
crosstab(PG_FUNCTION_ARGS)
{
    ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
    char *sql = text_to_cstring(PG_GETARG_TEXT_PP(0));   // 用户传入的 SQL
    SPITupleTable *spi_tuptable;
    TupleDesc spi_tupdesc;
    uint64 proc;
    int ret;

    // ====== 第 1 步:连接 SPI ======
    if ((ret = SPI_connect()) < 0)
        elog(ERROR, "crosstab: SPI_connect returned %d", ret);

    // ====== 第 2 步:执行查询 ======
    // 参数: SQL字符串, 只读?, 最大行数(0=全部)
    ret = SPI_execute(sql, true, 0);
    proc = SPI_processed;   // 获取返回的总行数

    // 检查执行结果
    if (ret != SPI_OK_SELECT || proc == 0)
    {
        SPI_finish();       // 不要忘记断开!
        rsinfo->isDone = ExprEndResult;
        PG_RETURN_NULL();
    }

    // ====== 第 3 步:获取结果集 ======
    spi_tuptable = SPI_tuptable;     // 全局结果集指针
    spi_tupdesc = spi_tuptable->tupdesc;  // 列描述符

    // 检查返回的列数
    if (spi_tupdesc->natts != 3)
        ereport(ERROR, ...);

    // ====== 第 4 步:遍历每一行 ======
    for (call_cntr = 0; call_cntr < proc; call_cntr++)
    {
        HeapTuple tuple = spi_tuptable->vals[call_cntr];
        char *rowname, *category, *values;

        // SPI_getvalue: 将第 i 列转为 C 字符串
        rowname   = SPI_getvalue(tuple, spi_tupdesc, 1);  // 第1列
        category  = SPI_getvalue(tuple, spi_tupdesc, 2);  // 第2列
        values    = SPI_getvalue(tuple, spi_tupdesc, 3);  // 第3列

        // ... 业务处理 ...

        pfree(rowname);   // SPI_getvalue 分配的字符串需要手动释放
        pfree(category);
        pfree(values);
    }

    // ====== 第 5 步:断开 SPI ======
    SPI_finish();
    // ... 返回结果 ...
}

关键模式:SPI_connect -> SPI_execute(sql,read_only,tcount) -> 读取SPI_tuptable -> SPI_finish

预编译计划 + 参数化查询

Datum
check_primary_key(PG_FUNCTION_ARGS)
{
    TriggerData *trigdata = (TriggerData *) fcinfo->context;
    Trigger    *trigger    = trigdata->tg_trigger;
    HeapTuple   tuple      = trigdata->tg_trigtuple;  // 触发器的行
    Relation    rel        = trigdata->tg_relation;
    TupleDesc   tupdesc    = rel->rd_att;
    char       *relname    = "referenced_table";        // 被引用表名
    char       *fkey_cols[] = {"col1", "col2"};        // 外键列名
    int         nkeys      = 2;
    Datum       kvals[2];    // 参数值数组
    Oid         argtypes[2]; // 参数类型数组
    char        sql[8192];
    SPIPlanPtr  pplan;
    int         ret, i;

    // ====== 第 1 步:连接 SPI ======
    if ((ret = SPI_connect()) < 0)
        elog(ERROR, "SPI_connect returned %d", ret);

    // ====== 第 2 步:提取触发器中变更行的字段值 ======
    for (i = 0; i < nkeys; i++)
    {
        int fnum = SPI_fnumber(tupdesc, fkey_cols[i]);  // 列名→列号

        if (fnum <= 0)
            ereport(ERROR, (errmsg("column \"%s\" not found", fkey_cols[i])));

        bool isnull;
        kvals[i] = SPI_getbinval(tuple, tupdesc, fnum, &isnull);  // 取 Datum 值

        if (isnull)
        {
            SPI_finish();  // NULL 值不需要检查,直接返回
            return PointerGetDatum(tuple);
        }

        argtypes[i] = SPI_gettypeid(tupdesc, fnum);  // 获取列类型 OID
    }

    // ====== 第 3 步:构造参数化 SQL ======
    // SELECT 1 FROM referenced_table WHERE col1 = $1 AND col2 = $2
    snprintf(sql, sizeof(sql), "select 1 from %s where ", relname);
    for (i = 0; i < nkeys; i++)
        snprintf(sql + strlen(sql), sizeof(sql) - strlen(sql),
                 "%s = $%d %s",
                 fkey_cols[i], i + 1,
                 (i < nkeys - 1) ? "and " : "");

    // ====== 第 4 步:预编译查询计划 ======
    pplan = SPI_prepare(sql, nkeys, argtypes);
    if (pplan == NULL)
        elog(ERROR, "SPI_prepare failed: %s", SPI_result_code_string(SPI_result));

    // ====== 第 5 步:将计划保存到长期内存(跨调用复用) ======
    if (SPI_keepplan(pplan))
        elog(ERROR, "SPI_keepplan failed");

    // ====== 第 6 步:执行预编译计划 ======
    // 参数: 计划, 值数组, NULL标记数组(全非NULL), 最大行数(1)
    ret = SPI_execp(pplan, kvals, NULL, 1);
    if (ret < 0)
        elog(ERROR, "SPI_execp returned %d", ret);

    // ====== 第 7 步:检查结果 ======
    if (SPI_processed == 0)
        ereport(ERROR,
                (errmsg("tuple references non-existent key"),
                 errdetail("Trigger \"%s\" found orphaned reference.", trigger->tgname)));

    SPI_finish();
    return PointerGetDatum(tuple);
}

关键模式:SPI_prepare(sql, nargs, argtypes) -> SPI_keepplan(plan) -> SPI_execp(plan, values, nulls, tcount)

带类型的参数执行

当不需要复用计划时,用SPI_execute_with_argsSPI_prepare + SPI_execp更简洁

Datum my_insert_function(PG_FUNCTION_ARGS)
{
    char *name = PG_GETARG_CSTRING(0);
    int32 age  = PG_GETARG_INT32(1);
    int ret;

    SPI_connect();

    // 构造参数化 INSERT
    Oid  argtypes[] = {TEXTOID, INT4OID};
    Datum values[]  = {CStringGetDatum(name), Int32GetDatum(age)};
    char nulls[]    = {' ', ' '};      // ' ' = NOT NULL, 'n' = NULL

    ret = SPI_execute_with_args(
        "INSERT INTO users (name, age) VALUES ($1, $2)",
        2,           // 参数个数
        argtypes,    // 类型数组
        values,      // 值数组
        nulls,       // NULL 标记数组
        false,       // read_only
        0            // tcount (0=unlimited)
    );

    if (ret != SPI_OK_INSERT)
        elog(ERROR, "insert failed: %s", SPI_result_code_string(ret));

    uint64 rows_inserted = SPI_processed;

    SPI_finish();
    PG_RETURN_INT64(rows_inserted);
}

游标操作

适用于返回大量数据、需要分批处理的场景

Datum fetch_large_result(PG_FUNCTION_ARGS)
{
    Portal portal;
    uint64 total_rows = 0;
    int ret;

    SPI_connect();

    // 打开游标
    portal = SPI_cursor_open_with_args(
        "my_cursor",
        "SELECT id, data FROM large_table WHERE status = $1",
        1,                       // nargs
        (Oid[]){INT4OID},        // argtypes
        (Datum[]){Int32GetDatum(1)},  // values
        (char[]){" "},           // nulls
        true,                    // read_only
        0                        // cursor_options
    );

    // 分批取数据
    while (true)
    {
        SPI_cursor_fetch(portal, true, 1000);  // 每次取 1000 行

        if (SPI_processed == 0)
            break;

        // 处理这 1000 行 ...
        total_rows += SPI_processed;
    }

    SPI_cursor_close(portal);
    SPI_finish();

    PG_RETURN_INT64(total_rows);
}

修改元组

Datum trigger_modify_func(PG_FUNCTION_ARGS)
{
    TriggerData *trigdata = (TriggerData *) fcinfo->context;
    HeapTuple oldtuple    = trigdata->tg_trigtuple;
    Relation   rel        = trigdata->tg_relation;

    SPI_connect();

    // 修改第3列为新值,第5列设为 NULL
    int  attnum[] = {3, 5};
    Datum newvals[] = {
        CStringGetTextDatum("new_value"),  // 列3的新值
        (Datum) 0                          // 列5设为 NULL
    };
    char nulls[] = {' ', 'n'};  // 列3非NULL, 列5为NULL

    HeapTuple newtuple = SPI_modifytuple(
        rel,
        oldtuple,
        2,        // 要修改的列数
        attnum,   // 要修改的列号
        newvals,  // 新值
        nulls     // NULL 标记
    );

    SPI_finish();
    return PointerGetDatum(newtuple);
}
posted @ 2026-06-25 00:15  songlh424  阅读(4)  评论(0)    收藏  举报