数字 : "" while value is not an empty varchar 类型的 postgresql 奇怪的无效输入语法

标签 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

我得到以下输出:

'N6° 6' 9.4824"';'E118° 26' 49.1172'' ';'9.4824';'49.1172'

这正是我所期望的。 但是使用以下查询:

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

我得到了错误

ERROR: invalid input syntax for type numeric: ""
SQL state: 22P02

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

SELECT CAST('9.4824' AS numeric);

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

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

然后发出一个

SELECT CAST(long_sec as numeric), CAST(lat_sec AS numeric) FROM dms2dd_test;

它完全有效。 即使这样也能正常工作:

SELECT test_dolf(E'N6° 6\' 9.4824"')::numeric as lat_sec

那么这里出了什么问题呢?看起来在我转换为数字的第二个查询中,一个不同的值被传递到我的函数,但我测试了排序列(索引)并且它只包含唯一的 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;

最佳答案

我想出了问题所在。它看起来像 postgresql,即使我执行了 LIMIT 和 OFFSET,仍然会为该框架外的其他行调用 select 中的函数。

我通过将引发异常的代码放入我的函数中并捕获由此产生的错误,并在该异常发生时引发 NOTICE 错误来解决这个问题(参见下面的函数,特别是函数末尾的 BEGIN EXCEPTION END block ).该通知显示为警告,但不会导致代码执行停止。突然间,我发现该函数不仅针对我期望它被调用的行调用,而且还针对一大堆其他行调用。这完全不是我所期望的,对我来说有点反直觉,但我想这就是 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;

编辑

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

CAST(NULLIF(test_dolf("Lat"), '') as numeric)

确实正如@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

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

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

关于数字 : "" while value is not an empty varchar 类型的 postgresql 奇怪的无效输入语法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44732534/

相关文章:

sql-server - PostgreSQL 和 SQL Server b 树存储基础问题

postgresql - 与 MySQL AUTO INCREMENT 等效的 PostgreSQL 数据类型是什么?

ruby-on-rails - 我该如何解释这个 Rails/PostgreSQL 错误?

postgresql 12 jsonb_path_query 如何从一个大的 jsonb 对象中选择一些键来构造一个小对象?

postgresql - NestJS、PortsgreSQL 和 TypeORM - 迁移运行不正常

postgresql - pg_dump : [archiver (db)] connection to database "testdb" failed: FATAL: password authentication failed for user "katie"

postgresql - 按列名排序

使用 OS X 的 PostgreSQL 9.3 调试器

sql - 未终止美元报价

postgresql - 如何在 Postgresql 中优化行级安全性