35.15. 将相关对象打包成扩展 #
一个有用的 PostgreSQL 扩展通常包含多个 SQL 对象;例如,一种新的数据类型将需要新的函数、新的操作符,并且很可能还需要新的索引操作符类。把所有这些对象收集到一个单独的包中,有助于简化数据库管理。PostgreSQL 将这样的包称为扩展。要定义一个扩展,至少需要一个脚本文件,其中包含用于创建扩展对象的 SQL 命令,以及一个控制文件,用来指定扩展本身的一些基本属性。如果扩展包含 C 代码,通常还会有一个共享库文件,C 代码已被构建到其中。一旦准备好这些文件,只需执行一个简单的CREATE EXTENSION命令,就能把这些对象装入数据库。
使用扩展而不是仅仅运行 SQL 脚本把一堆“松散”对象装入数据库,最主要的优点在于 PostgreSQL
能够理解这些对象是属于同一个扩展的。你可以用一条DROP EXTENSION命令删除全部对象(无需维护单独的“卸载”脚本)。更有用的是,
pg_dump 知道自己不应转储扩展的各个成员对象
— 它只会在转储中包含一条 CREATE EXTENSION
命令。这极大地简化了迁移到扩展新版本的过程,即使新版本包含比旧版本更多或不同的对象也是如此。不过要注意,在把这样的转储装载到新数据库时,必须能够访问该扩展的控制文件、脚本文件以及其他相关文件。
PostgreSQL 不允许你删除扩展中包含的单个对象,除非删除整个扩展。另外,虽然你可以修改扩展成员对象的定义(例如对函数使用 CREATE OR REPLACE FUNCTION),但要记住,被修改后的定义不会被 pg_dump 转储。这样的修改通常只有在你同时在扩展脚本文件中做出同样修改时才有意义。(不过,对于包含配置数据的表有特殊规定;见见下文。)
扩展机制还提供了用于打包修改脚本的支持,以便调整扩展中所含 SQL 对象的定义。例如,如果扩展 1.1 版相比 1.0 版增加了一个函数,并修改了另一个函数的函数体,那么扩展作者可以提供一个更新脚本来只完成这两项修改。随后就可以使用
ALTER EXTENSION UPDATE 命令应用这些修改,并跟踪在某个数据库中实际安装的是该扩展的哪个版本。
哪些 SQL 对象种类可以成为扩展成员,见ALTER EXTENSION的说明。特别是,数据库集簇范围内的对象,如数据库、角色和表空间,不能成为扩展成员,因为扩展只在单个数据库内可见。(尽管并不禁止扩展脚本创建这类对象,但如果这样做,它们不会作为扩展的一部分受到跟踪。)还要注意,虽然表可以成为扩展成员,但其附属对象(如索引)并不直接被视为扩展成员。另一个重要点是,模式可以属于扩展,但反过来不成立:扩展本身只有一个非限定名,并不“位于”任何模式中。不过,扩展的成员对象会在其对象类型适用的情况下属于某个模式。扩展是否应当拥有其成员对象所在的模式,则要视具体情况而定。
35.15.1. 扩展文件
CREATE EXTENSION命令依赖于每个扩展都有一个控制文件,其名称必须是扩展名加上 .control 后缀,并放在安装目录的 SHAREDIR/extension 目录中。还必须至少有一个 SQL 脚本文件,其名称遵循 的模式(例如,扩展 extension--version.sqlfoo 的 1.0 版本使用 foo--1.0.sql)。默认情况下,脚本文件也放在 SHAREDIR/extension 目录中,但控制文件可以为脚本文件指定其他目录。
扩展控制文件的格式与 postgresql.conf 文件相同,即由一组 parameter_name
= value 赋值组成,每行一条。允许空行和以 # 引入的注释。任何不是单个单词或数字的值都要记得加引号。
控制文件可以设置下列参数:
directory(string)包含扩展 SQL 脚本文件的目录。除非给出的是绝对路径,否则该名称相对于安装的
SHAREDIR目录。默认行为等价于指定directory = 'extension'。default_version(string)扩展的默认版本(即在
CREATE EXTENSION中未指定版本时将安装的版本)。虽然这个参数可以省略,但那样一来,如果没有给出VERSION选项,CREATE EXTENSION就会失败,因此一般不希望这样做。comment(string)关于扩展的注释(任意字符串)。另外,也可以通过脚本文件中的COMMENT命令来设置该注释。
encoding(string)脚本文件所使用的字符集编码。如果脚本文件包含任何非 ASCII 字符,就应指定这个参数。否则将假定这些文件使用数据库编码。
module_pathname(string)该参数的值会替换脚本文件中每次出现的
MODULE_PATHNAME。如果未设置该参数,则不会进行替换。通常会把它设置为$libdir/,然后在 C 语言函数的shared_library_nameCREATE FUNCTION命令中使用MODULE_PATHNAME,这样脚本文件就无需把共享库的名字硬编码进去。requires(string)本扩展所依赖的其他扩展名称列表,例如
requires = 'foo, bar'。这些被依赖的扩展必须先安装好,本扩展才能安装。superuser(boolean)如果此参数为
true(默认值),则只有超级用户可以创建扩展或将其更新到新版本。如果设为false,则只需具有执行安装或更新脚本中命令所需的权限。relocatable(boolean)如果一个扩展在初次创建之后仍然可以把其包含的对象移动到不同的模式中,那么它就是可重定位的。默认值为
false,也就是该扩展不可重定位。详见下文。schema(string)该参数只能为不可重定位扩展设置。它强制扩展装载到指定名称的模式中,而不能装载到其他模式。详见下文。
除主控制文件
外,扩展还可以有按如下样式命名的次级控制文件:extension.control。如果提供了这些文件,它们必须位于脚本文件目录中。次级控制文件遵循与主控制文件相同的格式。在安装或更新到该扩展的相应版本时,次级控制文件中设置的任何参数都会覆盖主控制文件中的设置。不过,extension--version.controldirectory 和 default_version
这两个参数不能在次级控制文件中设置。
扩展的 SQL 脚本文件可以包含任何 SQL 命令,但事务控制命令(BEGIN、COMMIT 等)和无法在事务块内执行的命令(例如 VACUUM)除外。这是因为脚本文件会被隐式地放在事务块中执行。
扩展的 SQL 脚本文件也可以包含以
\echo 开头的行,扩展机制会忽略这些行(将其视为注释)。这一约定通常用于在脚本文件被直接交给 psql
而不是通过 CREATE EXTENSION 装载时抛出错误(见下文中的示例脚本)。如果没有这种机制,用户可能会意外地把扩展内容作为“松散”对象装载,而不是作为一个扩展来装载,这种状态恢复起来会有些麻烦。
虽然脚本文件可以包含指定编码允许的任意字符,但控制文件应只包含纯
ASCII 字符,因为 PostgreSQL 无法知道控制文件采用的是什么编码。在实践中,只有当你想在扩展注释中使用非 ASCII 字符时这才会成为问题。对此推荐的做法是不要使用控制文件中的
comment 参数,而是在脚本文件中使用
COMMENT ON EXTENSION 来设置注释。
35.15.2. 扩展的可重定位性
用户经常希望把扩展中的对象装载到与扩展作者原先设想不同的模式中。对于这种可重定位性,支持三个级别:
完全可重定位的扩展可以在任何时候移动到另一个模式中,即使它已经被装载到数据库之后也是如此。这通过
ALTER EXTENSION SET SCHEMA命令完成,该命令会自动把所有成员对象重命名到新模式中。通常,只有当扩展对其任何对象所在模式都没有内部假设时,才有可能做到这一点。此外,扩展的对象一开始必须全部位于同一个模式中(不属于任何模式的对象,如过程语言,不算在内)。要把一个扩展标记为完全可重定位,只需在其控制文件中设置relocatable = true。扩展可能在安装期间可重定位,但安装之后不可重定位。如果扩展脚本文件需要显式引用目标模式,例如为 SQL 函数设置
search_path属性时,通常就是这种情况。对于这样的扩展,应在控制文件中设置relocatable = false,并在脚本文件中使用@extschema@来引用目标模式。在脚本执行前,该字符串的每次出现都会被替换为实际目标模式的名称。用户可以通过CREATE EXTENSION的SCHEMA选项设置目标模式。如果扩展完全不支持重定位,应在控制文件中设置
relocatable = false,并把schema设置为预定目标模式的名称。这样将阻止使用CREATE EXTENSION的SCHEMA选项,除非它指定的正是控制文件中命名的那个模式。如果扩展对模式名称有无法通过@extschema@替换解决的内部假设,通常就需要采用这种方式。在这种情况下,@extschema@替换机制仍然可用,只是由于模式名由控制文件决定,其用途比较有限。
在所有情况下,脚本文件执行时,其初始search_path都会指向目标模式;也就是说,CREATE EXTENSION 做的事情等价于:
SET LOCAL search_path TO @extschema@;
这使得脚本文件创建的对象能够进入目标模式。脚本文件当然也可以修改
search_path,但通常不建议这样做。CREATE EXTENSION 完成后,search_path 会恢复为先前的设置。
目标模式由控制文件中的 schema 参数决定(如果给出了该参数);否则由 CREATE EXTENSION 的
SCHEMA 选项决定(如果给出了该选项);否则由当前默认的对象创建模式决定(也就是调用者的 search_path
中第一个模式)。当使用控制文件中的 schema 参数时,如果目标模式尚不存在,则会自动创建;而在另外两种情况下,目标模式必须已经存在。
如果控制文件的 requires 中列出了任何前置扩展,它们的目标模式会追加到 search_path 的初始设置中。这样新扩展的脚本文件就可以看到这些扩展的对象。
虽然不可重定位扩展可以包含分布在多个模式中的对象,但通常仍然希望把所有供外部使用的对象放在单个模式中,这个模式会被视为该扩展的目标模式。这种安排与创建依赖扩展时 search_path 的默认设置配合起来会比较方便。
35.15.3. 扩展配置表
有些扩展包含配置表,其中存放的数据可能会在扩展安装后被用户新增或修改。通常,如果表属于某个扩展,那么 pg_dump 既不会转储该表的定义,也不会转储其内容。但这种行为对于配置表来说并不理想;用户所做的数据更改必须包含在转储中,否则在转储并恢复之后,扩展的行为就会发生变化。
为了解决这个问题,扩展脚本文件可以把它创建的某个表标记为配置表,这样 pg_dump 就会把该表的内容(而不是定义)包含到转储中。做法是在创建表之后调用函数
pg_extension_config_dump(regclass, text),例如:
CREATE TABLE my_config (key text, value text);
SELECT pg_catalog.pg_extension_config_dump('my_config', '');可以用这种方式标记任意数量的表。
当 pg_extension_config_dump 的第二个参数是空字符串时,pg_dump 会转储该表的全部内容。通常只有当该表在扩展脚本创建时最初为空时,这样做才是正确的。如果表中混有初始数据和用户提供的数据,那么
pg_extension_config_dump 的第二个参数就提供了一个用于选择要转储数据的 WHERE 条件。例如,你可以这样做:
CREATE TABLE my_config (key text, value text, standard_entry boolean);
SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entry');
然后确保只有扩展脚本创建的那些行,其
standard_entry 才为真。
更复杂的情况,例如用户可能会修改最初提供的数据行,可以通过在配置表上创建触发器来处理,以确保被修改的行被正确标记。
你可以再次调用 pg_extension_config_dump 来修改与配置表关联的过滤条件。(这通常在扩展更新脚本中很有用。)要把某个表标记为不再是配置表,唯一的方法是通过
ALTER EXTENSION ... DROP TABLE 将它与扩展解除关联。
注意,这些表之间的外键关系会决定 pg_dump 转储它们的顺序。具体来说,pg_dump 会尝试先转储被引用表,再转储引用表。由于外键关系是在 CREATE EXTENSION 时建立的(那时数据尚未装入这些表中),因此不支持环状依赖。若存在环状依赖,数据仍会被转储出来,但该转储将无法直接恢复,需要用户介入处理。
35.15.4. 扩展更新
扩展机制的一个优点是,它为管理定义扩展对象的 SQL 命令的更新提供了便利方式。这是通过为扩展安装脚本的每个已发布版本关联一个版本名或版本号来实现的。此外,如果你希望用户能够把数据库从一个版本动态更新到下一个版本,就应提供更新脚本,以完成从一个版本切换到下一版本所需的修改。更新脚本的名称遵循如下模式:(例如,extension--oldversion--newversion.sqlfoo--1.0--1.1.sql包含把扩展foo的版本 1.0修改为版本 1.1的命令)。
在有合适更新脚本可用的前提下,ALTER EXTENSION UPDATE 命令会把已安装的扩展更新到指定的新版本。更新脚本运行在
CREATE EXTENSION 为安装脚本提供的同一环境中:尤其是,search_path 的设置方式完全相同,而且脚本创建的任何新对象都会自动加入扩展中。
如果扩展有次级控制文件,那么用于更新脚本的控制参数就是与该脚本目标(新)版本相关联的那些参数。
更新机制可以用于解决一个重要的特殊情况:将“松散”对象集合转换为扩展。在 PostgreSQL 于 9.1 加入扩展机制之前,许多人编写的扩展模块只是创建各种未打包的对象。对于包含这些对象的现有数据库,如何将它们转换为正确打包的扩展?删除它们再执行普通的 CREATE EXTENSION 是一种办法,但如果对象具有依赖关系(例如,某些表列使用扩展创建的数据类型),就不适合这样做。解决方法是先创建一个空扩展,再使用 ALTER EXTENSION ADD 将每个现有对象附加到扩展中,最后创建当前扩展版本中存在、而未打包版本中没有的新对象。CREATE EXTENSION 通过 FROM old_version 选项支持这种情况:它不运行目标版本的普通安装脚本,而是运行名为 的更新脚本。用作 extension--old_version--target_version.sqlold_version 的虚拟版本名由扩展作者选择,不过通常约定使用 unpackaged。如果需要将多个先前版本更新为扩展形式,应使用不同的虚拟版本名来标识它们。
ALTER EXTENSION 能够执行一系列更新脚本文件来完成请求的更新。例如,如果只有 foo--1.0--1.1.sql 和
foo--1.1--2.0.sql 可用,那么当当前安装的是
1.0,而请求更新到 2.0 时,ALTER EXTENSION 就会按顺序应用这两个脚本。
PostgreSQL 并不对版本名称的属性作任何假设:例如,它并不知道 1.1 是否跟在
1.0 之后。它只是匹配可用的版本名,并选择需要应用更新脚本最少的那条路径。(实际上,版本名可以是任何不包含
--,且不以 - 开头或结尾的字符串。)
有时提供“降级”脚本也是有用的,例如
foo--1.1--1.0.sql 可用于回退与
1.1 版相关的修改。如果你这样做,要注意某个降级脚本可能会因为产生更短的路径而意外被采用。危险情况是,存在一个跨越多个版本的“快速路径”更新脚本,同时又有一个可降级到该快速路径起点的脚本。这时,先降级再走快速路径,可能比按版本逐步前进所需的步数更少。如果降级脚本删除了任何不可替代的对象,就会产生不希望看到的结果。
要检查是否存在意外的更新路径,可使用以下命令:
SELECT * FROM pg_extension_update_paths('extension_name');
它会显示指定扩展的每一对不同已知版本名,以及从源版本到目标版本将采用的更新路径序列;如果没有可用的更新路径,则显示 NULL。路径会以文本形式显示,并使用 -- 作为分隔符。如果你更喜欢数组形式,可以使用
regexp_split_to_array(path,'--')。
35.15.5. 扩展示例
下面给出一个纯 SQL 扩展的完整示例:一个双元素复合类型,它可以在两个槽位中存储任意类型的值,这两个槽位名为 “k” 和 “v”。非文本值会自动强制转换为文本后再存储。
脚本文件pair--1.0.sql如下所示:
-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pair" to load this file. \quit CREATE TYPE pair AS ( k text, v text ); CREATE OR REPLACE FUNCTION pair(anyelement, text) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::pair'; CREATE OR REPLACE FUNCTION pair(text, anyelement) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::pair'; CREATE OR REPLACE FUNCTION pair(anyelement, anyelement) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::pair'; CREATE OR REPLACE FUNCTION pair(text, text) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::pair;'; CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = anyelement, PROCEDURE = pair); CREATE OPERATOR ~> (LEFTARG = anyelement, RIGHTARG = text, PROCEDURE = pair); CREATE OPERATOR ~> (LEFTARG = anyelement, RIGHTARG = anyelement, PROCEDURE = pair); CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, PROCEDURE = pair);
控制文件pair.control如下所示:
# pair extension comment = 'A key/value pair data type' default_version = '1.0' relocatable = true
虽然你几乎不需要专门写一个 makefile 来把这两个文件安装到正确的目录中,但你也可以使用如下内容的 Makefile:
EXTENSION = pair DATA = pair--1.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS)
这个 makefile 依赖于 PGXS,其说明见第 35.16 节。执行 make install
命令会把控制文件和脚本文件安装到 pg_config
报告的正确目录中。
安装文件后,使用CREATE EXTENSION命令将这些对象装入某个具体数据库。