一个对PostgreSQL有用的扩展通常包括多个 SQL 对象,例如,一种新的数据类型将需要新函数、新操作符以及可能的新索引操作符类。
将所有这些对象收集到一个单一包中有助于简化数据库管理。PostgreSQL称这样一个包为一个扩展。
要定义一个扩展,你至少需要一个包含创建该扩展的对象的SQL命令的脚本文件以及一个指定扩展本身的一些基本属性的控制文件。
如果扩展包括 C 代码,通常还有一个 C 代码编译而成的共享库文件。
一旦你有了这些文件,一个简单的CREATE EXTENSION
命令可以把这些对象载入到你的数据库。
使用一个扩展而不是只运行SQL脚本载入一堆“松散”对象到数据库的主要优点是,PostgreSQL将能理解该扩展的对象是一起的。
你可以用一个单一的DROP EXTENSION
命令删除所有的对象(不用维护一个单独的“卸载”脚本)。
甚至更有用的一点是,pg_dump知道它不应该转储该扩展中的个体成员对象 — 它将只在转储中包括一个CREATE EXTENSION
命令。
这大大简化了迁移到一个包含不同于旧版扩展中对象的新版扩展的工作。不过,注意在把这样一个转储载入到一个新数据库时,该扩展的控制、脚本和其他文件必须可用。
PostgreSQL不会让你删除包含在一个扩展中的个体对象,除非删除整个扩展。还有,虽然你能够改变一个扩展的成员对象的定义(例如,通过CREATE OR REPLACE FUNCTION
改变一个函数),记住被修改后的定义将不会被pg_dump转储。这种改变通常只有在你并发地在扩展脚本文件中做出相同更改时才有意义(但是对于包含配置数据的表有特殊的规定,见第 38.17.3 节)。在生产环境中,通常更好的方式是创建一个扩展更新脚本来执行对扩展中成员对象的更改。
扩展脚本可能会设置扩展中所含对象的特权,使用GRANT
和REVOKE
语句。
每一个对象的最终特权集合(如果设置了任何特权)将被存储在pg_init_privs
系统目录中。
使用pg_dump时,CREATE EXTENSION
命令将被包括在转储中,后面会跟着必要的GRANT
和REVOKE
语句集合来将对象的特权设置成取得该转储时的样子。
PostgreSQL当前不支持扩展脚本发出CREATE POLICY
或者SECURITY LABEL
语句。这些东西的设置应该在扩展被创建好之后来进行。所有在扩展对象上创建的 RLS 策略和安全标签都将被包括在pg_dump创建的转储中。
扩展机制也对打包调整一个扩展中所含 SQL 对象定义的修改脚本有规定。例如,如果一个扩展的 1.1 版本比 1.0 版本增加了一个函数并且更改了另一个函数的函数体,该扩展的作者可以提供一个更新脚本来做这两个更改。那么ALTER EXTENSION UPDATE
命令可以被用来应用这些更改并且跟踪在给定数据库中实际安装的是该扩展的哪个版本。
能作为一个扩展的成员的 SQL 对象的种类如ALTER EXTENSION
所示。
尤其是数据库集簇范围的对象(例如数据库、角色和表空间)不能作为扩展成员,因为一个扩展只在一个数据库范围内可见(尽管一个扩展脚本并没有被禁止创建这些对象,但是这样做将无法把它们作为扩展的一部分来跟踪)。
还要注意虽然一个表可以是一个扩展的成员,它的扶助对象(例如索引)不会被直接认为是该扩展的成员。
另一个重点是模式可以属于扩展,但是反过来不行:一个扩展本身有一个不被限定的名称并且不存在于任何模式“中”。
不过,扩展的成员对象只要对象类型合适就可以属于模式。
一个扩展拥有包含其成员对象的模式可能合适也可能不合适。
如果一个扩展的脚本创建任何临时对象(例如临时表),在当前会话的剩余部分会把它们当作扩展的成员,但是在会话结束会自动删除它们,这和任何其他临时对象是一样的。对于不删除整个扩展就不能删除扩展的成员对象的规则来说,这是一种例外。
CREATE EXTENSION
命令依赖每一个扩展都有的控制文件,控制文件必须被命名为扩展的名称加上一个后缀.control
,并且必须被放在安装的SHAREDIR/extension
目录中。
还必须至少有一个SQL脚本文件,它遵循命名模式
(例如,extension
--version
.sqlfoo--1.0.sql
表示扩展foo
的1.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
(默认情况),只有超级用户能够创建该扩展或者将它更新到一个新版本。
(另请参见下面的 trusted
) 如果被设置为false
,只需要用来执行安装中命令或者更新脚本的特权。
如果任何脚本命令需要超级用户权限,这将通常被设置为true
。
(这样的命令总之都会失败,但提前给出错误对用户更友好。)
trusted
(boolean
)
这个参数,如果设置为 true
(非默认值),则允许某些非超级用户安装superuser
已设置为 true
的扩展。
具体地说,对于在当前数据库上具有CREATE
特权的任何人,都允许安装。
当执行CREATE EXTENSION
的用户不是超级用户,但允许通过此参数安装时,则此安装或更新脚本作为引导超级用户运行,而不是作为调用用户。
这个参数是不相干的,如果 superuser
为 false
。
通常,对于可能允许访问其他超级用户功能的扩展这将不被设为真(true),例如文件系统访问。
此外,标记一个可信的扩展需要大量额外的努力,以安全的编写扩展的安装和更新脚本;参见第 38.17.6 节。
relocatable
(boolean
)
如果一个扩展可能在初始创建之后将其所含的对象移动到一个不同的模式中,它就是relocatable。默认值是false
,即该扩展是不可重定位的。详见第 38.17.2 节。
schema
(string
)
这个参数只能为非可重定位扩展设置。它强制扩展被载入到给定的模式中而非其他模式中。只有在初始创建一个扩展时才会参考schema
参数,扩展更新时则不会参考这个参数。详见第 38.17.2 节。
除了主要控制文件
,一个扩展还可以有二级控制文件,它们以extension
.control
的风格命名。如果提供了二级控制文件,它们必须被放置在脚本文件的目录中。二级控制文件遵循主要控制文件相同的格式。在安装或更新该扩展的版本时,一个二级控制文件中设置的任何参数将覆盖主要控制文件中的设置。不过,参数extension
--version
.controldirectory
以及default_version
不能在二级控制文件中设置。
一个扩展的SQL脚本文件可以包含任何SQL命令,除了事务控制命令
(BEGIN
,COMMIT
等)和不能在事务块内执行的命令
(比如VACUUM
)。这是因为脚本文件隐式地在事务块内执行。
一个扩展的SQL脚本文件也能包含以\echo
开始的行,它将被扩展机制忽略(当作注释)。如果脚本文件被送给psql而不是由CREATE EXTENSION
载入(见第 38.17.7 节中的示例脚本),这种机制通常被用来抛出错误。如果没有这种功能,用户可能会意外地把该扩展的内容作为“松散的”对象而不是一整个扩展载入,这样的状态恢复起来比较麻烦。
如果扩展脚本包含字符串 @extowner@
,该字符串将替换为调用 CREATE EXTENSION
or ALTER EXTENSION
的用户的名称(适当引用)。
通常,此特性会被标记为受信任的扩展使用,将所选对象的所有权分配给调用用户,而不是引导超级用户。
(但是,在这样做时应该小心谨慎。例如,将c语言函数的所有权分配给非超级用户将为该用户创建一个权限升级路径。)
尽管脚本文件可以包含指定编码允许的任何字符,但是控制文件应该只包含纯 ASCII 字符,因为PostgreSQL没有办法知道一个控制文件是什么编码。实际上,如果你想在扩展的注释中使用非 ASCII 字符只有一个问题。推荐的方法是不使用控制文件的comment
参数,而是使用脚本文件中的COMMENT ON EXTENSION
来设置注释。
用户常常希望把扩展中包含的对象载入到一个与扩展的作者所设想的不一样的模式中。对于这种可重定位性,有三种支持的级别:
一个完全可重定位的扩展能在任何时候被移动到另一个模式中,即使在它被载入到一个数据库中之后。这种移动通过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@, pg_temp;
这允许由这个脚本文件创建的对象进入到目标模式中。如果脚本文件希望,它可以改变search_path
,但这种用法通常是不受欢迎的。在CREATE EXTENSION
结束后,search_path
会被恢复到之前的设置。
如果控制文件中给出了schema
参数,目标模式就由该参数决定,否则目标模式由CREATE EXTENSION
的SCHEMA
选项给出,如果以上两者都没有给出则会用当前默认的对象创建模式(在调用者search_path
中的第一个)。当使用扩展文件的schema
参数时,如果目标模式还不存在将创建它,但是在另外两种情况下它必须已经存在。
如果在控制文件中的requires
中列举了任何先导扩展,它们的目标模式会被追加到search_path
的初始设置中,遵循新扩展的目标模式。
这允许新扩展的脚本文件能够看到它们的对象。
为了安全,pg_temp
在所有情况下都会自动添加到 search_path
的末尾。
尽管一个不可重定位的扩展能够包含散布在多个模式中的对象,通常还是值得将意图用于外部使用的所有对象放置在一个模式中,这被认为是该扩展的目标模式。这样一种安排可以在依赖的扩展创建过程中方便地与search_path
的默认设置一起工作。
一些扩展包括配置表,其中包含用户在安装扩展后可能添加或更改的数据。 通常,如果表是扩展的一部分,那么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
列相关联的序列需要被直接标记以转储它们的状态。只标记它们的父关系不足以转储它们的状态。
扩展机制的一个优点是它提供了方便的方法来管理那些定义扩展中对象的 SQL 命令的更新。
这是通过为扩展的安装脚本的每一个发行版本关联一个版本名称或者版本号实现的。
此外,如果你希望用户能够动态地把他们的数据库从一个版本更新到下一个版本,你应该提供更新脚本来做必要的更改。
更新脚本的名称遵循
模式
(例如,extension
--old_version
--target_version
.sqlfoo--1.0--1.1.sql
包含着把扩展foo
的版本1.0
修改成版本1.1
的命令)。
假定有一个合适的更新脚本可用,命令ALTER EXTENSION UPDATE
将把一个已安装的扩展更新到指定的新版本。更新脚本运行在与CREATE EXTENSION
提供给安装脚本相同的环境中:特别是search_path
会按照相同的方式设置,并且该脚本创建的任何新对象会被自动地加入到扩展中。此外,如果脚本选择删除扩展的成员对象,它们会自动与扩展解除关联。
如果一个扩展具有二级控制文件,用于更新脚本的控制参数是那些与新目标版本相关的参数。
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,'--')
。
一个已经存在一段时间的扩展可能存在多个版本,作者将需要为它们编写更新脚本。例如,如果你已经发布了扩展foo
的版本1.0
、1.1
和1.2
,就应该有更新脚本foo--1.0--1.1.sql
和foo--1.1--1.2.sql
。在PostgreSQL 10之前,还有必要创建新的脚本文件foo--1.1.sql
和foo--1.2.sql
,它们直接构建比较新的扩展版本,或者新的版本无法被直接安装,而是通过先安装1.0
然后更新。那种方式是无聊的重复性工作,但是现在它是不必要的了,因为CREATE EXTENSION
能够自动遵循更新链。例如,如果只有脚本文件foo--1.0.sql
、foo--1.0--1.1.sql
和foo--1.1--1.2.sql
可用,那么安装版本1.2
的请求会通过按顺序运行上述三个脚本来实现。这种处理和先安装1.0
然后更新到1.2
是一样的(和ALTER EXTENSION UPDATE
一样,如果有多条路径可用则优先选择最短的)。按这种风格安排扩展 的脚本文件可以减少生产小更新所需的维护工作量。
如果以这种风格维护的扩展中使用了二级(版本相关的)控制文件,记住每个版本都需要一个控制文件,即使它没有单独的安装脚本,因为该控制文件将决定如何执行到这个版本的隐式更新。例如,如果foo--1.0.control
指定有requires
= 'bar'
,但foo
的其他控制文件没有这样做,在从1.0
更新到另一个版本时,该扩展对bar
的依赖将被删除。
广泛分布的扩展应该很少考虑它们所占用的数据库。 因此,以一种不会被基于搜索路径的攻击所破坏的安全风格编写扩展提供的函数是合适的.
将superuser
设置为真的扩展还必须考虑在其安装和更新脚本中执行的操作的安全危害。
对于恶意用户来说,创建特洛伊木马对象并不非常困难,这些木马对象会影响到后续粗心编写的扩展脚本的执行,从而允许该用户获得超级用户特权。
如果扩展标记为 trusted
,则安装用户可以选择其安装模式,安装用户可能会有意使用不安全的模式,希望获得超级用户权限。
因此,从安全角度来看,受信任的扩展是非常易受攻击的,并且必须仔细检查其所有脚本命令,以确保不会出现危害。
关于安全地编写函数的建议在下面的第 38.17.6.1 节 中, 关于安全编写安装脚本的建议在 第 38.17.6.2 节中提供。
扩展提供的SQL-language和PL-language 函数在执行时面临基于搜索路径的攻击风险,因为解析这些函数发生在执行时,而不是创建时。
CREATE FUNCTION
参考页包含有关安全地编写SECURITY DEFINER
函数的建议。
将这些技术应用于扩展提供的任何函数是一种很好的做法,因为该函数可能由具有高权限的用户调用。
如果无法将search_path
设置为仅包含安全模式,假设每个无资格的名称都可以解析为恶意用户定义的对象。
小心隐式依赖于search_path
的构造;例如,IN
和CASE
总是使用搜索路径选择操作符。
在它们的位置,使用expression
WHENOPERATOR(
和 schema
.=) ANYCASE WHEN
。
expression
通用扩展通常不应假定它已安装到安全模式中,这意味着即使是对其自身对象的模式限定引用也不是完全无风险的。
例如,如果扩展定义了一个函数myschema.myfunc(bigint)
,则诸如myschema.myfunc(42)
此类的调用可以被恶意函数myschema.myfunc(integer)
捕获。
请注意,函数和操作符形参的数据类型与声明的实参类型完全匹配,必要时可使用显式强制转换。
编写扩展安装或更新脚本以防止脚本执行时发生基于搜索路径的攻击。 如果脚本中的对象引用可以解析到脚本作者预期之外的其他对,则可能会立即发生妥协,或者在稍后使用错误定义的扩展对象时发生妥协。
DDL 命令例如 CREATE FUNCTION
和 CREATE OPERATOR CLASS
通常是安全的,但要注意任何具有通用表达式作为组件的命令。
例如, CREATE VIEW
需要经过审查, 就像 CREATE FUNCTION
中的 DEFAULT
表达式一样。
有时,扩展脚本可能需要执行通用 SQL,例如,通过DDL无法进行目录调整。
小心地使用安全操作执行此类search_path
;
不要 相信由 CREATE/ALTER EXTENSION
提供的路径是安全的。
最佳做法是暂时设置search_path
到 'pg_catalog, pg_temp'
,并根据需要显式插入对扩展安装架构的引用。
(这种做法可能也有助于创建视图。)
示例可以在PostgreSQL源代码分发版本的contrib
模块中找到。
交叉扩展引用极难做到完全安全,部分由于不确定另一个扩展位于哪个模式中。
如果两个扩展都安装在同一模式中,则危险会减少,因为在安装时间中无法将恶意对象置于引用的扩展search_path
。
但是,目前没有任何机制要求这样做。
目前,最佳做法是,如果扩展依赖于另一个扩展,则不要标记它受信任的扩展,除非该扩展始终安装在pg_catalog
中。
这里是一个只用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 FUNCTION pair(text, text) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::@extschema@.pair;'; CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, FUNCTION = pair); -- "SET search_path" 容易操作,但限定名称更好。 CREATE FUNCTION lower(pair) RETURNS pair LANGUAGE SQL AS 'SELECT ROW(lower($1.k), lower($1.v))::@extschema@.pair;' SET search_path = pg_temp; CREATE 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)::@extschema@.pair;';
控制文件pair.control
看起来像这样:
# pair 扩展 comment = 'A key/value pair data type' default_version = '1.0' # cannot be relocatable because of use of @extschema@ 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.18 节中描述。命令make install
将把控制和脚本文件安装到pg_config报告的正确的目录中。
一旦文件被安装,使用CREATE EXTENSION
命令就可以把对象载入到任何特定的数据库中。