編寫Postgres擴展之五:代碼組織和版本控制


在關於編寫Postgres擴展的系列文章的最後四篇文章中,咱們瞭解了基本的類型和操做符,介紹了調試器並完成了測試套件。git

如今讓咱們添加另外一種類型,看看如何在代碼庫增加時組織代碼庫。github

你能夠在github分支上找到最後一篇帖子的代碼庫part_iv今天的分支能夠在分支part_v上找到sql

版本控制

咱們可能對咱們的擴展感到滿意並在生產中使用它一段時間沒有任何問題。如今咱們的業務成功了,int的範圍可能已經不夠了。 這意味着咱們須要另外一個基於bigint的類型bigbase36,最多能夠包含13個字符。shell

這裏的問題是咱們不能簡單地刪除擴展並從新安裝新版本。數據庫

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文件:c#

文件名: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。 咱們將在下一篇文章中介紹這一點。

相關文章
相關標籤/搜索