Postgres提供了广泛的数据类型、函数、操作符以及聚合功能。但有时它仍然不能满足你的某个特定需求, 幸运的是,通过"扩展"可以很容易地扩展Postgres的功能。 那么为什么不写一个自己的Postgresql扩展呢?

这是编写Postgres扩展系列文章中的第一篇。 你可以按照分支part_i上的代码示例进行操作。

base36

你可能已经知道url缩短器使用的技巧——使用一些特殊的随机字符(如http://goo.gl/EAZSKW)指向其他内容。当然,你必须记住它指向何处,因此你需要将其存储在数据库中。但是,与其使用varchar(6)保存6个字符(从而浪费7个字节),为什么不使用一个包含4个字节的整数并将其表示为base36呢?

Postgresql扩展的骨架

要在数据库中运行CREATE EXTENSION命令,你的扩展最少需要两个文件:一个格式化的控制文件extension_name.control,用于告诉Postgresql关于你的扩展的一些基础信息;一个是格式化的扩展SQL脚本extension--version.sql。因此,我们先在工程目录中添加这两个文件。

控制文件的一个很好的起点可以像下面这样:

文件名:base32.control

# 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。

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语言函数的基础知识,在下一篇文章中,我们将进行下一步:实现一个全新的数据类型。

编写Postgres扩展之一:基础的更多相关文章

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

    原文:http://big-elephants.com/2015-11/writing-postgres-extensions-part-v/ 编译:Tacey Wong 在关于编写Postgres扩 ...

  2. 编写Postgres扩展之四:测试

    原文:http://big-elephants.com/2015-11/writing-postgres-extensions-part-iv/ 编译:http://big-elephants.com ...

  3. 编写Postgres扩展之二:类型和运算符

    原文:http://big-elephants.com/2015-10/writing-postgres-extensions-part-ii/ 编译:Tacey Wong 在上一篇关于编写Postg ...

  4. 编写Postgres扩展之三:调试

    原文:http://big-elephants.com/2015-10/writing-postgres-extensions-part-iii/ 编译:Tacey Wong 在上一篇关于编写Post ...

  5. 使用golang 编写postgresql 扩展

      postgresql 的扩展可以帮助我们做好多强大的事情,支持的开发语言有lua.perl.java.js.c 社区有人开发了一个可以基于golang开发pg 扩展的项目,使用起来很方便,同时为我 ...

  6. 在TypeScript中扩展JavaScript基础对象的功能

    最近工作中用到,记录一下:假设我们需要一个功能,把一个数字比如10000输出为下面的字符串格式“10,000”,一般是写一个方法,那么我希望更方便一点,直接向Number类型添加一个格式化方法,比如叫 ...

  7. 一步步入门编写PHP扩展

    1.写在最前 随着互联网飞速发展,lamp架构的流行,php支持的扩展也越来越多,这样直接促进了php的发展. 但是php也有脚本语言不可避免的问题,性能比例如C等编译型语言相差甚多,所以在考虑性能问 ...

  8. 用Zephir编写PHP扩展

    自从NodeJS,和Golang出来后,很多人都投奔过去了.不为什么,冲着那牛X的性能.那PHP的性能什么时候能提升一下呢?要不然就会被人鄙视了.其实大牛们也深刻体会到了这些威胁,于是都在秘密开发各种 ...

  9. 了解Java密码扩展的基础

      了解Java密码扩展的基础     Java密码扩展(The Java Cryptography Extension),是JDK1.4的一个重要部分,基本上,他是由一些包构成的,这些包形成了一个框 ...

随机推荐

  1. MXNet/Gluon 中网络和参数的存取方式

    https://blog.csdn.net/caroline_wendy/article/details/80494120 Gluon是MXNet的高层封装,网络设计简单易用,与Keras类似.随着深 ...

  2. Flutter -------- 解析JSON数据

    SON序列化方法: 手动序列化和反序列化通过代码生成自动序列化和反序列化 手动JSON序列化是指使使用dart:convert中内置的JSON解码器.它将原始JSON字符串传递给JSON.decode ...

  3. Mac OS docker挂载文件夹

    sudo docker run -p 3306:3306 --name mysql -v /var/run/docker.sock:/var/run/docker.sock -v ~/mysql/co ...

  4. 教你玩转Linux—用户账号的管理

    用户账号的管理工作主要涉及到用户账号的添加.修改和删除.添加用户账号就是在系统中创建一个新账号,然后为新账号分配用户号.用户组.主目录和登录Shell等资源.刚添加的账号是被锁定的,无法使用. 1.添 ...

  5. RabbitMQ 入门教程(PHP版) 第三部分:发布/订阅(Publish/Subscribe)

    发布/订阅 在上篇第二部分教程中,我们搭建了一个工作队列.每个任务之分发给一个工作者(worker).在本篇教程中,我们要做的之前完全不一样——分发一个消息给多个消费者(consumers).这种模式 ...

  6. 为什么在MySQL数据库中无法创建外键?(MyISAM和InnoDB详解)

    问题描述:为什么在MySQL数据库中不能创建外键,尝试了很多次,既没有报错,也没有显示创建成功,真实奇了怪,这是为什么呢? 问题解决:通过查找资料,每次在MySQL数据库中创建表时默认的情况是这样的: ...

  7. Ubuntu18.04 instsall XMind_8 and crack

    1.dowload XMind_8 linux install zip wget https://www.xmind.cn/xmind/downloads/xmind-8-update8-linux. ...

  8. Python - Django - 模板语言之变量

    前言: 在 Django 模板语言中变量用 {{ }},逻辑用 {% %} 在 urls.py 中添加对应关系 from django.conf.urls import url from django ...

  9. SQL Delta实用案例介绍,很好的东西,帮了我不少忙

    SQL Delta实用案例介绍 概述 本篇文章主要介绍SQL DELTA的简单使用.为了能够更加明了的说明其功能,本文将通过实际项目中的案例加以介绍. 主要容 Ÿ   SQL DELTA 简介 Ÿ   ...

  10. jqweui 正在加载样式的用法

    见下图: 代码说明: $.showLoading("加载中..."); $.ajax({ success : function(data) { $.hideLoading(); } ...