編寫Postgres擴展之一:基礎

Postgres提供了普遍的數據類型、函數、操做符以及聚合功能。但有時它仍然不能知足你的某個特定需求, 幸運的是,經過"擴展"能夠很容易地擴展Postgres的功能。 那麼爲何不寫一個本身的Postgresql擴展呢?html

這是編寫Postgres擴展系列文章中的第一篇。 你能夠按照分支part_i上的代碼示例進行操做。git

base36

你可能已經知道url縮短器使用的技巧——使用一些特殊的隨機字符(如http://goo.gl/EAZSKW)指向其餘內容。固然,你必須記住它指向何處,所以你須要將其存儲在數據庫中。可是,與其使用varchar(6)保存6個字符(從而浪費7個字節),爲何不使用一個包含4個字節的整數並將其表示爲base36呢?github

Postgresql擴展的骨架

要在數據庫中運行CREATE EXTENSION命令,你的擴展最少須要兩個文件:一個格式化的控制文件extension_name.control,用於告訴Postgresql關於你的擴展的一些基礎信息;一個是格式化的擴展SQL腳本extension--version.sql。所以,咱們先在工程目錄中添加這兩個文件。算法

控制文件的一個很好的起點能夠像下面這樣:sql

文件名:base32.controlshell

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

到目前爲止,咱們的擴展尚未任何功能。讓咱們在SQL腳本文件中添加一些:數據庫

文件名:base32--0.0.1.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 base36_encode(digits int)
RETURNS text
LANGUAGE plpgsql IMMUTABLE STRICT
  AS $$
    DECLARE
      chars char[];
      ret varchar;
      val int;
    BEGIN
      chars := ARRAY[
                '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h',
                'i','j','k','l','m','n','o','p','q','r','s','t', 'u','v','w','x','y','z'
              ];

      val := digits;
      ret := '';

    WHILE val != 0 LOOP
      ret := chars[(val % 36)+1] || ret;
      val := val / 36;
    END LOOP;

    RETURN(ret);
    END;
  $$;

第二行確保文件不會直接加載到數據庫中,而只能經過CREATE EXTENSION加載。函數

這個簡單的pl/pgsql函數容許咱們將任何整數編碼到它的base36表示形式中。若是咱們將這兩個文件複製到postgres 的SHAREDIR/extension目錄中(譯者注:能夠經過pg_config命令獲取),咱們就能夠經過CREATE EXTENSION使用這個擴展了。可是咱們不會麻煩用戶去弄清楚這些文件放在哪裏,以及如何手動複製它們,這是makefile該作的事。如今讓咱們在項目中添加一個makefile。post

Makefile

從9.1版本開始,每一個PostgreSQL安裝都爲擴展提供了一個名爲PGXS的構建基礎設施,容許在已經安裝的服務器上輕鬆構建擴展。構建擴展所需的大多數環境變量都是在pg_config中設置的,能夠簡單地重用。

對於咱們的示例,下面這個Makefile就符合咱們的需求。

文件名:Makefile

EXTENSION = base36        # 擴展名稱
DATA = base36--0.0.1.sql  # 要安裝的腳本文件

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

如今咱們能夠開始使用擴展了。 在你的工程運行make install。並在數據庫中執行以下操做:

test=# CREATE EXTENSION base36;
CREATE EXTENSION
Time: 3,329 ms
test=# SELECT base36_encode(123456789);
 base36_encode
---------------
 21i3v9
(1 row)

Time: 0,558 ms

HiaHiaHia,太棒了!

編寫測試

現在,每一個認真的開發人員都會編寫測試 做爲處理數據的數據庫開發人員(多是貴公司最有價值的東西),你也應該這樣作。

你能夠很容易地向項目中添加一些迴歸測試,這些測試能夠在完成make install以後經過make installcheck調用。爲此,你能夠將測試腳本文件放在名爲sql/的子目錄中。對於每一個測試文件,在名爲expected/的子目錄中也應該有一個對應包含預期輸出的文件,該文件具備與測試腳本相同的名稱,只不事後綴是.outmake installcheck命令使用psql執行每一個測試腳本,並將結果輸出與匹配的預期文件進行比較。任何差別都將寫入文件regression.diffs。咱們開始吧:
文件名:sql/base36_test.sql

CREATE EXTENSION base36;
SELECT base36_encode(0);
SELECT base36_encode(1);
SELECT base36_encode(10);
SELECT base36_encode(35);
SELECT base36_encode(36);
SELECT base36_encode(123456789);

咱們還須要告訴咱們的Makefile關於測試的信息(第3行):

文件名:Makefile

EXTENSION = base36     
DATA = base36--0.0.1.sql  
REGRESS = base36_test     # 咱們的測試腳本文件(沒有後綴名)

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

若是咱們如今運行make install && make installcheck,那麼咱們的測試將失敗。這是由於咱們沒有指定預期的輸出。可是,咱們將找到包含base36_test.outbase36 test.out.diff的新目錄result。前者包含測試腳本文件的實際輸出。讓咱們將它移動到所需的目錄中。:

mkdir expected
mv results/base36_test.out expected

若是如今從新運行咱們的測試,咱們會看到相似的結果:

============== running regression test queries        ==============
test base36_test              ... ok

=====================
 All 1 tests passed.
=====================

太好了! 可是,嘿,咱們在這裏做弊了。 若是咱們看一下咱們的指望,咱們會注意到這不是咱們所指望的。

cat expected/base36_test.out
CREATE EXTENSION base36;
SELECT base36_encode(0);
 base36_encode
---------------

(1 row)

SELECT base36_encode(1);
 base36_encode
---------------
 1
(1 row)

SELECT base36_encode(10);
 base36_encode
---------------
 a
(1 row)

SELECT base36_encode(35);
 base36_encode
---------------
 z
(1 row)

SELECT base36_encode(36);
 base36_encode
---------------
 10
(1 row)

SELECT base36_encode(123456789);
 base36_encode
---------------
 21i3v9
(1 row)

你會注意到在第6行,base36_encode(0)返回一個空字符串,而咱們指望的是0。若是咱們修正咱們的指望,咱們的測試將再次失敗。

============== running regression test queries        ==============
test base36_test              ... FAILED

======================
 1 of 1 tests failed.
======================

The differences that caused some tests to fail can be viewed in the
file "regression.diffs".  A copy of the test summary that you see
above is saved in the file "regression.out".

make: *** [installcheck] Error 1

咱們能夠經過查看前面提到的regression.diffs輕鬆地檢查失敗的測試.

*** 2,8 ****
  SELECT base36_encode(0);
   base36_encode
  ---------------
!  0
  (1 row)

  SELECT base36_encode(1);
--- 2,8 ----
  SELECT base36_encode(0);
   base36_encode
  ---------------
!
  (1 row)

  SELECT base36_encode(1);

你能夠按照「預期的0已得到」來閱讀它。

如今讓咱們在編碼函數中實現修復,使測試再次經過(第12-14行):

文件名:base36-0.0.1.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 base36_encode(digits int)
RETURNS character varying
LANGUAGE plpgsql IMMUTABLE STRICT
  AS $$
    DECLARE
      chars char[];
      ret varchar;
      val int;
    BEGIN
      IF digits = 0
        THEN RETURN('0');
      END IF;
      chars := ARRAY[
                '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h',
                'i','j','k','l','m','n','o','p','q','r','s','t', 'u','v','w','x','y','z'
              ];

      val := digits;
      ret := '';

    WHILE val != 0 LOOP
      ret := chars[(val % 36)+1] || ret;
      val := val / 36;
    END LOOP;

    RETURN(ret);
    END;
  $$;

優化速度,寫一些C代碼

雖然在擴展中提供相關功能是共享代碼的一種方便方法,但真正有趣的是用c語言實現。讓咱們得到第一個1M base36數字。

test=# SELECT i, base36_encode(i) FROM generate_series(1,1e6::int) i;
Time: 11289,610 ms

11秒? ......好吧,不是那麼快。

讓咱們看看咱們是否能在c語言中作得更好。編寫c語言函數並無那麼難。

文件名base36.c

#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(base36_encode);
Datum
base36_encode(PG_FUNCTION_ARGS)
{
    int32 arg = PG_GETARG_INT32(0);
    char base36[36] = "0123456789abcdefghijklmnopqrstuvwxyz";

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

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


    PG_RETURN_TEXT_P(cstring_to_text(&buffer[offset]));
}

你可能已經注意到實際的算法是維基百科提供的。讓咱們看看咱們添加了什麼來使它與Postgres一塊兒使用

#include "postgres.h"包括與Postgres接口所需的大部分基本內容。 這行必須包含在聲明Postgres函數的每一個C文件中。

#include "fmgr.h"須要包含以使用PG_GETARG_XXX和PG_RETURN_XXX宏。

#include "utils/builtins.h"在Postgres的內置數據類型上定義了一些操做(稍後使用cstring_to_text)

PG_MODULE_MAGIC 是PostgreSQL 8.2中包含頭文件fmgr.h後,模塊源文件中的一個(且僅一個)中須要的魔法塊。

PG_FUNCTION_INFO_V1(base36_encode);將該函數做爲版本1調用約定引入Postges,只有在但願用到函數->Postgres接口時才須要。

Dtum是每一個c語言Postgres函數的返回類型,能夠是任何數據類型。你能夠把它想象成相似於void *的東西。

base36_encode(PG_FUNCTION_ARGS) 咱們的函數名,PG_FUNCTION_ARGS能夠接受任何數字和任何類型的參數。

int32 arg = PG_GETARG_INT32(0);獲取第一個參數,參數的編號從0開始。必須使用fmgr.h中定義的PG GETARG XXX宏來獲取實際的參數值。

har *buffer = palloc(7 * sizeof(char));爲了在分配內存時防止內存泄漏,老是使用PostgreSQL函數palloc和pfree,而不是相應的C庫函數malloc和free。palloc分配的內存將在每一個事務結束時自動釋放。你也可使用palloc0來確保字節清零。

PG_RETURN_TEXT_P(cstring_to_text(&buffer[offset]));要將一個值返回給Postgres,你必須使用一個PG_RETURN_XXX宏。cstring_to_text將cstring轉換爲Postgres文本類型。

完成c語言代碼部分以後,須要修改SQL函數。

文件名:base36-0.0.1.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 base36_encode(integer) RETURNS text
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

爲了可以使用該函數,咱們還須要修改Makefile(第4行)

文件名:Makefile

EXTENSION = base36        # 擴展名稱
DATA = base36--0.0.1.sql  # 要安裝的腳本文件
REGRESS = base36_test     # 測試腳本文件 (沒有後綴名)
MODULES = base36          # 要構建的c模塊文件

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

幸運的是,咱們已經進行了測試,可使用make install && make installcheck進行測試。 打開數據庫控制檯也證實它的速度要快不少(30倍):

test=# SELECT i, base36_encode(i) FROM generate_series(1,1e6::int) i;
Time: 361,054 ms

返回錯誤

你可能已經注意到,咱們的簡單實現沒法處理負數。就像以前處理0同樣,它將返回一個空字符串。咱們可能想要爲負值添加一個負號,或者僅僅是錯誤輸出。咱們選後者吧。(12-20行):
文件名:base36.c

#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(base36_encode);
Datum
base36_encode(PG_FUNCTION_ARGS)
{
    int32 arg = PG_GETARG_INT32(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 6 char + '\0' */
    char *buffer = palloc(7 * sizeof(char));
    unsigned int offset = sizeof(buffer);
    buffer[--offset] = '\0';

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


    PG_RETURN_TEXT_P(cstring_to_text(&buffer[offset]));
}

這將會致使:

test=# SELECT base36_encode(-10);
ERROR:  negative values are not allowed
DETAIL:  value -10 is negative
HINT:  make it positive

Postgres內置了一些不錯的錯誤報告功能。雖然對於這個用例來講,一個簡單的錯誤消息就足夠了,可是你能夠(但不必定須要)添加細節、提示等等。

對於簡單的調試,使用下面的形式也很方便:

elog(INFO, "value here is %d", value);

INFO級別錯誤只會產生日誌消息,而不會當即中止函數調用。 嚴重級別從DEBUG到PANIC不等。

更多

既然咱們已經瞭解了編寫擴展和c語言函數的基礎知識,在下一篇文章中,咱們將進行下一步:實現一個全新的數據類型。

相關文章
相關標籤/搜索