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

38.17.1. 扩展文件
38.17.2. 扩展可再定位性
38.17.3. 扩展配置表
38.17.4. 扩展更新
38.17.5. 用更新脚本安装扩展
38.17.6. Security Considerations for Extensions
38.17.7. 扩展实例

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

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

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

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

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

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

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

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

38.17.1. 扩展文件

CREATE EXTENSION命令依赖每一个扩展都有的控制文件,控制文件必须被命名为扩展的名称加上一个后缀.control,并且必须被放在安装的SHAREDIR/extension目录中。还必须至少有一个SQL脚本文件,它遵循命名模式extension--version.sql(例如,foo--1.0.sql表示扩展foo1.0版本)。默认情况下,脚本文件也被放置在SHAREDIR/extension目录中,但是控制文件中可以为脚本文件指定一个不同的目录。

一个扩展控制文件的格式与lightdb.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的用户不是超级用户,但允许通过此参数安装时,则此安装或更新脚本作为引导超级用户运行,而不是作为调用用户。 这个参数是不相干的,如果 superuserfalse。 通常,对于可能允许访问其他超级用户功能的扩展这将不被设为真(true),例如文件系统访问。 此外,标记一个可信的扩展需要大量额外的努力,以安全的编写扩展的安装和更新脚本;参见Section 38.17.6

relocatable (boolean)

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

schema (string)

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

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

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

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

如果扩展脚本包含字符串 @extowner@,该字符串将替换为调用 CREATE EXTENSION or ALTER EXTENSION 的用户的名称(适当引用)。 通常,此特性会被标记为受信任的扩展使用,将所选对象的所有权分配给调用用户,而不是引导超级用户。 (但是,在这样做时应该小心谨慎。例如,将c语言函数的所有权分配给非超级用户将为该用户创建一个权限升级路径。)

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

38.17.2. 扩展可再定位性

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

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

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

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

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

SET LOCAL search_path TO @extschema@, pg_temp;

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

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

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

为了安全,pg_temp 在所有情况下都会自动添加到 search_path 的末尾。

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

38.17.3. 扩展配置表

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

要解决这个问题,一个扩展的脚本文件可以把一个它创建的表或者序列标记为配置关系,这将导致lt_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的第二个参数是一个空字符串时,该表的全部内容都会被lt_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将它与扩展脱离开。

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

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

38.17.4. 扩展更新

扩展机制的一个优点是它提供了方便的方法来管理那些定义扩展中对象的 SQL 命令的更新。 这是通过为扩展的安装脚本的每一个发行版本关联一个版本名称或者版本号实现的。 此外,如果你希望用户能够动态地把他们的数据库从一个版本更新到下一个版本,你应该提供更新脚本来做必要的更改。 更新脚本的名称遵循extension--old_version--target_version.sql模式 (例如,foo--1.0--1.1.sql包含着把扩展foo的版本1.0修改成版本1.1的命令)。

假定有一个合适的更新脚本可用,命令ALTER EXTENSION UPDATE将把一个已安装的扩展更新到指定的新版本。更新脚本运行在与CREATE EXTENSION提供给安装脚本相同的环境中:特别是search_path会按照相同的方式设置,并且该脚本创建的任何新对象会被自动地加入到扩展中。此外,如果脚本选择删除扩展的成员对象,它们会自动与扩展解除关联。

如果一个扩展具有二级控制文件,用于更新脚本的控制参数是那些与新目标版本相关的参数。

ALTER EXTENSION能够执行更新脚本的序列来实现一个要求的更新。例如,如果只有foo--1.0--1.1.sqlfoo--1.1--2.0.sql可用,当前安装了1.0版本并且要求更新到版本2.0ALTER EXTENSION将依次应用它们。

LightDB并不假定任何有关版本名称的性质:例如,它不知道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.17.5. 用更新脚本安装扩展

一个已经存在一段时间的扩展可能存在多个版本,作者将需要为它们编写更新脚本。例如,如果你已经发布了扩展foo的版本1.01.11.2,就应该有更新脚本foo--1.0--1.1.sqlfoo--1.1--1.2.sql。在LightDB 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.17.6. Security Considerations for Extensions

广泛分布的扩展应该很少考虑它们所占用的数据库。 因此,以一种不会被基于搜索路径的攻击所破坏的安全风格编写扩展提供的函数是合适的.

superuser设置为真的扩展还必须考虑在其安装和更新脚本中执行的操作的安全危害。 对于恶意用户来说,创建特洛伊木马对象并不非常困难,这些木马对象会影响到后续粗心编写的扩展脚本的执行,从而允许该用户获得超级用户特权。

如果扩展标记为 trusted,则安装用户可以选择其安装模式,安装用户可能会有意使用不安全的模式,希望获得超级用户权限。 因此,从安全角度来看,受信任的扩展是非常易受攻击的,并且必须仔细检查其所有脚本命令,以确保不会出现危害。

关于安全地编写函数的建议在下面的Section 38.17.6.1 中, 关于安全编写安装脚本的建议在 Section 38.17.6.2中提供。

38.17.6.1. Security Considerations for Extension Functions

扩展提供的SQL-language和PL-language 函数在执行时面临基于搜索路径的攻击风险,因为解析这些函数发生在执行时,而不是创建时。

CREATE FUNCTION参考页包含有关安全地编写SECURITY DEFINER函数的建议。 将这些技术应用于扩展提供的任何函数是一种很好的做法,因为该函数可能由具有高权限的用户调用。

如果无法将search_path设置为仅包含安全模式,假设每个无资格的名称都可以解析为恶意用户定义的对象。 小心隐式依赖于search_path 的构造;例如,INCASE expression WHEN总是使用搜索路径选择操作符。 在它们的位置,使用OPERATOR(schema.=) ANYCASE WHEN expression

通用扩展通常不应假定它已安装到安全模式中,这意味着即使是对其自身对象的模式限定引用也不是完全无风险的。 例如,如果扩展定义了一个函数myschema.myfunc(bigint),则诸如myschema.myfunc(42)此类的调用可以被恶意函数myschema.myfunc(integer)捕获。 请注意,函数和操作符形参的数据类型与声明的实参类型完全匹配,必要时可使用显式强制转换。

38.17.6.2. Security Considerations for Extension Scripts

编写扩展安装或更新脚本以防止脚本执行时发生基于搜索路径的攻击。 如果脚本中的对象引用可以解析到脚本作者预期之外的其他对,则可能会立即发生妥协,或者在稍后使用错误定义的扩展对象时发生妥协。

DDL 命令例如 CREATE FUNCTIONCREATE OPERATOR CLASS 通常是安全的,但要注意任何具有通用表达式作为组件的命令。 例如, CREATE VIEW 需要经过审查, 就像 CREATE FUNCTION中的 DEFAULT 表达式一样。

有时,扩展脚本可能需要执行通用 SQL,例如,通过DDL无法进行目录调整。 小心地使用安全操作执行此类search_path不要 相信由 CREATE/ALTER EXTENSION 提供的路径是安全的。 最佳做法是暂时设置search_path'pg_catalog, pg_temp',并根据需要显式插入对扩展安装架构的引用。 (这种做法可能也有助于创建视图。) 示例可以在LightDB源代码分发版本的contrib模块中找到。

交叉扩展引用极难做到完全安全,部分由于不确定另一个扩展位于哪个模式中。 如果两个扩展都安装在同一模式中,则危险会减少,因为在安装时间中无法将恶意对象置于引用的扩展search_path。 但是,目前没有任何机制要求这样做。 目前,最佳做法是,如果扩展依赖于另一个扩展,则不要标记它受信任的扩展,除非该扩展始终安装在pg_catalog中。

38.17.7. 扩展实例

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

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

-- 如果脚本是由 ltsql 而不是 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 = lt_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

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

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