postgresql奇怪的类型为numeric的无效输入语法:""而value不是空的varchar

Dol*_*nga 5 postgresql

我正在尝试调试一个非自己创建的函数(dms2dd).我制作了自己的测试功能(见下文)并将我的问题归结为特定的行/值.

如果我运行以下查询:

SELECT "Lat", "Long", test_dolf("Lat"), test_dolf("Long") FROM pawikan WHERE "Lat" IS NOT NULL AND "Long" IS NOT NULL ORDER BY index LIMIT 1 OFFSET 29130
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

'N6° 6' 9.4824"';'E118° 26' 49.1172'' ';'9.4824';'49.1172'
Run Code Online (Sandbox Code Playgroud)

这正是我所期待的.但是使用以下查询:

SELECT "Lat", "Long", CAST(test_dolf("Lat") as numeric), test_dolf("Long") FROM pawikan WHERE "Lat" IS NOT NULL AND "Long" IS NOT NULL ORDER BY index  LIMIT 1 OFFSET 29130
Run Code Online (Sandbox Code Playgroud)

我收到了错误

ERROR: invalid input syntax for type numeric: ""
SQL state: 22P02
Run Code Online (Sandbox Code Playgroud)

该错误表明我尝试转换为数字的varchar值为空,但正如您从上一个查询中看到的那样,它不是.它只是一个有效的数字varchar.实际上,如果我复制粘贴值并运行:

SELECT CAST('9.4824' AS numeric);
Run Code Online (Sandbox Code Playgroud)

它完全有效,查询实际上会生成一个有效的数字.更重要的是,如果我将第一个查询的结果存储在中间表中:

SELECT "Lat", "Long", test_dolf("Lat") as lat_sec, test_dolf("Long") as long_sec INTO dms2dd_test FROM pawikan WHERE "Lat" IS NOT NULL AND "Long" IS NOT NULL ORDER BY index LIMIT 11 OFFSET 29120
Run Code Online (Sandbox Code Playgroud)

然后发出一个

SELECT CAST(long_sec as numeric), CAST(lat_sec AS numeric) FROM dms2dd_test;
Run Code Online (Sandbox Code Playgroud)

它完全有效.即使这样也可以:

SELECT test_dolf(E'N6° 6\' 9.4824"')::numeric as lat_sec
Run Code Online (Sandbox Code Playgroud)

那么这里出了什么问题?它看起来像我转换为数字的第二个查询,一个不同的值传递给我的函数,但我测试了排序列(索引),它只包含唯一的bigints.

这是test_dolf函数的代码:

CREATE OR REPLACE FUNCTION public.test_dolf(strdegminsec character varying)
  RETURNS varchar AS
$BODY$
    DECLARE
       i               numeric;
       intDmsLen       numeric;          -- Length of original string
       strCompassPoint Char(1);
       strNorm         varchar(16) = ''; -- Will contain normalized string
       strDegMinSecB   varchar(100);
       blnGotSeparator integer;          -- Keeps track of separator sequences
       arrDegMinSec    varchar[];        -- TYPE stringarray is table of varchar(2048) ;
       strChr          Char(1);
    BEGIN
       strDegMinSec := regexp_replace(replace(strdegminsec,E'\'\'','"'),' "([0-9]+)',E' \\1"');
       -- Remove leading and trailing spaces
       strDegMinSecB := REPLACE(strDegMinSec,' ','');
       intDmsLen := Length(strDegMinSecB);

       blnGotSeparator := 0; -- Not in separator sequence right now

       -- Loop over string, replacing anything that is not a digit or a
       -- decimal separator with
       -- a single blank
       FOR i in 1..intDmsLen LOOP
          -- Get current character
          strChr := SubStr(strDegMinSecB, i, 1);
          -- either add character to normalized string or replace
          -- separator sequence with single blank         
          If strpos('0123456789,.', strChr) > 0 Then
             -- add character but replace comma with point
             If (strChr <> ',') Then
                strNorm := strNorm || strChr;
             Else
                strNorm := strNorm || '.';
             End If;
             blnGotSeparator := 0;
          ElsIf strpos('neswNESW',strChr) > 0 Then -- Extract Compass Point if present
            strCompassPoint := strChr;
          Else
             -- ensure only one separator is replaced with a blank -
             -- suppress the rest
             If blnGotSeparator = 0 Then
                strNorm := strNorm || ' ';
                blnGotSeparator := 0;
             End If;
          End If;
       End Loop;

       -- Split normalized string into array of max 3 components
       arrDegMinSec := string_to_array(strNorm, ' ');
       return arrDegMinSec[3];
    End 
$BODY$
  LANGUAGE plpgsql IMMUTABLE
  COST 100;
Run Code Online (Sandbox Code Playgroud)

Dol*_*nga 4

我明白问题出在哪里了。它看起来像 postgresql,即使我做了 LIMIT 和 OFFSET,仍然调用该框架之外的其他行的 select 中的函数。

我通过将引发异常的代码放入函数中并捕获产生的错误,并在异常发生时引发 NOTICE 错误来解决这个问题(请参阅下面的函数,特别是函数末尾的 BEGIN EXCEPTION END 块)。该通知显示为警告,但不会导致代码执行停止。突然发现,该函数不仅是为我期望调用的行调用的,而且还为一大堆其他行调用。这完全不是我所期望的,对我来说有点违反直觉,但我想这就是 postgresql 应该如何工作。

由于在 postgresql 中捕获异常非常昂贵,我想我需要添加一个测试来首先防止异常(我可以测试arrDegMinSec该数组的第 1-3 项的长度和值,并在无效的情况下返回 NULL价值观。

CREATE OR REPLACE FUNCTION public.test_dolf(strdegminsec character varying)
  RETURNS numeric AS
$BODY$
    DECLARE
       i               numeric;
       intDmsLen       numeric;          -- Length of original string
       strCompassPoint Char(1);
       strNorm         varchar(16) = ''; -- Will contain normalized string
       strDegMinSecB   varchar(100);
       blnGotSeparator integer;          -- Keeps track of separator sequences
       arrDegMinSec    varchar[];        -- TYPE stringarray is table of varchar(2048) ;
       strChr          Char(1);
       retval          numeric;
    BEGIN

       strDegMinSec := regexp_replace(replace(strdegminsec,E'\'\'','"'),' "([0-9]+)',E' \\1"');
       -- Remove leading and trailing spaces
       strDegMinSecB := REPLACE(strDegMinSec,' ','');
       intDmsLen := Length(strDegMinSecB);

       blnGotSeparator := 0; -- Not in separator sequence right now

       -- Loop over string, replacing anything that is not a digit or a
       -- decimal separator with
       -- a single blank
       FOR i in 1..intDmsLen LOOP
          -- Get current character
          strChr := SubStr(strDegMinSecB, i, 1);
          -- either add character to normalized string or replace
          -- separator sequence with single blank         
          If strpos('0123456789,.', strChr) > 0 Then
             -- add character but replace comma with point
             If (strChr <> ',') Then
                strNorm := strNorm || strChr;
             Else
                strNorm := strNorm || '.';
             End If;
             blnGotSeparator := 0;
          ElsIf strpos('neswNESW',strChr) > 0 Then -- Extract Compass Point if present
            strCompassPoint := strChr;
          Else
             -- ensure only one separator is replaced with a blank -
             -- suppress the rest
             If blnGotSeparator = 0 Then
                strNorm := strNorm || ' ';
                blnGotSeparator := 0;
             End If;
          End If;
       End Loop;
       -- Split normalized string into array of max 3 components
       arrDegMinSec := string_to_array(strNorm, ' ');
       BEGIN
          retval := arrDegMinSec[3]::numeric;
          return retval;
       EXCEPTION
          WHEN SQLSTATE '22P02' THEN
             RAISE NOTICE 'Incorrect value %', strDegMinSec;
             RETURN NULL;
       END;
    End 
$BODY$
  LANGUAGE plpgsql IMMUTABLE
  COST 100;
Run Code Online (Sandbox Code Playgroud)

编辑

由 @michel.milezzi 提供另一个不需要修改函数的解决方案是将查询中的函数调用更改为

CAST(NULLIF(test_dolf("Lat"), '') as numeric)
Run Code Online (Sandbox Code Playgroud)

事实上,正如 @abelisto 所建议的,我也可以将查询放入子查询中,然后仅在主查询中将其转换为数字,如下所示:

SELECT "Lat", "Long", CAST(test_dolf("Lat") as numeric), test_dolf("Long") FROM (SELECT * FROM pawikan WHERE "Lat" IS NOT NULL AND "Long" IS NOT NULL ORDER BY index LIMIT 1 OFFSET 29130) as t
Run Code Online (Sandbox Code Playgroud)

这确实可以防止问题的发生,从而简化调试过程。

话虽这么说,无论如何我都会修改该函数(以使其对脏数据更加健壮),所以对我来说,在这种情况下这是最好的解决方案。