编写Postgres扩展之五:代码组织和版本控制


在关于编写Postgres扩展的系列文章的最后四篇文章中,我们了解了基本的类型和操作符,介绍了调试器并完成了测试套件。

现在让我们添加另一种类型,看看如何在代码库增长时组织代码库。

你可以在github分支上找到最后一篇帖子的代码库part_iv今天的分支可以在分支part_v上找到

版本控制

我们可能对我们的扩展感到满意并在生产中使用它一段时间没有任何问题。现在我们的业务成功了,int的范围可能已经不够了。 这意味着我们需要另一个基于bigint的类型bigbase36,最多可以包含13个字符。

这里的问题是我们不能简单地删除扩展并重新安装新版本。

test=# drop extension base36 ;
ERROR:  cannot drop extension base36 because other objects depend on it
DETAIL:  table important_data column token depends on type base36
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

如果我们在这里DROP ... CASCADE,我们所有的数据都会丢失。 此外,对于TB级数据库而言,转储和重新创建不是一种选择。我们想要的是ALTER EXTENSION UPDATE TO '0.0.2'。幸运的是,Postgres内建了扩展的版本控制。请记住我们定义的base36.control文件:

文件名:base36.control

# base36 extension
comment = 'base36 datatype'
default_version = '0.0.1'
relocatable = true

版本“0.0.1”是我们执行CREATE EXTENSION base36时使用的默认版本,导致导入base36--0.0.1.sql脚本文件。 让我们另创建一个:

cp base36--0.0.1.sql base36--0.0.2.sql

默认是这样子

文件名:base36.control

# base36 extension
comment = 'base36 datatype'
default_version = '0.0.2'
relocatable = true

构建

make clean && make && make install && make installcheck

得到

...
ERROR:  could not stat file "/usr/local/Cellar/postgresql/9.4.0/share/postgresql/extension/base36--0.0.2.sql": No such file or directory
command failed: "/usr/local/Cellar/postgresql/9.4.0/bin/psql" -X -c "CREATE EXTENSION IF NOT EXISTS \"base36\"" "contrib_regression"
make: *** [installcheck] Error 2

嗯,它想使用extension / base36--0.0.2.sql但无法找到它。

让我们修复Makefile并告诉Postgres使用——.sql模式下的所有文件。

文件名:Makefile

EXTENSION     = base36                          # the extensions name
DATA          = $(wildcard *--*.sql)            # script files to install

我们现在可以在base36--0.0.2.sql中添加bigbase36类型

文件:base36-0.0.2.sql

-- base36 stuff omitted

CREATE FUNCTION bigbase36_in(cstring)
RETURNS bigbase36
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION bigbase36_out(bigbase36)
RETURNS cstring
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE bigbase36 (
  INPUT          = bigbase36_in,
  OUTPUT         = bigbase36_out,
  LIKE           = bigint
);

CREATE FUNCTION bigbase36_eq(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8eq';

CREATE FUNCTION bigbase36_ne(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8ne';

CREATE FUNCTION bigbase36_lt(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8lt';

CREATE FUNCTION bigbase36_le(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8le';

CREATE FUNCTION bigbase36_gt(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8gt';

CREATE FUNCTION bigbase36_ge(bigbase36, bigbase36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int8ge';

CREATE FUNCTION bigbase36_cmp(bigbase36, bigbase36)
RETURNS integer LANGUAGE internal IMMUTABLE AS 'btint8cmp';

CREATE FUNCTION hash_bigbase36(bigbase36)
RETURNS integer LANGUAGE internal IMMUTABLE AS 'hashint8';

CREATE OPERATOR = (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_eq,
  COMMUTATOR = '=',
  NEGATOR = '<>',
  RESTRICT = eqsel,
  JOIN = eqjoinsel,
  HASHES, MERGES
);

CREATE OPERATOR <> (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_ne,
  COMMUTATOR = '<>',
  NEGATOR = '=',
  RESTRICT = neqsel,
  JOIN = neqjoinsel
);

CREATE OPERATOR < (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_lt,
  COMMUTATOR = > ,
  NEGATOR = >= ,
  RESTRICT = scalarltsel,
  JOIN = scalarltjoinsel
);

CREATE OPERATOR <= (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_le,
  COMMUTATOR = >= ,
  NEGATOR = > ,
  RESTRICT = scalarltsel,
  JOIN = scalarltjoinsel
);

CREATE OPERATOR > (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_gt,
  COMMUTATOR = < ,
  NEGATOR = <= ,
  RESTRICT = scalargtsel,
  JOIN = scalargtjoinsel
);

CREATE OPERATOR >= (
  LEFTARG = bigbase36,
  RIGHTARG = bigbase36,
  PROCEDURE = bigbase36_ge,
  COMMUTATOR = <= ,
  NEGATOR = < ,
  RESTRICT = scalargtsel,
  JOIN = scalargtjoinsel
);

CREATE OPERATOR CLASS btree_bigbase36_ops
DEFAULT FOR TYPE bigbase36 USING btree
AS
        OPERATOR        1       <  ,
        OPERATOR        2       <= ,
        OPERATOR        3       =  ,
        OPERATOR        4       >= ,
        OPERATOR        5       >  ,
        FUNCTION        1       bigbase36_cmp(bigbase36, bigbase36);

CREATE OPERATOR CLASS hash_bigbase36_ops
DEFAULT FOR TYPE bigbase36 USING hash AS
        OPERATOR        1       = ,
        FUNCTION        1       hash_bigbase36(bigbase36);

CREATE CAST (bigint as bigbase36) WITHOUT FUNCTION AS ASSIGNMENT;
CREATE CAST (bigbase36 as bigint) WITHOUT FUNCTION AS ASSIGNMENT;

如你所见,这主要是针对base36bigbase36int4int8的查找和替换。

现在来添加C语言部分

组织C语言

为了更好地组织c代码,我们将把base36.c放在src目录下。

mkdir src
mv base36.c src/

现在,我们可以为src中的bigbase36输入和输出函数添加另一个文件。

文件名:src/bigbase64.c

PG_FUNCTION_INFO_V1(bigbase36_in);
Datum
bigbase36_in(PG_FUNCTION_ARGS)
{
    long result;
    char *bad;
    char *str = PG_GETARG_CSTRING(0);
    result = strtol(str, &bad, 36);
    if (bad[0] != '\0' || strlen(str)==0)
        ereport(ERROR,
            (
             errcode(ERRCODE_SYNTAX_ERROR),
             errmsg("invalid input syntax for bigbase36: \"%s\"", str)
            )
        );
    if (result < 0)
        ereport(ERROR,
            (
             errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("negative values are not allowed"),
             errdetail("value %ld is negative", result),
             errhint("make it positive")
            )
        );
    PG_RETURN_INT64((int64)result);
}

PG_FUNCTION_INFO_V1(bigbase36_out);
Datum
bigbase36_out(PG_FUNCTION_ARGS)
{
    int64 arg = PG_GETARG_INT64(0);
    if (arg < 0)
        ereport(ERROR,
            (
             errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("negative values are not allowed"),
             errdetail("value %d is negative", arg),
             errhint("make it positive")
            )
        );
    char base36[36] = "0123456789abcdefghijklmnopqrstuvwxyz";

    /* max 13 char + '\0' */
    char buffer[14];
    unsigned int offset = sizeof(buffer);
    buffer[--offset] = '\0';

    do {
        buffer[--offset] = base36[arg % 36];
    } while (arg /= 36);

    PG_RETURN_CSTRING(pstrdup(&buffer[offset]));
}

它或多或少与base36的代码相同。在bigbase36_in中,我们不再需要溢出安全类型转换为int32,并且可以用PG_RETURN_INT64直接返回结果(result);。对于bigbase36_out,我们将缓冲区扩展为14个字符,因为结果可能很长。

为了能够将两个文件编译成一个共享库对象,我们还需要调整Makefile。

文件名:Makefile

# the extensions name
EXTENSION     = base36
DATA          = $(wildcard *--*.sql)            # script files to install
TESTS         = $(wildcard test/sql/*.sql)      # use test/sql/*.sql as testfiles

# find the sql and expected directories under test
# load plpgsql into test db
# load base36 extension into test db
# dbname
REGRESS_OPTS  = --inputdir=test         \
                --load-extension=base36 \
                --load-language=plpgsql
REGRESS       = $(patsubst test/sql/%.sql,%,$(TESTS))
OBJS          = $(patsubst %.c,%.o,$(wildcard src/*.c)) # object files
# final shared library to be build from multiple source files (OBJS)
MODULE_big    = $(EXTENSION)


# postgres build stuff
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

在这里(第13行),我们定义所有src/*。c文件将成为目标文件,应该从这些多个对象构建在一个共享库中(第15行)。

因此,我们再次将Makefile一般化以备将来使用。

如果我们现在构建并测试扩展,那么一切都会很好。

但是,我们还应该为bigbase36类型添加测试。

文件名:sql/bigbase36_io.sql

-- simple input
SELECT '120'::bigbase36;
SELECT '3c'::bigbase36;
-- case insensitivity
SELECT '3C'::bigbase36;
SELECT 'FoO'::bigbase36;
-- invalid characters
SELECT 'foo bar'::bigbase36;
SELECT 'abc$%2'::bigbase36;
-- negative values
SELECT '-10'::bigbase36;
-- to big values
SELECT 'abcdefghijklmn'::bigbase36;

-- storage
BEGIN;
CREATE TABLE base36_test(val bigbase36);
INSERT INTO base36_test VALUES ('123'), ('3c'), ('5A'), ('zZz');
SELECT * FROM base36_test;
UPDATE base36_test SET val = '567a' where val = '123';
SELECT * FROM base36_test;
UPDATE base36_test SET val = '-aa' where val = '3c';
SELECT * FROM base36_test;
ROLLBACK;

如果我们看看results / bigbase36_io.out,我们会再次看到一些过于大的值的奇怪行为。

-- to big values
SELECT 'abcdefghijklmn'::bigbase36;
ERROR:  negative values is not allowed
LINE 1: SELECT 'abcdefghijklmn'::bigbase36;
               ^
DETAIL:  value -1 is negative
HINT:  make it positive```

您将注意到,如果结果溢出,strtol()将返回LONG MAX。如果您查看一下在postgres源代码中如何将文本转换为数字,您可以看到有许多特定于平台的边和边角情况。为简单起见,我们假设我们处于具有64位长结果的64位环境中。在32位机器上,我们的测试套件会使installcheck失败,告诉我们的用户扩展不会像预期的那样工作。

文件名:sec/bigbase36.c

#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"
#include <limits.h>

PG_FUNCTION_INFO_V1(bigbase36_in);
Datum
bigbase36_in(PG_FUNCTION_ARGS)
{
    long result;
    char *bad;
    char *str = PG_GETARG_CSTRING(0);
    result = strtol(str, &bad, 36);
    if (result == LONG_MIN || result == LONG_MAX)
        ereport(ERROR,
            (
             errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("base36 out of range")
            )
        );

    if (bad[0] != '\0' || strlen(str)==0)
        ereport(ERROR,
            (
             errcode(ERRCODE_SYNTAX_ERROR),
             errmsg("invalid input syntax for bigbase36: \"%s\"", str)
            )
        );
    if (result < 0)
        ereport(ERROR,
            (
             errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("negative values are not allowed"),
             errdetail("value %ld is negative", result),
             errhint("make it positive")
            )
        );
    PG_RETURN_INT64((int64)result);
}

/* bigbase36_out omitted */

在这里,通过包含<limits.h>,我们可以检查结果是否溢出。这同样适用于base36_in检查result < INT_MIN || result > INT_MAX,从而避免``DirectFunctionCall1(int84,result)。这里唯一需要注意的是,我们不能将LONG MAXLONG MIN转换为base36`。

现在我们已经创建了一堆代码复制,让我们使用一个公共头文件来提高可读性,并在宏中定义错误。

文件名:src/base36.c

#ifndef BASE36_H
#define BASE36_H

#include "postgres.h"
#include "utils/builtins.h"
#include "utils/int8.h"
#include "libpq/pqformat.h"
#include <limits.h>

extern const char base36_digits[36];

#define BASE36OUTOFRANGE_ERROR(_str, _typ)                      \
  do {                                                          \
    ereport(ERROR,                                              \
      (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),             \
        errmsg("value \"%s\" is out of range for type %s",      \
          _str, _typ)));                                        \
  } while(0)                                                    \

#define BASE36SYNTAX_ERROR(_str, _typ)                          \
  do {                                                          \
    ereport(ERROR,                                              \
      (errcode(ERRCODE_SYNTAX_ERROR),                           \
      errmsg("invalid input syntax for %s: \"%s\"",             \
             _typ, _str)));                                     \
  } while(0)                                                    \


#endif // BASE36_H

此外,我们没有理由不允许负值。

迁移

最后我们的新版本已准备好发布! 我们来添加一个更新测试。

文件名:test/sql/update.sql

BEGIN;
DROP EXTENSION base36;
CREATE EXTENSION base36 VERSION '0.0.1';
ALTER EXTENSION base36 UPDATE TO '0.0.2';
SELECT 'abcdefg'::bigbase36;

之后运行

make clean && make && make install && make installcheck

我们看到

文件名:results/update.out

EGIN;
DROP EXTENSION base36;
CREATE EXTENSION base36 VERSION '0.0.1';
ALTER EXTENSION base36 UPDATE TO '0.0.2';
ERROR:  extension "base36" has no update path from version "0.0.1" to version "0.0.2"
SELECT 'abcdefg'::bigbase36;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

虽然存在0.0.2版本,但是我们不能运行Update命令。我们需要一个extension--oldversion--newversion.sql形式的更新脚本,这个脚本包括从一个版本升级到另一个版本所需的所有命令。

所以我们需要将所有base36实现的sql复制到base36--0.0.1--0.0.2.sql

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION base36" to load this file. \quit

CREATE FUNCTION bigbase36_in(cstring)
RETURNS bigbase36
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION bigbase36_out(bigbase36)
RETURNS cstring
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE bigbase36 (
  INPUT          = bigbase36_in,
  OUTPUT         = bigbase36_out,
  LIKE           = bigint
);

---... rest omitted

MODULE_PATHNAME

对于使用C-Function定义的AS'$ libdir / base36'的每个SQL函数,都在告诉Postgres使用哪个共享库。如果重命名共享库,则需要重写所有SQL函数。我们可以更好的处理这个:

文件名:base36.control

# base36 extension
comment = 'base36 datatype'
default_version = '0.0.2'
relocatable = true
module_pathname = '$libdir/base36'

这里我们定义module_pathname指向'$ libdir / base36',因此我们可以像这样定义我们的SQL函数

CREATE FUNCTION base36_in(cstring)
RETURNS base36
AS 'MODULE_PATHNAME'
LANGUAGE C IMMUTABLE STRICT;

总结

在过去五篇文章中,你看到你可以定义自己的数据类型并完全指定所需的行为。然而,权力越大,责任越大。你不仅能用意外的结果将将用户弄晕,还能完全破坏服务器并丢失数据。幸运的是,你学会了如何调试和编写正确的测试。

在开始实现之前,您应该首先看看Postgres是如何实现的,并尽可能地重用功能。因此,您不仅避免了重复开发,而且还拥有来自经过良好测试的PostgreSQL代码库的可信代码。完成后,请务必始终考虑边缘情况,将所有内容写入测试以防止破坏,并尝试更高的工作负载和复杂的语句,以避免以后在生产环境中出现错误。

由于测试是如此重要,我们在adjust编写了自己的测试工具pg_spec。 我们将在下一篇文章中介绍这一点。

posted @ 2019-09-10 13:32  Tacey Wong  阅读(756)  评论(0编辑  收藏  举报