受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

68.2. 系统目录初始数据 #

每个含有手工创建初始数据的目录(有些目录没有)都有一个对应的 .dat 文件,其中以可编辑格式保存该目录的初始数据。

68.2.1. 数据文件格式 #

每个 .dat 文件都包含 Perl 数据结构字面量,只需直接用 eval 求值,就会得到一个由哈希引用数组组成的内存数据结构,其中每个数组元素对应一行目录数据。下面摘自 pg_database.dat 的一小段、经过轻微修改的内容,可以展示其关键特性:

[

# A comment could appear here.
{ oid => '1', oid_symbol => 'Template1DbOid',
  descr => 'database\'s default template',
  datname => 'template1', encoding => 'ENCODING',
  datlocprovider => 'LOCALE_PROVIDER', datistemplate => 't',
  datallowconn => 't', dathasloginevt => 'f', datconnlimit => '-1', datfrozenxid => '0',
  datminmxid => '1', dattablespace => 'pg_default', datcollate => 'LC_COLLATE',
  datctype => 'LC_CTYPE', datlocale => 'DATLOCALE', datacl => '_null_' },

]

需要注意的几点:

  • 整个文件的布局是:一个左方括号、一个或多个花括号集合(每个集合代表一行目录数据),最后是一个右方括号。每个右花括号后面都要写一个逗号。

  • 在每一行目录数据内部,写成以逗号分隔的 key => value 对。允许的 key 包括该目录各列的名字,以及元数据键 oidoid_symbolarray_type_oiddescr。(oidoid_symbol 的用法见下文 Section 68.2.2array_type_oid 的用法见 Section 68.2.4descr 为对象提供描述字符串,并会按需要插入到 pg_descriptionpg_shdescription 中。) 元数据键虽然是可选的,但目录定义的列都必须提供,除非该目录的 .h 文件为该列指定了默认值。(在上面的例子中,datdba 字段被省略,是因为 pg_database.h 为它提供了合适的默认值。)

  • 所有值都必须用单引号括起来。值内部出现的单引号要用反斜线转义。作为数据的反斜线可以写成双反斜线,但也不必;这遵循 Perl 对简单单引号字面量的规则。注意,作为数据出现的反斜线也会按照转义字符串常量相同的规则被 bootstrap 扫描器当作转义处理(见 Section 4.1.2.2);例如 \t 会转换为一个制表符。如果你确实希望最终值里包含一个反斜线,就需要写四个:Perl 会先去掉两个,留给 bootstrap 扫描器看到的是 \\

  • NULL 值用 _null_ 表示。(注意,没有办法创建一个值恰好等于这个字符串。)

  • 注释以前缀 # 表示,并且必须单独成行。

  • 如果字段值是其他目录条目的 OID,应当使用符号名而不是真实的数字 OID 来表示。(上面的例子中,dattablespace 就包含了这样的引用。)这将在下文 Section 68.2.3 中说明。

  • 由于哈希是无序数据结构,字段顺序和行布局在语义上并不重要。不过,为了保持外观一致,我们规定了几条由格式化脚本 reformat_dat_file.pl 应用的规则:

    • 在每对花括号内部,元数据字段 oidoid_symbolarray_type_oiddescr(若存在)按这个顺序排在最前面,然后才是该目录自身的字段,并按它们的定义顺序排列。

    • 如有可能,会根据需要在字段之间插入换行,以便把行长限制在 80 个字符以内。元数据字段和普通字段之间也会插入一次换行。

    • 如果目录的 .h 文件为某列指定了默认值,而某条数据又使用了同样的值,reformat_dat_file.pl 就会把该字段从数据文件中省略掉,以保持数据表示紧凑。

    • reformat_dat_file.pl 会原样保留空行和注释行。

    建议在提交目录数据补丁前先运行 reformat_dat_file.pl。为方便起见,你可以直接切换到 src/include/catalog/ 并运行 make reformat-dat-files

  • 如果你想新增一种缩小数据表示的方法,就必须在 reformat_dat_file.pl 中实现它,并且还要让 Catalog::ParseData() 知道如何把数据重新展开为完整表示。

68.2.2. OID 分配 #

出现在初始数据中的目录行,可以通过写一个 oid => nnnn 元数据字段来手工指定 OID。此外,如果已经指定了 OID,还可以通过写一个 oid_symbol => name 元数据字段,为该 OID 创建一个 C 宏。

如果其他预装载行中有对某个预装载目录行的 OID 引用,那么该行就必须预先分配 OID。如果该行的 OID 需要在 C 代码中被引用,也同样需要预分配 OID。如果这两种情况都不适用,就可以省略 oid 元数据字段,由 bootstrap 代码自动分配 OID。实际中,即便在某个目录里只有部分预装载行真的被交叉引用,我们通常也会对该目录中的预装载行要么全部预先分配 OID,要么一个都不分配。

在 C 代码里直接写任何 OID 的实际数值,都被认为是很不好的做法;应始终改用宏。对 pg_proc OID 的直接引用十分常见,因此有一个专门机制可以自动创建所需宏;见 src/backend/utils/Gen_fmgrtab.pl。类似地,pg_type OID 也有一种自动创建宏的方法,只是出于历史原因,做法并不完全相同。因此,这两个目录中并不需要 oid_symbol 项。同样,系统目录和索引的 pg_class OID 宏也是自动建立的。对于其他所有系统目录,你必须通过 oid_symbol 项手工指定所需的宏。

要为一个新的预装载行找到可用 OID,可以运行脚本 src/include/catalog/unused_oids。它会输出未使用 OID 的闭区间范围(例如输出行 45-900 表示 OID 45 到 900 尚未分配)。目前,OID 1–9999 保留给手工分配;unused_oids 脚本只是简单地扫描目录头文件和 .dat 文件,看看哪些 OID 尚未出现。你也可以使用 duplicate_oids 脚本检查错误。(genbki.pl 会为没有手工指定 OID 的行自动分配 OID,并且也会在编译时检测重复 OID。)

为一个预计不会立刻提交的补丁选择 OID 时,最佳实践是使用一组大致连续的 OID,从 8000—9999 范围内某个随机选取的值开始。这样可以尽量降低与其他并行开发补丁发生 OID 冲突的风险。为了让 8000—9999 这一区间保留给开发用途,在补丁提交到主 git 仓库之后,应把其 OID 重新编号到该范围以下的可用空间。通常这会在每个开发周期接近尾声时统一进行,同时移动该周期中各补丁占用的所有 OID。renumber_oids.pl 脚本可用于此目的。如果发现某个未提交补丁与近期已提交补丁发生 OID 冲突,renumber_oids.pl 也可能有助于从这种情况中恢复。

由于遵循这一约定,补丁分配的 OID 后续可能会被重新编号,因此在补丁进入正式发布版本之前,不应认为这些 OID 是稳定的。不过,一旦发布,我们就不会再修改手工分配的对象 OID,因为那会带来各种兼容性问题。

如果 genbki.pl 需要为某个没有手工分配 OID 的目录条目分配 OID,它会使用 10000—11999 范围内的值。服务器的 OID 计数器会在 bootstrap 运行开始时被设为 10000,因此在 bootstrap 处理期间动态创建的对象也会得到这一范围内的 OID。(通常的 OID 分配机制会负责防止任何冲突。)

OID 低于 FirstUnpinnedObjectId(12000)的对象会被视为固定,从而无法删除。(有少量例外情形,它们被硬编码在 IsPinnedObject() 中。)一旦 initdb 准备好创建未固定对象,就会把 OID 计数器强制提升到 FirstUnpinnedObjectId。因此,在 initdb 后续阶段创建的对象,例如运行 information_schema.sql 脚本时创建的对象,就不会被固定,而所有 genbki.pl 已知的对象则都会被固定。

在正常数据库运行期间分配的 OID 被限制为不小于 16384。这保证了 10000—16383 区间可供 genbki.plinitdb 期间自动分配 OID 使用。这些自动分配的 OID 不被视为稳定值,并且可能因安装不同而不同。

68.2.3. OID 引用查找 #

原则上,一个初始目录行到另一个初始目录行的交叉引用,只要在引用字段中写入被引用行预先分配的 OID 即可。不过,这违反项目策略,因为这样容易出错、难以阅读,而且如果新分配的 OID 被重新编号,也很容易失效。因此,genbki.pl 提供了使用符号引用的机制。规则如下:

  • 在某个特定目录列上启用符号引用的方法,是在该列定义后附加 BKI_LOOKUP(lookuprule),其中 lookuprule 是被引用目录的名字,例如 pg_procBKI_LOOKUP 可以附加在类型为 OidregprocoidvectorOid[] 的列上;在后两种情况下,它表示对数组中的每个元素都执行一次查找。

  • 也允许把 BKI_LOOKUP(encoding) 附加到整数列上,用于引用字符集编码。字符集编码目前并不以目录 OID 的形式表示,但 genbki.pl 知道一组可用值。

  • 在某些目录列中,允许条目为零,而不必是一个有效引用。如果允许这样做,就应写 BKI_LOOKUP_OPT 而不是 BKI_LOOKUP。这样你就可以把某个条目写成 0。(如果该列声明为 regproc,还可以选择写成 - 而不是 0。)除这一特殊情况外,BKI_LOOKUP 列中的所有条目都必须是符号引用。genbki.pl 会对无法识别的名字发出警告。

  • 大多数类型的目录对象都只通过其名称引用。注意,类型名必须与被引用的 pg_type 条目中的 typname 完全一致;不能使用诸如用 integer 代表 int4 这样的别名。

  • 如果某个函数在 pg_proc.dat 条目中按 proname 唯一可辨识,那么可以直接使用它的 proname 来表示(这和 regproc 输入的行为类似)。否则,应把它写成 proname(argtypename,argtypename,...) 的形式,类似 regprocedure。参数类型名的拼写必须与该 pg_proc.dat 条目中 proargtypes 字段里的写法完全一致。不要插入任何空格。

  • 操作符写成 oprname(lefttype,righttype) 的形式,其中类型名必须与 pg_operator.dat 条目中 oprleftoprright 字段里的写法完全一致。(对于一元操作符中省略的操作数,写 0。)

  • 操作符类和操作符族的名字只在某个访问方法内部唯一,因此它们写成 access_method_name/object_name 的形式。

  • 在以上所有情形中,都不支持模式限定;bootstrap 期间创建的所有对象都应位于 pg_catalog 模式中。

genbki.pl 在运行时会解析所有符号引用,并在输出的 BKI 文件中写入简单的数字 OID。因此,bootstrap 后端无需处理符号引用。

即使某个目录没有需要查找的初始数据,也仍然值得把 OID 引用列标记为 BKI_LOOKUPBKI_LOOKUP_OPT。这样可以让 genbki.pl 记录系统目录中存在的外键关系。该信息会在回归测试中用于检查错误条目。另见宏 DECLARE_FOREIGN_KEYDECLARE_FOREIGN_KEY_OPTDECLARE_ARRAY_FOREIGN_KEYDECLARE_ARRAY_FOREIGN_KEY_OPT,它们用于声明对 BKI_LOOKUP 来说过于复杂的外键关系(通常是多列外键)。

68.2.4. 数组类型的自动创建 #

大多数标量数据类型都应有一个对应的数组类型(也就是一种标准的 varlena 数组类型,其元素类型就是该标量类型,并由该标量类型的 pg_type 条目中的 typarray 字段引用)。在大多数情况下,genbki.pl 能够自动为该数组类型生成 pg_type 条目。

要使用这一功能,只需在该标量类型的 pg_type 条目中写一个 array_type_oid => nnnn 元数据字段,指定数组类型要使用的 OID。这样就可以省略 typarray 字段,因为它会自动填入该 OID。

生成出的数组类型名,是在标量类型名前面加一个下划线。数组条目的其他字段会根据 pg_type.h 中的 BKI_ARRAY_DEFAULT(value) 标注填写;如果没有该标注,就从标量类型复制。(typalign 另有一个特殊情况。)然后,这两个条目的 typelemtyparray 字段会被设置为彼此交叉引用。

68.2.5. 编辑数据文件的方法 #

下面给出一些建议,说明在更新目录数据文件时,执行常见任务最简便的方法。

向目录中增加一个带默认值的新列:.  在头文件中加入该列,并加上 BKI_DEFAULT(value) 标注。数据文件通常只需在那些需要非默认值的现有行中补上该字段。

为现有列增加一个原本没有的默认值:.  在头文件中加上 BKI_DEFAULT 标注,然后运行 make reformat-dat-files,移除现在已经冗余的字段项。

移除一列,不论它是否有默认值:.  从头文件中删掉该列,然后运行 make reformat-dat-files,移除现在已经无用的字段项。

修改或移除现有默认值:.  不能只改头文件,因为那样会导致当前数据被错误解释。首先运行 make expand-dat-files,把所有默认值都显式写入数据文件;然后修改或移除 BKI_DEFAULT 标注;再运行 make reformat-dat-files,重新去掉多余字段。

临时性的批量编辑:.  reformat_dat_file.pl 可以改造为执行许多种批量修改。请查找其中标示可插入一次性代码位置的块注释。下面这个例子里,我们准备把 pg_proc 中两个布尔字段合并成一个 char 字段:

  1. pg_proc.h 中加入一个带默认值的新列:

    +    /* see PROKIND_ categories below */
    +    char        prokind BKI_DEFAULT(f);
    
  2. 基于 reformat_dat_file.pl 新建一个脚本,在处理过程中插入合适的值:

    -           # At this point we have the full row in memory as a hash
    -           # and can do any operations we want. As written, it only
    -           # removes default values, but this script can be adapted to
    -           # do one-off bulk-editing.
    +           # One-off change to migrate to prokind
    +           # Default has already been filled in by now, so change to other
    +           # values as appropriate
    +           if ($values{proisagg} eq 't')
    +           {
    +               $values{prokind} = 'a';
    +           }
    +           elsif ($values{proiswindow} eq 't')
    +           {
    +               $values{prokind} = 'w';
    +           }
    
  3. 运行这个新脚本:

    $ cd src/include/catalog
    $ perl  rewrite_dat_with_prokind.pl  pg_proc.dat
    

    此时,pg_proc.dat 会同时具有 prokindproisaggproiswindow 这三列,不过它们只会出现在取值非默认的那些行里。

  4. pg_proc.h 中移除旧列:

    -    /* is it an aggregate? */
    -    bool        proisagg BKI_DEFAULT(f);
    -
    -    /* is it a window function? */
    -    bool        proiswindow BKI_DEFAULT(f);
    
  5. 最后,运行 make reformat-dat-files,从 pg_proc.dat 中移除无用的旧项。

关于用于批量编辑的脚本的更多例子,见这条消息所附的 convert_oid2name.plremove_pg_type_oid_symbols.plhttps://www.postgresql.org/message-id/CAJVSVGVX8gXnPm+Xa=DxR7kFYprcQ1tNcCT5D0O3ShfnM6jehA@mail.gmail.com

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。