38.16. 打包相关对象到一个扩展中

一个对PostgreSQL有用的扩展通常包括多个 SQL 对象,例如,一种新的数据类型将需要新函数、新操作符以及可能的新索引操作符类。将所有这些对象收集到一个单一包中有助于简化数据库管理。PostgreSQL称这样一个包为一个 扩展 。要定义一个扩展,你至少需要一个包含创建该扩展的对象的SQL命令的 脚本文件 以及一个指定扩展本身的一些基本属性的 控制文件 。如果扩展包括 C 代码,通常还有一个 C 代码编译而成的共享库文件。一旦你有了这些文件,一个简单的CREATE EXTENSION命令可以把这些对象载入到你的数据库。

使用一个扩展而不是只运行SQL脚本载入一堆"松散"对象到数据库的主要优点是,PostgreSQL将能理解该扩展的对象是一起的。你可以用一个单一的DROP EXTENSION命令删除所有的对象(不用维护一个单独的"卸载"脚本)。 甚至更有用的一点是,pg_dump知道它不应该转储该扩展中的个体成员对象 — 它将只在转储中包括一个CREATE EXTENSION命令。这大大简化了迁移到一个包含不同于旧版扩展中对象的新版扩展的工作。不过,注意在把这样一个转储载入到一个新数据库时,该扩展的控制、脚本和其他文件必须可用。

PostgreSQL不会让你删除包含在一个扩展中的个体对象,除非删除整个扩展。还有,虽然你能够改变一个扩展的成员对象的定义(例如,通过CREATE OR REPLACE FUNCTION改变一个函数),记住被修改后的定义将不会被pg_dump转储。这种改变通常只有在你并发地在扩展脚本文件中做出相同更改时才有意义(但是对于包含配置数据的表有特殊的规定,见第 38.16.4 节)。在生产环境中,通常更好的方式是创建一个扩展更新脚本来执行对扩展中成员对象的更改。

扩展脚本可能会通过GRANTREVOKE语句设置扩展中所含对象的特权。每一个对象的最终特权集合(如果设置了任何特权)将被存储在pg_init_privs系统目录中。使用pg_dump时,CREATE EXTENSION命令将被包括在转储中,后面会跟着必要的GRANTREVOKE语句集合来将对象的特权设置成取得该转储时的样子。

PostgreSQL当前不支持扩展脚本发出CREATE POLICY或者SECURITY LABEL语句。这些东西的设置应该在扩展被创建好之后来进行。所有在扩展对象上创建的 RLS 策略和安全标签都将被包括在pg_dump创建的转储中。

扩展机制也对打包调整一个扩展中所含 SQL 对象定义的修改脚本有规定。例如,如果一个扩展的 1.1 版本比 1.0 版本增加了一个函数并且更改了另一个函数的函数体,该扩展的作者可以提供一个 更新脚本 来做这两个更改。那么ALTER EXTENSION UPDATE命令可以被用来应用这些更改并且跟踪在给定数据库中实际安装的是该扩展的哪个版本。

能作为一个扩展的成员的 SQL 对象的种类如ALTER EXTENSION所示。尤其是数据库集簇范围的对象(例如数据库、角色和表空间)不能作为扩展成员,因为一个扩展只在一个数据库范围内可见(尽管一个扩展脚本并没有被禁止创建这些对象,但是这样做将无法把它们作为扩展的一部分来跟踪)。还要注意虽然一个表可以是一个扩展的成员,它的扶助对象(例如索引)不会被直接认为是该扩展的成员。另一个重点是模式可以属于扩展,但是反过来不行:一个扩展本身有一个不被限定的名称并且不存在于任何模式"中"。不过,扩展的成员对象只要对象类型合适就可以属于模式。一个扩展拥有包含其成员对象的模式可能合适也可能不合适。

如果一个扩展的脚本创建任何临时对象(例如临时表),在当前会话的剩余部分会把它们当作扩展的成员,但是在会话结束会自动删除它们,这和任何其他临时对象是一样的。对于不删除整个扩展就不能删除扩展的成员对象的规则来说,这是一种例外。

38.16.1. 定义扩展对象

广泛分发的扩展应该尽量少地假定它们所占据的数据库。特别是,除非你发出了SET search_path = pg_temp,应该假定每一个未限定的名称都可能解析成恶意用户定义的对象。要小心隐式依赖于search_path的结构:IN以及CASE expression WHEN总是使用搜索路径选择操作符。对于它们,可使用OPERATOR(schema.=) ANYCASE WHEN expression

38.16.2. 扩展文件

CREATE EXTENSION命令依赖每一个扩展都有的控制文件,控制文件必须被命名为扩展的名称加上一个后缀.control,并且必须被放在安装的SHAREDIR/extension目录中。还必须至少有一个SQL脚本文件,它遵循命名模式extension--version.sql(例如,foo--1.0.sql表示扩展foo1.0版本)。默认情况下,脚本文件也被放置在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/shared_library_name并且接着MODULE_PATHNAME被用在CREATE FUNCTION命令中进行 C-语言函数的创建,因此该脚本文件不必把共享库的名称硬编码在其中。

requires (string)

这个扩展依赖的其他扩展名的一个列表,例如requires = 'foo, bar'。被依赖的扩展必须先于这个扩展安装。

superuser (boolean)

如果这个参数为true(默认情况),只有超级用户能够创建该扩展或者将它更新到一个新版本。如果被设置为false,只需要用来执行安装中命令或者更新脚本的特权。

relocatable (boolean)

如果一个扩展可能在初始创建之后将其所含的对象移动到一个不同的模式中,它就是 relocatable 。默认值是false,即该扩展是不可重定位的。详见第 38.16.3 节

schema (string)

这个参数只能为非可重定位扩展设置。它强制扩展被载入到给定的模式中而非其他模式中。只有在初始创建一个扩展时才会参考schema参数,扩展更新时则不会参考这个参数。详见第 38.16.3 节

除了主要控制文件extension.control,一个扩展还可以有二级控制文件,它们以extension--version.control的风格命名。如果提供了二级控制文件,它们必须被放置在脚本文件的目录中。二级控制文件遵循主要控制文件相同的格式。在安装或更新该扩展的版本时,一个二级控制文件中设置的任何参数将覆盖主要控制文件中的设置。不过,参数directory以及default_version不能在二级控制文件中设置。

一个扩展的SQL脚本文件能够包含任何 SQL 命令,除了事务控制命令(BEGINCOMMIT等)以及不能在一个事务块中执行的命令(如VACUUM)。这是因为脚本文件会被隐式地在一个事务块中被执行。

一个扩展的SQL脚本文件也能包含以\echo开始的行,它将被扩展机制忽略(当作注释)。如果脚本文件被送给psql而不是由CREATE EXTENSION载入(见第 38.16.7 节中的示例脚本),这种机制通常被用来抛出错误。如果没有这种功能,用户可能会意外地把该扩展的内容作为"松散的"对象而不是一整个扩展载入,这样的状态恢复起来比较麻烦。

尽管脚本文件可以包含指定编码允许的任何字符,但是控制文件应该只包含纯 ASCII 字符,因为PostgreSQL没有办法知道一个控制文件是什么编码。实际上,如果你想在扩展的注释中使用非 ASCII 字符只有一个问题。推荐的方法是不使用控制文件的comment参数,而是使用脚本文件中的COMMENT ON EXTENSION来设置注释。

38.16.3. 扩展可再定位性

用户常常希望把扩展中包含的对象载入到一个与扩展的作者所设想的不一样的模式中。对于这种可重定位性,有三种支持的级别:

  • 一个完全可重定位的扩展能在任何时候被移动到另一个模式中,即使在它被载入到一个数据库中之后。这种移动通过ALTER EXTENSION SET SCHEMA命令完成,该命令会自动地把所有成员对象重命名到新的模式中。通常,只有扩展不包含任何对其所在模式的内部假设时才可能这样做。还有,该扩展的对象必须全部在同一个模式中(忽略那些不属于任何模式的对象,例如过程语言)。要让一个扩展变成完全可定位,在它的控制文件中设置relocatable = true

  • 一个扩展可能在安装过程中是可重定位的,但是安装完后就不再可重定位。典型的情况是扩展的脚本文件需要显式地引用目标模式,例如为 SQL 函数设置search_path属性。对于这样一种扩展,在其控制文件中设置relocatable = false,并且使用@[email protected]在脚本文件中引用目标模式。在脚本被执行前,所有这个字符串的出现都将被替换为实际的目标模式名。用户可以使用CREATE EXTENSIONSCHEMA选项设置目标模式名。

  • 如果扩展根本就不支持重定位,在它的控制文件中设置relocatable = false,并且还设置schema为想要的目标模式的名称。这将阻止使用CREATE EXTENSIONSCHEMA选项修改目标模式,除非它指定的是和控制文件中相同的模式。如果该扩展包括关于模式名的内部假设且模式名不能使用@[email protected]的方法替换,这种选择通常是必须的。@[email protected]替换机制在这种情况中也是可用的,不过由于模式名已经被控制文件所决定,它的使用受到了很大的限制。

在所有情况下,脚本文件将被用search_path执行,它最初会被设置为指向目标模式,也就是说CREATE EXTENSION做的也是等效的工作:

SET LOCAL search_path TO @extschema@;

这允许由这个脚本文件创建的对象进入到目标模式中。如果脚本文件希望,它可以改变search_path,但这种用法通常是不受欢迎的。在CREATE EXTENSION结束后,search_path会被恢复到之前的设置。

如果控制文件中给出了schema参数,目标模式就由该参数决定,否则目标模式由CREATE EXTENSIONSCHEMA选项给出,如果以上两者都没有给出则会用当前默认的对象创建模式(在调用者search_path中的第一个)。当使用扩展文件的schema参数时,如果目标模式还不存在将创建它,但是在另外两种情况下它必须已经存在。

如果在控制文件中的requires中列举了任何先导扩展,它们的目标模式会被追加到search_path的初始设置中。这允许新扩展的脚本文件能够看到它们的对象。

尽管一个不可重定位的扩展能够包含散布在多个模式中的对象,通常还是值得将意图用于外部使用的所有对象放置在一个模式中,这被认为是该扩展的目标模式。这样一种安排可以在依赖的扩展创建过程中方便地与search_path的默认设置一起工作。

38.16.4. 扩展配置表

某些扩展包括配置表,其中包含可以由用户在扩展安装后增加或修改的数据。通常,如果一个表是一个扩展的一部分,该表的定义和内容都不会被pg_dump转储。但是对于一个配置表来说并不希望是这样的行为,任何用户做出的数据修改都需要被包括在转储中,否则该扩展在重载之后的行为将和转储之前不同。

要解决这个问题,一个扩展的脚本文件可以把一个它创建的表或者序列标记为配置关系,这将导致pg_dump把该表或者序列的内容(而不是它的定义)包括在转储中。要这样做,在创建表或序列之后调用函数pg_extension_config_dump(regclass, text),例如

CREATE TABLE my_config (key text, value text);
CREATE SEQUENCE my_config_seq;

SELECT pg_catalog.pg_extension_config_dump('my_config', '');
SELECT pg_catalog.pg_extension_config_dump('my_config_seq', '');

可以用这种方法标记任意数量的表或者序列。与serial或者bigserial列相关联的序列也可以被标记。

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的第二个参数没有影响。

更复杂的情况(例如用户可能会修改初始提供的数据)可以通过在配置表上创建触发器来处理,触发器将负责保证被修改的行会被正确地标记。

你可通过再次调用pg_extension_config_dump来修改与一个配置表相关的过滤条件(这通常对于一个扩展更新脚本有用)。将一个表标记为不再是一个配置表的方法是用ALTER EXTENSION ... DROP TABLE将它与扩展脱离开。

注意这些表之间的外键关系将会指导这些表被 pg_dump 转储的顺序。特别地,pg_dump 将尝试 先转储被引用的表再转储引用表。由于外键关系是在 CREATE EXTENSION 时间(先于数据被载入 到表中)建立的,环状依赖还没有建立。当环状依赖存在时,数据将仍然被转储,但是该转储无法被 直接恢复并且必须要用户的介入。

serial或者bigserial列相关联的序列需要被直接标记以转储它们的状态。只标记它们的父关系不足以转储它们的状态。

38.16.5. 扩展更新

扩展机制的一个优点是它提供了方便的方法来管理那些定义扩展中对象的 SQL 命令的更新。这是通过为扩展的安装脚本的每一个发行版本关联一个版本名称或者版本号实现的。此外,如果你希望用户能够动态地把他们的数据库从一个版本更新到下一个版本,你应该提供 更新脚本 来做必要的更改。更新脚本的名称遵循extension--oldversion--newversion.sql模式(例如,foo--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.sql的更新脚本。选择作为 old_version 使用的虚假版本名称是扩展作者的工作,不过unpackaged是一种习惯用法。如果你有多个早期版本需要更新到扩展风格,使用多个虚假版本名称来标识它们。

ALTER EXTENSION能够执行更新脚本的序列来实现一个要求的更新。例如,如果只有foo--1.0--1.1.sqlfoo--1.1--2.0.sql可用,当前安装了1.0版本并且要求更新到版本2.0ALTER 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,'--')

38.16.6. 用更新脚本安装扩展

一个已经存在一段时间的扩展可能存在多个版本,作者将需要为它们编写更新脚本。例如,如果你已经发布了扩展foo的版本1.01.11.2,就应该有更新脚本foo--1.0--1.1.sqlfoo--1.1--1.2.sql。在PostgreSQL 10之前,还有必要创建新的脚本文件foo--1.1.sqlfoo--1.2.sql,它们直接构建比较新的扩展版本,或者新的版本无法被直接安装,而是通过先安装1.0然后更新。那种方式是无聊的重复性工作,但是现在它是不必要的了,因为CREATE EXTENSION能够自动遵循更新链。例如,如果只有脚本文件foo--1.0.sqlfoo--1.0--1.1.sqlfoo--1.1--1.2.sql可用,那么安装版本1.2的请求会通过按顺序运行上述三个脚本来实现。这种处理和先安装1.0然后更新到1.2是一样的(和ALTER EXTENSION UPDATE一样,如果有多条路径可用则优先选择最短的)。按这种风格安排扩展 的脚本文件可以减少生产小更新所需的维护工作量。

如果以这种风格维护的扩展中使用了二级(版本相关的)控制文件,记住每个版本都需要一个控制文件,即使它没有单独的安装脚本,因为该控制文件将决定如何执行到这个版本的隐式更新。例如,如果foo--1.0.control指定有requires = 'bar',但foo的其他控制文件没有这样做,在从1.0更新到另一个版本时,该扩展对bar的依赖将被删除。

38.16.7. 扩展实例

这里是一个只用SQL的扩展的完整例子,一个两个元素的组合类型,它可以在它的槽(命名为"k"和"v")中存储任何类型的值。非文本值会被自动强制为文本进行存储。

脚本文件pair--1.0.sql看起来像这样:

-- 如果脚本是由 psql 而不是 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(text, text)
RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::@[email protected];';

CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, FUNCTION = pair);

-- "SET search_path"容易操作,但限定名称更好。
CREATE OR REPLACE FUNCTION lower(pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW(lower($1.k), lower($1.v))::@[email protected];'
SET search_path = pg_temp;

CREATE OR REPLACE FUNCTION pair_concat(pair, pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW($1.k OPERATOR(pg_catalog.||) $2.k,
               $1.v OPERATOR(pg_catalog.||) $2.v)::@[email protected];';

控制文件pair.control看起来像这样:

# pair 扩展
comment = 'A key/value pair data type'
default_version = '1.0'
relocatable = false

虽然你几乎不会需要一个 makefile 来安装这两个文件到正确的目录,你还是可以使用一个Makefile

EXTENSION = pair
DATA = pair--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

这个 makefile 依赖于PGXS,它在第 38.17 节中描述。命令make install将把控制和脚本文件安装到pg_config报告的正确的目录中。

一旦文件被安装,使用CREATE EXTENSION命令就可以把对象载入到任何特定的数据库中。