在使用C语言编写PostgreSQL用户自定义函数(UDF)时,使用数组是很常见的需求。本篇将基于一个完整的int32数组插入操作示例,详细讲解如何在C语言自定义函数中正确处理PostgreSQL数组类型,包括数组的解构、验证、元素操作和重构等核心技术点。示例程序如下:
#include "postgres.h"
#include "fmgr.h"
#include "utils/array.h"
#include "catalog/pg_type.h"
#include "utils/lsyscache.h"
PG_MODULE_MAGIC;
static Datum
int32_array_insert_at(FunctionCallInfo fcinfo, int32 position, bool is_prepend);
/* 版本1:添加到末尾 */
PG_FUNCTION_INFO_V1(int32_array_append);
Datum
int32_array_append(PG_FUNCTION_ARGS)
{
return int32_array_insert_at(fcinfo, 0, false);
}
/* 版本2:添加到开头 */
PG_FUNCTION_INFO_V1(int32_array_prepend);
Datum
int32_array_prepend(PG_FUNCTION_ARGS)
{
return int32_array_insert_at(fcinfo, 1, true);
}
/* 版本3:在指定位置插入 */
PG_FUNCTION_INFO_V1(int32_array_insert);
Datum
int32_array_insert(PG_FUNCTION_ARGS)
{
int32 position;
if (PG_ARGISNULL(2))
position = 1; /* 默认位置为1 */
else
position = PG_GETARG_INT32(2);
if (position < 1)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("位置必须是正整数")));
return int32_array_insert_at(fcinfo, position, false);
}
/* 通用插入函数 */
static Datum
int32_array_insert_at(FunctionCallInfo fcinfo, int32 position, bool is_prepend)
{
ArrayType *input_array = NULL;
int32 new_value;
Datum *elems = NULL;
bool *nulls = NULL;
int nelems = 0;
int ndim;
Oid elem_type;
int16 elem_width;
bool elem_byval;
char elem_align;
ArrayType *result_array;
Datum *new_elems;
bool *new_nulls;
int i, j;
int insert_pos;
/* 处理数组参数 */
if (!PG_ARGISNULL(0))
{
input_array = PG_GETARG_ARRAYTYPE_P(0);
/* 验证数组元素类型 */
elem_type = ARR_ELEMTYPE(input_array);
if (elem_type != INT4OID)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("输入数组元素类型必须为integer")));
/* 获取数组信息 */
get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);
/* 解构数组 */
deconstruct_array(input_array, elem_type, elem_width,
elem_byval, elem_align,
&elems, &nulls, &nelems);
ndim = ARR_NDIM(input_array);
if (ndim != 1 && ndim != 0)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("只支持一维数组")));
}
else
{
/* 没有输入数组,设置默认值 */
elem_type = INT4OID;
get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);
}
/* 获取要添加的值 */
if (PG_ARGISNULL(1))
{
/* 新值为NULL */
new_value = 0;
}
else
{
new_value = PG_GETARG_INT32(1);
}
/* 确定插入位置 */
if (is_prepend)
insert_pos = 1; /* 添加到开头 */
else if (position > 0)
insert_pos = position;
else
insert_pos = nelems + 1; /* 添加到末尾 */
/* 调整位置到有效范围 */
if (insert_pos < 1)
insert_pos = 1;
if (insert_pos > nelems + 1)
insert_pos = nelems + 1;
/* 分配新数组内存 */
new_elems = (Datum *) palloc((nelems + 1) * sizeof(Datum));
new_nulls = (bool *) palloc((nelems + 1) * sizeof(bool));
/* 复制插入点之前的元素 */
for (i = 0, j = 0; i < insert_pos - 1; i++, j++)
{
if (elems)
{
new_elems[j] = elems[i];
new_nulls[j] = nulls[i];
}
else
{
break; /* 没有输入数组 */
}
}
/* 插入新元素 */
new_elems[j] = Int32GetDatum(new_value);
new_nulls[j] = PG_ARGISNULL(1);
j++;
/* 复制插入点之后的元素 */
for (; i < nelems; i++, j++)
{
new_elems[j] = elems[i];
new_nulls[j] = nulls[i];
}
/* 创建新数组 */
result_array = construct_array(new_elems, nelems + 1, elem_type,
elem_width, elem_byval, elem_align);
/* 释放内存 */
if (elems)
{
pfree(elems);
pfree(nulls);
}
pfree(new_elems);
pfree(new_nulls);
PG_RETURN_ARRAYTYPE_P(result_array);
}

一、PostgreSQL C函数开发基础
在开始数组操作前,先了解C语言编写PostgreSQL函数的核心要素:
1.1 必要的头文件
示例代码中引入的核心头文件各有其用途:
#include "postgres.h" // PostgreSQL核心定义
#include "fmgr.h" // 函数调用接口
#include "utils/array.h" // 数组操作核心函数
#include "catalog/pg_type.h"// 内置类型OID定义(如INT4OID)
#include "utils/lsyscache.h"// 类型信息缓存函数
1.2 模块标识与函数注册
PG_MODULE_MAGIC; // 必须的模块魔术标识,用于版本校验
// 函数注册宏,V1表示使用版本1的调用接口
PG_FUNCTION_INFO_V1(int32_array_append);
二、数组操作核心流程解析
示例代码实现了int32数组的追加、前置和指定位置插入功能,核心逻辑封装在int32_array_insert_at函数中,完整的数组操作流程分为以下步骤:
2.1 数组参数接收与验证
2.1.1 获取数组参数
// 从函数调用信息中获取数组参数(第一个参数)
ArrayType *input_array = NULL;
if (!PG_ARGISNULL(0)) {
input_array = PG_GETARG_ARRAYTYPE_P(0);
}
PG_ARGISNULL(n):检查第n个参数是否为NULLPG_GETARG_ARRAYTYPE_P(n):获取第n个参数的数组指针
2.1.2 验证数组类型与维度
// 获取数组元素类型OID
elem_type = ARR_ELEMTYPE(input_array);
// 验证元素类型是否为int32(INT4OID是int4类型的内置OID)
if (elem_type != INT4OID)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("输入数组元素类型必须为integer")));
// 验证数组维度(仅支持一维数组)
ndim = ARR_NDIM(input_array);
if (ndim != 1 && ndim != 0)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("只支持一维数组")));
核心宏说明:
ARR_ELEMTYPE(arr):获取数组元素的类型OIDARR_NDIM(arr):获取数组的维度ereport(ERROR, ...):抛出PostgreSQL错误,包含错误码和错误信息
2.2 数组解构(转换为原生C数组)
PostgreSQL的数组是封装的ArrayType类型,需要转换为原生C数组才能操作:
// 获取元素类型的内存属性(长度、是否传值、对齐方式)
get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);
// 解构数组为原生C数组
Datum *elems = NULL; // 存储元素值的数组
bool *nulls = NULL; // 存储元素是否为NULL的数组
int nelems = 0; // 元素个数
deconstruct_array(input_array, elem_type, elem_width,
elem_byval, elem_align,
&elems, &nulls, &nelems);
get_typlenbyvalalign:获取类型的内存布局信息,为数组解构做准备Datum:PostgreSQL中存储任意数据类型的通用容器nulls:布尔数组,标记对应位置的元素是否为NULLdeconstruct_array:用于从数组对象提取数据的简单方法,说明如下:- input_array: 待解析的数组对象(必须非空)
- elem_type: 元素数据类型信息
- elem_width: 元素类型长度
- elem_byval: 是否按值传递
- elem_align: 内存对齐方式
- elems: 返回值,指向已分配内存的数据值数组
- nulls: 返回值,指向已分配内存的空值标记数组
- nelems: 返回值,设置为提取的元素数量
若调用方不支持数组中的空值,可传入 nullsp == NULL。注意这将产生信息量较少的错误提示,因此仅在确实不会出现空值的场景下使用。
若数组元素为按引用传递的数据类型,返回的 Datum 值将指向数组对象内部。
2.3 数组元素操作(插入新元素)
2.3.1 确定插入位置
// 处理插入位置逻辑,确保位置在有效范围(1 ~ nelems+1)
if (insert_pos < 1)
insert_pos = 1;
if (insert_pos > nelems + 1)
insert_pos = nelems + 1;
PostgreSQL数组默认是1-based(从1开始计数),而C数组是0-based,需要注意转换。
2.3.2 内存分配与元素复制
// 分配新数组内存(原数组长度+1)
new_elems = (Datum *) palloc((nelems + 1) * sizeof(Datum));
new_nulls = (bool *) palloc((nelems + 1) * sizeof(bool));
// 复制插入点之前的元素
for (i = 0, j = 0; i < insert_pos - 1; i++, j++) {
new_elems[j] = elems[i];
new_nulls[j] = nulls[i];
}
// 插入新元素
new_elems[j] = Int32GetDatum(new_value); // 将int32转换为Datum
new_nulls[j] = PG_ARGISNULL(1); // 标记新元素是否为NULL
j++;
// 复制插入点之后的元素
for (; i < nelems; i++, j++) {
new_elems[j] = elems[i];
new_nulls[j] = nulls[i];
}
关键要点:
- 使用
palloc而非malloc:PostgreSQL的内存分配函数,内存会在事务结束时自动释放 Int32GetDatum:将int32类型转换为Datum类型(PostgreSQL通用数据类型)- 分三段复制:插入点前 → 新元素 → 插入点后
2.4 重构数组(转换回PostgreSQL数组)
操作完成后,需要将原生C数组转换回PostgreSQL的ArrayType:
// 构建新的PostgreSQL数组
result_array = construct_array(new_elems, nelems + 1, elem_type,
elem_width, elem_byval, elem_align);
construct_array:将Datum数组重构为PostgreSQL数组,参数说明如下:- new_elems: 组成数组内容的数据项数组(不支持空元素值)
- nelems: 元素个数
- elem_type: 元素数据类型信息
- elem_width: 元素类型长度
- elem_byval: 是否按值传递
- elem_align: 内存对齐方式
本函数会构造并返回一个已分配内存的一维数组对象。注意:即使元素是按引用传递的类型,其值也会被复制到数组对象中。另请注意:若 nelems = 0,返回的将是0维数组而非1维数组。
2.5 内存释放与结果返回
// 释放解构后的原数组内存
if (elems) {
pfree(elems);
pfree(nulls);
}
// 释放新数组的临时内存
pfree(new_elems);
pfree(new_nulls);
// 返回结果数组
PG_RETURN_ARRAYTYPE_P(result_array);
pfree:释放palloc分配的内存PG_RETURN_ARRAYTYPE_P:将数组指针作为函数结果返回
三、函数编译与部署
3.1 编译命令
使用CMake构建,CMake配置文件如下:
cmake_minimum_required(VERSION 3.25)
project(add_c C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_BUILD_TYPE debug)
list(APPEND flags "-fPIC")
find_program(PG_CONFIG pg_config REQUIRED)
execute_process(COMMAND ${PG_CONFIG} --includedir-server
OUTPUT_VARIABLE POSTGRESQL_INCLUDE_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${PG_CONFIG} --libdir
OUTPUT_VARIABLE POSTGRESQL_LIB_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND ${PG_CONFIG} --libs
OUTPUT_VARIABLE POSTGRESQL_LIBS
OUTPUT_STRIP_TRAILING_WHITESPACE)
add_library(add_c SHARED add.c)
# 设置生成的共享库名称,去掉lib前缀
set_target_properties(add_c PROPERTIES PREFIX "")
# 最终so文件名
set_target_properties(add_c PROPERTIES OUTPUT_NAME add_c)
# 安装目录
install(TARGETS add_c LIBRARY DESTINATION ${POSTGRESQL_LIB_DIR})
target_compile_options(add_c PUBLIC "-fPIC" "-g3" "-O0")
target_link_options(add_c PUBLIC "-shared")
target_include_directories(add_c PUBLIC ${POSTGRESQL_INCLUDE_DIR})
target_link_directories(add_c PUBLIC ${POSTGRESQL_LIB_DIR})

3.2 注册函数
在PostgreSQL中执行以下SQL注册函数:
-- 追加元素到数组末尾
CREATE OR REPLACE FUNCTION int32_array_append(integer[], integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_append'
LANGUAGE C STRICT;
-- 前置元素到数组开头
CREATE OR REPLACE FUNCTION int32_array_prepend(integer[], integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_prepend'
LANGUAGE C STRICT;
-- 在指定位置插入元素
CREATE OR REPLACE FUNCTION int32_array_insert(integer[], integer, integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_insert'
LANGUAGE C STRICT;
3.3 测试函数
-- 测试追加
SELECT int32_array_append(ARRAY[1,2,3], 4); -- 返回 {1,2,3,4}
-- 测试前置
SELECT int32_array_prepend(ARRAY[1,2,3], 0); -- 返回 {0,1,2,3}
-- 测试指定位置插入
SELECT int32_array_insert(ARRAY[1,2,3], 99, 2); -- 返回 {1,99,2,3}

四、要点说明
4.1 内存管理
- 始终使用
palloc/pfree而非malloc/free:PostgreSQL的内存上下文会自动管理这些内存 - 及时释放临时内存:避免内存泄漏,尤其是在循环或复杂逻辑中
- 处理NULL参数:必须检查
PG_ARGISNULL,避免空指针访问
4.2 错误处理
- 使用
ereport抛出标准化错误:包含错误码(errcode)和用户友好的错误信息(errmsg) - 验证输入参数:包括数组类型、维度、位置合法性等
- 处理边界情况:如空数组、插入位置超出范围等
4.3 性能优化
- 复用类型信息:使用
get_typlenbyvalalign缓存类型属性,避免重复查询 - 减少内存拷贝:尽量在原数组上操作,避免不必要的数组复制
- 使用适当的数据类型:根据需求选择
int2/int4/int8,避免类型转换开销