Postgres提供了普遍的數據類型、函數、操做符以及聚合功能。但有時它仍然不能知足你的某個特定需求, 幸運的是,經過"擴展"能夠很容易地擴展Postgres的功能。 那麼爲何不寫一個本身的Postgresql擴展呢?html
這是編寫Postgres擴展系列文章中的第一篇。 你能夠按照分支part_i上的代碼示例進行操做。git
你可能已經知道url縮短器使用的技巧——使用一些特殊的隨機字符(如http://goo.gl/EAZSKW)指向其餘內容。固然,你必須記住它指向何處,所以你須要將其存儲在數據庫中。可是,與其使用varchar(6)
保存6個字符(從而浪費7個字節),爲何不使用一個包含4個字節的整數並將其表示爲base36呢?github
要在數據庫中運行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
從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/
的子目錄中也應該有一個對應包含預期輸出的文件,該文件具備與測試腳本相同的名稱,只不事後綴是.out
。make 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 installchec
k,那麼咱們的測試將失敗。這是由於咱們沒有指定預期的輸出。可是,咱們將找到包含base36_test.out
和base36 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語言實現。讓咱們得到第一個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_tex
t將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語言函數的基礎知識,在下一篇文章中,咱們將進行下一步:實現一個全新的數據類型。