第四十八章.服务器编程接口
内容
接口函数
接口支持函数
存储器管理
数据改变的可视性
例子
服务器编程接口(Server Programming Interface) (SPI)给我们在用户定义的 C 函数里面运行 SQL查询的能力.可用的过程语言(PL)给我们一个实现这些功能的可选的手段.
实际上,SPI 只是一套用于访问分析器,规划器,优化器和执行器(Parser,Planner,Optimizer and Executor)的本机接口函数.SPI 同样做一些存储器管理工作.
为了避免混淆,我们将使用 函数(function) 来代表 SPI 接口函数,用 过程(procedure) 代表用户用 SPI定义 C 函数.
SPI 过程总是被一些(上层)执行器和 SPI 管理器用执行器调用来运行你的查询.其他过程可以通过执行器从你的过程里运行查询来调用.
注意,如果在你的过程里执行查询时,事务退出了,那么控制不会返回到你的过程中.相反,所有工作都将回卷并且服务器将等待客户端的另一个命令.这一点将在以后的版本中修正.
其他限制是不能执行 BEGIN,END 和 ABORT (交易控制语句)和游标操作.这些同样在将来的版本中要被修改.
如果执行成功了,SPI 函数返回一个非负结果(或者通过返回一个整数值或放在 SPI_result 全局变量,象下面描述的那样).出错时,返回一个负数或 NULL 结果.
接口函数
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_connect
名称
SPI_connect ― 把你的过程与 SPI 管理器连接起来.
语法
int SPI_connect(void)
输入
无
输出
int
返回状态
SPI_OK_CONNECT
如果联接成功
SPI_ERROR_CONNECT
如果联接失败
描述SPI_connect 打开一个与 Postgres 后端的联接.如果你需要执行查询你就要调用这个函数.有些使用 SPI 函数的应用可以从非联接的过程调用.
如果试图对一个已经联接的过程调用 SPI_connect 你可能得到一个 SPI_ERROR_CONNECT 错误信息 - 例如,如果你直接从另一个已联接的过程里调用一个过程.实际上,因为子进程可能使用 SPI,子进程返回后你的父进程将不能继续使用 SPI (如果子进程调用了 SPI_finish).这是一个糟糕的方面.
用法
算法
SPI_connect 执行下面操作:
•
初始化 SPI 用于查询执行和存储器管理的内部结构.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_finish
名称
SPI_finish ― 把你的过程与 SPI 管理器断开.
语法
SPI_finish(void)
输入
无
输出
int
SPI_OK_FINISH 如果正常断开,返回此信息
SPI_ERROR_UNCONNECTED 如果从一个未联接过程调用,返回此信息
描述
SPI_finish 关闭一个现有的与 Postgres 后端的联接.你应该在结束通过 SPI 管理器的操作后调用此函数.
如果 SPI_finish 是在当前没有有效联接的情况下被调用的,你可能会得到一个 SPI_ERROR_UNCONNECTED 的返回.这样做没有什么根本性的错误,这意味着 SPI 管理器不做任何事情.
用法
SPI_finish 必须 作为一个已联接的过程的最后一步被调用,否则你可能得到不可预料的结果!注意:如果你从事务退出(通过 elog(ERROR)),你可以安全的忽略对 SPI_finish 的调用.
算法
SPI_finish 执行下列操作:
•
断开你的过程与 SPI 管理器的连接并且释放所有你的过程自 SPI_connect 起通过 palloc 分配的存储器.这些存储器不能再利用!请参考存储器管理.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_exec
名称
SPI_exec ― 创建一个执行规划 (分析器+规划器+优化器)并且执行一个查询.
语法
SPI_exec(query, tcount)
输入
char *query
包含查询规划的字符串
int tcount
返回的最大记录数
输出
int
SPI_OK_EXEC 如果正确断开,返回此值
SPI_ERROR_UNCONNECTED 如果从一个未联接的过程调用,返回此值
SPI_ERROR_ARGUMENT 如果查询是 NULL(空)或 tcount < 0,返回此值
SPI_ERROR_UNCONNECTED 如果过程未联接,返回此值.
SPI_ERROR_COPY 如果 COPY TO/FROM stdin(标准输入),返回此值.
SPI_ERROR_CURSOR 如果 DECLARE/CLOSE CURSOR,FETCH,返回此值
SPI_ERROR_TRANSACTION 如果 BEGIN/ABORT/END,返回此值.
SPI_ERROR_OPUNKNOWN 如果查询类型未知(这种情况不应发生).
如果你的查询执行成功,那么将返回下列非负数值之一:
SPI_OK_UTILITY 如果执行了某些应用(例如 CREATE TABLE ...)
SPI_OK_SELECT 如果执行了 SELECT (但不是 SELECT ... INTO!)
SPI_OK_SELINTO 如果执行了 SELECT ... INTO
SPI_OK_INSERT 如果执行了 INSERT(或 INSERT ... SELECT)
SPI_OK_DELETE 如果执行了 DELETE
SPI_OK_UPDATE 如果执行了 UPDATE
描述
SPI_exec 创建一个执行规划(分析器+规划器+优化器)并且执行查询以获取 tcount 条记录.
用法
这个(函数)只能从一个以联接的过程中调用.如果 tcount 是零则对查询扫描返回的所有记录都执行查询.使用 tcount > 0 你可以限制查询执行的记录数.例如,
SPI_exec ("insert into table select * from table", 5);
将最多允许 5 条记录插入表中.如果你的查询执行成功则返回一个非负数.
注意:你可能在一个字符串里传递许多查询或一个查询字符串可能被 RULE (规则)重写.SPI_exec 返回最后一个执行的查询的结果.
最后一个被执行的查询的实际记录数放在全局变量 SPI_processed 里返回(如果不是 SPI_OK_UTILITY).如果返回了SPI_OK_SELECT 而且 SPI_processed > 0 那么你可以通过全局指针 SPITupleTable *SPI_tuptable 来访问这些选择了的记录:同样要注意,SPI_finish 将释放所有 SPITupleTable 并令所有 SPITupleTable 不可用!(参阅存储器管理).
SPI_exec 可能返回下面的(本地)值:
SPI_ERROR_ARGUMENT 如果查询是 NULL (空)或 tcount < 0,返回此值.
SPI_ERROR_UNCONNECTED 如果过程没有联接,返回此值.
SPI_ERROR_COPY如果是 COPY TO/FROM stdin(标准输入),返回此值.
SPI_ERROR_CURSOR 如果是 DECLARE/CLOSE CURSOR,FETCH,返回此值.
SPI_ERROR_TRANSACTION 如果 BEGIN/ABORT/END,返回此值.
SPI_ERROR_OPUNKNOWN 如果查询类型未知(不应发生这种情况),返回此值.
算法
SPI_exec 执行下面操作:
•
断开你的过程与 SPI 管理器的连接并且释放所有你的过程自 SPI_connect 起通过 palloc 分配的存储器.这些存储器不能再利用!请参考存储器管理。
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_prepare
名称
SPI_prepare ― 将你的过程与 SPI 管理器连接.
语法
SPI_prepare(query, nargs, argtypes)
输入
query
查询字符串
nargs
输入的参数个数($1 ... $nargs -象 SQL-函数里一样)
argtypes
指向输入参数的类型为 OID 的指针数组
输出
void *
指向一个执行规划的指针(分析器+规划器+优化器)
描述
SPI_prepare 创建和返回一个执行规划(分析器+规划器+优化器)但是不执行查询.应该只从一个已联接的过程内部调用.
用法
nargs 是参数个数($1 ... $nargs - 象 SQL-函数里一样),并且 nargs 可以是 0 --只有在查询里没有任何 $1 时是这样.
准备好的执行规划的执行速度有时快很多,所以如果某个查询会被执行多次时这个特性可能会很有用.
SPI_prepare 返回的规划可能只能被用于目前的过程,因为 SPI_finish 将释放为规划分配的存储器.参考 SPI_saveplan.
如果成功,将返回一个非空的指针.否则,你会得到一个 NULL(空)的规划.不管那种情况 SPI_result 都将象 SPI_exec 返回的值那样被设置,除非它被设置为 SPI_ERROR_ARGUMENT --因为查询是 NULL 或 nargs < 0 或 nargs > 0 && argtypes 是 NULL.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_saveplan
名称
SPI_saveplan ― 保存传递进来的分析器规划
语法
SPI_saveplan(plan)
输入
void *query
传入的规划
输出
void *
执行规划定位.如果成功返回 NULL.
SPI_result
SPI_ERROR_ARGUMENT 如果规划是 NULL,返回此值
SPI_ERROR_UNCONNECTED 如果过程没有联接,返回此值
描述
SPI_saveplan 把一个由 SPI_prepare 准备的规划存储在安全的存储器中,以避免被 SPI_finish 或事务管理器释放.
目前的 Postgres 版本不能把计划好的规划存储在系统表里并从中获取它们执行.这个(特性)将在未来的版本中实现.做为可选的方法,目前的版本可以在当前会话中随后击活的过程中重新使用准备好的规划.用 SPI_execp 执行这些保存了的规划.
用法
SPI_saveplan 把一个传入的规划(由 SPI_prepare 准备)保存在存储器中防止被 SPI_finish 和事务管理器释放并且返回一个指向被保存的规划的指针.你可以把返回的指针保存在一个局部变量里.在准备一个规划或在 SPI_execp (见下面)中使用已准备的规划时,注意检查这个指针是否为 NULL(空).
注意:如果已准备的规划参考的对象之一 (一个关系,函数,等.)在你的会话过程中被删除(被你的后端或其他过程)那么对此规划的 SPI_execp 执行结果将不可预料.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_execp
名称
SPI_execp ― 执行一个从 SPI_saveplan 来的规划
语法
SPI_execp(plan,
values,
nulls,
tcount)
输入
void *plan
执行规划
Datum *values
实际参数值
char *nulls
描述哪个参数获得 NULL 值的数组
'n' 表明允许 NULL
' ' 表示不允许 NULL
int tcount
将被执行的规划的记录数
输出
int
返回与 SPI_exec 一样的值以及
SPI_ERROR_ARGUMENT 如果 plan 是 NULL 或 tcount < 0,返回此值
SPI_ERROR_PARAM 如果 values 是 NULL 并且所准备的 plan 带有一些参数.
SPI_tuptable
如果成功,则象 SPI_exec 一样初始化
SPI_processed
如果成功,则象 SPI_exec 一样初始化
描述
SPI_execp 把一个由 SPI_prepare 准备的规划存储在安全存储器中,以免被 SPI_finish 或事务管理器释放.
目前的 Postgres 版本不能把计划好的规划存储在系统表里并从中获取它们执行.这个(特性)将在未来的版本中实现.做为绕开的方法,目前的版本可以在当前会话中随后击活的过程中重新使用准备好的规划.用 SPI_execp 执行这些保存了的规划.
用法
如果 nulls 是 NULL 那么 SPI_execp 假设所有值(如果有的话)都是 NOT NULL.
注意:如果已准备的规划参考的对象之一(一个关系,函数,等.)在你的会话过程中被删除(被你的后端或其他过程)那么对此规划的 SPI_execp 执行结果将不可预料.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
接口支持函数
所有后面描述的函数都可被联接或未联接的过程使用.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_copytuple
名称
SPI_copytuple ― 把记录的拷贝放到上层执行器环境
语法
SPI_copytuple(tuple)
输入
HeapTuple tuple
Input tuple to be copied
Outputs
HeapTuple
输入被拷贝的记录
non-NULL 如果 tuple 为非空(not NULL)并且拷贝成功
NULL 只有 tuple 是 NULL
描述
SPI_copytuple 把一个记录的拷贝放到上层高级执行器环境.参考存储器管理章节.
用法
TBD
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_modifytuple
名称
SPI_modifytuple ― 修改关系的记录
语法
SPI_modifytuple(rel, tuple , nattrs
, attnum , Values , Nulls)
输入
Relation rel
HeapTuple tuple
要修改的输入记录
int nattrs
attnum 里字段号的个数
int * attnum
将要修改的字段号的数组
Datum * Values
给声明的属性的新值
char * Nulls
若存在,哪个字段是 NULL.
输出
HeapTuple
修改后的新记录
non-NULL 如果 tuple 为非空(not NULL)并且修改成功
NULL 只有当 tuple 为 NULL(空)
SPI_result
SPI_ERROR_ARGUMENT 如果 rel 是 NULL 或 tuple 是 NULL 或 natts &le(小于)0 或 attnum 是 NULL 或 Values 是 NULL.
SPI_ERROR_NOATTRIBUTE 如果在 attnum 里有一个非法的数字 (attnum &le(小于)0 或 > 记录中字段数)
描述
SPI_modifytuple 修改一个上层执行器环境的记录.参考存储器管理章节.
用法
如果成功,返回一个指向新记录的指针.新记录在执行器上层环境分配(参见 存储器管理).传入的记录没有改变.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_fnumber
名称
SPI_fnumber ― 查找声明的字段的字段号
语法
SPI_fnumber(tupdesc, fname)
输入
TupleDesc tupdesc
输入记录的描述
char * fname
字段名
输出
int
字段号
有效的以1为基的字段索引号
SPI_ERROR_NOATTRIBUTE 如果命名的字段没有找到
描述
SPI_fnumber 返回 fname 指明的字段的字段号.
用法
字段号是以1为基的.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_fname
名称
SPI_fname ― 找出指明的字段的字段名
语法
SPI_fname(tupdesc, fname)
输入
TupleDesc tupdesc
输入的记录描述
char * fnumber
字段号
输出
char *
字段名
NULL -- 如果 fnumber 超出范围
出错时,SPI_result 设置为 SPI_ERROR_NOATTRIBUTE
描述
SPI_fname 返回指明的字段的字段名.
用法
字段号是以 1 为基的.
算法
返回一个新分配的字段名的拷贝.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_getvalue
名称
SPI_getvalue ― 返回指明的字段的字符串值
语法
SPI_getvalue(tuple, tupdesc, fnumber)
输入
HeapTuple tuple
输入的待检查的字段
TupleDesc tupdesc
输入字段描述
int fnumber
字段号
输出
char *
字段值或 NULL(空),如果
字段为 NULL(空)
fnumber 超出范围(这时 SPI_result 设置为 SPI_ERROR_NOATTRIBUTE)
没有可用的输出函数(这时 SPI_result 设置为 SPI_ERROR_NOOUTFUNC)
描述
SPI_getvalue 返回指明字段的一个外部(字符串)形式的值.
用法
字段号是以 1 为基的.
算法
根据数值的要求分配存储器.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_getbinval
名称
SPI_getbinval ― 返回指明的字段的二进制数值
语法
SPI_getbinval(tuple, tupdesc, fnumber, isnull)
输入
HeapTuple tuple
要检查的输入记录
TupleDesc tupdesc
输入记录描述
int fnumber
字段号
输出
Datum
字段二进制数值
bool * isnull
字段是否为空的标志
SPI_result
SPI_ERROR_NOATTRIBUTE
描述
SPI_getbinval 返回指明字段的二进制数值
用法
字段号是以1为基的.
算法
不为二进制数值分配新空间.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_gettype
名称
SPI_gettype ― 返回指明字段的类型名
语法
SPI_gettype(tupdesc, fnumber)
输入
TupleDesc tupdesc
输入字段描述
int fnumber
字段号
输出
char *
指明字段号的类型名称
SPI_result
SPI_ERROR_NOATTRIBUTE
描述
SPI_gettype 返回一个指明字段的类型名的拷贝.
用法
字段号是以1为基的.
算法
不为二进制数值分配新空间.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_gettypeid
名称
SPI_gettypeid ― 返回指明字段的类型 OID
语法
SPI_gettypeid(tupdesc, fnumber)
输入
TupleDesc tupdesc
输入字段的描述
int fnumber
字段号
输出
OID
指明字段号的类型 OID.
SPI_result
SPI_ERROR_NOATTRIBUTE
描述
SPI_gettypeid 返回指明的字段的类型 OID.
用法
字段号是以 1 为基的.
算法
TBD
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_getrelname
名称
SPI_getrelname ― 返回指明关系的名称
语法
SPI_getrelname(rel)
输入
Relation rel
输入的关系
输出
char *
指定的关系的名称
描述
SPI_getrelname 返回指明关系的名称.
用法
TBD
算法
把关系名称拷贝到新的存储器中去.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_palloc
名称
SPI_palloc ― 在上层执行器环境中分配存储器
语法
SPI_palloc(size)
输入
Size size
八进制的待分配存储空间大小
输出
void *
指明大小的新存储区
描述
SPI_palloc 在执行器上层环境分配存储器.参阅存储器管理章节.
用法
TBD
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_repalloc
名称
SPI_repalloc ― 重新在执行器上层环境中分配存储器
语法
SPI_repalloc(pointer, size)
输入
void * pointer
指向现有存储器
Size size
八进制的要分配的存储空间尺寸
输出
void *
新分配的存储空间, 带有从现存区域拷贝来的内容.
描述
SPI_repalloc 在上层执行器环境中重新分配存储器.参考存储器管理章节.
用法
TBD
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
SPI_pfree
名称
SPI_pfree ― 从上层执行器环境中释放存储器
语法
SPI_pfree(pointer)
输入
void * pointer
指向现有存储器(区)的指针
输出
无
描述
SPI_pfree 释放在上层执行器环境中的存储器.参考存储器管理章节.
用法
TBD
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
存储器管理
服务器在存储器环境按这样的方法分配存储器:在某个环境分配的存储器可以被环境析构器释放而不会影响其他环境中分配的存储器.所有存储器分配(通过 palloc 等)都被当作在当前环境的区域中分配存储器.如果你试图释放(或再分配)不在当前环境的存储器,你将得到不可预料的结果.
创建存储器环境和切换存储器环境是 SPI 管理器中存储器管理器的任务.
SPI 过程处理两种存储器环境:上层执行器存储器环境和过程存储器环境(如果已联接).
在一个过程与 SPI 管理器联接之前,当前存储器环境是上层执行器环境,所以所有由过程自身通过 palloc/repalloc 或通过 SPI 工具函数在联接到 SPI 管理器之前分配的存储器都在这个环境里.
在进行 SPI_connect 调用之后,当前环境是过程自身所有的.通过 palloc/repalloc 或通过 SPI 应用函数分配的存储器(除了 SPI_copytuple,SPI_modifytuple,SPI_palloc 和 SPI_repalloc 以外)都在这个环境中分配.
当进程与 SPI 管理器断开(通过调用 SPI_finish)后,当前环境恢复为上层执行器环境并且所有在过程存储器环境分配的存储器都被释放,并且不可继续使用!
如果你想返回一些东西给上层执行器,那么你必须为此在上层环境分配一片存储器!
SPI 不能自动释放在上层执行器环境里分配的存储器!
SPI 在查询完成后自动释放查询执行期间分配的存储器!
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
数据改变的可视性
Postgres 数据修改的可视性规则:在查询执行过程中,由查询本身造成的数据修改(通过 SQL-函数, SPI-函数,触发器)对查询扫描而言是不可见的.例如,在查询 INSERT INTO a SELECT * FROM a 里,插入的记录对 SELECT 的扫描是不可见的.实际上,这么做在数据库内部形成非递归的数据库表的复制品(当然是要受到唯一索引规则的制约的喽)。
由查询 Q 造成的改变可以为查询 Q 以后运行的查询可见,不管这些查询是在查询 Q 内部开始运行(在 Q 运行期间)的还是Q运行完毕后开始运行的.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
例子
这个 SPI 使用的样例演示了可视性规则.在 src/test/regress/regress.c 和 contrib/spi 里有更复杂的例子.
这是一个非常简单的 SPI 使用的例子.过程 execq 在其第一个参数里接收一个 SQL 查询,第二个参数接收一个 tcount(译注:记录个数),用 SPI_exec 执行这个查询并且返回查询执行过的记录个数:
#include "executor/spi.h" /* this is what you need to work with SPI(这个是你用SPI所要用的头文件) */
int execq(text *sql, int cnt);
int
execq(text *sql, int cnt)
{
int ret;
int proc = 0;
SPI_connect();
ret = SPI_exec(textout(sql), cnt);
proc = SPI_processed;
/*
* If this is SELECT and some tuple(s) fetched -
* returns tuples to the caller via elog (NOTICE).
*/
if ( ret == SPI_OK_SELECT && SPI_processed > 0 )
{
TupleDesc tupdesc = SPI_tuptable->tupdesc;
SPITupleTable *tuptable = SPI_tuptable;
char buf[8192];
int i;
for (ret = 0; ret < proc; ret++)
{
HeapTuple tuple = tuptable->vals[ret];
for (i = 1, buf[0] = 0; i <= tupdesc->natts; i++)
sprintf(buf + strlen (buf), " %s%s",
SPI_getvalue(tuple, tupdesc, i),
(i == tupdesc->natts) ? " " : " |");
elog (NOTICE, "EXECQ: %s", buf);
}
}
SPI_finish();
return (proc);
}
然后,编译并创建函数:
create function execq (text, int4) returns int4 as '...path_to_so' language 'c';
vac=> select execq('create table a (x int4)', 0);
execq
-----
0
(1 row)
vac=> insert into a values (execq('insert into a values (0)',0));
INSERT 167631 1
vac=> select execq('select * from a',0);
NOTICE:EXECQ: 0 <<< inserted by execq
NOTICE:EXECQ: 1 <<< value returned by execq and inserted by upper INSERT
execq
-----
2
(1 row)
vac=> select execq('insert into a select x + 2 from a',1);
execq
-----
1
(1 row)
vac=> select execq('select * from a', 10);
NOTICE:EXECQ: 0
NOTICE:EXECQ: 1
NOTICE:EXECQ: 2 <<< 0 + 2, only one tuple inserted - as specified
execq
-----
3 <<< 10 is max value only, 3 is real # of tuples
(1 row)
vac=> delete from a;
DELETE 3
vac=> insert into a values (execq('select * from a', 0) + 1);
INSERT 167712 1
vac=> select * from a;
x
-
1 <<< no tuples in a (0) + 1
(1 row)
vac=> insert into a values (execq('select * from a', 0) + 1);
NOTICE:EXECQ: 0
INSERT 167713 1
vac=> select * from a;
x
-
1
2 <<< there was single tuple in a + 1
(2 rows)
-- This demonstrates data changes visibility rule:
vac=> insert into a select execq('select * from a', 0) * x from a;
NOTICE:EXECQ: 1
NOTICE:EXECQ: 2
NOTICE:EXECQ: 1
NOTICE:EXECQ: 2
NOTICE:EXECQ: 2
INSERT 0 2
vac=> select * from a;
x
-
1
2
2 <<< 2 tuples * 1 (x in first tuple)
6 <<< 3 tuples (2 + 1 just inserted) * 2 (x in second tuple)
(4 rows) ^^^^^^^^
tuples visible to execq() in different invocations
--------------------------------------------------------------------------------