c - 自定义基本数据类型的输入函数中的 Postgresql 损坏数据

标签 c postgresql sql-function

我创建了一个自定义类型 gp 来模拟 DND 5e 货币系统。我在 gp.c 中定义了自定义输入和输出函数:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"

#include <stdio.h>

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

static const char* inputFormat = " %i %s2 ";
static const char* invalidFormat = "invalid input syntax for gp: \"%s\"";

PG_FUNCTION_INFO_V1(gp_input);

Datum gp_input(PG_FUNCTION_ARGS) {
    char* raw = PG_GETARG_CSTRING(0);
    int32 amt;
    char unit[3];

    if (sscanf(raw, inputFormat, &amt, &unit[0]) != 2) {
        ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg(invalidFormat, raw)));
    }

    switch(unit[1]) {
        case 'p':
            break;
        default:
            ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg(invalidFormat, raw)));
    }
    switch(unit[0]) {
        case 'c':
            break;
        case 's':
            amt *= 10;
            break;
        case 'e':
            amt *= 50;
            break;
        case 'g':
            amt *= 100;
            break;
        case 'p':
            amt *= 1000;
            break;
        default:
            ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg(invalidFormat, raw)));
    }

    int32* result = (int32*)palloc(sizeof(int32));
    *result = amt;

    PG_RETURN_POINTER(result);
}

PG_FUNCTION_INFO_V1(gp_output);

Datum gp_output(PG_FUNCTION_ARGS) {
    int32* raw = (int32*)PG_GETARG_POINTER(0);
    int32 val = *raw;
    unsigned int bufsz = sizeof(unsigned char)*9 + 2;// allow up to 999999999[pgsc]p
    char* buf = (char*) palloc(bufsz+1); // +1 b/c '\0'

    if (val >= 10 && val % 10 == 0) {
        val /= 10;

        if (val >= 10 && val % 10 == 0) {
            val /= 10;

            if (val >= 10 && val % 10 == 0) {
                val /= 10;

                if (sprintf(buf, "%dpp", val) <= 0) {
                    ereport(ERROR, (errcode(ERRCODE_UNTRANSLATABLE_CHARACTER), errmsg("Bad value for gp")));
                }
            }
            else {
                if (sprintf(buf, "%dgp", val) <= 0) {
                    ereport(ERROR, (errcode(ERRCODE_UNTRANSLATABLE_CHARACTER), errmsg("Bad value for gp")));
                }
            }
        }
        else {
            if (sprintf(buf, "%dsp", val) <= 0) {
                ereport(ERROR, (errcode(ERRCODE_UNTRANSLATABLE_CHARACTER), errmsg("Bad value for gp")));
            }
        }
    }
    else {
        if (sprintf(buf, "%dcp", val) <= 0) {
            ereport(ERROR, (errcode(ERRCODE_UNTRANSLATABLE_CHARACTER), errmsg("Bad value for gp")));
        }
    }
    PG_RETURN_CSTRING(buf);
}

我知道我没有检查数字是否超出范围或存储的值是否适合缓冲区,但我还没有遇到那个问题。我的问题是 postgres 似乎正在编辑,在某些情况下会破坏我正在存储的值。我有这个测试 SQL 文件:

DROP TYPE IF EXISTS gp CASCADE;
DROP TABLE IF EXISTS test;

CREATE TYPE gp;

CREATE FUNCTION gp_input(cstring) RETURNS gp AS '$libdir/gp.so' LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION gp_output(gp) RETURNS cstring AS '$libdir/gp.so' LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE gp (input=gp_input, output=gp_output);

CREATE TABLE test (val gp);

INSERT INTO test VALUES ('12sp'), ('100gp'), ('1000cp'), ('101cp');
SELECT * FROM test;
INSERT INTO test VALUES ('101sp');

SELECT 的输出是:

  val  
-------
 12sp
 10pp
 1pp
 212cp
(4 rows)

所以我们可以看到除了最后一个值外,所有值都被正确存储和表示:101cp 被存储为指向 int32212。使用 ereport 警告,我能够确定在输入函数返回之前,result 指向正确的值:101。但是,作为参数传递给我的输出函数的指针指向一个我没有存储的值:212。在输入代码结尾和输出代码开头之间的某处,postgres 损坏了该值。这总是发生在输入字符串 101cp 上,与表的状态或同时插入的任何其他值无关。

但是现在真正奇怪的部分是;最后一个 INSERT 使客户端崩溃。解析该 gp 值后,它会打印错误:

psql:./gptest.sql:15: ERROR:  compressed data is corrupted
LINE 1: INSERT INTO test VALUES ('101sp');
                                 ^

总是发生在值 101sp 上,无论表状态或任何其他值与它一起插入。使用 ereport 警告,我能够在 return 语句之前看到,result 指向正确的值:1010。这也意味着崩溃发生在返回宏扩展或某些底层代码中。

所以我真的不知道发生了什么。我正在做 palloc 所以不允许覆盖内存,而且我想不出包含 101 的值总是有问题的任何原因 - 以及不同的问题取决于单位。 int32 应该能够存储我正在测试的小值,但事实并非如此。我不知道这是否应该是这样实现的,但我已经检查过并且传递给输出的指针与这些值中的任何一个的 result 指针的地址不同,所以我假设它在幕后错误地执行了某种memcpy,但随后我知道如何期望任何人定义自定义基本数据类型。

最佳答案

CREATE TYPE 采用大量可选参数,其中一些与数据的物理布局有关,并且这些参数需要与您的 I/O 函数期望/返回的结构一致。

文档似乎没有提到这些参数的默认值,但是提到“压缩数据”的错误表明您的值是 TOASTed ,即 INTERNALLENGTH 默认为 VARIABLE 。此类类型应以描述值的总长度的 varlena header 开头,这肯定不是您要返回的内容(尽管 Postgres 仍会这样解释它,从而导致各种奇怪的情况行为,更不用说将大量随机字节保存到您的表中,并且迟早会出现段错误......)。

如果您的目标是创建一个在内部表示为简单整数(固定长度、按值传递等)的类型,您可以只复制内置类型的特征:

CREATE TYPE gp (input=gp_input, output=gp_output, like=integer);

然后您应该能够取消 palloc() 和指针,使用 PG_GETARG_INT32(0) 获取您的参数,然后返回 PG_RETURN_INT32 (amt)


如果您想要内置类型的所有行为,但使用自定义显示格式,这比您预期的要容易得多。

numeric 之类的内部 C 例程与您为自己实现此类类型而编写的例程相同。因此,您只需复制粘贴其 SQL 级别的定义,然后让指向现有 C 处理程序的函数完成所有实际工作,即可创建您自己的此类内置类型版本:

CREATE TYPE gp;
CREATE FUNCTION gp_in(cstring,oid,integer) RETURNS gp LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numeric_in';
CREATE FUNCTION gp_out(gp) RETURNS cstring LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numeric_out';
CREATE FUNCTION gp_send(gp) RETURNS bytea LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numeric_send';
CREATE FUNCTION gp_recv(internal,oid,integer) RETURNS gp LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numeric_recv';
CREATE FUNCTION gptypmodin(cstring[]) RETURNS integer LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numerictypmodin';
CREATE FUNCTION gptypmodout(integer) RETURNS cstring LANGUAGE internal IMMUTABLE STRICT PARALLEL SAFE AS 'numerictypmodout';
CREATE TYPE gp (
  INPUT = gp_in,
  OUTPUT = gp_out,
  RECEIVE = gp_recv,
  SEND = gp_send,
  TYPMOD_IN = gptypmodin,
  TYPMOD_OUT = gptypmodout,
  LIKE = numeric
);
CREATE TABLE t (x gp(10,2), y gp);
INSERT INTO t VALUES ('123.45', '2387456987623498765324.2837654987364987269837456981');
SELECT * FROM t;

   x    |                          y
--------+-----------------------------------------------------
 123.45 | 2387456987623498765324.2837654987364987269837456981

从那里,您可以用您自己的 C 函数替换输入/输出处理程序,从 internal functions 复制粘贴代码作为起点。在您的情况下,最简单的方法可能是在函数开头将 DnD 货币字符串转换为简单的十进制字符串,并让其余代码担心将其转换为 Numeric.

如果你想要算术/比较运算符、索引操作类、最小/最大聚合、类型转换等,你也可以从原始类型复制粘贴这些定义,只要你不弄乱内部二进制格式。

关于c - 自定义基本数据类型的输入函数中的 Postgresql 损坏数据,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54674045/

相关文章:

c# - 使用 ASP .NET MVC Identity 应用程序对外部 API (PostgreSQL) 中的用户进行身份验证

oracle - 从 PL/SQL 函数返回多个值

mysql - 为什么MySQL给出错误 "Not allowed to return a result set from a function"?

c - 对 `printf' 的 undefined reference

java - 在 .c 和 .cpp 文件 (JNI) 中声明的文件范围变量的生命周期是否有保证?

c - 为什么我无法访问所定位的内存?

c - 从 C 字符串中提取 POST 参数

sql - psycopg2 相当于 mysqldb.escape_string?

performance - 为什么 Solr 比 Postgres 快这么多?

java - PostgreSQL 函数 : relation does not exist error